From 9ff04669fe5fca71ba7173df658cf7f5b4b703d2 Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Fri, 24 Jan 2025 15:18:26 -0600 Subject: [PATCH 01/45] initial commit --- .artifactignore | 7 - .ci/e2e_integration_test/start-e2e.ps1 | 102 -- .devcontainer/Dockerfile | 21 - .devcontainer/devcontainer.json | 57 - .flake8 | 1 + CODEOWNERS | 2 +- README.md | 33 +- azure_functions_worker/__init__.py | 4 + azure_functions_worker/__main__.py | 6 - azure_functions_worker/bindings/__init__.py | 29 - azure_functions_worker/bindings/context.py | 26 +- azure_functions_worker/bindings/datumdef.py | 139 +- azure_functions_worker/bindings/generic.py | 22 +- azure_functions_worker/bindings/meta.py | 125 +- .../bindings/nullable_converters.py | 24 +- azure_functions_worker/bindings/out.py | 1 - .../bindings/retrycontext.py | 27 +- .../bindings/rpcexception.py | 25 - .../shared_memory_data_transfer/__init__.py | 25 - .../file_accessor.py | 98 -- .../file_accessor_factory.py | 26 - .../file_accessor_unix.py | 200 --- .../file_accessor_windows.py | 69 - .../shared_memory_constants.py | 80 -- .../shared_memory_exception.py | 10 - .../shared_memory_manager.py | 203 --- .../shared_memory_map.py | 119 -- .../shared_memory_metadata.py | 13 - azure_functions_worker/dispatcher.py | 1115 ---------------- azure_functions_worker/extension.py | 254 ---- azure_functions_worker/functions.py | 116 +- azure_functions_worker/handle_event.py | 410 ++++++ azure_functions_worker/http_v2.py | 4 +- azure_functions_worker/loader.py | 124 +- azure_functions_worker/logging.py | 84 +- azure_functions_worker/main.py | 78 -- azure_functions_worker/otel.py | 106 ++ azure_functions_worker/protos/.gitignore | 3 - azure_functions_worker/protos/_src/.gitignore | 288 ----- azure_functions_worker/protos/_src/LICENSE | 21 - azure_functions_worker/protos/_src/README.md | 98 -- .../protos/_src/src/proto/FunctionRpc.proto | 730 ----------- .../proto/identity/ClaimsIdentityRpc.proto | 26 - .../_src/src/proto/shared/NullableTypes.proto | 30 - .../protos/shared/__init__.py | 0 azure_functions_worker/utils/__init__.py | 2 +- .../utils/app_setting_manager.py | 51 - azure_functions_worker/utils/common.py | 157 --- .../{ => utils}/constants.py | 16 +- azure_functions_worker/utils/current.py | 31 + azure_functions_worker/utils/dependency.py | 411 ------ azure_functions_worker/utils/env_state.py | 76 ++ azure_functions_worker/utils/helpers.py | 29 + azure_functions_worker/utils/tracing.py | 17 + .../{_thirdparty => utils}/typing_inspect.py | 13 +- azure_functions_worker/utils/validators.py | 22 + azure_functions_worker/utils/wrappers.py | 5 +- azure_functions_worker/version.py | 2 +- docs/.gitignore | 2 - docs/Azure.Functions.svg | 16 - docs/CODE_OF_CONDUCT.md | 38 - docs/Functions.Fox.Python.png | Bin 1153747 -> 0 bytes docs/Makefile | 20 - docs/api.rst | 85 -- docs/conf.py | 167 --- docs/index.rst | 466 ------- docs/make.bat | 36 - docs/sharedmemory_existing.png | Bin 50289 -> 0 bytes docs/sharedmemory_new.png | Bin 53205 -> 0 bytes docs/usage.rst | 5 - eng/ci/code-mirror.yml | 6 +- eng/ci/core-tools-tests.yml | 26 - eng/ci/custom-image-tests.yml | 26 - eng/ci/docker-consumption-tests.yml | 37 - eng/ci/docker-dedicated-tests.yml | 37 - eng/ci/emulator-tests.yml | 46 - eng/ci/integration-tests.yml | 53 - eng/ci/official-build.yml | 27 +- eng/ci/package-worker.yml | 37 + eng/ci/public-build.yml | 18 +- eng/scripts/install-dependencies.sh | 12 - eng/scripts/test-extensions.sh | 13 - eng/scripts/test-sdk.sh | 12 - eng/scripts/test-setup.sh | 6 - eng/templates/jobs/build.yml | 6 +- eng/templates/jobs/ci-emulator-tests.yml | 100 -- eng/templates/jobs/ci-unit-tests.yml | 25 +- .../official/jobs/aggregate-artifacts.yml | 31 + .../official/jobs/build-artifacts.yml | 276 +--- .../official/jobs/ci-core-tools-tests.yml | 35 - .../official/jobs/ci-custom-image-tests.yml | 34 - .../jobs/ci-docker-consumption-tests.yml | 71 -- .../jobs/ci-docker-dedicated-tests.yml | 71 -- eng/templates/official/jobs/ci-e2e-tests.yml | 148 --- eng/templates/official/jobs/ci-lc-tests.yml | 42 - eng/templates/utils/official-variables.yml | 4 - eng/templates/utils/variables.yml | 5 - ...osoft.Azure.Functions.PythonWorker.targets | 43 - ...oft.Azure.Functions.V4.PythonWorker.nuspec | 45 - pack/scripts/mac_arm64_deps.sh | 16 - pack/scripts/nix_deps.sh | 16 - pack/scripts/win_deps.ps1 | 18 - pack/templates/macos_64_env_gen.yml | 44 - pack/templates/nix_env_gen.yml | 44 - pack/templates/win_env_gen.yml | 44 - pack/utils/__init__.py | 4 - pyproject.toml | 77 +- python/prodV4/worker.config.json | 17 - python/prodV4/worker.py | 68 - python/test/worker.config.json | 13 - python/test/worker.py | 19 - requirements.txt | 2 - tests/.gitignore | 3 - tests/__init__.py | 21 - .../test_linux_consumption.py | 383 ------ .../generic/function_app.py | 471 ------- .../blob_functions/blob_trigger/function.json | 19 - .../blob_functions/blob_trigger/main.py | 13 - .../get_blob_as_bytes/function.json | 24 - .../blob_functions/get_blob_as_bytes/main.py | 8 - .../function.json | 24 - .../main.py | 31 - .../function.json | 24 - .../main.py | 31 - .../get_blob_as_str/function.json | 24 - .../blob_functions/get_blob_as_str/main.py | 8 - .../function.json | 24 - .../main.py | 32 - .../get_blob_bytes/function.json | 23 - .../blob_functions/get_blob_bytes/main.py | 7 - .../get_blob_filelike/function.json | 23 - .../blob_functions/get_blob_filelike/main.py | 7 - .../get_blob_return/function.json | 23 - .../blob_functions/get_blob_return/main.py | 7 - .../blob_functions/get_blob_str/function.json | 23 - .../blob_functions/get_blob_str/main.py | 7 - .../get_blob_triggered/function.json | 23 - .../blob_functions/get_blob_triggered/main.py | 7 - .../function.json | 24 - .../main.py | 42 - .../function.json | 24 - .../main.py | 40 - .../put_blob_bytes/function.json | 23 - .../blob_functions/put_blob_bytes/main.py | 8 - .../put_blob_filelike/function.json | 23 - .../blob_functions/put_blob_filelike/main.py | 10 - .../put_blob_return/function.json | 18 - .../blob_functions/put_blob_return/main.py | 7 - .../blob_functions/put_blob_str/function.json | 23 - .../blob_functions/put_blob_str/main.py | 8 - .../put_blob_trigger/function.json | 23 - .../blob_functions/put_blob_trigger/main.py | 8 - .../function.json | 48 - .../main.py | 64 - .../function_app.py | 126 -- .../eventhub_multiple/__init__.py | 17 - .../eventhub_multiple/function.json | 22 - .../eventhub_output_batch/__init__.py | 9 - .../eventhub_output_batch/function.json | 19 - .../get_eventhub_batch_triggered/__init__.py | 8 - .../function.json | 26 - .../get_metadata_batch_triggered/__init__.py | 10 - .../function.json | 23 - .../metadata_multiple/__init__.py | 25 - .../metadata_multiple/function.json | 21 - .../metadata_output_batch/__init__.py | 36 - .../metadata_output_batch/function.json | 16 - .../eventhub_functions_stein/function_app.py | 107 -- .../generic/function_app.py | 126 -- .../eventhub_output/__init__.py | 10 - .../eventhub_output/function.json | 24 - .../eventhub_trigger/__init__.py | 9 - .../eventhub_trigger/function.json | 20 - .../get_eventhub_triggered/function.json | 23 - .../get_eventhub_triggered/main.py | 8 - .../get_metadata_triggered/__init__.py | 10 - .../get_metadata_triggered/function.json | 23 - .../metadata_output/__init__.py | 35 - .../metadata_output/function.json | 17 - .../metadata_trigger/__init__.py | 23 - .../metadata_trigger/function.json | 19 - .../generic_functions_stein/function_app.py | 159 --- .../return_bool/function.json | 21 - .../generic_functions/return_bool/main.py | 11 - .../return_bytes/function.json | 21 - .../generic_functions/return_bytes/main.py | 11 - .../return_dict/function.json | 21 - .../generic_functions/return_dict/main.py | 11 - .../return_double/function.json | 21 - .../generic_functions/return_double/main.py | 11 - .../return_int/function.json | 21 - .../generic_functions/return_int/main.py | 11 - .../return_list/function.json | 21 - .../generic_functions/return_list/main.py | 11 - .../return_none/function.json | 21 - .../generic_functions/return_none/main.py | 10 - .../return_none_no_type_hint/function.json | 21 - .../return_none_no_type_hint/main.py | 10 - .../return_not_processed_last/__init__.py | 13 - .../return_not_processed_last/function.json | 26 - .../return_processed_last/__init__.py | 13 - .../return_processed_last/function.json | 26 - .../return_string/function.json | 21 - .../generic_functions/return_string/main.py | 11 - .../table_out_binding/__init__.py | 13 - .../table_out_binding/function.json | 24 - .../get_queue_blob/function.json | 23 - .../queue_functions/get_queue_blob/main.py | 11 - .../function.json | 23 - .../get_queue_blob_message_return/main.py | 7 - .../get_queue_blob_return/function.json | 23 - .../get_queue_blob_return/main.py | 7 - .../function.json | 24 - .../get_queue_untyped_blob_return/main.py | 7 - .../queue_functions/put_queue/function.json | 24 - .../queue_functions/put_queue/main.py | 9 - .../put_queue_message_return/function.json | 19 - .../put_queue_message_return/main.py | 7 - .../put_queue_multiple_out/function.json | 24 - .../put_queue_multiple_out/main.py | 10 - .../put_queue_return/function.json | 19 - .../queue_functions/put_queue_return/main.py | 7 - .../put_queue_return_multiple/function.json | 19 - .../put_queue_return_multiple/main.py | 9 - .../put_queue_untyped_return/function.json | 20 - .../put_queue_untyped_return/main.py | 7 - .../queue_functions_stein/function_app.py | 185 --- .../generic/function_app.py | 253 ---- .../queue_trigger/function.json | 20 - .../queue_functions/queue_trigger/main.py | 22 - .../function.json | 20 - .../queue_trigger_message_return/main.py | 7 - .../queue_trigger_return/function.json | 20 - .../queue_trigger_return/main.py | 7 - .../function.json | 13 - .../queue_trigger_return_multiple/main.py | 11 - .../queue_trigger_untyped/function.json | 21 - .../queue_trigger_untyped/main.py | 9 - .../get_servicebus_triggered/__init__.py | 8 - .../get_servicebus_triggered/function.json | 23 - .../put_message/__init__.py | 9 - .../put_message/function.json | 24 - .../function_app.py | 73 -- .../generic/function_app.py | 81 -- .../servicebus_trigger/__init__.py | 46 - .../servicebus_trigger/function.json | 20 - .../table_functions_stein/function_app.py | 32 - .../generic/function_app.py | 40 - .../table_in_binding/__init__.py | 7 - .../table_in_binding/function.json | 28 - .../table_out_binding/__init__.py | 14 - .../table_out_binding/function.json | 24 - tests/emulator_tests/test_blob_functions.py | 166 --- .../test_eventhub_batch_functions.py | 242 ---- .../emulator_tests/test_eventhub_functions.py | 124 -- .../emulator_tests/test_generic_functions.py | 78 -- tests/emulator_tests/test_queue_functions.py | 103 -- .../test_servicebus_functions.py | 65 - tests/emulator_tests/test_table_functions.py | 73 -- .../emulator_tests/utils/eventhub/config.json | 51 - .../utils/eventhub/docker-compose.yml | 34 - .../utils/servicebus/config.json | 28 - .../utils/servicebus/docker-compose.yml | 40 - .../blueprint_directory/blueprint.py | 41 - .../blueprint_different_dir/function_app.py | 6 - .../functions_in_blueprint_only/blueprint.py | 41 - .../function_app.py | 6 - .../blueprint.py | 31 - .../function_app.py | 12 - .../function_app.py | 12 - .../only_blueprint/function_app.py | 31 - .../cosmosdb_functions_stein/function_app.py | 46 - .../generic/function_app.py | 61 - .../cosmosdb_input/__init__.py | 7 - .../cosmosdb_input/function.json | 28 - .../cosmosdb_trigger/__init__.py | 7 - .../cosmosdb_trigger/function.json | 24 - .../function_app.py | 47 - .../generic/function_app.py | 62 - .../get_cosmosdb_triggered/function.json | 23 - .../get_cosmosdb_triggered/main.py | 7 - .../put_document/__init__.py | 9 - .../put_document/function.json | 28 - .../site-packages/azure/functions/__init__.py | 23 - .../lib/site-packages/azure/functions/_abc.py | 422 ------ .../site-packages/azure/functions/_http.py | 230 ---- .../azure/functions/_thirdparty/__init__.py | 0 .../functions/_thirdparty/typing_inspect.py | 0 .../_thirdparty/werkzeug/datastructures.py | 0 .../_thirdparty/werkzeug/formparser.py | 0 .../functions/_thirdparty/werkzeug/http.py | 0 .../site-packages/azure/functions/_utils.py | 89 -- .../lib/site-packages/azure/functions/http.py | 134 -- .../lib/site-packages/azure/functions/meta.py | 401 ------ .../site-packages/azure/functions/__init__.py | 23 - .../lib/site-packages/azure/functions/_abc.py | 422 ------ .../site-packages/azure/functions/_http.py | 230 ---- .../azure/functions/_thirdparty/__init__.py | 0 .../functions/_thirdparty/typing_inspect.py | 0 .../_thirdparty/werkzeug/datastructures.py | 0 .../_thirdparty/werkzeug/formparser.py | 0 .../functions/_thirdparty/werkzeug/http.py | 0 .../site-packages/azure/functions/_utils.py | 89 -- .../lib/site-packages/azure/functions/http.py | 134 -- .../lib/site-packages/azure/functions/meta.py | 401 ------ .../site-packages/google/protobuf/__init__.py | 7 - .../lib/site-packages/grpc/__init__.py | 7 - .../report_dependencies/__init__.py | 50 - .../report_dependencies/function.json | 20 - .../DurableFunctionsHttpStart/__init__.py | 20 - .../DurableFunctionsHttpStart/function.json | 26 - .../DurableFunctionsOrchestrator/__init__.py | 18 - .../function.json | 10 - .../durable_functions/Hello/__init__.py | 10 - .../durable_functions/Hello/function.json | 10 - .../durable_functions_stein/function_app.py | 29 - .../eventgrid_functions_stein/function_app.py | 83 -- .../generic/function_app.py | 100 -- .../eventgrid_output_binding/__init__.py | 27 - .../eventgrid_output_binding/function.json | 24 - .../__init__.py | 8 - .../function.json | 19 - .../__init__.py | 8 - .../function.json | 23 - .../eventgrid_trigger/__init__.py | 15 - .../eventgrid_trigger/function.json | 18 - .../get_eventgrid_triggered/function.json | 23 - .../get_eventgrid_triggered/main.py | 7 - .../function_app.py | 84 -- .../dotenv_func/__init__.py | 15 - .../dotenv_func/function.json | 20 - .../numpy_func/__init__.py | 15 - .../numpy_func/function.json | 20 - .../opencv_func/__init__.py | 15 - .../opencv_func/function.json | 20 - .../pandas_func/__init__.py | 19 - .../pandas_func/function.json | 20 - .../plotly_func/__init__.py | 15 - .../plotly_func/function.json | 20 - .../requests_func/__init__.py | 16 - .../requests_func/function.json | 20 - .../sklearn_func/__init__.py | 17 - .../sklearn_func/function.json | 20 - .../default_template/__init__.py | 28 - .../default_template/function.json | 20 - .../http_functions/http_func/__init__.py | 14 - .../http_functions/http_func/function.json | 20 - .../http_functions_stein/file_name/main.py | 44 - .../http_functions_stein/function_app.py | 44 - .../generic/function_app.py | 38 - .../async_thread/__init__.py | 33 - .../async_thread/function.json | 20 - .../async_thread_pool_executor/__init__.py | 25 - .../async_thread_pool_executor/function.json | 20 - .../user_thread_logging/thread/__init__.py | 33 - .../user_thread_logging/thread/function.json | 20 - .../thread_pool_executor/__init__.py | 25 - .../thread_pool_executor/function.json | 20 - .../exponential_strategy/function_app.py | 23 - .../fixed_strategy/function_app.py | 22 - .../sql_functions_stein/function_app.py | 69 - .../generic/function_app.py | 75 -- .../sql_functions/sql_input/__init__.py | 16 - .../sql_functions/sql_input/function.json | 29 - .../sql_functions/sql_input2/__init__.py | 16 - .../sql_functions/sql_input2/function.json | 29 - .../sql_functions/sql_output/__init__.py | 18 - .../sql_functions/sql_output/function.json | 26 - .../sql_functions/sql_trigger/__init__.py | 11 - .../sql_functions/sql_trigger/function.json | 20 - tests/endtoend/test_blueprint_functions.py | 69 - tests/endtoend/test_cosmosdb_functions.py | 102 -- .../test_dependency_isolation_functions.py | 235 ---- tests/endtoend/test_durable_functions.py | 65 - tests/endtoend/test_eventgrid_functions.py | 171 --- tests/endtoend/test_file_name_functions.py | 123 -- tests/endtoend/test_http_functions.py | 289 ----- tests/endtoend/test_retry_policy_functions.py | 48 - tests/endtoend/test_sql_functions.py | 68 - .../test_third_party_http_functions.py | 162 --- .../test_threadpool_thread_count_functions.py | 62 - tests/endtoend/test_timer_functions.py | 44 - tests/endtoend/test_warmup_functions.py | 47 - .../test_worker_process_count_functions.py | 100 -- .../stein/asgi_function/function_app.py | 41 - .../stein/wsgi_function/function_app.py | 36 - .../timer_functions/timer_func/__init__.py | 10 - .../timer_functions/timer_func/function.json | 12 - .../timer_functions_stein/function_app.py | 16 - .../warmup_functions/warmup/__init__.py | 8 - .../warmup_functions/warmup/function.json | 9 - .../warmup_functions_stein/function_app.py | 13 - .../function_app.py | 294 ----- .../function_app.py | 22 - .../deferred_bindings_enabled/function_app.py | 16 - .../function_app.py | 33 - .../test_deferred_bindings.py | 198 --- .../test_deferred_bindings_blob_functions.py | 232 ---- .../http_functions_v2/fastapi/function_app.py | 100 -- .../http_v2_tests/test_http_v2.py | 194 --- tests/protos/FunctionRpc_pb2.py | 215 ++++ tests/protos/FunctionRpc_pb2_grpc.py | 69 + .../protos/__init__.py | 0 .../protos/identity/ClaimsIdentityRpc_pb2.py | 29 + .../identity/ClaimsIdentityRpc_pb2_grpc.py | 4 + .../protos/identity}/__init__.py | 0 tests/protos/shared/NullableTypes_pb2.py | 33 + tests/protos/shared/NullableTypes_pb2_grpc.py | 4 + .../protos/shared}/__init__.py | 0 tests/test_setup.py | 304 ----- .../function_app.py | 92 +- tests/unit_tests/test_handle_event.py | 117 ++ .../azure_namespace_import.py | 56 - .../azure/module_a/__init__.py | 3 - .../test_azure_namespace_import.sh | 10 - tests/unittests/broken_functions/README.md | 3 - .../bad_out_annotation/function.json | 20 - .../bad_out_annotation/main.py | 7 - .../import_error/function.json | 15 - .../broken_functions/import_error/main.py | 7 - .../inout_param/function.json | 15 - .../broken_functions/inout_param/main.py | 6 - .../invalid_app_stein/function_app.py | 5 - .../invalid_context_param/function.json | 15 - .../invalid_context_param/main.py | 6 - .../invalid_datatype/function.json | 11 - .../broken_functions/invalid_datatype/main.py | 7 - .../invalid_http_trigger_anno/function.json | 15 - .../invalid_http_trigger_anno/main.py | 6 - .../invalid_in_anno/function.json | 10 - .../broken_functions/invalid_in_anno/main.py | 7 - .../invalid_in_anno_non_type/function.json | 10 - .../invalid_in_anno_non_type/main.py | 6 - .../invalid_out_anno/function.json | 15 - .../broken_functions/invalid_out_anno/main.py | 7 - .../invalid_return_anno/function.json | 15 - .../invalid_return_anno/main.py | 6 - .../function.json | 15 - .../invalid_return_anno_non_type/main.py | 6 - .../invalid_stein/function_app.py | 8 - .../missing_json_param/function.json | 15 - .../missing_json_param/main.py | 6 - .../missing_module/function.json | 15 - .../broken_functions/missing_module/main.py | 13 - .../missing_py_param/function.json | 15 - .../broken_functions/missing_py_param/main.py | 6 - .../module_not_found_error/function.json | 15 - .../module_not_found_error/main.py | 7 - .../return_param_in/function.json | 15 - .../broken_functions/return_param_in/main.py | 6 - .../syntax_error/function.json | 15 - .../broken_functions/syntax_error/main.py | 6 - .../wrong_binding_dir/function.json | 20 - .../wrong_binding_dir/main.py | 7 - .../wrong_param_dir/function.json | 20 - .../broken_functions/wrong_param_dir/main.py | 6 - .../function_app.py | 9 - .../http_v2/fastapi/function_app.py | 9 - .../show_context/__init__.py | 14 - .../show_context/function.json | 15 - .../show_context_async/__init__.py | 15 - .../show_context_async/function.json | 15 - .../activity_trigger/function.json | 10 - .../activity_trigger/main.py | 6 - .../activity_trigger_dict/function.json | 10 - .../activity_trigger_dict/main.py | 11 - .../function.json | 10 - .../activity_trigger_int_to_float/main.py | 6 - .../activity_trigger_no_anno/function.json | 10 - .../activity_trigger_no_anno/main.py | 6 - .../orchestration_trigger/function.json | 10 - .../orchestration_trigger/main.py | 15 - .../eventhub_cardinality_many/__init__.py | 10 - .../eventhub_cardinality_many/function.json | 21 - .../__init__.py | 9 - .../function.json | 21 - .../eventhub_cardinality_one/__init__.py | 8 - .../eventhub_cardinality_one/function.json | 21 - .../__init__.py | 8 - .../function.json | 21 - .../eventhub_trigger_iot/__init__.py | 9 - .../eventhub_trigger_iot/function.json | 20 - .../default_file_name/function_app.py | 11 - .../invalid_file_name/main | 11 - .../file_name_functions/new_file_name/test.py | 11 - .../foobar_as_bytes/function.json | 17 - .../generic_functions/foobar_as_bytes/main.py | 6 - .../foobar_as_bytes_no_anno/function.json | 17 - .../foobar_as_bytes_no_anno/main.py | 7 - .../foobar_as_none/function.json | 11 - .../generic_functions/foobar_as_none/main.py | 6 - .../foobar_as_str/function.json | 17 - .../generic_functions/foobar_as_str/main.py | 6 - .../foobar_as_str_no_anno/function.json | 17 - .../foobar_as_str_no_anno/main.py | 7 - .../foobar_implicit_output/function.json | 11 - .../foobar_implicit_output/main.py | 7 - .../function.json | 12 - .../foobar_implicit_output_exemption/main.py | 7 - .../foobar_nil_data/function.json | 11 - .../generic_functions/foobar_nil_data/main.py | 7 - .../foobar_return_bool/function.json | 11 - .../foobar_return_bool/main.py | 6 - .../foobar_return_dict/function.json | 11 - .../foobar_return_dict/main.py | 6 - .../foobar_return_double/function.json | 11 - .../foobar_return_double/main.py | 6 - .../foobar_return_int/function.json | 11 - .../foobar_return_int/main.py | 6 - .../foobar_return_list/function.json | 11 - .../foobar_return_list/main.py | 6 - .../foobar_with_no_datatype/function.json | 10 - .../foobar_with_no_datatype/main.py | 6 - .../http_functions/accept_json/function.json | 15 - .../http_functions/accept_json/main.py | 16 - .../async_logging/function.json | 15 - .../http_functions/async_logging/main.py | 29 - .../async_return_str/function.json | 15 - .../http_functions/async_return_str/main.py | 10 - .../create_task_with_context/function.json | 15 - .../create_task_with_context/main.py | 35 - .../create_task_without_context/function.json | 15 - .../create_task_without_context/main.py | 20 - .../debug_logging/function.json | 15 - .../http_functions/debug_logging/main.py | 14 - .../hijack_current_event_loop/function.json | 19 - .../hijack_current_event_loop/main.py | 77 -- .../http_functions_stein/function_app.py | 455 ------- .../http_v2_functions/fastapi/function_app.py | 438 ------- .../function.json | 15 - .../multiple_set_cookie_resp_headers/main.py | 23 - .../http_functions/no_return/function.json | 10 - .../http_functions/no_return/main.py | 9 - .../no_return_returns/function.json | 10 - .../http_functions/no_return_returns/main.py | 6 - .../print_logging/function.json | 15 - .../http_functions/print_logging/main.py | 29 - .../raw_body_bytes/function.json | 18 - .../http_functions/raw_body_bytes/main.py | 11 - .../remapped_context/function.json | 15 - .../http_functions/remapped_context/main.py | 6 - .../function.json | 15 - .../main.py | 18 - .../function.json | 15 - .../main.py | 18 - .../function.json | 15 - .../main.py | 15 - .../http_functions/return_bytes/function.json | 15 - .../http_functions/return_bytes/main.py | 7 - .../return_context/function.json | 15 - .../http_functions/return_context/main.py | 16 - .../http_functions/return_http/function.json | 15 - .../http_functions/return_http/main.py | 8 - .../return_http_404/function.json | 15 - .../http_functions/return_http_404/main.py | 7 - .../return_http_auth_admin/function.json | 16 - .../return_http_auth_admin/main.py | 8 - .../return_http_no_body/function.json | 15 - .../return_http_no_body/main.py | 7 - .../return_http_redirect/function.json | 15 - .../return_http_redirect/main.py | 10 - .../http_functions/return_out/function.json | 15 - .../http_functions/return_out/main.py | 7 - .../return_request/function.json | 15 - .../http_functions/return_request/main.py | 20 - .../return_route_params/function.json | 16 - .../return_route_params/main.py | 9 - .../http_functions/return_str/function.json | 15 - .../http_functions/return_str/main.py | 7 - .../function.json | 15 - .../main.py | 15 - .../function.json | 15 - .../set_cookie_resp_header_empty/main.py | 15 - .../http_functions/sync_logging/function.json | 15 - .../http_functions/sync_logging/main.py | 18 - .../unhandled_error/function.json | 15 - .../http_functions/unhandled_error/main.py | 7 - .../function.json | 15 - .../unhandled_unserializable_error/main.py | 12 - .../unhandled_urllib_error/function.json | 15 - .../unhandled_urllib_error/main.py | 10 - .../user_event_loop/function.json | 19 - .../http_functions/user_event_loop/main.py | 22 - .../absolute_thirdparty/function.json | 16 - .../absolute_thirdparty/main.py | 9 - .../load_functions/entrypoint/function.json | 16 - .../load_functions/entrypoint/main.py | 6 - .../implicit_import/function.json | 16 - .../load_functions/implicit_import/main.py | 10 - .../load_outside_main/function.json | 15 - .../load_functions/load_outside_main/main.py | 23 - .../module_not_found/function.json | 16 - .../load_functions/module_not_found/main.py | 8 - .../name_collision/function.json | 16 - .../load_functions/name_collision/main.py | 10 - .../name_collision_app_import/function.json | 16 - .../name_collision_app_import/main.py | 10 - .../no_script_file/function.json | 14 - .../load_functions/no_script_file/main.py | 6 - .../outside_main_code_in_init/__init__.py | 12 - .../outside_main_code_in_init/count.py | 19 - .../outside_main_code_in_init/function.json | 15 - .../outside_main_code_in_main/count.py | 19 - .../outside_main_code_in_main/function.json | 15 - .../outside_main_code_in_main/main.py | 12 - .../load_functions/parentmodule/function.json | 15 - .../load_functions/parentmodule/module.py | 3 - .../parentmodule/sub_module/__init__.py | 2 - .../parentmodule/sub_module/main.py | 7 - .../load_functions/pytest/__init__.py | 7 - .../load_functions/relimport/function.json | 15 - .../load_functions/relimport/main.py | 7 - .../load_functions/relimport/relative.py | 2 - .../load_functions/simple/function.json | 15 - tests/unittests/load_functions/simple/main.py | 6 - .../stub_http_trigger/__init__.py | 6 - .../stub_http_trigger/function.json | 15 - .../stub_http_trigger/stub_tools.py | 4 - .../load_functions/subdir/function.json | 15 - .../load_functions/subdir/sub/main.py | 6 - .../load_functions/submodule/function.json | 15 - .../load_functions/submodule/main.py | 7 - .../submodule/sub_module/__init__.py | 2 - .../submodule/sub_module/module.py | 3 - .../debug_logging/function.json | 15 - .../debug_logging/main.py | 13 - .../debug_user_logging/function.json | 15 - .../debug_user_logging/main.py | 15 - .../sdk_logging/__init__.py | 15 - .../sdk_logging/function.json | 16 - .../sdk_submodule_logging/__init__.py | 15 - .../sdk_submodule_logging/function.json | 16 - tests/unittests/path_import/path_import.py | 45 - .../unittests/path_import/test_path_import.sh | 9 - .../customer_deps_path/azure/__init__.py | 4 - .../azure/functions/__init__.py | 9 - .../common_module/__init__.py | 9 - .../common_namespace/__init__.py | 4 - .../nested_module/__init__.py | 9 - .../resources/customer_deps_path/readme.md | 9 - .../HttpTrigger/__init__.py | 10 - .../HttpTrigger/function.json | 20 - .../common_module/__init__.py | 10 - .../func_specific_module/__init__.py | 9 - .../resources/customer_func_path/host.json | 15 - .../customer_func_path/requirements.txt | 1 - tests/unittests/resources/functions.png | Bin 1976 -> 0 bytes .../mock_azure_functions/azure/__init__.py | 4 - .../azure/functions/__init__.py | 4 - .../resources/mock_azure_functions/readme.md | 3 - .../worker_deps_path/azure/__init__.py | 4 - .../azure/functions/__init__.py | 9 - .../common_module/__init__.py | 9 - .../common_namespace/__init__.py | 4 - .../nested_module/__init__.py | 9 - .../resources/worker_deps_path/readme.md | 9 - tests/unittests/test-binding/foo/__init__.py | 2 - tests/unittests/test-binding/foo/binding.py | 8 - .../test-binding/functions/foo/function.json | 11 - .../test-binding/functions/foo/main.py | 6 - tests/unittests/test-binding/setup.py | 14 - tests/unittests/test_app_setting_manager.py | 99 -- tests/unittests/test_broken_functions.py | 299 ----- tests/unittests/test_code_quality.py | 54 - tests/unittests/test_datumref.py | 148 --- tests/unittests/test_dispatcher.py | 1127 ----------------- .../test_enable_debug_logging_functions.py | 133 -- tests/unittests/test_extension.py | 864 ------------- tests/unittests/test_file_accessor.py | 100 -- tests/unittests/test_file_accessor_factory.py | 49 - tests/unittests/test_functions_registry.py | 40 - tests/unittests/test_http_functions.py | 480 ------- tests/unittests/test_http_functions_v2.py | 472 ------- tests/unittests/test_http_v2.py | 253 ---- tests/unittests/test_invalid_stein.py | 43 - tests/unittests/test_loader.py | 281 ---- .../unittests/test_log_filtering_functions.py | 108 -- tests/unittests/test_logging.py | 60 - tests/unittests/test_main.py | 80 -- .../test_mock_blob_shared_memory_functions.py | 620 --------- .../unittests/test_mock_durable_functions.py | 158 --- .../unittests/test_mock_eventhub_functions.py | 155 --- .../unittests/test_mock_generic_functions.py | 390 ------ tests/unittests/test_mock_http_functions.py | 88 -- .../test_mock_log_filtering_functions.py | 97 -- tests/unittests/test_mock_timer_functions.py | 69 - tests/unittests/test_nullable_converters.py | 110 -- tests/unittests/test_opentelemetry.py | 110 -- tests/unittests/test_rpc_messages.py | 147 --- tests/unittests/test_script_file_name.py | 109 -- tests/unittests/test_shared_memory_manager.py | 394 ------ tests/unittests/test_shared_memory_map.py | 139 -- .../test_third_party_http_functions.py | 237 ---- tests/unittests/test_types.py | 196 --- tests/unittests/test_typing_inspect.py | 144 --- tests/unittests/test_utilities.py | 390 ------ tests/unittests/test_utilities_dependency.py | 784 ------------ .../stein/asgi_function/function_app.py | 173 --- .../stein/wsgi_function/function_app.py | 96 -- .../return_pastdue/function.json | 16 - .../timer_functions/return_pastdue/main.py | 7 - .../user_event_loop_timer/function.json | 11 - .../user_event_loop_timer/main.py | 19 - tests/utils/constants.py | 82 -- tests/utils/testutils.py | 1051 --------------- tests/utils/testutils_docker.py | 214 ---- tests/utils/testutils_lc.py | 339 ----- 707 files changed, 1587 insertions(+), 35509 deletions(-) delete mode 100644 .artifactignore delete mode 100644 .ci/e2e_integration_test/start-e2e.ps1 delete mode 100644 .devcontainer/Dockerfile delete mode 100644 .devcontainer/devcontainer.json delete mode 100644 azure_functions_worker/__main__.py delete mode 100644 azure_functions_worker/bindings/__init__.py delete mode 100644 azure_functions_worker/bindings/rpcexception.py delete mode 100644 azure_functions_worker/bindings/shared_memory_data_transfer/__init__.py delete mode 100644 azure_functions_worker/bindings/shared_memory_data_transfer/file_accessor.py delete mode 100644 azure_functions_worker/bindings/shared_memory_data_transfer/file_accessor_factory.py delete mode 100644 azure_functions_worker/bindings/shared_memory_data_transfer/file_accessor_unix.py delete mode 100644 azure_functions_worker/bindings/shared_memory_data_transfer/file_accessor_windows.py delete mode 100644 azure_functions_worker/bindings/shared_memory_data_transfer/shared_memory_constants.py delete mode 100644 azure_functions_worker/bindings/shared_memory_data_transfer/shared_memory_exception.py delete mode 100644 azure_functions_worker/bindings/shared_memory_data_transfer/shared_memory_manager.py delete mode 100644 azure_functions_worker/bindings/shared_memory_data_transfer/shared_memory_map.py delete mode 100644 azure_functions_worker/bindings/shared_memory_data_transfer/shared_memory_metadata.py delete mode 100644 azure_functions_worker/dispatcher.py delete mode 100644 azure_functions_worker/extension.py create mode 100644 azure_functions_worker/handle_event.py delete mode 100644 azure_functions_worker/main.py create mode 100644 azure_functions_worker/otel.py delete mode 100644 azure_functions_worker/protos/.gitignore delete mode 100644 azure_functions_worker/protos/_src/.gitignore delete mode 100644 azure_functions_worker/protos/_src/LICENSE delete mode 100644 azure_functions_worker/protos/_src/README.md delete mode 100644 azure_functions_worker/protos/_src/src/proto/FunctionRpc.proto delete mode 100644 azure_functions_worker/protos/_src/src/proto/identity/ClaimsIdentityRpc.proto delete mode 100644 azure_functions_worker/protos/_src/src/proto/shared/NullableTypes.proto delete mode 100644 azure_functions_worker/protos/shared/__init__.py delete mode 100644 azure_functions_worker/utils/app_setting_manager.py delete mode 100644 azure_functions_worker/utils/common.py rename azure_functions_worker/{ => utils}/constants.py (86%) create mode 100644 azure_functions_worker/utils/current.py delete mode 100644 azure_functions_worker/utils/dependency.py create mode 100644 azure_functions_worker/utils/env_state.py create mode 100644 azure_functions_worker/utils/helpers.py rename azure_functions_worker/{_thirdparty => utils}/typing_inspect.py (95%) create mode 100644 azure_functions_worker/utils/validators.py delete mode 100644 docs/.gitignore delete mode 100755 docs/Azure.Functions.svg delete mode 100644 docs/CODE_OF_CONDUCT.md delete mode 100644 docs/Functions.Fox.Python.png delete mode 100644 docs/Makefile delete mode 100644 docs/api.rst delete mode 100644 docs/conf.py delete mode 100644 docs/index.rst delete mode 100644 docs/make.bat delete mode 100644 docs/sharedmemory_existing.png delete mode 100644 docs/sharedmemory_new.png delete mode 100644 docs/usage.rst delete mode 100644 eng/ci/core-tools-tests.yml delete mode 100644 eng/ci/custom-image-tests.yml delete mode 100644 eng/ci/docker-consumption-tests.yml delete mode 100644 eng/ci/docker-dedicated-tests.yml delete mode 100644 eng/ci/emulator-tests.yml delete mode 100644 eng/ci/integration-tests.yml create mode 100644 eng/ci/package-worker.yml delete mode 100644 eng/scripts/install-dependencies.sh delete mode 100644 eng/scripts/test-extensions.sh delete mode 100644 eng/scripts/test-sdk.sh delete mode 100644 eng/scripts/test-setup.sh delete mode 100644 eng/templates/jobs/ci-emulator-tests.yml create mode 100644 eng/templates/official/jobs/aggregate-artifacts.yml delete mode 100644 eng/templates/official/jobs/ci-core-tools-tests.yml delete mode 100644 eng/templates/official/jobs/ci-custom-image-tests.yml delete mode 100644 eng/templates/official/jobs/ci-docker-consumption-tests.yml delete mode 100644 eng/templates/official/jobs/ci-docker-dedicated-tests.yml delete mode 100644 eng/templates/official/jobs/ci-e2e-tests.yml delete mode 100644 eng/templates/official/jobs/ci-lc-tests.yml delete mode 100644 eng/templates/utils/official-variables.yml delete mode 100644 eng/templates/utils/variables.yml delete mode 100644 pack/Microsoft.Azure.Functions.PythonWorker.targets delete mode 100644 pack/Microsoft.Azure.Functions.V4.PythonWorker.nuspec delete mode 100644 pack/scripts/mac_arm64_deps.sh delete mode 100644 pack/scripts/nix_deps.sh delete mode 100644 pack/scripts/win_deps.ps1 delete mode 100644 pack/templates/macos_64_env_gen.yml delete mode 100644 pack/templates/nix_env_gen.yml delete mode 100644 pack/templates/win_env_gen.yml delete mode 100644 pack/utils/__init__.py delete mode 100644 python/prodV4/worker.config.json delete mode 100644 python/prodV4/worker.py delete mode 100644 python/test/worker.config.json delete mode 100644 python/test/worker.py delete mode 100644 requirements.txt delete mode 100644 tests/.gitignore delete mode 100644 tests/__init__.py delete mode 100644 tests/consumption_tests/test_linux_consumption.py delete mode 100644 tests/emulator_tests/blob_functions/blob_functions_stein/generic/function_app.py delete mode 100644 tests/emulator_tests/blob_functions/blob_trigger/function.json delete mode 100644 tests/emulator_tests/blob_functions/blob_trigger/main.py delete mode 100644 tests/emulator_tests/blob_functions/get_blob_as_bytes/function.json delete mode 100644 tests/emulator_tests/blob_functions/get_blob_as_bytes/main.py delete mode 100644 tests/emulator_tests/blob_functions/get_blob_as_bytes_return_http_response/function.json delete mode 100644 tests/emulator_tests/blob_functions/get_blob_as_bytes_return_http_response/main.py delete mode 100644 tests/emulator_tests/blob_functions/get_blob_as_bytes_stream_return_http_response/function.json delete mode 100644 tests/emulator_tests/blob_functions/get_blob_as_bytes_stream_return_http_response/main.py delete mode 100644 tests/emulator_tests/blob_functions/get_blob_as_str/function.json delete mode 100644 tests/emulator_tests/blob_functions/get_blob_as_str/main.py delete mode 100644 tests/emulator_tests/blob_functions/get_blob_as_str_return_http_response/function.json delete mode 100644 tests/emulator_tests/blob_functions/get_blob_as_str_return_http_response/main.py delete mode 100644 tests/emulator_tests/blob_functions/get_blob_bytes/function.json delete mode 100644 tests/emulator_tests/blob_functions/get_blob_bytes/main.py delete mode 100644 tests/emulator_tests/blob_functions/get_blob_filelike/function.json delete mode 100644 tests/emulator_tests/blob_functions/get_blob_filelike/main.py delete mode 100644 tests/emulator_tests/blob_functions/get_blob_return/function.json delete mode 100644 tests/emulator_tests/blob_functions/get_blob_return/main.py delete mode 100644 tests/emulator_tests/blob_functions/get_blob_str/function.json delete mode 100644 tests/emulator_tests/blob_functions/get_blob_str/main.py delete mode 100644 tests/emulator_tests/blob_functions/get_blob_triggered/function.json delete mode 100644 tests/emulator_tests/blob_functions/get_blob_triggered/main.py delete mode 100644 tests/emulator_tests/blob_functions/put_blob_as_bytes_return_http_response/function.json delete mode 100644 tests/emulator_tests/blob_functions/put_blob_as_bytes_return_http_response/main.py delete mode 100644 tests/emulator_tests/blob_functions/put_blob_as_str_return_http_response/function.json delete mode 100644 tests/emulator_tests/blob_functions/put_blob_as_str_return_http_response/main.py delete mode 100644 tests/emulator_tests/blob_functions/put_blob_bytes/function.json delete mode 100644 tests/emulator_tests/blob_functions/put_blob_bytes/main.py delete mode 100644 tests/emulator_tests/blob_functions/put_blob_filelike/function.json delete mode 100644 tests/emulator_tests/blob_functions/put_blob_filelike/main.py delete mode 100644 tests/emulator_tests/blob_functions/put_blob_return/function.json delete mode 100644 tests/emulator_tests/blob_functions/put_blob_return/main.py delete mode 100644 tests/emulator_tests/blob_functions/put_blob_str/function.json delete mode 100644 tests/emulator_tests/blob_functions/put_blob_str/main.py delete mode 100644 tests/emulator_tests/blob_functions/put_blob_trigger/function.json delete mode 100644 tests/emulator_tests/blob_functions/put_blob_trigger/main.py delete mode 100644 tests/emulator_tests/blob_functions/put_get_multiple_blobs_as_bytes_return_http_response/function.json delete mode 100644 tests/emulator_tests/blob_functions/put_get_multiple_blobs_as_bytes_return_http_response/main.py delete mode 100644 tests/emulator_tests/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py delete mode 100644 tests/emulator_tests/eventhub_batch_functions/eventhub_multiple/__init__.py delete mode 100644 tests/emulator_tests/eventhub_batch_functions/eventhub_multiple/function.json delete mode 100644 tests/emulator_tests/eventhub_batch_functions/eventhub_output_batch/__init__.py delete mode 100644 tests/emulator_tests/eventhub_batch_functions/eventhub_output_batch/function.json delete mode 100644 tests/emulator_tests/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py delete mode 100644 tests/emulator_tests/eventhub_batch_functions/get_eventhub_batch_triggered/function.json delete mode 100644 tests/emulator_tests/eventhub_batch_functions/get_metadata_batch_triggered/__init__.py delete mode 100644 tests/emulator_tests/eventhub_batch_functions/get_metadata_batch_triggered/function.json delete mode 100644 tests/emulator_tests/eventhub_batch_functions/metadata_multiple/__init__.py delete mode 100644 tests/emulator_tests/eventhub_batch_functions/metadata_multiple/function.json delete mode 100644 tests/emulator_tests/eventhub_batch_functions/metadata_output_batch/__init__.py delete mode 100644 tests/emulator_tests/eventhub_batch_functions/metadata_output_batch/function.json delete mode 100644 tests/emulator_tests/eventhub_functions/eventhub_functions_stein/function_app.py delete mode 100644 tests/emulator_tests/eventhub_functions/eventhub_functions_stein/generic/function_app.py delete mode 100644 tests/emulator_tests/eventhub_functions/eventhub_output/__init__.py delete mode 100644 tests/emulator_tests/eventhub_functions/eventhub_output/function.json delete mode 100644 tests/emulator_tests/eventhub_functions/eventhub_trigger/__init__.py delete mode 100644 tests/emulator_tests/eventhub_functions/eventhub_trigger/function.json delete mode 100644 tests/emulator_tests/eventhub_functions/get_eventhub_triggered/function.json delete mode 100644 tests/emulator_tests/eventhub_functions/get_eventhub_triggered/main.py delete mode 100644 tests/emulator_tests/eventhub_functions/get_metadata_triggered/__init__.py delete mode 100644 tests/emulator_tests/eventhub_functions/get_metadata_triggered/function.json delete mode 100644 tests/emulator_tests/eventhub_functions/metadata_output/__init__.py delete mode 100644 tests/emulator_tests/eventhub_functions/metadata_output/function.json delete mode 100644 tests/emulator_tests/eventhub_functions/metadata_trigger/__init__.py delete mode 100644 tests/emulator_tests/eventhub_functions/metadata_trigger/function.json delete mode 100644 tests/emulator_tests/generic_functions/generic_functions_stein/function_app.py delete mode 100644 tests/emulator_tests/generic_functions/return_bool/function.json delete mode 100644 tests/emulator_tests/generic_functions/return_bool/main.py delete mode 100644 tests/emulator_tests/generic_functions/return_bytes/function.json delete mode 100644 tests/emulator_tests/generic_functions/return_bytes/main.py delete mode 100644 tests/emulator_tests/generic_functions/return_dict/function.json delete mode 100644 tests/emulator_tests/generic_functions/return_dict/main.py delete mode 100644 tests/emulator_tests/generic_functions/return_double/function.json delete mode 100644 tests/emulator_tests/generic_functions/return_double/main.py delete mode 100644 tests/emulator_tests/generic_functions/return_int/function.json delete mode 100644 tests/emulator_tests/generic_functions/return_int/main.py delete mode 100644 tests/emulator_tests/generic_functions/return_list/function.json delete mode 100644 tests/emulator_tests/generic_functions/return_list/main.py delete mode 100644 tests/emulator_tests/generic_functions/return_none/function.json delete mode 100644 tests/emulator_tests/generic_functions/return_none/main.py delete mode 100644 tests/emulator_tests/generic_functions/return_none_no_type_hint/function.json delete mode 100644 tests/emulator_tests/generic_functions/return_none_no_type_hint/main.py delete mode 100644 tests/emulator_tests/generic_functions/return_not_processed_last/__init__.py delete mode 100644 tests/emulator_tests/generic_functions/return_not_processed_last/function.json delete mode 100644 tests/emulator_tests/generic_functions/return_processed_last/__init__.py delete mode 100644 tests/emulator_tests/generic_functions/return_processed_last/function.json delete mode 100644 tests/emulator_tests/generic_functions/return_string/function.json delete mode 100644 tests/emulator_tests/generic_functions/return_string/main.py delete mode 100644 tests/emulator_tests/generic_functions/table_out_binding/__init__.py delete mode 100644 tests/emulator_tests/generic_functions/table_out_binding/function.json delete mode 100644 tests/emulator_tests/queue_functions/get_queue_blob/function.json delete mode 100644 tests/emulator_tests/queue_functions/get_queue_blob/main.py delete mode 100644 tests/emulator_tests/queue_functions/get_queue_blob_message_return/function.json delete mode 100644 tests/emulator_tests/queue_functions/get_queue_blob_message_return/main.py delete mode 100644 tests/emulator_tests/queue_functions/get_queue_blob_return/function.json delete mode 100644 tests/emulator_tests/queue_functions/get_queue_blob_return/main.py delete mode 100644 tests/emulator_tests/queue_functions/get_queue_untyped_blob_return/function.json delete mode 100644 tests/emulator_tests/queue_functions/get_queue_untyped_blob_return/main.py delete mode 100644 tests/emulator_tests/queue_functions/put_queue/function.json delete mode 100644 tests/emulator_tests/queue_functions/put_queue/main.py delete mode 100644 tests/emulator_tests/queue_functions/put_queue_message_return/function.json delete mode 100644 tests/emulator_tests/queue_functions/put_queue_message_return/main.py delete mode 100644 tests/emulator_tests/queue_functions/put_queue_multiple_out/function.json delete mode 100644 tests/emulator_tests/queue_functions/put_queue_multiple_out/main.py delete mode 100644 tests/emulator_tests/queue_functions/put_queue_return/function.json delete mode 100644 tests/emulator_tests/queue_functions/put_queue_return/main.py delete mode 100644 tests/emulator_tests/queue_functions/put_queue_return_multiple/function.json delete mode 100644 tests/emulator_tests/queue_functions/put_queue_return_multiple/main.py delete mode 100644 tests/emulator_tests/queue_functions/put_queue_untyped_return/function.json delete mode 100644 tests/emulator_tests/queue_functions/put_queue_untyped_return/main.py delete mode 100644 tests/emulator_tests/queue_functions/queue_functions_stein/function_app.py delete mode 100644 tests/emulator_tests/queue_functions/queue_functions_stein/generic/function_app.py delete mode 100644 tests/emulator_tests/queue_functions/queue_trigger/function.json delete mode 100644 tests/emulator_tests/queue_functions/queue_trigger/main.py delete mode 100644 tests/emulator_tests/queue_functions/queue_trigger_message_return/function.json delete mode 100644 tests/emulator_tests/queue_functions/queue_trigger_message_return/main.py delete mode 100644 tests/emulator_tests/queue_functions/queue_trigger_return/function.json delete mode 100644 tests/emulator_tests/queue_functions/queue_trigger_return/main.py delete mode 100644 tests/emulator_tests/queue_functions/queue_trigger_return_multiple/function.json delete mode 100644 tests/emulator_tests/queue_functions/queue_trigger_return_multiple/main.py delete mode 100644 tests/emulator_tests/queue_functions/queue_trigger_untyped/function.json delete mode 100644 tests/emulator_tests/queue_functions/queue_trigger_untyped/main.py delete mode 100644 tests/emulator_tests/servicebus_functions/get_servicebus_triggered/__init__.py delete mode 100644 tests/emulator_tests/servicebus_functions/get_servicebus_triggered/function.json delete mode 100644 tests/emulator_tests/servicebus_functions/put_message/__init__.py delete mode 100644 tests/emulator_tests/servicebus_functions/put_message/function.json delete mode 100644 tests/emulator_tests/servicebus_functions/servicebus_functions_stein/function_app.py delete mode 100644 tests/emulator_tests/servicebus_functions/servicebus_functions_stein/generic/function_app.py delete mode 100644 tests/emulator_tests/servicebus_functions/servicebus_trigger/__init__.py delete mode 100644 tests/emulator_tests/servicebus_functions/servicebus_trigger/function.json delete mode 100644 tests/emulator_tests/table_functions/table_functions_stein/function_app.py delete mode 100644 tests/emulator_tests/table_functions/table_functions_stein/generic/function_app.py delete mode 100644 tests/emulator_tests/table_functions/table_in_binding/__init__.py delete mode 100644 tests/emulator_tests/table_functions/table_in_binding/function.json delete mode 100644 tests/emulator_tests/table_functions/table_out_binding/__init__.py delete mode 100644 tests/emulator_tests/table_functions/table_out_binding/function.json delete mode 100644 tests/emulator_tests/test_blob_functions.py delete mode 100644 tests/emulator_tests/test_eventhub_batch_functions.py delete mode 100644 tests/emulator_tests/test_eventhub_functions.py delete mode 100644 tests/emulator_tests/test_generic_functions.py delete mode 100644 tests/emulator_tests/test_queue_functions.py delete mode 100644 tests/emulator_tests/test_servicebus_functions.py delete mode 100644 tests/emulator_tests/test_table_functions.py delete mode 100644 tests/emulator_tests/utils/eventhub/config.json delete mode 100644 tests/emulator_tests/utils/eventhub/docker-compose.yml delete mode 100644 tests/emulator_tests/utils/servicebus/config.json delete mode 100644 tests/emulator_tests/utils/servicebus/docker-compose.yml delete mode 100644 tests/endtoend/blueprint_functions/blueprint_different_dir/blueprint_directory/blueprint.py delete mode 100644 tests/endtoend/blueprint_functions/blueprint_different_dir/function_app.py delete mode 100644 tests/endtoend/blueprint_functions/functions_in_blueprint_only/blueprint.py delete mode 100644 tests/endtoend/blueprint_functions/functions_in_blueprint_only/function_app.py delete mode 100644 tests/endtoend/blueprint_functions/functions_in_both_blueprint_functionapp/blueprint.py delete mode 100644 tests/endtoend/blueprint_functions/functions_in_both_blueprint_functionapp/function_app.py delete mode 100644 tests/endtoend/blueprint_functions/multiple_function_registers/function_app.py delete mode 100644 tests/endtoend/blueprint_functions/only_blueprint/function_app.py delete mode 100644 tests/endtoend/cosmosdb_functions/cosmosdb_functions_stein/function_app.py delete mode 100644 tests/endtoend/cosmosdb_functions/cosmosdb_functions_stein/generic/function_app.py delete mode 100644 tests/endtoend/cosmosdb_functions/cosmosdb_input/__init__.py delete mode 100644 tests/endtoend/cosmosdb_functions/cosmosdb_input/function.json delete mode 100644 tests/endtoend/cosmosdb_functions/cosmosdb_trigger/__init__.py delete mode 100644 tests/endtoend/cosmosdb_functions/cosmosdb_trigger/function.json delete mode 100644 tests/endtoend/cosmosdb_functions/cosmosdb_v3_functions_stein/function_app.py delete mode 100644 tests/endtoend/cosmosdb_functions/cosmosdb_v3_functions_stein/generic/function_app.py delete mode 100644 tests/endtoend/cosmosdb_functions/get_cosmosdb_triggered/function.json delete mode 100644 tests/endtoend/cosmosdb_functions/get_cosmosdb_triggered/main.py delete mode 100644 tests/endtoend/cosmosdb_functions/put_document/__init__.py delete mode 100644 tests/endtoend/cosmosdb_functions/put_document/function.json delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/__init__.py delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_abc.py delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_http.py delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_thirdparty/__init__.py delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_thirdparty/typing_inspect.py delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_thirdparty/werkzeug/datastructures.py delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_thirdparty/werkzeug/formparser.py delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_thirdparty/werkzeug/http.py delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_utils.py delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/http.py delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/meta.py delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/__init__.py delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_abc.py delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_http.py delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_thirdparty/__init__.py delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_thirdparty/typing_inspect.py delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_thirdparty/werkzeug/datastructures.py delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_thirdparty/werkzeug/formparser.py delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_thirdparty/werkzeug/http.py delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_utils.py delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/http.py delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/meta.py delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_grpc_protobuf/lib/site-packages/google/protobuf/__init__.py delete mode 100644 tests/endtoend/dependency_isolation_functions/.python_packages_grpc_protobuf/lib/site-packages/grpc/__init__.py delete mode 100644 tests/endtoend/dependency_isolation_functions/report_dependencies/__init__.py delete mode 100644 tests/endtoend/dependency_isolation_functions/report_dependencies/function.json delete mode 100644 tests/endtoend/durable_functions/DurableFunctionsHttpStart/__init__.py delete mode 100644 tests/endtoend/durable_functions/DurableFunctionsHttpStart/function.json delete mode 100644 tests/endtoend/durable_functions/DurableFunctionsOrchestrator/__init__.py delete mode 100644 tests/endtoend/durable_functions/DurableFunctionsOrchestrator/function.json delete mode 100644 tests/endtoend/durable_functions/Hello/__init__.py delete mode 100644 tests/endtoend/durable_functions/Hello/function.json delete mode 100644 tests/endtoend/durable_functions/durable_functions_stein/function_app.py delete mode 100644 tests/endtoend/eventgrid_functions/eventgrid_functions_stein/function_app.py delete mode 100644 tests/endtoend/eventgrid_functions/eventgrid_functions_stein/generic/function_app.py delete mode 100644 tests/endtoend/eventgrid_functions/eventgrid_output_binding/__init__.py delete mode 100644 tests/endtoend/eventgrid_functions/eventgrid_output_binding/function.json delete mode 100644 tests/endtoend/eventgrid_functions/eventgrid_output_binding_message_to_blobstore/__init__.py delete mode 100644 tests/endtoend/eventgrid_functions/eventgrid_output_binding_message_to_blobstore/function.json delete mode 100644 tests/endtoend/eventgrid_functions/eventgrid_output_binding_success/__init__.py delete mode 100644 tests/endtoend/eventgrid_functions/eventgrid_output_binding_success/function.json delete mode 100644 tests/endtoend/eventgrid_functions/eventgrid_trigger/__init__.py delete mode 100644 tests/endtoend/eventgrid_functions/eventgrid_trigger/function.json delete mode 100644 tests/endtoend/eventgrid_functions/get_eventgrid_triggered/function.json delete mode 100644 tests/endtoend/eventgrid_functions/get_eventgrid_triggered/main.py delete mode 100644 tests/endtoend/http_functions/common_libs_functions/common_libs_functions_stein/function_app.py delete mode 100644 tests/endtoend/http_functions/common_libs_functions/dotenv_func/__init__.py delete mode 100644 tests/endtoend/http_functions/common_libs_functions/dotenv_func/function.json delete mode 100644 tests/endtoend/http_functions/common_libs_functions/numpy_func/__init__.py delete mode 100644 tests/endtoend/http_functions/common_libs_functions/numpy_func/function.json delete mode 100644 tests/endtoend/http_functions/common_libs_functions/opencv_func/__init__.py delete mode 100644 tests/endtoend/http_functions/common_libs_functions/opencv_func/function.json delete mode 100644 tests/endtoend/http_functions/common_libs_functions/pandas_func/__init__.py delete mode 100644 tests/endtoend/http_functions/common_libs_functions/pandas_func/function.json delete mode 100644 tests/endtoend/http_functions/common_libs_functions/plotly_func/__init__.py delete mode 100644 tests/endtoend/http_functions/common_libs_functions/plotly_func/function.json delete mode 100644 tests/endtoend/http_functions/common_libs_functions/requests_func/__init__.py delete mode 100644 tests/endtoend/http_functions/common_libs_functions/requests_func/function.json delete mode 100644 tests/endtoend/http_functions/common_libs_functions/sklearn_func/__init__.py delete mode 100644 tests/endtoend/http_functions/common_libs_functions/sklearn_func/function.json delete mode 100644 tests/endtoend/http_functions/default_template/__init__.py delete mode 100644 tests/endtoend/http_functions/default_template/function.json delete mode 100644 tests/endtoend/http_functions/http_func/__init__.py delete mode 100644 tests/endtoend/http_functions/http_func/function.json delete mode 100644 tests/endtoend/http_functions/http_functions_stein/file_name/main.py delete mode 100644 tests/endtoend/http_functions/http_functions_stein/function_app.py delete mode 100644 tests/endtoend/http_functions/http_functions_stein/generic/function_app.py delete mode 100644 tests/endtoend/http_functions/user_thread_logging/async_thread/__init__.py delete mode 100644 tests/endtoend/http_functions/user_thread_logging/async_thread/function.json delete mode 100644 tests/endtoend/http_functions/user_thread_logging/async_thread_pool_executor/__init__.py delete mode 100644 tests/endtoend/http_functions/user_thread_logging/async_thread_pool_executor/function.json delete mode 100644 tests/endtoend/http_functions/user_thread_logging/thread/__init__.py delete mode 100644 tests/endtoend/http_functions/user_thread_logging/thread/function.json delete mode 100644 tests/endtoend/http_functions/user_thread_logging/thread_pool_executor/__init__.py delete mode 100644 tests/endtoend/http_functions/user_thread_logging/thread_pool_executor/function.json delete mode 100644 tests/endtoend/retry_policy_functions/exponential_strategy/function_app.py delete mode 100644 tests/endtoend/retry_policy_functions/fixed_strategy/function_app.py delete mode 100644 tests/endtoend/sql_functions/sql_functions_stein/function_app.py delete mode 100644 tests/endtoend/sql_functions/sql_functions_stein/generic/function_app.py delete mode 100644 tests/endtoend/sql_functions/sql_input/__init__.py delete mode 100644 tests/endtoend/sql_functions/sql_input/function.json delete mode 100644 tests/endtoend/sql_functions/sql_input2/__init__.py delete mode 100644 tests/endtoend/sql_functions/sql_input2/function.json delete mode 100644 tests/endtoend/sql_functions/sql_output/__init__.py delete mode 100644 tests/endtoend/sql_functions/sql_output/function.json delete mode 100644 tests/endtoend/sql_functions/sql_trigger/__init__.py delete mode 100644 tests/endtoend/sql_functions/sql_trigger/function.json delete mode 100644 tests/endtoend/test_blueprint_functions.py delete mode 100644 tests/endtoend/test_cosmosdb_functions.py delete mode 100644 tests/endtoend/test_dependency_isolation_functions.py delete mode 100644 tests/endtoend/test_durable_functions.py delete mode 100644 tests/endtoend/test_eventgrid_functions.py delete mode 100644 tests/endtoend/test_file_name_functions.py delete mode 100644 tests/endtoend/test_http_functions.py delete mode 100644 tests/endtoend/test_retry_policy_functions.py delete mode 100644 tests/endtoend/test_sql_functions.py delete mode 100644 tests/endtoend/test_third_party_http_functions.py delete mode 100644 tests/endtoend/test_threadpool_thread_count_functions.py delete mode 100644 tests/endtoend/test_timer_functions.py delete mode 100644 tests/endtoend/test_warmup_functions.py delete mode 100644 tests/endtoend/test_worker_process_count_functions.py delete mode 100644 tests/endtoend/third_party_http_functions/stein/asgi_function/function_app.py delete mode 100644 tests/endtoend/third_party_http_functions/stein/wsgi_function/function_app.py delete mode 100644 tests/endtoend/timer_functions/timer_func/__init__.py delete mode 100644 tests/endtoend/timer_functions/timer_func/function.json delete mode 100644 tests/endtoend/timer_functions/timer_functions_stein/function_app.py delete mode 100644 tests/endtoend/warmup_functions/warmup/__init__.py delete mode 100644 tests/endtoend/warmup_functions/warmup/function.json delete mode 100644 tests/endtoend/warmup_functions/warmup_functions_stein/function_app.py delete mode 100644 tests/extension_tests/deferred_bindings_tests/deferred_bindings_blob_functions/function_app.py delete mode 100644 tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_disabled/function_app.py delete mode 100644 tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_enabled/function_app.py delete mode 100644 tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_enabled_dual/function_app.py delete mode 100644 tests/extension_tests/deferred_bindings_tests/test_deferred_bindings.py delete mode 100644 tests/extension_tests/deferred_bindings_tests/test_deferred_bindings_blob_functions.py delete mode 100644 tests/extension_tests/http_v2_tests/http_functions_v2/fastapi/function_app.py delete mode 100644 tests/extension_tests/http_v2_tests/test_http_v2.py create mode 100644 tests/protos/FunctionRpc_pb2.py create mode 100644 tests/protos/FunctionRpc_pb2_grpc.py rename {azure_functions_worker => tests}/protos/__init__.py (100%) create mode 100644 tests/protos/identity/ClaimsIdentityRpc_pb2.py create mode 100644 tests/protos/identity/ClaimsIdentityRpc_pb2_grpc.py rename {azure_functions_worker/_thirdparty => tests/protos/identity}/__init__.py (100%) create mode 100644 tests/protos/shared/NullableTypes_pb2.py create mode 100644 tests/protos/shared/NullableTypes_pb2_grpc.py rename {azure_functions_worker/protos/identity => tests/protos/shared}/__init__.py (100%) delete mode 100644 tests/test_setup.py rename tests/{emulator_tests/blob_functions/blob_functions_stein => unit_tests}/function_app.py (81%) create mode 100644 tests/unit_tests/test_handle_event.py delete mode 100644 tests/unittests/azure_namespace_import/azure_namespace_import.py delete mode 100644 tests/unittests/azure_namespace_import/namespace_location_a/azure/module_a/__init__.py delete mode 100644 tests/unittests/azure_namespace_import/test_azure_namespace_import.sh delete mode 100644 tests/unittests/broken_functions/README.md delete mode 100644 tests/unittests/broken_functions/bad_out_annotation/function.json delete mode 100644 tests/unittests/broken_functions/bad_out_annotation/main.py delete mode 100644 tests/unittests/broken_functions/import_error/function.json delete mode 100644 tests/unittests/broken_functions/import_error/main.py delete mode 100644 tests/unittests/broken_functions/inout_param/function.json delete mode 100644 tests/unittests/broken_functions/inout_param/main.py delete mode 100644 tests/unittests/broken_functions/invalid_app_stein/function_app.py delete mode 100644 tests/unittests/broken_functions/invalid_context_param/function.json delete mode 100644 tests/unittests/broken_functions/invalid_context_param/main.py delete mode 100644 tests/unittests/broken_functions/invalid_datatype/function.json delete mode 100644 tests/unittests/broken_functions/invalid_datatype/main.py delete mode 100644 tests/unittests/broken_functions/invalid_http_trigger_anno/function.json delete mode 100644 tests/unittests/broken_functions/invalid_http_trigger_anno/main.py delete mode 100644 tests/unittests/broken_functions/invalid_in_anno/function.json delete mode 100644 tests/unittests/broken_functions/invalid_in_anno/main.py delete mode 100644 tests/unittests/broken_functions/invalid_in_anno_non_type/function.json delete mode 100644 tests/unittests/broken_functions/invalid_in_anno_non_type/main.py delete mode 100644 tests/unittests/broken_functions/invalid_out_anno/function.json delete mode 100644 tests/unittests/broken_functions/invalid_out_anno/main.py delete mode 100644 tests/unittests/broken_functions/invalid_return_anno/function.json delete mode 100644 tests/unittests/broken_functions/invalid_return_anno/main.py delete mode 100644 tests/unittests/broken_functions/invalid_return_anno_non_type/function.json delete mode 100644 tests/unittests/broken_functions/invalid_return_anno_non_type/main.py delete mode 100644 tests/unittests/broken_functions/invalid_stein/function_app.py delete mode 100644 tests/unittests/broken_functions/missing_json_param/function.json delete mode 100644 tests/unittests/broken_functions/missing_json_param/main.py delete mode 100644 tests/unittests/broken_functions/missing_module/function.json delete mode 100644 tests/unittests/broken_functions/missing_module/main.py delete mode 100644 tests/unittests/broken_functions/missing_py_param/function.json delete mode 100644 tests/unittests/broken_functions/missing_py_param/main.py delete mode 100644 tests/unittests/broken_functions/module_not_found_error/function.json delete mode 100644 tests/unittests/broken_functions/module_not_found_error/main.py delete mode 100644 tests/unittests/broken_functions/return_param_in/function.json delete mode 100644 tests/unittests/broken_functions/return_param_in/main.py delete mode 100644 tests/unittests/broken_functions/syntax_error/function.json delete mode 100644 tests/unittests/broken_functions/syntax_error/main.py delete mode 100644 tests/unittests/broken_functions/wrong_binding_dir/function.json delete mode 100644 tests/unittests/broken_functions/wrong_binding_dir/main.py delete mode 100644 tests/unittests/broken_functions/wrong_param_dir/function.json delete mode 100644 tests/unittests/broken_functions/wrong_param_dir/main.py delete mode 100644 tests/unittests/dispatcher_functions/dispatcher_functions_stein/function_app.py delete mode 100644 tests/unittests/dispatcher_functions/http_v2/fastapi/function_app.py delete mode 100644 tests/unittests/dispatcher_functions/show_context/__init__.py delete mode 100644 tests/unittests/dispatcher_functions/show_context/function.json delete mode 100644 tests/unittests/dispatcher_functions/show_context_async/__init__.py delete mode 100644 tests/unittests/dispatcher_functions/show_context_async/function.json delete mode 100644 tests/unittests/durable_functions/activity_trigger/function.json delete mode 100644 tests/unittests/durable_functions/activity_trigger/main.py delete mode 100644 tests/unittests/durable_functions/activity_trigger_dict/function.json delete mode 100644 tests/unittests/durable_functions/activity_trigger_dict/main.py delete mode 100644 tests/unittests/durable_functions/activity_trigger_int_to_float/function.json delete mode 100644 tests/unittests/durable_functions/activity_trigger_int_to_float/main.py delete mode 100644 tests/unittests/durable_functions/activity_trigger_no_anno/function.json delete mode 100644 tests/unittests/durable_functions/activity_trigger_no_anno/main.py delete mode 100644 tests/unittests/durable_functions/orchestration_trigger/function.json delete mode 100644 tests/unittests/durable_functions/orchestration_trigger/main.py delete mode 100644 tests/unittests/eventhub_mock_functions/eventhub_cardinality_many/__init__.py delete mode 100644 tests/unittests/eventhub_mock_functions/eventhub_cardinality_many/function.json delete mode 100644 tests/unittests/eventhub_mock_functions/eventhub_cardinality_many_bad_anno/__init__.py delete mode 100644 tests/unittests/eventhub_mock_functions/eventhub_cardinality_many_bad_anno/function.json delete mode 100644 tests/unittests/eventhub_mock_functions/eventhub_cardinality_one/__init__.py delete mode 100644 tests/unittests/eventhub_mock_functions/eventhub_cardinality_one/function.json delete mode 100644 tests/unittests/eventhub_mock_functions/eventhub_cardinality_one_bad_anno/__init__.py delete mode 100644 tests/unittests/eventhub_mock_functions/eventhub_cardinality_one_bad_anno/function.json delete mode 100644 tests/unittests/eventhub_mock_functions/eventhub_trigger_iot/__init__.py delete mode 100644 tests/unittests/eventhub_mock_functions/eventhub_trigger_iot/function.json delete mode 100644 tests/unittests/file_name_functions/default_file_name/function_app.py delete mode 100644 tests/unittests/file_name_functions/invalid_file_name/main delete mode 100644 tests/unittests/file_name_functions/new_file_name/test.py delete mode 100644 tests/unittests/generic_functions/foobar_as_bytes/function.json delete mode 100644 tests/unittests/generic_functions/foobar_as_bytes/main.py delete mode 100644 tests/unittests/generic_functions/foobar_as_bytes_no_anno/function.json delete mode 100644 tests/unittests/generic_functions/foobar_as_bytes_no_anno/main.py delete mode 100644 tests/unittests/generic_functions/foobar_as_none/function.json delete mode 100644 tests/unittests/generic_functions/foobar_as_none/main.py delete mode 100644 tests/unittests/generic_functions/foobar_as_str/function.json delete mode 100644 tests/unittests/generic_functions/foobar_as_str/main.py delete mode 100644 tests/unittests/generic_functions/foobar_as_str_no_anno/function.json delete mode 100644 tests/unittests/generic_functions/foobar_as_str_no_anno/main.py delete mode 100644 tests/unittests/generic_functions/foobar_implicit_output/function.json delete mode 100644 tests/unittests/generic_functions/foobar_implicit_output/main.py delete mode 100644 tests/unittests/generic_functions/foobar_implicit_output_exemption/function.json delete mode 100644 tests/unittests/generic_functions/foobar_implicit_output_exemption/main.py delete mode 100644 tests/unittests/generic_functions/foobar_nil_data/function.json delete mode 100644 tests/unittests/generic_functions/foobar_nil_data/main.py delete mode 100644 tests/unittests/generic_functions/foobar_return_bool/function.json delete mode 100644 tests/unittests/generic_functions/foobar_return_bool/main.py delete mode 100644 tests/unittests/generic_functions/foobar_return_dict/function.json delete mode 100644 tests/unittests/generic_functions/foobar_return_dict/main.py delete mode 100644 tests/unittests/generic_functions/foobar_return_double/function.json delete mode 100644 tests/unittests/generic_functions/foobar_return_double/main.py delete mode 100644 tests/unittests/generic_functions/foobar_return_int/function.json delete mode 100644 tests/unittests/generic_functions/foobar_return_int/main.py delete mode 100644 tests/unittests/generic_functions/foobar_return_list/function.json delete mode 100644 tests/unittests/generic_functions/foobar_return_list/main.py delete mode 100644 tests/unittests/generic_functions/foobar_with_no_datatype/function.json delete mode 100644 tests/unittests/generic_functions/foobar_with_no_datatype/main.py delete mode 100644 tests/unittests/http_functions/accept_json/function.json delete mode 100644 tests/unittests/http_functions/accept_json/main.py delete mode 100644 tests/unittests/http_functions/async_logging/function.json delete mode 100644 tests/unittests/http_functions/async_logging/main.py delete mode 100644 tests/unittests/http_functions/async_return_str/function.json delete mode 100644 tests/unittests/http_functions/async_return_str/main.py delete mode 100644 tests/unittests/http_functions/create_task_with_context/function.json delete mode 100644 tests/unittests/http_functions/create_task_with_context/main.py delete mode 100644 tests/unittests/http_functions/create_task_without_context/function.json delete mode 100644 tests/unittests/http_functions/create_task_without_context/main.py delete mode 100644 tests/unittests/http_functions/debug_logging/function.json delete mode 100644 tests/unittests/http_functions/debug_logging/main.py delete mode 100644 tests/unittests/http_functions/hijack_current_event_loop/function.json delete mode 100644 tests/unittests/http_functions/hijack_current_event_loop/main.py delete mode 100644 tests/unittests/http_functions/http_functions_stein/function_app.py delete mode 100644 tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py delete mode 100644 tests/unittests/http_functions/multiple_set_cookie_resp_headers/function.json delete mode 100644 tests/unittests/http_functions/multiple_set_cookie_resp_headers/main.py delete mode 100644 tests/unittests/http_functions/no_return/function.json delete mode 100644 tests/unittests/http_functions/no_return/main.py delete mode 100644 tests/unittests/http_functions/no_return_returns/function.json delete mode 100644 tests/unittests/http_functions/no_return_returns/main.py delete mode 100644 tests/unittests/http_functions/print_logging/function.json delete mode 100644 tests/unittests/http_functions/print_logging/main.py delete mode 100644 tests/unittests/http_functions/raw_body_bytes/function.json delete mode 100644 tests/unittests/http_functions/raw_body_bytes/main.py delete mode 100644 tests/unittests/http_functions/remapped_context/function.json delete mode 100644 tests/unittests/http_functions/remapped_context/main.py delete mode 100644 tests/unittests/http_functions/response_cookie_header_nullable_bool_err/function.json delete mode 100644 tests/unittests/http_functions/response_cookie_header_nullable_bool_err/main.py delete mode 100644 tests/unittests/http_functions/response_cookie_header_nullable_double_err/function.json delete mode 100644 tests/unittests/http_functions/response_cookie_header_nullable_double_err/main.py delete mode 100644 tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/function.json delete mode 100644 tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/main.py delete mode 100644 tests/unittests/http_functions/return_bytes/function.json delete mode 100644 tests/unittests/http_functions/return_bytes/main.py delete mode 100644 tests/unittests/http_functions/return_context/function.json delete mode 100644 tests/unittests/http_functions/return_context/main.py delete mode 100644 tests/unittests/http_functions/return_http/function.json delete mode 100644 tests/unittests/http_functions/return_http/main.py delete mode 100644 tests/unittests/http_functions/return_http_404/function.json delete mode 100644 tests/unittests/http_functions/return_http_404/main.py delete mode 100644 tests/unittests/http_functions/return_http_auth_admin/function.json delete mode 100644 tests/unittests/http_functions/return_http_auth_admin/main.py delete mode 100644 tests/unittests/http_functions/return_http_no_body/function.json delete mode 100644 tests/unittests/http_functions/return_http_no_body/main.py delete mode 100644 tests/unittests/http_functions/return_http_redirect/function.json delete mode 100644 tests/unittests/http_functions/return_http_redirect/main.py delete mode 100644 tests/unittests/http_functions/return_out/function.json delete mode 100644 tests/unittests/http_functions/return_out/main.py delete mode 100644 tests/unittests/http_functions/return_request/function.json delete mode 100644 tests/unittests/http_functions/return_request/main.py delete mode 100644 tests/unittests/http_functions/return_route_params/function.json delete mode 100644 tests/unittests/http_functions/return_route_params/main.py delete mode 100644 tests/unittests/http_functions/return_str/function.json delete mode 100644 tests/unittests/http_functions/return_str/main.py delete mode 100644 tests/unittests/http_functions/set_cookie_resp_header_default_values/function.json delete mode 100644 tests/unittests/http_functions/set_cookie_resp_header_default_values/main.py delete mode 100644 tests/unittests/http_functions/set_cookie_resp_header_empty/function.json delete mode 100644 tests/unittests/http_functions/set_cookie_resp_header_empty/main.py delete mode 100644 tests/unittests/http_functions/sync_logging/function.json delete mode 100644 tests/unittests/http_functions/sync_logging/main.py delete mode 100644 tests/unittests/http_functions/unhandled_error/function.json delete mode 100644 tests/unittests/http_functions/unhandled_error/main.py delete mode 100644 tests/unittests/http_functions/unhandled_unserializable_error/function.json delete mode 100644 tests/unittests/http_functions/unhandled_unserializable_error/main.py delete mode 100644 tests/unittests/http_functions/unhandled_urllib_error/function.json delete mode 100644 tests/unittests/http_functions/unhandled_urllib_error/main.py delete mode 100644 tests/unittests/http_functions/user_event_loop/function.json delete mode 100644 tests/unittests/http_functions/user_event_loop/main.py delete mode 100644 tests/unittests/load_functions/absolute_thirdparty/function.json delete mode 100644 tests/unittests/load_functions/absolute_thirdparty/main.py delete mode 100644 tests/unittests/load_functions/entrypoint/function.json delete mode 100644 tests/unittests/load_functions/entrypoint/main.py delete mode 100644 tests/unittests/load_functions/implicit_import/function.json delete mode 100644 tests/unittests/load_functions/implicit_import/main.py delete mode 100644 tests/unittests/load_functions/load_outside_main/function.json delete mode 100644 tests/unittests/load_functions/load_outside_main/main.py delete mode 100644 tests/unittests/load_functions/module_not_found/function.json delete mode 100644 tests/unittests/load_functions/module_not_found/main.py delete mode 100644 tests/unittests/load_functions/name_collision/function.json delete mode 100644 tests/unittests/load_functions/name_collision/main.py delete mode 100644 tests/unittests/load_functions/name_collision_app_import/function.json delete mode 100644 tests/unittests/load_functions/name_collision_app_import/main.py delete mode 100644 tests/unittests/load_functions/no_script_file/function.json delete mode 100644 tests/unittests/load_functions/no_script_file/main.py delete mode 100644 tests/unittests/load_functions/outside_main_code_in_init/__init__.py delete mode 100644 tests/unittests/load_functions/outside_main_code_in_init/count.py delete mode 100644 tests/unittests/load_functions/outside_main_code_in_init/function.json delete mode 100644 tests/unittests/load_functions/outside_main_code_in_main/count.py delete mode 100644 tests/unittests/load_functions/outside_main_code_in_main/function.json delete mode 100644 tests/unittests/load_functions/outside_main_code_in_main/main.py delete mode 100644 tests/unittests/load_functions/parentmodule/function.json delete mode 100644 tests/unittests/load_functions/parentmodule/module.py delete mode 100644 tests/unittests/load_functions/parentmodule/sub_module/__init__.py delete mode 100644 tests/unittests/load_functions/parentmodule/sub_module/main.py delete mode 100644 tests/unittests/load_functions/pytest/__init__.py delete mode 100644 tests/unittests/load_functions/relimport/function.json delete mode 100644 tests/unittests/load_functions/relimport/main.py delete mode 100644 tests/unittests/load_functions/relimport/relative.py delete mode 100644 tests/unittests/load_functions/simple/function.json delete mode 100644 tests/unittests/load_functions/simple/main.py delete mode 100644 tests/unittests/load_functions/stub_http_trigger/__init__.py delete mode 100644 tests/unittests/load_functions/stub_http_trigger/function.json delete mode 100644 tests/unittests/load_functions/stub_http_trigger/stub_tools.py delete mode 100644 tests/unittests/load_functions/subdir/function.json delete mode 100644 tests/unittests/load_functions/subdir/sub/main.py delete mode 100644 tests/unittests/load_functions/submodule/function.json delete mode 100644 tests/unittests/load_functions/submodule/main.py delete mode 100644 tests/unittests/load_functions/submodule/sub_module/__init__.py delete mode 100644 tests/unittests/load_functions/submodule/sub_module/module.py delete mode 100644 tests/unittests/log_filtering_functions/debug_logging/function.json delete mode 100644 tests/unittests/log_filtering_functions/debug_logging/main.py delete mode 100644 tests/unittests/log_filtering_functions/debug_user_logging/function.json delete mode 100644 tests/unittests/log_filtering_functions/debug_user_logging/main.py delete mode 100644 tests/unittests/log_filtering_functions/sdk_logging/__init__.py delete mode 100644 tests/unittests/log_filtering_functions/sdk_logging/function.json delete mode 100644 tests/unittests/log_filtering_functions/sdk_submodule_logging/__init__.py delete mode 100644 tests/unittests/log_filtering_functions/sdk_submodule_logging/function.json delete mode 100644 tests/unittests/path_import/path_import.py delete mode 100644 tests/unittests/path_import/test_path_import.sh delete mode 100644 tests/unittests/resources/customer_deps_path/azure/__init__.py delete mode 100644 tests/unittests/resources/customer_deps_path/azure/functions/__init__.py delete mode 100644 tests/unittests/resources/customer_deps_path/common_module/__init__.py delete mode 100644 tests/unittests/resources/customer_deps_path/common_namespace/__init__.py delete mode 100644 tests/unittests/resources/customer_deps_path/common_namespace/nested_module/__init__.py delete mode 100644 tests/unittests/resources/customer_deps_path/readme.md delete mode 100644 tests/unittests/resources/customer_func_path/HttpTrigger/__init__.py delete mode 100644 tests/unittests/resources/customer_func_path/HttpTrigger/function.json delete mode 100644 tests/unittests/resources/customer_func_path/common_module/__init__.py delete mode 100644 tests/unittests/resources/customer_func_path/func_specific_module/__init__.py delete mode 100644 tests/unittests/resources/customer_func_path/host.json delete mode 100644 tests/unittests/resources/customer_func_path/requirements.txt delete mode 100644 tests/unittests/resources/functions.png delete mode 100644 tests/unittests/resources/mock_azure_functions/azure/__init__.py delete mode 100644 tests/unittests/resources/mock_azure_functions/azure/functions/__init__.py delete mode 100644 tests/unittests/resources/mock_azure_functions/readme.md delete mode 100644 tests/unittests/resources/worker_deps_path/azure/__init__.py delete mode 100644 tests/unittests/resources/worker_deps_path/azure/functions/__init__.py delete mode 100644 tests/unittests/resources/worker_deps_path/common_module/__init__.py delete mode 100644 tests/unittests/resources/worker_deps_path/common_namespace/__init__.py delete mode 100644 tests/unittests/resources/worker_deps_path/common_namespace/nested_module/__init__.py delete mode 100644 tests/unittests/resources/worker_deps_path/readme.md delete mode 100644 tests/unittests/test-binding/foo/__init__.py delete mode 100644 tests/unittests/test-binding/foo/binding.py delete mode 100644 tests/unittests/test-binding/functions/foo/function.json delete mode 100644 tests/unittests/test-binding/functions/foo/main.py delete mode 100644 tests/unittests/test-binding/setup.py delete mode 100644 tests/unittests/test_app_setting_manager.py delete mode 100644 tests/unittests/test_broken_functions.py delete mode 100644 tests/unittests/test_code_quality.py delete mode 100644 tests/unittests/test_datumref.py delete mode 100644 tests/unittests/test_dispatcher.py delete mode 100644 tests/unittests/test_enable_debug_logging_functions.py delete mode 100644 tests/unittests/test_extension.py delete mode 100644 tests/unittests/test_file_accessor.py delete mode 100644 tests/unittests/test_file_accessor_factory.py delete mode 100644 tests/unittests/test_functions_registry.py delete mode 100644 tests/unittests/test_http_functions.py delete mode 100644 tests/unittests/test_http_functions_v2.py delete mode 100644 tests/unittests/test_http_v2.py delete mode 100644 tests/unittests/test_invalid_stein.py delete mode 100644 tests/unittests/test_loader.py delete mode 100644 tests/unittests/test_log_filtering_functions.py delete mode 100644 tests/unittests/test_logging.py delete mode 100644 tests/unittests/test_main.py delete mode 100644 tests/unittests/test_mock_blob_shared_memory_functions.py delete mode 100644 tests/unittests/test_mock_durable_functions.py delete mode 100644 tests/unittests/test_mock_eventhub_functions.py delete mode 100644 tests/unittests/test_mock_generic_functions.py delete mode 100644 tests/unittests/test_mock_http_functions.py delete mode 100644 tests/unittests/test_mock_log_filtering_functions.py delete mode 100644 tests/unittests/test_mock_timer_functions.py delete mode 100644 tests/unittests/test_nullable_converters.py delete mode 100644 tests/unittests/test_opentelemetry.py delete mode 100644 tests/unittests/test_rpc_messages.py delete mode 100644 tests/unittests/test_script_file_name.py delete mode 100644 tests/unittests/test_shared_memory_manager.py delete mode 100644 tests/unittests/test_shared_memory_map.py delete mode 100644 tests/unittests/test_third_party_http_functions.py delete mode 100644 tests/unittests/test_types.py delete mode 100644 tests/unittests/test_typing_inspect.py delete mode 100644 tests/unittests/test_utilities.py delete mode 100644 tests/unittests/test_utilities_dependency.py delete mode 100644 tests/unittests/third_party_http_functions/stein/asgi_function/function_app.py delete mode 100644 tests/unittests/third_party_http_functions/stein/wsgi_function/function_app.py delete mode 100644 tests/unittests/timer_functions/return_pastdue/function.json delete mode 100644 tests/unittests/timer_functions/return_pastdue/main.py delete mode 100644 tests/unittests/timer_functions/user_event_loop_timer/function.json delete mode 100644 tests/unittests/timer_functions/user_event_loop_timer/main.py delete mode 100644 tests/utils/constants.py delete mode 100644 tests/utils/testutils_docker.py delete mode 100644 tests/utils/testutils_lc.py diff --git a/.artifactignore b/.artifactignore deleted file mode 100644 index 827a68ebf..000000000 --- a/.artifactignore +++ /dev/null @@ -1,7 +0,0 @@ -_manifest\** -bcde-output\** -Experiment_PipReport_** -GovCompDisc_Log_** -GovCompDisc_Manifest_** -GovCompDisc_Metadata_** -ScanTelemetry_** \ No newline at end of file diff --git a/.ci/e2e_integration_test/start-e2e.ps1 b/.ci/e2e_integration_test/start-e2e.ps1 deleted file mode 100644 index 653ed0bc4..000000000 --- a/.ci/e2e_integration_test/start-e2e.ps1 +++ /dev/null @@ -1,102 +0,0 @@ -# -# Copyright (c) Microsoft. All rights reserved. -# Licensed under the MIT license. See LICENSE file in the project root for full license information. -# - -# Python worker E2E integration test -# The E2E integration test will test the worker against a prerelease version of core tools -$FUNC_RUNTIME_VERSION = '4' -$FUNC_RUNTIME_LANGUAGE = 'python' -$AZURE_FUNCTIONS_ENVIRONMENT = "development" -$PYAZURE_WEBHOST_DEBUG = "true" -$PYAZURE_INTEGRATION_TEST = "true" - -# Speed up Invoke-RestMethod by turning off progress bar -$ProgressPreference = 'SilentlyContinue' - -function get_architecture() { - # Return "x64" or "x86" - return [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant(); -} - -function get_os() { - # Return either "win", "linux", "osx", or "unknown" - if ($IsWindows) { - return "win" - } elseif ($IsLinux) { - return "linux" - } elseif ($IsMacOS) { - return "osx" - } - return "unknown" -} - -function get_core_tool_download_url() { - $os = get_os - $arch = get_architecture - return "https://functionsintegclibuilds.blob.core.windows.net/builds/$FUNC_RUNTIME_VERSION/latest/Azure.Functions.Cli.$os-$arch.zip" -} - -function get_core_tools_version_url() { - return "https://functionsintegclibuilds.blob.core.windows.net/builds/$FUNC_RUNTIME_VERSION/latest/version.txt" -} - -function get_func_execuable_path($path) { - $exe_name = "func" - if ($IsWindows) { - $exe_name = "func.exe" - } - return Join-Path $path $exe_name -} - -$FUNC_CLI_DIRECTORY = Join-Path $PSScriptRoot 'Azure.Functions.Cli' -$FUNC_CLI_DIRECTORY_EXIST = Test-Path -Path $FUNC_CLI_DIRECTORY -PathType Container -if ($FUNC_CLI_DIRECTORY_EXIST) { - Write-Host 'Deleting Functions Core Tools...' - Remove-Item -Force "$FUNC_CLI_DIRECTORY.zip" -ErrorAction Ignore - Remove-Item -Recurse -Force $FUNC_CLI_DIRECTORY -ErrorAction Ignore -} - -$version = Invoke-RestMethod -Uri "$(get_core_tools_version_url)" -Write-Host "Downloading Functions Core Tools $version..." - -$output = "$FUNC_CLI_DIRECTORY.zip" -Invoke-RestMethod -Uri "$(get_core_tool_download_url)" -OutFile $output - -Write-Host 'Extracting Functions Core Tools...' -Expand-Archive $output -DestinationPath $FUNC_CLI_DIRECTORY -InformationAction SilentlyContinue - -Write-Host "Starting Functions Host..." -$env:FUNCTIONS_WORKER_RUNTIME = $FUNC_RUNTIME_LANGUAGE -$env:FUNCTIONS_WORKER_RUNTIME_VERSION = $env:PythonVersion -$env:AZURE_FUNCTIONS_ENVIRONMENT = $AZURE_FUNCTIONS_ENVIRONMENT -$env:PYAZURE_WEBHOST_DEBUG = $PYAZURE_WEBHOST_DEBUG -$env:PYAZURE_INTEGRATION_TEST = $PYAZURE_INTEGRATION_TEST - -$env:Path = "$env:Path$([System.IO.Path]::PathSeparator)$FUNC_CLI_DIRECTORY" -$funcExePath = $(get_func_execuable_path $FUNC_CLI_DIRECTORY) - -if ($IsMacOS -or $IsLinux) { - chmod -R 755 $FUNC_CLI_DIRECTORY -} -Write-Host "Function Exe Path: $funcExePath" - -Set-Location $env:BUILD_SOURCESDIRECTORY -Write-Host "Set-Location: $env:BUILD_SOURCESDIRECTORY" - -Write-Host "-----------------------------------------------------------------------------`n" -ForegroundColor Green -Write-Host "Preparing E2E integration tests..." -ForegroundColor Green -Write-Host "-----------------------------------------------------------------------------`n" -ForegroundColor Green -python -m pip install -U pip -python -m pip install -U -e .[dev] -cd tests -python -m invoke -c test_setup build-protos -python -m invoke -c test_setup extensions -Write-Host "-----------------------------------------------------------------------------`n" -ForegroundColor Green -Write-Host "-----------------------------------------------------------------------------`n" -ForegroundColor Green -Write-Host "-----------------------------------------------------------------------------`n" -ForegroundColor Green -Write-Host "Running E2E integration tests..." -ForegroundColor Green -Write-Host "-----------------------------------------------------------------------------`n" -ForegroundColor Green -$env:CORE_TOOLS_EXE_PATH = "$funcExePath" -python -m pytest --junitxml=e2e-integration-test-report.xml --reruns 4 tests/endtoend -Write-Host "-----------------------------------------------------------------------------`n" -ForegroundColor Green diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index b2c13c80b..000000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.234.0/containers/python-3/.devcontainer/base.Dockerfile - -# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster -ARG VARIANT="3.9" -FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} - -# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 -ARG NODE_VERSION="none" -RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi - -# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. -# COPY requirements.txt /tmp/pip-tmp/ -# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ -# && rm -rf /tmp/pip-tmp - -# [Optional] Uncomment this section to install additional OS packages. -# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ -# && apt-get -y install --no-install-recommends - -# [Optional] Uncomment this line to install global node packages. -# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 03ff47808..000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,57 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.234.0/containers/python-3 -{ - "name": "Python 3", - "build": { - "dockerfile": "Dockerfile", - "context": "..", - "args": { - // Update 'VARIANT' to pick a Python version: 3, 3.10, 3.9, 3.8, 3.7 - // Append -bullseye or -buster to pin to an OS version. - // Use -bullseye variants on local on arm64/Apple Silicon. - "VARIANT": "3.9", - // Options - "NODE_VERSION": "none" - } - }, - - // Set *default* container specific settings.json values on container create. - "settings": { - "python.defaultInterpreterPath": "/usr/local/bin/python", - "python.linting.enabled": true, - "python.linting.pylintEnabled": true, - "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", - "python.formatting.blackPath": "/usr/local/py-utils/bin/black", - "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", - "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", - "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", - "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", - "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", - "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", - "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint", - "python.testing.pytestArgs": [ - "tests/unittests" - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true - }, - - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "ms-python.python", - "ms-python.vscode-pylance" - ], - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "sudo python -m pip install -U pip && sudo python -m pip install -U -e .[dev] && cd tests && sudo python -m invoke -c test_setup webhost", - - // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "vscode", - - "features": { - "dotnet": "latest" - } -} diff --git a/.flake8 b/.flake8 index 94a2f1926..8e3282da0 100644 --- a/.flake8 +++ b/.flake8 @@ -10,6 +10,7 @@ exclude = .git, __pycache__, build, dist, .eggs, .github, .local, docs/, azure_functions_worker/_thirdparty/typing_inspect.py, tests/unittests/test_typing_inspect.py, tests/unittests/broken_functions/syntax_error/main.py, + tests/protos/, .env*, .vscode, venv*, *.venv* max-line-length = 88 diff --git a/CODEOWNERS b/CODEOWNERS index e5f28aee3..f93501102 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -10,4 +10,4 @@ # For all file changes, github would automatically # include the following people in the PRs. -* @vrdmr @gavin-aguiar @hallvictoria +* @vrdmr @gavin-aguiar @YunchuWang @pdthummar @hallvictoria diff --git a/README.md b/README.md index 08baeb6d1..a889aef3d 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,35 @@ # Functions Header Image - Lightning Logo Azure Functions Python Worker -| Branch | Build Status | CodeCov | Test Status | -|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| dev | [![Build Status](https://img.shields.io/azure-devops/build/azfunc/public/658/dev)](https://azfunc.visualstudio.com/public/_build/latest?definitionId=658&branchName=dev) | [![codecov](https://codecov.io/gh/Azure/azure-functions-python-worker/branch/dev/graph/badge.svg)](https://codecov.io/gh/Azure/azure-functions-python-worker) | [![Test Status](https://img.shields.io/azure-devops/build/azfunc/public/658/dev)](https://azfunc.visualstudio.com/public/_build/latest?definitionId=658&branchName=dev) | +| Branch | Status | CodeCov | Unittests | E2E tests | +|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------| +| main | [![Build Status](https://azfunc.visualstudio.com/Azure%20Functions/_apis/build/status/Azure.azure-functions-python-worker?branchName=main)](https://azfunc.visualstudio.com/Azure%20Functions/_build/latest?definitionId=57&branchName=main) | [![codecov](https://codecov.io/gh/Azure/azure-functions-python-worker/branch/main/graph/badge.svg)](https://codecov.io/gh/Azure/azure-functions-python-worker) | ![CI Unit tests](https://github.com/Azure/azure-functions-python-worker/workflows/CI%20Unit%20tests/badge.svg?branch=main) | ![CI E2E tests](https://github.com/Azure/azure-functions-python-worker/workflows/CI%20E2E%20tests/badge.svg?branch=main) | +| dev | [![Build Status](https://azfunc.visualstudio.com/Azure%20Functions/_apis/build/status/Azure.azure-functions-python-worker?branchName=dev)](https://azfunc.visualstudio.com/Azure%20Functions/_build/latest?definitionId=57&branchName=dev) | [![codecov](https://codecov.io/gh/Azure/azure-functions-python-worker/branch/dev/graph/badge.svg)](https://codecov.io/gh/Azure/azure-functions-python-worker) | ![CI Unit tests](https://github.com/Azure/azure-functions-python-worker/workflows/CI%20Unit%20tests/badge.svg?branch=dev) | ![CI E2E tests](https://github.com/Azure/azure-functions-python-worker/workflows/CI%20E2E%20tests/badge.svg?branch=dev) | -Python support for Azure Functions is based on Python 3.8, 3.9, 3.10, 3.11, and 3.12 serverless hosting on Linux and the Functions 4.0 runtime. +Python support for Azure Functions is based on Python 3.6, 3.7, 3.8, 3.9, and 3.10 serverless hosting on Linux and the Functions 2.0, 3.0 and 4.0 runtime. Here is the current status of Python in Azure Functions: What are the supported Python versions? -| Azure Functions Runtime | Python 3.8 | Python 3.9 | Python 3.10 | Python 3.11 | Python 3.12 | -|----------------------------------|------------|------------|-------------|-------------|-------------| -| Azure Functions 3.0 (deprecated) | ✔ | ✔ | - | - | - | -| Azure Functions 4.0 | ✔ | ✔ | ✔ | ✔ | ✔ | +| Azure Functions Runtime | Python 3.6 | Python 3.7 | Python 3.8 | Python 3.9 | Python 3.10 | Python 3.11 | +|----------------------------------|------------|------------|------------|------------|-------------|-------------| +| Azure Functions 2.0 (deprecated) | ✔ | ✔ | - | - | - | - | +| Azure Functions 3.0 (deprecated) | ✔ | ✔ | ✔ | ✔ | - | - | +| Azure Functions 4.0 | - | - | ✔ | ✔ | ✔ | ✔ | For information about Azure Functions Runtime, please refer to [Azure Functions runtime versions overview](https://docs.microsoft.com/en-us/azure/azure-functions/functions-versions) page. ### What's available? -- Build, test, debug, and publish using Azure Functions Core Tools (CLI) or Visual Studio Code -- Deploy Python Function project onto consumption, dedicated, elastic premium, or flex consumption plan. -- Deploy Python Function project in a custom docker image onto dedicated or elastic premium plan. -- Triggers / Bindings : Blob, Cosmos DB, Event Grid, Event Hub, HTTP, Kafka, MySQL, Queue, ServiceBus, SQL, Timer, and Warmup +- Build, test, debug and publish using Azure Functions Core Tools (CLI) or Visual Studio Code +- Deploy Python Function project onto consumption, dedicated, or elastic premium plan. +- Deploy Python Function project in a custom docker image onto dedicated, or elastic premium plan. +- Triggers / Bindings : HTTP, Blob, Queue, Timer, Cosmos DB, Event Grid, Event Hubs and Service Bus - Triggers / Bindings : Custom binding support -### What's new? +What's coming? -- [SDK Type Bindings for Blob](https://techcommunity.microsoft.com/t5/azure-compute-blog/azure-functions-sdk-type-bindings-for-azure-blob-storage-with/ba-p/4146744) -- [HTTP Streaming](https://techcommunity.microsoft.com/t5/azure-compute-blog/azure-functions-support-for-http-streams-in-python-is-now-in/ba-p/4146697) +- [Durable Functions For Python](https://github.com/Azure/azure-functions-durable-python) ### Get Started @@ -71,4 +72,4 @@ provided by the bot. You will only need to do this once across all repos using o This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. \ No newline at end of file +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/azure_functions_worker/__init__.py b/azure_functions_worker/__init__.py index 5b7f7a925..b567df2db 100644 --- a/azure_functions_worker/__init__.py +++ b/azure_functions_worker/__init__.py @@ -1,2 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. + +from .handle_event import worker_init_request, functions_metadata_request, function_environment_reload_request, invocation_request, functions_load_request + +__all__ = ('worker_init_request', 'functions_metadata_request', 'function_environment_reload_request', 'invocation_request', 'functions_load_request') diff --git a/azure_functions_worker/__main__.py b/azure_functions_worker/__main__.py deleted file mode 100644 index 4197fda14..000000000 --- a/azure_functions_worker/__main__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from azure_functions_worker import main - -if __name__ == '__main__': - main.main() diff --git a/azure_functions_worker/bindings/__init__.py b/azure_functions_worker/bindings/__init__.py deleted file mode 100644 index e64ba3bd6..000000000 --- a/azure_functions_worker/bindings/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from .retrycontext import RetryContext # isort: skip -from .tracecontext import TraceContext # isort: skip -from .context import Context -from .meta import ( - check_deferred_bindings_enabled, - check_input_type_annotation, - check_output_type_annotation, - from_incoming_proto, - get_deferred_raw_bindings, - has_implicit_output, - is_trigger_binding, - load_binding_registry, - to_outgoing_param_binding, - to_outgoing_proto, -) -from .out import Out - -__all__ = ( - 'Out', 'Context', - 'is_trigger_binding', - 'load_binding_registry', - 'check_input_type_annotation', 'check_output_type_annotation', - 'has_implicit_output', - 'from_incoming_proto', 'to_outgoing_proto', 'TraceContext', 'RetryContext', - 'to_outgoing_param_binding', 'check_deferred_bindings_enabled', - 'get_deferred_raw_bindings' -) diff --git a/azure_functions_worker/bindings/context.py b/azure_functions_worker/bindings/context.py index 7effbf76d..b74a53173 100644 --- a/azure_functions_worker/bindings/context.py +++ b/azure_functions_worker/bindings/context.py @@ -1,13 +1,15 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. + import threading + from typing import Type -from . import RetryContext, TraceContext +from .retrycontext import RetryContext +from .tracecontext import TraceContext class Context: - def __init__(self, func_name: str, func_dir: str, @@ -45,3 +47,23 @@ def trace_context(self) -> TraceContext: @property def retry_context(self) -> RetryContext: return self.__retry_context + + +def get_context(invoc_request, name: str, + directory: str) -> Context: + """ For more information refer: + https://aka.ms/azfunc-invocation-context + """ + trace_context = TraceContext( + invoc_request.trace_context.trace_parent, + invoc_request.trace_context.trace_state, + invoc_request.trace_context.attributes) + + retry_context = RetryContext( + invoc_request.retry_context.retry_count, + invoc_request.retry_context.max_retry_count, + invoc_request.retry_context.exception) + + return Context( + name, directory, invoc_request.invocation_id, + threading.local(), trace_context, retry_context) diff --git a/azure_functions_worker/bindings/datumdef.py b/azure_functions_worker/bindings/datumdef.py index 34fb9b0af..da1321f03 100644 --- a/azure_functions_worker/bindings/datumdef.py +++ b/azure_functions_worker/bindings/datumdef.py @@ -1,19 +1,11 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. + import json import logging -from typing import Any, List, Optional - -from .. import protos -from ..logging import logger - -try: - from http.cookies import SimpleCookie -except ImportError: - from Cookie import SimpleCookie -from dateutil import parser -from dateutil.parser import ParserError +from datetime import datetime +from typing import Any, List, Optional from .nullable_converters import ( to_nullable_bool, @@ -22,6 +14,13 @@ to_nullable_timestamp, ) +from ..logging import logger + +try: + from http.cookies import SimpleCookie +except ImportError: + from Cookie import SimpleCookie + class Datum: def __init__(self, value, type): @@ -67,7 +66,12 @@ def __repr__(self): return ''.format(self.type, val_repr) @classmethod - def from_typed_data(cls, td: protos.TypedData): + def from_typed_data(cls, protos): + try: + td = protos.TypedData + except Exception as ex: + # Todo: better catch for Datum.from_typed_data(http.body) -- if the data being sent in is already protos.TypedData + td = protos tt = td.WhichOneof('data') if tt == 'http': http = td.http @@ -111,84 +115,8 @@ def from_typed_data(cls, td: protos.TypedData): return cls(val, tt) - @classmethod - def from_rpc_shared_memory( - cls, - shmem: protos.RpcSharedMemory, - shmem_mgr) -> Optional['Datum']: - """ - Reads the specified shared memory region and converts the read data - into a datum object of the corresponding type. - """ - if shmem is None: - logger.warning('Cannot read from shared memory. ' - 'RpcSharedMemory is None.') - return None - - mem_map_name = shmem.name - offset = shmem.offset - count = shmem.count - data_type = shmem.type - ret_val = None - - if data_type == protos.RpcDataType.bytes: - val = shmem_mgr.get_bytes(mem_map_name, offset, count) - if val is not None: - ret_val = cls(val, 'bytes') - elif data_type == protos.RpcDataType.string: - val = shmem_mgr.get_string(mem_map_name, offset, count) - if val is not None: - ret_val = cls(val, 'string') - - if ret_val is not None: - logger.info( - 'Read %s bytes from memory map %s for data type %s', count, - mem_map_name, data_type) - return ret_val - return None - - @classmethod - def to_rpc_shared_memory( - cls, - datum: 'Datum', - shmem_mgr) -> Optional[protos.RpcSharedMemory]: - """ - Writes the given value to shared memory and returns the corresponding - RpcSharedMemory object which can be sent back to the functions host over - RPC. - """ - if datum.type == 'bytes': - value = datum.value - shared_mem_meta = shmem_mgr.put_bytes(value) - data_type = protos.RpcDataType.bytes - elif datum.type == 'string': - value = datum.value - shared_mem_meta = shmem_mgr.put_string(value) - data_type = protos.RpcDataType.string - else: - raise NotImplementedError( - f'Unsupported datum type ({datum.type}) for shared memory' - ) - - if shared_mem_meta is None: - logger.warning('Cannot write to shared memory for type: %s', - datum.type) - return None - - shmem = protos.RpcSharedMemory( - name=shared_mem_meta.mem_map_name, - offset=0, - count=shared_mem_meta.count_bytes, - type=data_type) - - logger.info( - 'Wrote %s bytes to memory map %s for data type %s', - shared_mem_meta.count_bytes, shared_mem_meta.mem_map_name, - data_type) - return shmem - -def datum_as_proto(datum: Datum) -> protos.TypedData: +def datum_as_proto(datum: Datum, protos): if datum.type == 'string': return protos.TypedData(string=datum.value) elif datum.type == 'bytes': @@ -202,9 +130,9 @@ def datum_as_proto(datum: Datum) -> protos.TypedData: k: v.value for k, v in datum.value['headers'].items() }, - cookies=parse_to_rpc_http_cookie_list(datum.value.get('cookies')), + cookies=parse_to_rpc_http_cookie_list(datum.value.get('cookies'), protos), enable_content_negotiation=False, - body=datum_as_proto(datum.value['body']), + body=datum_as_proto(datum.value['body'], protos), )) elif datum.type is None: return None @@ -227,7 +155,7 @@ def datum_as_proto(datum: Datum) -> protos.TypedData: ) -def parse_to_rpc_http_cookie_list(cookies: Optional[List[SimpleCookie]]): +def parse_to_rpc_http_cookie_list(cookies: Optional[List[SimpleCookie]], protos): if cookies is None: return cookies @@ -240,23 +168,30 @@ def parse_to_rpc_http_cookie_list(cookies: Optional[List[SimpleCookie]]): value=cookie_entity.value, domain=to_nullable_string( cookie_entity['domain'], - 'cookie.domain'), + 'cookie.domain', + protos), path=to_nullable_string( - cookie_entity['path'], 'cookie.path'), + cookie_entity['path'], + 'cookie.path', + protos), expires=to_nullable_timestamp( parse_cookie_attr_expires( - cookie_entity), 'cookie.expires'), + cookie_entity), 'cookie.expires', + protos), secure=to_nullable_bool( bool(cookie_entity['secure']), - 'cookie.secure'), + 'cookie.secure', + protos), http_only=to_nullable_bool( bool(cookie_entity['httponly']), - 'cookie.httpOnly'), + 'cookie.httpOnly', + protos), same_site=parse_cookie_attr_same_site( - cookie_entity), + cookie_entity, protos), max_age=to_nullable_double( cookie_entity['max-age'], - 'cookie.maxAge'))) + 'cookie.maxAge', + protos))) return rpc_http_cookies @@ -266,8 +201,8 @@ def parse_cookie_attr_expires(cookie_entity): if expires is not None and len(expires) != 0: try: - return parser.parse(expires) - except ParserError: + return datetime.strptime(expires, "%a, %d %b %Y %H:%M:%S GMT") + except ValueError: logging.error( f"Can not parse value {expires} of expires in the cookie " f"due to invalid format.") @@ -282,7 +217,7 @@ def parse_cookie_attr_expires(cookie_entity): return None -def parse_cookie_attr_same_site(cookie_entity): +def parse_cookie_attr_same_site(cookie_entity, protos): same_site = getattr(protos.RpcHttpCookie.SameSite, "None") try: raw_same_site_str = cookie_entity['samesite'].lower() diff --git a/azure_functions_worker/bindings/generic.py b/azure_functions_worker/bindings/generic.py index d5a0f8ab7..876aefbeb 100644 --- a/azure_functions_worker/bindings/generic.py +++ b/azure_functions_worker/bindings/generic.py @@ -3,7 +3,7 @@ import typing from typing import Any, Optional -from . import datumdef +from .datumdef import Datum class GenericBinding: @@ -22,29 +22,29 @@ def check_output_type_annotation(cls, pytype: type) -> bool: @classmethod def encode(cls, obj: Any, *, - expected_type: Optional[type]) -> datumdef.Datum: + expected_type: Optional[type]) -> Datum: if isinstance(obj, str): - return datumdef.Datum(type='string', value=obj) + return Datum(type='string', value=obj) elif isinstance(obj, (bytes, bytearray)): - return datumdef.Datum(type='bytes', value=bytes(obj)) + return Datum(type='bytes', value=bytes(obj)) elif obj is None: - return datumdef.Datum(type=None, value=obj) + return Datum(type=None, value=obj) elif isinstance(obj, dict): - return datumdef.Datum(type='dict', value=obj) + return Datum(type='dict', value=obj) elif isinstance(obj, list): - return datumdef.Datum(type='list', value=obj) + return Datum(type='list', value=obj) elif isinstance(obj, int): - return datumdef.Datum(type='int', value=obj) + return Datum(type='int', value=obj) elif isinstance(obj, float): - return datumdef.Datum(type='double', value=obj) + return Datum(type='double', value=obj) elif isinstance(obj, bool): - return datumdef.Datum(type='bool', value=obj) + return Datum(type='bool', value=obj) else: raise NotImplementedError @classmethod - def decode(cls, data: datumdef.Datum, *, trigger_metadata) -> typing.Any: + def decode(cls, data: Datum, *, trigger_metadata) -> typing.Any: # Enabling support for Dapr bindings # https://github.com/Azure/azure-functions-python-worker/issues/1316 if data is None: diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py index ae40ce398..f82eed2eb 100644 --- a/azure_functions_worker/bindings/meta.py +++ b/azure_functions_worker/bindings/meta.py @@ -2,19 +2,21 @@ # Licensed under the MIT License. import os import sys -import typing -from .. import protos -from ..constants import ( +from typing import Any, Dict, Optional + +from .datumdef import Datum, datum_as_proto +from .generic import GenericBinding + +from ..http_v2 import HttpV2Registry +from ..logging import logger +from ..utils.constants import ( BASE_EXT_SUPPORTED_PY_MINOR_VERSION, CUSTOMER_PACKAGES_PATH, HTTP, HTTP_TRIGGER, ) -from ..http_v2 import HttpV2Registry -from ..logging import logger -from . import datumdef, generic -from .shared_memory_data_transfer import SharedMemoryManager + PB_TYPE = 'rpc_data' PB_TYPE_DATA = 'data' @@ -91,7 +93,7 @@ def load_binding_registry() -> None: def get_binding(bind_name: str, - is_deferred_binding: typing.Optional[bool] = False)\ + is_deferred_binding: Optional[bool] = False)\ -> object: """ First checks if the binding is a non-deferred binding. This is @@ -105,7 +107,7 @@ def get_binding(bind_name: str, if binding is None and is_deferred_binding: binding = DEFERRED_BINDING_REGISTRY.get(bind_name) if binding is None: - binding = generic.GenericBinding + binding = GenericBinding return binding @@ -140,7 +142,7 @@ def has_implicit_output(bind_name: str) -> bool: binding = get_binding(bind_name) # Need to pass in bind_name to exempt Durable Functions - if binding is generic.GenericBinding: + if binding is GenericBinding: return (getattr(binding, 'has_implicit_output', lambda: False) (bind_name)) @@ -152,16 +154,15 @@ def has_implicit_output(bind_name: str) -> bool: def from_incoming_proto( binding: str, - pb: protos.ParameterBinding, *, - pytype: typing.Optional[type], - trigger_metadata: typing.Optional[typing.Dict[str, protos.TypedData]], - shmem_mgr: SharedMemoryManager, + pb, *, + pytype: Optional[type], + trigger_metadata: Optional[Dict[str, Any]], function_name: str, - is_deferred_binding: typing.Optional[bool] = False) -> typing.Any: + is_deferred_binding: Optional[bool] = False) -> Any: binding = get_binding(binding, is_deferred_binding) if trigger_metadata: metadata = { - k: datumdef.Datum.from_typed_data(v) + k: Datum.from_typed_data(v) for k, v in trigger_metadata.items() } else: @@ -170,11 +171,7 @@ def from_incoming_proto( pb_type = pb.WhichOneof(PB_TYPE) if pb_type == PB_TYPE_DATA: val = pb.data - datum = datumdef.Datum.from_typed_data(val) - elif pb_type == PB_TYPE_RPC_SHARED_MEMORY: - # Data was sent over shared memory, attempt to read - datum = datumdef.Datum.from_rpc_shared_memory(pb.rpc_shared_memory, - shmem_mgr) + datum = Datum.from_typed_data(val) else: raise TypeError(f'Unknown ParameterBindingType: {pb_type}') @@ -197,8 +194,8 @@ def from_incoming_proto( f'and expected binding type {binding}') -def get_datum(binding: str, obj: typing.Any, - pytype: typing.Optional[type]) -> datumdef.Datum: +def get_datum(binding: str, obj: Any, + pytype: Optional[type]) -> Datum: """ Convert an object to a datum with the specified type. """ @@ -214,76 +211,36 @@ def get_datum(binding: str, obj: typing.Any, return datum -def _does_datatype_support_caching(datum: datumdef.Datum): +def _does_datatype_support_caching(datum: Datum): supported_datatypes = ('bytes', 'string') return datum.type in supported_datatypes -def _can_transfer_over_shmem(shmem_mgr: SharedMemoryManager, - is_function_data_cache_enabled: bool, - datum: datumdef.Datum): - """ - If shared memory is enabled and supported for the given datum, try to - transfer to host over shared memory as a default. - If caching is enabled, then also check if this type is supported - if so, - transfer over shared memory. - In case of caching, some conditions like object size may not be - applicable since even small objects are also allowed to be cached. - """ - if not shmem_mgr.is_enabled(): - # If shared memory usage is not enabled, no further checks required - return False - if shmem_mgr.is_supported(datum): - # If transferring this object over shared memory is supported, do so. - return True - if is_function_data_cache_enabled and _does_datatype_support_caching(datum): - # If caching is enabled and this object can be cached, transfer over - # shared memory (since the cache uses shared memory). - # In this case, some requirements (like object size) for using shared - # memory may be ignored since we want to support caching of small - # objects (those that have sizes smaller that the minimum we transfer - # over shared memory when the cache is not enabled) as well. - return True - return False - - -def to_outgoing_proto(binding: str, obj: typing.Any, *, - pytype: typing.Optional[type]) -> protos.TypedData: +def to_outgoing_proto(binding: str, obj: Any, *, + pytype: Optional[type], + protos): datum = get_datum(binding, obj, pytype) - return datumdef.datum_as_proto(datum) + return datum_as_proto(datum, protos) -def to_outgoing_param_binding(binding: str, obj: typing.Any, *, - pytype: typing.Optional[type], +def to_outgoing_param_binding(binding: str, obj: Any, *, + pytype: Optional[type], out_name: str, - shmem_mgr: SharedMemoryManager, - is_function_data_cache_enabled: bool) \ - -> protos.ParameterBinding: + protos): datum = get_datum(binding, obj, pytype) - shared_mem_value = None - if _can_transfer_over_shmem(shmem_mgr, is_function_data_cache_enabled, - datum): - shared_mem_value = datumdef.Datum.to_rpc_shared_memory(datum, shmem_mgr) - # Check if data was written into shared memory - if shared_mem_value is not None: - # If it was, then use the rpc_shared_memory field in response message - return protos.ParameterBinding( - name=out_name, - rpc_shared_memory=shared_mem_value) - else: - # If not, send it as part of the response message over RPC - # rpc_val can be None here as we now support a None return type - rpc_val = datumdef.datum_as_proto(datum) - return protos.ParameterBinding( - name=out_name, - data=rpc_val) - - -def deferred_bindings_decode(binding: typing.Any, - pb: protos.ParameterBinding, *, - pytype: typing.Optional[type], - datum: typing.Any, - metadata: typing.Any, + # If not, send it as part of the response message over RPC + # rpc_val can be None here as we now support a None return type + rpc_val = datum_as_proto(datum, protos) + return protos.ParameterBinding( + name=out_name, + data=rpc_val) + + +def deferred_bindings_decode(binding: Any, + pb: Any, *, + pytype: Optional[type], + datum: Any, + metadata: Any, function_name: str): """ This cache holds deferred binding types (ie. BlobClient, ContainerClient) diff --git a/azure_functions_worker/bindings/nullable_converters.py b/azure_functions_worker/bindings/nullable_converters.py index e1c75aecc..fa38c2f1f 100644 --- a/azure_functions_worker/bindings/nullable_converters.py +++ b/azure_functions_worker/bindings/nullable_converters.py @@ -1,13 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from datetime import datetime from typing import Optional, Union -from google.protobuf.timestamp_pb2 import Timestamp - -from azure_functions_worker import protos - -def to_nullable_string(nullable: Optional[str], property_name: str) -> \ - Optional[protos.NullableString]: +def to_nullable_string(nullable: Optional[str], property_name: str, protos): """Converts string input to an 'NullableString' to be sent through the RPC layer. Input that is not a string but is also not null or undefined logs a function app level warning. @@ -16,6 +14,7 @@ def to_nullable_string(nullable: Optional[str], property_name: str) -> \ valid string :param property_name The name of the property that the caller will assign the output to. Used for debugging. + :param: protos The protos object used for returning the appropriate value """ if isinstance(nullable, str): return protos.NullableString(value=nullable) @@ -28,8 +27,7 @@ def to_nullable_string(nullable: Optional[str], property_name: str) -> \ return None -def to_nullable_bool(nullable: Optional[bool], property_name: str) -> \ - Optional[protos.NullableBool]: +def to_nullable_bool(nullable: Optional[bool], property_name: str, protos): """Converts boolean input to an 'NullableBool' to be sent through the RPC layer. Input that is not a boolean but is also not null or undefined logs a function app level warning. @@ -38,6 +36,7 @@ def to_nullable_bool(nullable: Optional[bool], property_name: str) -> \ valid boolean :param property_name The name of the property that the caller will assign the output to. Used for debugging. + :param protos The protos object used for returning the appropriate value """ if isinstance(nullable, bool): return protos.NullableBool(value=nullable) @@ -51,8 +50,7 @@ def to_nullable_bool(nullable: Optional[bool], property_name: str) -> \ def to_nullable_double(nullable: Optional[Union[str, int, float]], - property_name: str) -> \ - Optional[protos.NullableDouble]: + property_name: str, protos): """Converts int or float or str that parses to a number to an 'NullableDouble' to be sent through the RPC layer. Input that is not a valid number but is also not null or undefined logs a function app level @@ -61,6 +59,7 @@ def to_nullable_double(nullable: Optional[Union[str, int, float]], valid number :param property_name The name of the property that the caller will assign the output to. Used for debugging. + :param protos The protos object used for returning the appropriate value """ if isinstance(nullable, int) or isinstance(nullable, float): return protos.NullableDouble(value=nullable) @@ -85,7 +84,7 @@ def to_nullable_double(nullable: Optional[Union[str, int, float]], def to_nullable_timestamp(date_time: Optional[Union[datetime, int]], - property_name: str) -> protos.NullableTimestamp: + property_name: str, protos): """Converts Date or number input to an 'NullableTimestamp' to be sent through the RPC layer. Input that is not a Date or number but is also not null or undefined logs a function app level warning. @@ -94,6 +93,7 @@ def to_nullable_timestamp(date_time: Optional[Union[datetime, int]], valid input :param property_name The name of the property that the caller will assign the output to. Used for debugging. + :param protos The protos object used for returning the appropriate value """ if date_time is not None: try: @@ -102,7 +102,7 @@ def to_nullable_timestamp(date_time: Optional[Union[datetime, int]], date_time.timestamp() return protos.NullableTimestamp( - value=Timestamp(seconds=int(time_in_seconds))) + value=protos.Timestamp(seconds=int(time_in_seconds))) except Exception: raise TypeError( f"A 'datetime' or 'int'" diff --git a/azure_functions_worker/bindings/out.py b/azure_functions_worker/bindings/out.py index 53ac0199d..6e9d0a999 100644 --- a/azure_functions_worker/bindings/out.py +++ b/azure_functions_worker/bindings/out.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - class Out: def __init__(self) -> None: diff --git a/azure_functions_worker/bindings/retrycontext.py b/azure_functions_worker/bindings/retrycontext.py index 8c2166385..d68b21ddf 100644 --- a/azure_functions_worker/bindings/retrycontext.py +++ b/azure_functions_worker/bindings/retrycontext.py @@ -4,12 +4,31 @@ from dataclasses import dataclass from enum import Enum -from . import rpcexception + +class RpcException: + def __init__(self, + source: str, + stack_trace: str, + message: str) -> None: + self.__source = source + self.__stack_trace = stack_trace + self.__message = message + + @property + def source(self) -> str: + return self.__source + + @property + def stack_trace(self) -> str: + return self.__stack_trace + + @property + def message(self) -> str: + return self.__message class RetryPolicy(Enum): """Retry policy for the function invocation""" - MAX_RETRY_COUNT = "max_retry_count" STRATEGY = "strategy" DELAY_INTERVAL = "delay_interval" @@ -21,8 +40,6 @@ class RetryPolicy(Enum): class RetryContext: """Gets the current retry count from retry-context""" retry_count: int - """Gets the max retry count from retry-context""" max_retry_count: int - - rpc_exception: rpcexception.RpcException + rpc_exception: RpcException diff --git a/azure_functions_worker/bindings/rpcexception.py b/azure_functions_worker/bindings/rpcexception.py deleted file mode 100644 index d51c517c8..000000000 --- a/azure_functions_worker/bindings/rpcexception.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -class RpcException: - - def __init__(self, - source: str, - stack_trace: str, - message: str) -> None: - self.__source = source - self.__stack_trace = stack_trace - self.__message = message - - @property - def source(self) -> str: - return self.__source - - @property - def stack_trace(self) -> str: - return self.__stack_trace - - @property - def message(self) -> str: - return self.__message diff --git a/azure_functions_worker/bindings/shared_memory_data_transfer/__init__.py b/azure_functions_worker/bindings/shared_memory_data_transfer/__init__.py deleted file mode 100644 index a68b5ec4e..000000000 --- a/azure_functions_worker/bindings/shared_memory_data_transfer/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -""" -This module provides functionality for accessing shared memory maps. -These are used for transferring data between functions host and the worker -proces. -The initial set of corresponding changes to enable shared memory maps in the -functions host can be found in the following Pull Request: -https://github.com/Azure/azure-functions-host/pull/6836 -The issue tracking shared memory transfer related changes is: -https://github.com/Azure/azure-functions-host/issues/6791 -""" - -from .file_accessor import FileAccessor -from .file_accessor_factory import FileAccessorFactory -from .shared_memory_constants import SharedMemoryConstants -from .shared_memory_exception import SharedMemoryException -from .shared_memory_manager import SharedMemoryManager -from .shared_memory_map import SharedMemoryMap - -__all__ = ( - 'FileAccessorFactory', 'FileAccessor', 'SharedMemoryConstants', - 'SharedMemoryException', 'SharedMemoryMap', 'SharedMemoryManager' -) diff --git a/azure_functions_worker/bindings/shared_memory_data_transfer/file_accessor.py b/azure_functions_worker/bindings/shared_memory_data_transfer/file_accessor.py deleted file mode 100644 index 3838bcdaa..000000000 --- a/azure_functions_worker/bindings/shared_memory_data_transfer/file_accessor.py +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import mmap -from abc import ABCMeta, abstractmethod -from typing import Optional - -from .shared_memory_constants import SharedMemoryConstants as consts - - -class FileAccessor(metaclass=ABCMeta): - """ - For accessing memory maps. - This is an interface that must be implemented by sub-classes to provide - platform-specific support for accessing memory maps. - Currently the following two sub-classes are implemented: - 1) FileAccessorWindows - 2) FileAccessorUnix - Note: Platform specific details of mmap can be found in the official docs: - https://docs.python.org/3/library/mmap.html - """ - @abstractmethod - def open_mem_map( - self, - mem_map_name: str, - mem_map_size: int, - access: int = mmap.ACCESS_READ) -> Optional[mmap.mmap]: - """ - Opens an existing memory map. - Returns the opened mmap if successful, None otherwise. - """ - raise NotImplementedError - - @abstractmethod - def create_mem_map(self, mem_map_name: str, mem_map_size: int) \ - -> Optional[mmap.mmap]: - """ - Creates a new memory map. - Returns the created mmap if successful, None otherwise. - """ - raise NotImplementedError - - @abstractmethod - def delete_mem_map(self, mem_map_name: str, mem_map: mmap.mmap) -> bool: - """ - Deletes the memory map and any backing resources associated with it. - If there is no memory map with the given name, then no action is - performed. - Returns True if the memory map was successfully deleted, False - otherwise. - """ - raise NotImplementedError - - def _is_mem_map_initialized(self, mem_map: mmap.mmap) -> bool: - """ - Checks if the dirty bit of the memory map has been set or not. - This is used to check if a new memory map was created successfully and - we don't end up using an existing one. - """ - original_pos = mem_map.tell() - # The dirty bit is the first byte of the header so seek to the beginning - mem_map.seek(0) - # Read the first byte - byte_read = mem_map.read(1) - # Check if the dirty bit was set or not - if byte_read == consts.HeaderFlags.Initialized: - is_set = True - else: - is_set = False - # Seek back the memory map to the original position - mem_map.seek(original_pos) - return is_set - - def _set_mem_map_initialized(self, mem_map: mmap.mmap): - """ - Sets the dirty bit in the header of the memory map to indicate that this - memory map is not new anymore. - """ - original_pos = mem_map.tell() - # The dirty bit is the first byte of the header so seek to the beginning - mem_map.seek(0) - # Set the dirty bit - mem_map.write(consts.HeaderFlags.Initialized) - # Seek back the memory map to the original position - mem_map.seek(original_pos) - - -class DummyFileAccessor(FileAccessor): - def open_mem_map(self, mem_map_name: str, mem_map_size: int, - access: int = mmap.ACCESS_READ) -> Optional[mmap.mmap]: - pass - - def create_mem_map(self, mem_map_name: str, - mem_map_size: int) -> Optional[mmap.mmap]: - pass - - def delete_mem_map(self, mem_map_name: str, mem_map: mmap.mmap) -> bool: - pass diff --git a/azure_functions_worker/bindings/shared_memory_data_transfer/file_accessor_factory.py b/azure_functions_worker/bindings/shared_memory_data_transfer/file_accessor_factory.py deleted file mode 100644 index eb97f0f54..000000000 --- a/azure_functions_worker/bindings/shared_memory_data_transfer/file_accessor_factory.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os -import sys - -from ...constants import FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED -from ...utils.common import is_envvar_true -from .file_accessor import DummyFileAccessor -from .file_accessor_unix import FileAccessorUnix -from .file_accessor_windows import FileAccessorWindows - - -class FileAccessorFactory: - """ - For creating the platform-appropriate instance of FileAccessor to perform - memory map related operations. - """ - @staticmethod - def create_file_accessor(): - if sys.platform == "darwin" and not is_envvar_true( - FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED): - return DummyFileAccessor() - elif os.name == 'nt': - return FileAccessorWindows() - return FileAccessorUnix() diff --git a/azure_functions_worker/bindings/shared_memory_data_transfer/file_accessor_unix.py b/azure_functions_worker/bindings/shared_memory_data_transfer/file_accessor_unix.py deleted file mode 100644 index ae4f6206c..000000000 --- a/azure_functions_worker/bindings/shared_memory_data_transfer/file_accessor_unix.py +++ /dev/null @@ -1,200 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import mmap -import os -from io import BufferedRandom -from typing import List, Optional - -from azure_functions_worker import constants - -from ...logging import logger -from ...utils.common import get_app_setting -from .file_accessor import FileAccessor -from .shared_memory_constants import SharedMemoryConstants as consts -from .shared_memory_exception import SharedMemoryException - - -class FileAccessorUnix(FileAccessor): - """ - For accessing memory maps. - This implements the FileAccessor interface for Unix platforms. - """ - def __init__(self): - # From the list of configured directories where memory maps can be - # stored, get the list of directories which are valid (either existed - # already or have been created successfully for use). - self.valid_dirs = self._get_valid_mem_map_dirs() - - def __del__(self): - del self.valid_dirs - - def open_mem_map( - self, - mem_map_name: str, - mem_map_size: int, - access: int = mmap.ACCESS_READ) -> Optional[mmap.mmap]: - """ - Note: mem_map_size = 0 means open the entire mmap. - """ - if mem_map_name is None or mem_map_name == '': - raise SharedMemoryException( - f'Cannot open memory map. Invalid name {mem_map_name}') - if mem_map_size < 0: - raise SharedMemoryException( - f'Cannot open memory map. Invalid size {mem_map_size}') - fd = self._open_mem_map_file(mem_map_name) - if fd is None: - logger.warning('Cannot open file: %s', mem_map_name) - return None - mem_map = mmap.mmap(fd.fileno(), mem_map_size, access=access) - return mem_map - - def create_mem_map(self, mem_map_name: str, mem_map_size: int) \ - -> Optional[mmap.mmap]: - if mem_map_name is None or mem_map_name == '': - raise SharedMemoryException( - f'Cannot create memory map. Invalid name {mem_map_name}') - if mem_map_size <= 0: - raise SharedMemoryException( - f'Cannot create memory map. Invalid size {mem_map_size}') - file = self._create_mem_map_file(mem_map_name, mem_map_size) - if file is None: - logger.warning('Cannot create file: %s', mem_map_name) - return None - mem_map = mmap.mmap(file.fileno(), mem_map_size, mmap.MAP_SHARED, - mmap.PROT_WRITE) - if self._is_mem_map_initialized(mem_map): - raise SharedMemoryException(f'Memory map {mem_map_name} ' - 'already exists') - self._set_mem_map_initialized(mem_map) - return mem_map - - def delete_mem_map(self, mem_map_name: str, mem_map: mmap.mmap) -> bool: - if mem_map_name is None or mem_map_name == '': - raise SharedMemoryException( - f'Cannot delete memory map. Invalid name {mem_map_name}') - try: - fd = self._open_mem_map_file(mem_map_name) - os.remove(fd.name) - except Exception as e: - # In this case, we don't want to fail right away but log that - # deletion was unsuccessful. - # These logs can help identify if we may be leaking memory and not - # cleaning up the created memory maps. - logger.error('Cannot delete memory map %s - %s', mem_map_name, e, - exc_info=True) - return False - mem_map.close() - return True - - def _get_allowed_mem_map_dirs(self) -> List[str]: - """ - Get the list of directories where memory maps can be created. - If specified in AppSetting, that list will be used. - Otherwise, the default value will be used. - """ - setting = constants.UNIX_SHARED_MEMORY_DIRECTORIES - allowed_mem_map_dirs_str = get_app_setting(setting) - if allowed_mem_map_dirs_str is None: - allowed_mem_map_dirs = consts.UNIX_TEMP_DIRS - logger.info( - 'Using allowed directories for shared memory: %s from App ' - 'Setting: %s', - allowed_mem_map_dirs, setting) - else: - allowed_mem_map_dirs = allowed_mem_map_dirs_str.split(',') - logger.info( - 'Using default allowed directories for shared memory: %s', - allowed_mem_map_dirs) - return allowed_mem_map_dirs - - def _get_valid_mem_map_dirs(self) -> List[str]: - """ - From the configured list of allowed directories where memory maps can be - stored, return all those that either already existed or were created - successfully for use. - Returns list of directories, in decreasing order of preference, where - memory maps can be created. - """ - allowed_dirs = self._get_allowed_mem_map_dirs() - # Iterate over all the possible directories where the memory map could - # be created and try to create each of them if they don't exist already. - valid_dirs = [] - for temp_dir in allowed_dirs: - dir_path = os.path.join(temp_dir, consts.UNIX_TEMP_DIR_SUFFIX) - if os.path.exists(dir_path): - # A valid directory already exists - valid_dirs.append(dir_path) - logger.debug('Found directory %s to store memory maps', - dir_path) - else: - try: - os.makedirs(dir_path) - valid_dirs.append(dir_path) - except Exception as e: - # We keep trying to check/create others - logger.warning('Cannot create directory %s to ' - 'store memory maps - %s', dir_path, e, - exc_info=True) - if len(valid_dirs) == 0: - logger.error('No valid directory for memory maps in %s', - allowed_dirs) - return valid_dirs - - def _open_mem_map_file(self, mem_map_name: str) -> Optional[BufferedRandom]: - """ - Get the file descriptor of an existing memory map. - Returns the BufferedRandom stream to the file. - """ - # Iterate over all the possible directories where the memory map could - # be present and try to open it. - for temp_dir in self.valid_dirs: - file_path = os.path.join(temp_dir, mem_map_name) - if os.path.exists(file_path): - try: - fd = open(file_path, 'r+b') - return fd - except Exception as e: - logger.error('Cannot open file %s - %s', file_path, e, - exc_info=True) - # The memory map was not found in any of the known directories - logger.error( - 'Cannot open memory map %s in any of the following directories: ' - '%s', - mem_map_name, self.valid_dirs) - return None - - def _create_mem_map_file(self, mem_map_name: str, mem_map_size: int) \ - -> Optional[BufferedRandom]: - """ - Create the file descriptor for a new memory map. - Returns the BufferedRandom stream to the file. - """ - # Ensure that the file does not already exist - for temp_dir in self.valid_dirs: - file_path = os.path.join(temp_dir, mem_map_name) - if os.path.exists(file_path): - raise SharedMemoryException( - f'File {file_path} for memory map {mem_map_name} ' - f'already exists') - # Create the file - for temp_dir in self.valid_dirs: - file_path = os.path.join(temp_dir, mem_map_name) - try: - file = open(file_path, 'wb+') - file.truncate(mem_map_size) - return file - except Exception as e: - # If the memory map could not be created in this directory, we - # keep trying in other applicable directories. - logger.warning('Cannot create memory map in %s - %s.' - ' Trying other directories.', file_path, e, - exc_info=True) - # Could not create the memory map in any of the applicable directory - # paths so we fail. - logger.error( - 'Cannot create memory map %s with size %s in any of the ' - 'following directories: %s', - mem_map_name, mem_map_size, self.valid_dirs) - return None diff --git a/azure_functions_worker/bindings/shared_memory_data_transfer/file_accessor_windows.py b/azure_functions_worker/bindings/shared_memory_data_transfer/file_accessor_windows.py deleted file mode 100644 index 1b45056c7..000000000 --- a/azure_functions_worker/bindings/shared_memory_data_transfer/file_accessor_windows.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import mmap -from typing import Optional - -from ...logging import logger -from .file_accessor import FileAccessor -from .shared_memory_exception import SharedMemoryException - - -class FileAccessorWindows(FileAccessor): - """ - For accessing memory maps. - This implements the FileAccessor interface for Windows. - """ - def open_mem_map( - self, - mem_map_name: str, - mem_map_size: int, - access: int = mmap.ACCESS_READ) -> Optional[mmap.mmap]: - """ - Note: mem_map_size = 0 means open the entire mmap. - Note: On Windows, an mmap is created if one does not exist even when - attempting to open it. - """ - if mem_map_name is None or mem_map_name == '': - raise SharedMemoryException( - f'Cannot open memory map. Invalid name {mem_map_name}') - if mem_map_size < 0: - raise SharedMemoryException( - f'Cannot open memory map. Invalid size {mem_map_size}') - try: - mem_map = mmap.mmap(-1, mem_map_size, mem_map_name, access=access) - return mem_map - except Exception as e: - logger.warning( - 'Cannot open memory map %s with size %s - %s', mem_map_name, - mem_map_size, e) - return None - - def create_mem_map(self, mem_map_name: str, mem_map_size: int) \ - -> Optional[mmap.mmap]: - # Windows also creates the mmap when trying to open it, if it does not - # already exist. - if mem_map_name is None or mem_map_name == '': - raise SharedMemoryException( - f'Cannot create memory map. Invalid name {mem_map_name}') - if mem_map_size <= 0: - raise SharedMemoryException( - f'Cannot create memory map. Invalid size {mem_map_size}') - mem_map = self.open_mem_map(mem_map_name, mem_map_size, - mmap.ACCESS_WRITE) - if mem_map is None: - return None - if self._is_mem_map_initialized(mem_map): - raise SharedMemoryException( - f'Cannot create memory map {mem_map_name} as it ' - f'already exists') - self._set_mem_map_initialized(mem_map) - return mem_map - - def delete_mem_map(self, mem_map_name: str, mem_map: mmap.mmap) -> bool: - """ - In Windows, an mmap is not backed by a file so no file needs to be - deleted. - """ - mem_map.close() - return True diff --git a/azure_functions_worker/bindings/shared_memory_data_transfer/shared_memory_constants.py b/azure_functions_worker/bindings/shared_memory_data_transfer/shared_memory_constants.py deleted file mode 100644 index ac25170b3..000000000 --- a/azure_functions_worker/bindings/shared_memory_data_transfer/shared_memory_constants.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -class SharedMemoryConstants: - class HeaderFlags: - """ - Flags that are present in the header region of the memory maps. - """ - Initialized = b'\x01' - """ - Indicates that the memory map has been initialized, may be in use and - is not new. - This represents a boolean value of True. - """ - - MEM_MAP_INITIALIZED_FLAG_NUM_BYTES = 1 - """ - The length of a bool which is the length of the part of the header flag - specifying if the memory map is already created and used. - This is to distinguish between new memory maps and ones that were - previously created and may be in use already. - Header flags are defined in the class SharedMemoryConstants.HeaderFlags. - """ - - CONTENT_LENGTH_NUM_BYTES = 8 - """ - The length of a long which is the length of the part of the header - specifying content length in the memory map. - """ - - CONTENT_HEADER_TOTAL_BYTES = MEM_MAP_INITIALIZED_FLAG_NUM_BYTES + \ - CONTENT_LENGTH_NUM_BYTES - """ - The total length of the header - """ - - MIN_BYTES_FOR_SHARED_MEM_TRANSFER = 1024 * 1024 # 1 MB - """ - Minimum size (in number of bytes) an object must be in order for it to be - transferred over shared memory. - If the object is smaller than this, gRPC is used. - Note: This needs to be consistent among the host and workers. - e.g. in the host, it is defined in SharedMemoryConstants.cs - """ - - MAX_BYTES_FOR_SHARED_MEM_TRANSFER = 2 * 1024 * 1024 * 1024 # 2 GB - """ - Maximum size (in number of bytes) an object must be in order for it to be - transferred over shared memory. - This limit is imposed because initializing objects like greater than 2GB - is not allowed in DotNet. - Ref: https://stackoverflow.com/a/3944336/3132415 - Note: This needs to be consistent among the host and workers. - e.g. in the host, it is defined in SharedMemoryConstants.cs - """ - - SIZE_OF_CHAR_BYTES = 2 - """ - This is what the size of a character is in DotNet. Can be verified by - doing "sizeof(char)". - To keep the limits consistent, when determining if a string can be - transferred over shared memory, we multiply the number of characters - by this constant. - Corresponding logic in the host can be found in SharedMemoryManager.cs - """ - - UNIX_TEMP_DIRS = ["/dev/shm"] - """ - Default directories in Unix where the memory maps can be found. - These list is in order of preference, starting with the highest preference - directory. - A user can override this by using the AppSetting: - UNIX_SHARED_MEMORY_DIRECTORIES. - """ - - UNIX_TEMP_DIR_SUFFIX = "AzureFunctions" - """ - Suffix for the temp directories containing memory maps in Unix - """ diff --git a/azure_functions_worker/bindings/shared_memory_data_transfer/shared_memory_exception.py b/azure_functions_worker/bindings/shared_memory_data_transfer/shared_memory_exception.py deleted file mode 100644 index cf802d336..000000000 --- a/azure_functions_worker/bindings/shared_memory_data_transfer/shared_memory_exception.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -class SharedMemoryException(Exception): - """ - Exception raised when using shared memory. - """ - def __init__(self, msg: str) -> None: - super().__init__(msg) diff --git a/azure_functions_worker/bindings/shared_memory_data_transfer/shared_memory_manager.py b/azure_functions_worker/bindings/shared_memory_data_transfer/shared_memory_manager.py deleted file mode 100644 index ec1a1a7cb..000000000 --- a/azure_functions_worker/bindings/shared_memory_data_transfer/shared_memory_manager.py +++ /dev/null @@ -1,203 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import uuid -from typing import Dict, Optional - -from ...constants import FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED -from ...logging import logger -from ...utils.common import is_envvar_true -from ..datumdef import Datum -from .file_accessor_factory import FileAccessorFactory -from .shared_memory_constants import SharedMemoryConstants as consts -from .shared_memory_map import SharedMemoryMap -from .shared_memory_metadata import SharedMemoryMetadata - - -class SharedMemoryManager: - """ - Performs all operations related to reading/writing data from/to shared - memory. - This is used for transferring input/output data of the function from/to the - functions host over shared memory as opposed to RPC to improve the rate of - data transfer and the function's end-to-end latency. - """ - def __init__(self): - # The allocated memory maps are tracked here so that a reference to them - # is kept open until they have been used (e.g. if they contain a - # function's output, it is read by the functions host). - # Having a mapping of the name and the memory map is then later used to - # close a given memory map by its name, after it has been used. - # key: mem_map_name, val: SharedMemoryMap - self._allocated_mem_maps: Dict[str, SharedMemoryMap] = {} - self._file_accessor = FileAccessorFactory.create_file_accessor() - - def __del__(self): - del self._file_accessor - del self._allocated_mem_maps - - @property - def allocated_mem_maps(self): - """ - List of allocated shared memory maps. - """ - return self._allocated_mem_maps - - @property - def file_accessor(self): - """ - FileAccessor instance for accessing memory maps. - """ - return self._file_accessor - - def is_enabled(self) -> bool: - """ - Whether supported types should be transferred between functions host and - the worker using shared memory. - """ - return is_envvar_true( - FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED) - - def is_supported(self, datum: Datum) -> bool: - """ - Whether the given Datum object can be transferred to the functions host - using shared memory. - This logic is kept consistent with the host's which can be found in - SharedMemoryManager.cs - """ - if datum.type == 'bytes': - num_bytes = len(datum.value) - if num_bytes >= consts.MIN_BYTES_FOR_SHARED_MEM_TRANSFER and \ - num_bytes <= consts.MAX_BYTES_FOR_SHARED_MEM_TRANSFER: - return True - elif datum.type == 'string': - num_bytes = len(datum.value) * consts.SIZE_OF_CHAR_BYTES - if num_bytes >= consts.MIN_BYTES_FOR_SHARED_MEM_TRANSFER and \ - num_bytes <= consts.MAX_BYTES_FOR_SHARED_MEM_TRANSFER: - return True - return False - - def put_bytes(self, content: bytes) -> Optional[SharedMemoryMetadata]: - """ - Writes the given bytes into shared memory. - Returns metadata about the shared memory region to which the content was - written if successful, None otherwise. - """ - if content is None: - return None - mem_map_name = str(uuid.uuid4()) - content_length = len(content) - shared_mem_map = self._create(mem_map_name, content_length) - if shared_mem_map is None: - return None - try: - num_bytes_written = shared_mem_map.put_bytes(content) - except Exception as e: - logger.warning('Cannot write %s bytes into shared memory %s - %s', - content_length, mem_map_name, e) - shared_mem_map.dispose() - return None - if num_bytes_written != content_length: - logger.error( - 'Cannot write data into shared memory %s (%s != %s)', - mem_map_name, num_bytes_written, content_length) - shared_mem_map.dispose() - return None - self.allocated_mem_maps[mem_map_name] = shared_mem_map - return SharedMemoryMetadata(mem_map_name, content_length) - - def put_string(self, content: str) -> Optional[SharedMemoryMetadata]: - """ - Writes the given string into shared memory. - Returns the name of the memory map into which the data was written if - succesful, None otherwise. - Note: The encoding used here must be consistent with what is used by the - host in SharedMemoryManager.cs (GetStringAsync/PutStringAsync). - """ - if content is None: - return None - content_bytes = content.encode('utf-8') - return self.put_bytes(content_bytes) - - def get_bytes(self, mem_map_name: str, offset: int, count: int) \ - -> Optional[bytes]: - """ - Reads data from the given memory map with the provided name, starting at - the provided offset and reading a total of count bytes. - Returns the data read from shared memory as bytes if successful, None - otherwise. - """ - if offset != 0: - logger.error( - 'Cannot read bytes. Non-zero offset (%s) not supported.', - offset) - return None - shared_mem_map = self._open(mem_map_name, count) - if shared_mem_map is None: - return None - try: - content = shared_mem_map.get_bytes(content_offset=0, - bytes_to_read=count) - finally: - shared_mem_map.dispose(is_delete_file=False) - return content - - def get_string(self, mem_map_name: str, offset: int, count: int) \ - -> Optional[str]: - """ - Reads data from the given memory map with the provided name, starting at - the provided offset and reading a total of count bytes. - Returns the data read from shared memory as a string if successful, None - otherwise. - Note: The encoding used here must be consistent with what is used by the - host in SharedMemoryManager.cs (GetStringAsync/PutStringAsync). - """ - content_bytes = self.get_bytes(mem_map_name, offset, count) - if content_bytes is None: - return None - content_str = content_bytes.decode('utf-8') - return content_str - - def free_mem_map(self, mem_map_name: str, - to_delete_backing_resources: bool = True) -> bool: - """ - Frees the memory map and, if specified, any backing resources (e.g. - file in the case of Unix) associated with it. - If there is no memory map with the given name being tracked, then no - action is performed. - Returns True if the memory map was freed successfully, False otherwise. - """ - if mem_map_name not in self.allocated_mem_maps: - logger.error( - 'Cannot find memory map in list of allocations %s', - mem_map_name) - return False - shared_mem_map = self.allocated_mem_maps[mem_map_name] - success = shared_mem_map.dispose(to_delete_backing_resources) - del self.allocated_mem_maps[mem_map_name] - return success - - def _create(self, mem_map_name: str, content_length: int) \ - -> Optional[SharedMemoryMap]: - """ - Creates a new SharedMemoryMap with the given name and content length. - Returns the SharedMemoryMap object if successful, None otherwise. - """ - mem_map_size = consts.CONTENT_HEADER_TOTAL_BYTES + content_length - mem_map = self.file_accessor.create_mem_map(mem_map_name, mem_map_size) - if mem_map is None: - return None - return SharedMemoryMap(self.file_accessor, mem_map_name, mem_map) - - def _open(self, mem_map_name: str, content_length: int) \ - -> Optional[SharedMemoryMap]: - """ - Opens an existing SharedMemoryMap with the given name and content - length. - Returns the SharedMemoryMap object if successful, None otherwise. - """ - mem_map_size = consts.CONTENT_HEADER_TOTAL_BYTES + content_length - mem_map = self.file_accessor.open_mem_map(mem_map_name, mem_map_size) - if mem_map is None: - return None - return SharedMemoryMap(self.file_accessor, mem_map_name, mem_map) diff --git a/azure_functions_worker/bindings/shared_memory_data_transfer/shared_memory_map.py b/azure_functions_worker/bindings/shared_memory_data_transfer/shared_memory_map.py deleted file mode 100644 index d84eb81c4..000000000 --- a/azure_functions_worker/bindings/shared_memory_data_transfer/shared_memory_map.py +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import mmap -import os -import struct -import sys -from typing import Optional - -from ...logging import logger -from .file_accessor import FileAccessor -from .shared_memory_constants import SharedMemoryConstants as consts -from .shared_memory_exception import SharedMemoryException - - -class SharedMemoryMap: - """ - Shared memory region to read/write data from. - """ - def __init__( - self, - file_accessor: FileAccessor, - mem_map_name: str, - mem_map: mmap.mmap): - if mem_map is None: - raise SharedMemoryException( - 'Cannot initialize SharedMemoryMap. Invalid memory map ' - 'provided') - if mem_map_name is None or mem_map_name == '': - raise SharedMemoryException( - f'Cannot initialize SharedMemoryMap. Invalid name ' - f'{mem_map_name}') - self.file_accessor = file_accessor - self.mem_map_name = mem_map_name - self.mem_map = mem_map - - def put_bytes(self, content: bytes) -> Optional[int]: - """ - Writes the given content bytes into this SharedMemoryMap. - The number of bytes written must be less than or equal to the size of - the SharedMemoryMap. - Returns the number of bytes of content written. - """ - if content is None: - return None - content_length = len(content) - # Seek past the MemoryMapInitialized flag section of the header - self.mem_map.seek(consts.MEM_MAP_INITIALIZED_FLAG_NUM_BYTES) - # Write the content length into the header - content_length_bytes = content_length.to_bytes( - consts.CONTENT_LENGTH_NUM_BYTES, byteorder=sys.byteorder) - num_content_length_bytes = len(content_length_bytes) - num_content_length_bytes_written = self.mem_map.write( - content_length_bytes) - if num_content_length_bytes_written != num_content_length_bytes: - logger.error( - 'Cannot write content size to memory map %s (%s != %s)', - self.mem_map_name, num_content_length_bytes_written, - num_content_length_bytes) - return 0 - # Write the content - num_content_bytes_written = self.mem_map.write(content) - self.mem_map.flush() - return num_content_bytes_written - - def get_bytes(self, content_offset: int = 0, bytes_to_read: int = 0) \ - -> Optional[bytes]: - """ - Read content from this SharedMemoryMap with the given name and starting - at the given offset. - content_offset = 0 means read from the beginning of the content. - bytes_to_read = 0 means read the entire content. - Returns the content as bytes if successful, None otherwise. - """ - content_length = self._get_content_length() - if content_length is None: - return None - # Seek past the header and get to the content - self.mem_map.seek(consts.CONTENT_HEADER_TOTAL_BYTES) - if content_offset > 0: - self.mem_map.seek(content_offset, os.SEEK_CUR) - if bytes_to_read > 0: - # Read up to the specified number of bytes to read - content = self.mem_map.read(bytes_to_read) - else: - # Read the entire content - content = self.mem_map.read() - return content - - def dispose(self, is_delete_file: bool = True) -> bool: - """ - Close the underlying memory map. - Returns True if the resources were disposed, False otherwise. - """ - success = True - if is_delete_file: - success = self.file_accessor.delete_mem_map(self.mem_map_name, - self.mem_map) - self.mem_map.close() - return success - - def _bytes_to_long(self, input_bytes) -> int: - """ - Decode a set of bytes representing a long. - This uses the format that the functions host (i.e. C#) uses. - """ - return struct.unpack(" Optional[int]: - """ - Read the header of the memory map to determine the length of content - contained in that memory map. - Returns the content length as a non-negative integer if successful, - None otherwise. - """ - self.mem_map.seek(consts.MEM_MAP_INITIALIZED_FLAG_NUM_BYTES) - header_bytes = self.mem_map.read(consts.CONTENT_LENGTH_NUM_BYTES) - content_length = self._bytes_to_long(header_bytes) - return content_length diff --git a/azure_functions_worker/bindings/shared_memory_data_transfer/shared_memory_metadata.py b/azure_functions_worker/bindings/shared_memory_data_transfer/shared_memory_metadata.py deleted file mode 100644 index ee5c50e07..000000000 --- a/azure_functions_worker/bindings/shared_memory_data_transfer/shared_memory_metadata.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -class SharedMemoryMetadata: - """ - Information about a shared memory region. - """ - def __init__(self, mem_map_name, count_bytes): - # Name of the memory map - self.mem_map_name = mem_map_name - # Number of bytes of content in the memory map - self.count_bytes = count_bytes diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py deleted file mode 100644 index 897a3499a..000000000 --- a/azure_functions_worker/dispatcher.py +++ /dev/null @@ -1,1115 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""GRPC client. - -Implements loading and execution of Python workers. -""" - -import asyncio -import concurrent.futures -import logging -import os -import platform -import queue -import sys -import threading -from asyncio import BaseEventLoop -from datetime import datetime -from logging import LogRecord -from typing import List, Optional - -import grpc - -from . import bindings, constants, functions, loader, protos -from .bindings.shared_memory_data_transfer import SharedMemoryManager -from .constants import ( - APPLICATIONINSIGHTS_CONNECTION_STRING, - HTTP_URI, - METADATA_PROPERTIES_WORKER_INDEXED, - PYTHON_AZURE_MONITOR_LOGGER_NAME, - PYTHON_AZURE_MONITOR_LOGGER_NAME_DEFAULT, - PYTHON_ENABLE_DEBUG_LOGGING, - PYTHON_ENABLE_INIT_INDEXING, - PYTHON_ENABLE_OPENTELEMETRY, - PYTHON_ENABLE_OPENTELEMETRY_DEFAULT, - PYTHON_LANGUAGE_RUNTIME, - PYTHON_ROLLBACK_CWD_PATH, - PYTHON_SCRIPT_FILE_NAME, - PYTHON_SCRIPT_FILE_NAME_DEFAULT, - PYTHON_THREADPOOL_THREAD_COUNT, - PYTHON_THREADPOOL_THREAD_COUNT_DEFAULT, - PYTHON_THREADPOOL_THREAD_COUNT_MAX_37, - PYTHON_THREADPOOL_THREAD_COUNT_MIN, - REQUIRES_ROUTE_PARAMETERS -) -from .extension import ExtensionManager -from .http_v2 import ( - HttpServerInitError, - HttpV2Registry, - http_coordinator, - initialize_http_server, - sync_http_request, -) -from .logging import ( - CONSOLE_LOG_PREFIX, - disable_console_logging, - enable_console_logging, - error_logger, - format_exception, - is_system_log_category, - logger, -) -from .utils.app_setting_manager import get_python_appsetting_state -from .utils.common import get_app_setting, is_envvar_true, validate_script_file_name -from .utils.dependency import DependencyManager -from .utils.tracing import marshall_exception_trace -from .utils.wrappers import disable_feature_by -from .version import VERSION - -_TRUE = "true" -_TRACEPARENT = "traceparent" -_TRACESTATE = "tracestate" - - -class DispatcherMeta(type): - __current_dispatcher__ = None - - @property - def current(mcls): - disp = mcls.__current_dispatcher__ - if disp is None: - raise RuntimeError('no currently running Dispatcher is found') - return disp - - -class Dispatcher(metaclass=DispatcherMeta): - _GRPC_STOP_RESPONSE = object() - - def __init__(self, loop: BaseEventLoop, host: str, port: int, - worker_id: str, request_id: str, - grpc_connect_timeout: float, - grpc_max_msg_len: int = -1) -> None: - self._loop = loop - self._host = host - self._port = port - self._request_id = request_id - self._worker_id = worker_id - self._function_data_cache_enabled = False - self._functions = functions.Registry() - self._shmem_mgr = SharedMemoryManager() - self._old_task_factory = None - - # Used to store metadata returns - self._function_metadata_result = None - self._function_metadata_exception = None - - # Used for checking if open telemetry is enabled - self._azure_monitor_available = False - self._context_api = None - self._trace_context_propagator = None - - # We allow the customer to change synchronous thread pool max worker - # count by setting the PYTHON_THREADPOOL_THREAD_COUNT app setting. - # For 3.[6|7|8] The default value is 1. - # For 3.9, we don't set this value by default but we honor incoming - # the app setting. - self._sync_call_tp: concurrent.futures.Executor = ( - self._create_sync_call_tp(self._get_sync_tp_max_workers()) - ) - - self._grpc_connect_timeout: float = grpc_connect_timeout - # This is set to -1 by default to remove the limitation on msg size - self._grpc_max_msg_len: int = grpc_max_msg_len - self._grpc_resp_queue: queue.Queue = queue.Queue() - self._grpc_connected_fut = loop.create_future() - self._grpc_thread: threading.Thread = threading.Thread( - name='grpc-thread', target=self.__poll_grpc) - - @staticmethod - def get_worker_metadata(): - return protos.WorkerMetadata( - runtime_name=PYTHON_LANGUAGE_RUNTIME, - runtime_version=f"{sys.version_info.major}." - f"{sys.version_info.minor}", - worker_version=VERSION, - worker_bitness=platform.machine(), - custom_properties={}) - - def get_sync_tp_workers_set(self): - """We don't know the exact value of the threadcount set for the Python - 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 - """ - return self._sync_call_tp._max_workers - - @classmethod - async def connect(cls, host: str, port: int, worker_id: str, - request_id: str, connect_timeout: float): - loop = asyncio.events.get_event_loop() - disp = cls(loop, host, port, worker_id, request_id, connect_timeout) - disp._grpc_thread.start() - await disp._grpc_connected_fut - logger.info('Successfully opened gRPC channel to %s:%s ', host, port) - return disp - - 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') - - self._old_task_factory = self._loop.get_task_factory() - - loader.install() - - DispatcherMeta.__current_dispatcher__ = self - try: - forever = self._loop.create_future() - - self._grpc_resp_queue.put_nowait( - protos.StreamingMessage( - request_id=self.request_id, - start_stream=protos.StartStream( - worker_id=self.worker_id))) - - # In Python 3.11+, constructing a task has an optional context - # parameter. Allow for this param to be passed to ContextEnabledTask - self._loop.set_task_factory( - lambda loop, coro, context=None: ContextEnabledTask( - coro, loop=loop, context=context)) - - # Detach console logging before enabling GRPC channel logging - logger.info('Detaching console logging.') - disable_console_logging() - - # Attach gRPC logging to the root logger. Since gRPC channel is - # established, should use it for system and user logs - 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.') - logging_handler.flush() - - try: - await forever - finally: - logger.warning('Detaching gRPC logging due to exception.') - logging_handler.flush() - root_logger.removeHandler(logging_handler) - - # Reenable console logging when there's an exception - enable_console_logging() - logger.warning('Switched to console logging due to exception.') - finally: - DispatcherMeta.__current_dispatcher__ = None - - loader.uninstall() - - self._loop.set_task_factory(self._old_task_factory) - self.stop() - - def stop(self) -> None: - if self._grpc_thread is not None: - self._grpc_resp_queue.put_nowait(self._GRPC_STOP_RESPONSE) - self._grpc_thread.join() - self._grpc_thread = None - - self._stop_sync_call_tp() - - 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: - log_level = protos.RpcLog.Error - elif record.levelno >= logging.WARNING: - log_level = protos.RpcLog.Warning - elif record.levelno >= logging.INFO: - log_level = protos.RpcLog.Information - elif record.levelno >= logging.DEBUG: - log_level = protos.RpcLog.Debug - else: - log_level = getattr(protos.RpcLog, 'None') - - if is_system_log_category(record.name): - log_category = protos.RpcLog.RpcLogCategory.Value('System') - else: # customers using logging will yield 'root' in record.name - log_category = protos.RpcLog.RpcLogCategory.Value('User') - - log = dict( - level=log_level, - message=formatted_msg, - category=record.name, - log_category=log_category - ) - - invocation_id = get_current_invocation_id() - if invocation_id is not None: - log['invocation_id'] = invocation_id - - self._grpc_resp_queue.put_nowait( - protos.StreamingMessage( - request_id=self.request_id, - rpc_log=protos.RpcLog(**log))) - - @property - def request_id(self) -> str: - return self._request_id - - @property - def worker_id(self) -> str: - return self._worker_id - - # noinspection PyBroadException - @staticmethod - def _serialize_exception(exc: Exception): - try: - message = f'{type(exc).__name__}: {exc}' - except Exception: - message = ('Unhandled exception in function. ' - 'Could not serialize original exception message.') - - try: - stack_trace = marshall_exception_trace(exc) - except Exception: - stack_trace = '' - - return protos.RpcException(message=message, stack_trace=stack_trace) - - async def _dispatch_grpc_request(self, request): - content_type = request.WhichOneof('content') - request_handler = getattr(self, f'_handle__{content_type}', None) - if request_handler is None: - # Don't crash on unknown messages. Some of them can be ignored; - # and if something goes really wrong the host can always just - # kill the worker's process. - logger.error('unknown StreamingMessage content type %s', - content_type) - return - - resp = await request_handler(request) - self._grpc_resp_queue.put_nowait(resp) - - def initialize_azure_monitor(self): - """Initializes OpenTelemetry and Azure monitor distro - """ - self.update_opentelemetry_status() - try: - from azure.monitor.opentelemetry import configure_azure_monitor - - # Set functions resource detector manually until officially - # include in Azure monitor distro - os.environ.setdefault( - "OTEL_EXPERIMENTAL_RESOURCE_DETECTORS", - "azure_functions", - ) - - configure_azure_monitor( - # Connection string can be explicitly specified in Appsetting - # If not set, defaults to env var - # APPLICATIONINSIGHTS_CONNECTION_STRING - connection_string=get_app_setting( - setting=APPLICATIONINSIGHTS_CONNECTION_STRING - ), - logger_name=get_app_setting( - setting=PYTHON_AZURE_MONITOR_LOGGER_NAME, - default_value=PYTHON_AZURE_MONITOR_LOGGER_NAME_DEFAULT - ), - ) - self._azure_monitor_available = True - - logger.info("Successfully configured Azure monitor distro.") - except ImportError: - logger.exception( - "Cannot import Azure Monitor distro." - ) - self._azure_monitor_available = False - except Exception: - logger.exception( - "Error initializing Azure monitor distro." - ) - self._azure_monitor_available = False - - def update_opentelemetry_status(self): - """Check for OpenTelemetry library availability and - update the status attribute.""" - try: - from opentelemetry import context as context_api - from opentelemetry.trace.propagation.tracecontext import ( - TraceContextTextMapPropagator, - ) - - self._context_api = context_api - self._trace_context_propagator = TraceContextTextMapPropagator() - - except ImportError: - logger.exception( - "Cannot import OpenTelemetry libraries." - ) - - 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 get_app_setting(setting=PYTHON_ENABLE_OPENTELEMETRY, - default_value=PYTHON_ENABLE_OPENTELEMETRY_DEFAULT): - self.initialize_azure_monitor() - - if self._azure_monitor_available: - capabilities[constants.WORKER_OPEN_TELEMETRY_ENABLED] = _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") - - if HttpV2Registry.http_v2_enabled(): - capabilities[HTTP_URI] = \ - initialize_http_server(self._host) - capabilities[REQUIRES_ROUTE_PARAMETERS] = _TRUE - - except HttpServerInitError: - raise - except Exception as ex: - self._function_metadata_exception = ex - - 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))) - - 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. - # Having log here will reduce the responsiveness of the worker. - return protos.StreamingMessage( - request_id=request.request_id, - worker_status_response=protos.WorkerStatusResponse()) - - def load_function_metadata(self, function_app_directory, caller_info): - """ - This method is called to index the functions in the function app - directory and save the results in function_metadata_result or - function_metadata_exception in case of an exception. - """ - script_file_name = get_app_setting( - setting=PYTHON_SCRIPT_FILE_NAME, - default_value=f'{PYTHON_SCRIPT_FILE_NAME_DEFAULT}') - - logger.debug( - 'Received load metadata request from %s, request ID %s, ' - 'script_file_name: %s', - caller_info, self.request_id, script_file_name) - - validate_script_file_name(script_file_name) - function_path = os.path.join(function_app_directory, - script_file_name) - - # For V1, the function path will not exist and - # return None. - self._function_metadata_result = ( - self.index_functions(function_path, function_app_directory)) \ - 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 - - script_file_name = get_app_setting( - setting=PYTHON_SCRIPT_FILE_NAME, - default_value=f'{PYTHON_SCRIPT_FILE_NAME_DEFAULT}') - function_path = os.path.join(function_app_directory, - script_file_name) - - logger.info( - 'Received WorkerMetadataRequest, request ID %s, ' - 'function_path: %s', - self.request_id, function_path) - - if not is_envvar_true(PYTHON_ENABLE_INIT_INDEXING): - try: - self.load_function_metadata( - function_app_directory, - caller_info="functions_metadata_request") - except Exception as ex: - self._function_metadata_exception = ex - - if self._function_metadata_exception: - return protos.StreamingMessage( - request_id=request.request_id, - function_metadata_response=protos.FunctionMetadataResponse( - result=protos.StatusResult( - status=protos.StatusResult.Failure, - exception=self._serialize_exception( - self._function_metadata_exception)))) - else: - metadata_result = self._function_metadata_result - - return protos.StreamingMessage( - request_id=request.request_id, - function_metadata_response=protos.FunctionMetadataResponse( - use_default_metadata_indexing=False if metadata_result else - True, - function_metadata_results=metadata_result, - result=protos.StatusResult( - status=protos.StatusResult.Success))) - - async def _handle__function_load_request(self, request): - func_request = request.function_load_request - function_id = func_request.function_id - function_metadata = func_request.metadata - function_name = function_metadata.name - function_app_directory = function_metadata.directory - - logger.info( - 'Received WorkerLoadRequest, request ID %s, function_id: %s,' - 'function_name: %s, function_app_directory : %s', - self.request_id, function_id, function_name, - function_app_directory) - - programming_model = "V2" - try: - if not self._functions.get_function(function_id): - - if function_metadata.properties.get( - METADATA_PROPERTIES_WORKER_INDEXED, False): - # This is for the second worker and above where the worker - # indexing is enabled and load request is called without - # calling the metadata request. In this case we index the - # function and update the workers registry - - try: - self.load_function_metadata( - function_app_directory, - caller_info="functions_load_request") - except Exception as ex: - self._function_metadata_exception = ex - - # For the second worker, if there was an exception in - # indexing, we raise it here - if self._function_metadata_exception: - raise Exception(self._function_metadata_exception) - - else: - # legacy function - programming_model = "V1" - - func = loader.load_function( - function_name, - function_app_directory, - func_request.metadata.script_file, - func_request.metadata.entry_point) - - self._functions.add_function( - function_id, func, func_request.metadata) - - try: - ExtensionManager.function_load_extension( - function_name, - func_request.metadata.directory - ) - except Exception as ex: - logging.error("Failed to load extensions: ", ex) - raise - - logger.info('Successfully processed FunctionLoadRequest, ' - 'request ID: %s, ' - 'function ID: %s,' - 'function Name: %s,' - 'programming model: %s', - self.request_id, - function_id, - function_name, - programming_model) - - return protos.StreamingMessage( - request_id=self.request_id, - function_load_response=protos.FunctionLoadResponse( - function_id=function_id, - result=protos.StatusResult( - status=protos.StatusResult.Success))) - - except Exception as ex: - return protos.StreamingMessage( - request_id=self.request_id, - function_load_response=protos.FunctionLoadResponse( - function_id=function_id, - result=protos.StatusResult( - status=protos.StatusResult.Failure, - exception=self._serialize_exception(ex)))) - - async def _handle__invocation_request(self, request): - invocation_time = datetime.utcnow() - invoc_request = request.invocation_request - invocation_id = invoc_request.invocation_id - function_id = invoc_request.function_id - http_v2_enabled = False - - # Set the current `invocation_id` to the current task so - # that our logging handler can find it. - current_task = asyncio.current_task(self._loop) - assert isinstance(current_task, ContextEnabledTask) - current_task.set_azure_invocation_id(invocation_id) - - try: - fi: functions.FunctionInfo = self._functions.get_function( - function_id) - assert fi is not None - - function_invocation_logs: List[str] = [ - 'Received FunctionInvocationRequest', - f'request ID: {self.request_id}', - f'function ID: {function_id}', - f'function name: {fi.name}', - f'invocation ID: {invocation_id}', - f'function type: {"async" if fi.is_async else "sync"}', - f'timestamp (UTC): {invocation_time}' - ] - if not fi.is_async: - function_invocation_logs.append( - f'sync threadpool max workers: ' - f'{self.get_sync_tp_workers_set()}' - ) - logger.info(', '.join(function_invocation_logs)) - - args = {} - - http_v2_enabled = self._functions.get_function(function_id) \ - .is_http_func and \ - HttpV2Registry.http_v2_enabled() - - for pb in invoc_request.input_data: - pb_type_info = fi.input_types[pb.name] - 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, - function_name=self._functions.get_function( - function_id).name, - is_deferred_binding=pb_type_info.deferred_bindings_enabled) - - if http_v2_enabled: - http_request = await http_coordinator.get_http_request_async( - invocation_id) - - trigger_arg_name = fi.trigger_metadata.get('param_name') - func_http_request = args[trigger_arg_name] - await sync_http_request(http_request, func_http_request) - args[trigger_arg_name] = http_request - - 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 - fi_context.thread_local_storage.invocation_id = invocation_id - if fi.requires_context: - args['context'] = fi_context - - if fi.output_types: - for name in fi.output_types: - args[name] = bindings.Out() - - if fi.is_async: - if self._azure_monitor_available: - self.configure_opentelemetry(fi_context) - - 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 - if fi.output_types: - for out_name, out_type_info in fi.output_types.items(): - val = args[out_name].get() - if val is None: - # TODO: is the "Out" parameter optional? - # Can "None" be marshaled into protos.TypedData? - continue - - param_binding = bindings.to_outgoing_param_binding( - out_type_info.binding_name, val, - pytype=out_type_info.pytype, - out_name=out_name, shmem_mgr=self._shmem_mgr, - is_function_data_cache_enabled=cache_enabled) - output_data.append(param_binding) - - return_value = 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, - ) - - # Actively flush customer print() function to console - sys.stdout.flush() - - return protos.StreamingMessage( - request_id=self.request_id, - invocation_response=protos.InvocationResponse( - invocation_id=invocation_id, - return_value=return_value, - result=protos.StatusResult( - status=protos.StatusResult.Success), - 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( - invocation_id=invocation_id, - result=protos.StatusResult( - 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 - worker init request will be called directly. - """ - try: - logger.info('Received FunctionEnvironmentReloadRequest, ' - 'request ID: %s, ' - 'App Settings state: %s. ' - 'To enable debug level logging, please refer to ' - 'https://aka.ms/python-enable-debug-logging', - self.request_id, - get_python_appsetting_state()) - - func_env_reload_request = \ - request.function_environment_reload_request - directory = func_env_reload_request.function_app_directory - - # Append function project root to module finding sys.path - if func_env_reload_request.function_app_directory: - sys.path.append(func_env_reload_request.function_app_directory) - - # Clear sys.path import cache, reload all module from new sys.path - sys.path_importer_cache.clear() - - # Reload environment variables - os.environ.clear() - env_vars = func_env_reload_request.environment_variables - for var in env_vars: - os.environ[var] = env_vars[var] - - # Apply PYTHON_THREADPOOL_THREAD_COUNT - self._stop_sync_call_tp() - self._sync_call_tp = ( - self._create_sync_call_tp(self._get_sync_tp_max_workers()) - ) - - if is_envvar_true(PYTHON_ENABLE_DEBUG_LOGGING): - root_logger = logging.getLogger() - root_logger.setLevel(logging.DEBUG) - - # Reload azure google namespaces - DependencyManager.reload_customer_libraries(directory) - - # calling load_binding_registry again since the - # reload_customer_libraries call clears the registry - bindings.load_binding_registry() - - capabilities = {} - if get_app_setting( - setting=PYTHON_ENABLE_OPENTELEMETRY, - default_value=PYTHON_ENABLE_OPENTELEMETRY_DEFAULT): - self.initialize_azure_monitor() - - if self._azure_monitor_available: - capabilities[constants.WORKER_OPEN_TELEMETRY_ENABLED] = ( - _TRUE) - - if is_envvar_true(PYTHON_ENABLE_INIT_INDEXING): - try: - self.load_function_metadata( - directory, - caller_info="environment_reload_request") - - if HttpV2Registry.http_v2_enabled(): - capabilities[HTTP_URI] = \ - initialize_http_server(self._host) - capabilities[REQUIRES_ROUTE_PARAMETERS] = _TRUE - except HttpServerInitError: - raise - except Exception as ex: - self._function_metadata_exception = ex - - # Change function app directory - if getattr(func_env_reload_request, - 'function_app_directory', None): - self._change_cwd( - func_env_reload_request.function_app_directory) - - success_response = protos.FunctionEnvironmentReloadResponse( - capabilities=capabilities, - worker_metadata=self.get_worker_metadata(), - result=protos.StatusResult( - status=protos.StatusResult.Success)) - - return protos.StreamingMessage( - request_id=self.request_id, - function_environment_reload_response=success_response) - - except Exception as ex: - failure_response = protos.FunctionEnvironmentReloadResponse( - result=protos.StatusResult( - status=protos.StatusResult.Failure, - exception=self._serialize_exception(ex))) - - return protos.StreamingMessage( - request_id=self.request_id, - function_environment_reload_response=failure_response) - - def index_functions(self, function_path: str, function_dir: str): - indexed_functions = loader.index_function_app(function_path) - logger.info( - "Indexed function app and found %s functions", - len(indexed_functions) - ) - - if indexed_functions: - fx_metadata_results, fx_bindings_logs = ( - loader.process_indexed_function( - self._functions, - indexed_functions, - function_dir)) - - indexed_function_logs: List[str] = [] - indexed_function_bindings_logs = [] - for func in indexed_functions: - func_binding_logs = fx_bindings_logs.get(func) - for binding in func.get_bindings(): - deferred_binding_info = func_binding_logs.get( - binding.name)\ - if func_binding_logs.get(binding.name) else "" - indexed_function_bindings_logs.append(( - binding.type, binding.name, deferred_binding_info)) - - function_log = "Function Name: {}, Function Binding: {}" \ - .format(func.get_function_name(), - indexed_function_bindings_logs) - indexed_function_logs.append(function_log) - - logger.info( - 'Successfully processed FunctionMetadataRequest for ' - 'functions: %s. Deferred bindings enabled: %s.', " ".join( - indexed_function_logs), - self._functions.deferred_bindings_enabled()) - - return fx_metadata_results - - async def _handle__close_shared_memory_resources_request(self, request): - """ - Frees any memory maps that were produced as output for a given - invocation. - This is called after the functions host is done reading the output from - the worker and wants the worker to free up those resources. - If the cache is enabled, let the host decide when to delete the - resources. Just drop the reference from the worker. - If the cache is not enabled, the worker should free the resources as at - this point the host has read the memory maps and does not need them. - """ - close_request = request.close_shared_memory_resources_request - map_names = close_request.map_names - # Assign default value of False to all result values. - # If we are successfully able to close a memory map, its result will be - # set to True. - results = {mem_map_name: False for mem_map_name in map_names} - - try: - for map_name in map_names: - try: - to_delete_resources = not self._function_data_cache_enabled - success = self._shmem_mgr.free_mem_map(map_name, - to_delete_resources) - results[map_name] = success - except Exception as e: - logger.error('Cannot free memory map %s - %s', map_name, e, - exc_info=True) - finally: - response = protos.CloseSharedMemoryResourcesResponse( - close_map_results=results) - return protos.StreamingMessage( - request_id=self.request_id, - close_shared_memory_resources_response=response) - - def configure_opentelemetry(self, invocation_context): - carrier = {_TRACEPARENT: invocation_context.trace_context.trace_parent, - _TRACESTATE: invocation_context.trace_context.trace_state} - ctx = self._trace_context_propagator.extract(carrier) - self._context_api.attach(ctx) - - @staticmethod - def _get_context(invoc_request: protos.InvocationRequest, name: str, - directory: str) -> bindings.Context: - """ For more information refer: - https://aka.ms/azfunc-invocation-context - """ - trace_context = bindings.TraceContext( - invoc_request.trace_context.trace_parent, - invoc_request.trace_context.trace_state, - invoc_request.trace_context.attributes) - - retry_context = bindings.RetryContext( - invoc_request.retry_context.retry_count, - invoc_request.retry_context.max_retry_count, - invoc_request.retry_context.exception) - - return bindings.Context( - name, directory, invoc_request.invocation_id, - _invocation_id_local, trace_context, retry_context) - - @disable_feature_by(PYTHON_ROLLBACK_CWD_PATH) - def _change_cwd(self, new_cwd: str): - if os.path.exists(new_cwd): - os.chdir(new_cwd) - logger.info('Changing current working directory to %s', new_cwd) - else: - logger.warning('Directory %s is not found when reloading', new_cwd) - - def _stop_sync_call_tp(self): - """Deallocate the current synchronous thread pool and assign - self._sync_call_tp to None. If the thread pool does not exist, - this will be a no op. - """ - if getattr(self, '_sync_call_tp', None): - self._sync_call_tp.shutdown() - self._sync_call_tp = None - - @staticmethod - def _get_sync_tp_max_workers() -> Optional[int]: - def tp_max_workers_validator(value: str) -> bool: - try: - int_value = int(value) - except ValueError: - logger.warning('%s must be an integer', - PYTHON_THREADPOOL_THREAD_COUNT) - return False - - if int_value < PYTHON_THREADPOOL_THREAD_COUNT_MIN: - logger.warning( - '%s must be set to a value between %s and sys.maxint. ' - 'Reverting to default value for max_workers', - PYTHON_THREADPOOL_THREAD_COUNT, - PYTHON_THREADPOOL_THREAD_COUNT_MIN) - return False - return True - - # Starting Python 3.9, worker won't be putting a limit on the - # max_workers count in the created threadpool. - default_value = None if sys.version_info.minor >= 9 \ - else f'{PYTHON_THREADPOOL_THREAD_COUNT_DEFAULT}' - - max_workers = get_app_setting(setting=PYTHON_THREADPOOL_THREAD_COUNT, - default_value=default_value, - validator=tp_max_workers_validator) - - if sys.version_info.minor <= 7: - max_workers = min(int(max_workers), - PYTHON_THREADPOOL_THREAD_COUNT_MAX_37) - - # We can box the app setting as int for earlier python versions. - return int(max_workers) if max_workers else None - - def _create_sync_call_tp( - self, max_worker: Optional[int]) -> concurrent.futures.Executor: - """Create a thread pool executor with max_worker. This is a wrapper - over ThreadPoolExecutor constructor. Consider calling this method after - _stop_sync_call_tp() to ensure only 1 synchronous thread pool is - running. - """ - return concurrent.futures.ThreadPoolExecutor( - max_workers=max_worker - ) - - def _run_sync_func(self, invocation_id, context, func, params): - # This helper exists because we need to access the current - # invocation_id from ThreadPoolExecutor's threads. - context.thread_local_storage.invocation_id = invocation_id - try: - if self._azure_monitor_available: - self.configure_opentelemetry(context) - return ExtensionManager.get_sync_invocation_wrapper(context, - func)(params) - finally: - context.thread_local_storage.invocation_id = None - - async def _run_async_func(self, context, func, params): - return await ExtensionManager.get_async_invocation_wrapper( - context, func, params - ) - - def __poll_grpc(self): - options = [] - if self._grpc_max_msg_len: - options.append(('grpc.max_receive_message_length', - self._grpc_max_msg_len)) - options.append(('grpc.max_send_message_length', - self._grpc_max_msg_len)) - - channel = grpc.insecure_channel( - f'{self._host}:{self._port}', options) - - try: - grpc.channel_ready_future(channel).result( - timeout=self._grpc_connect_timeout) - except Exception as ex: - self._loop.call_soon_threadsafe( - self._grpc_connected_fut.set_exception, ex) - return - else: - self._loop.call_soon_threadsafe( - self._grpc_connected_fut.set_result, True) - - stub = protos.FunctionRpcStub(channel) - - def gen(resp_queue): - while True: - msg = resp_queue.get() - if msg is self._GRPC_STOP_RESPONSE: - grpc_req_stream.cancel() - return - yield msg - - grpc_req_stream = stub.EventStream(gen(self._grpc_resp_queue)) - try: - for req in grpc_req_stream: - self._loop.call_soon_threadsafe( - self._loop.create_task, self._dispatch_grpc_request(req)) - except Exception as ex: - if ex is grpc_req_stream: - # Yes, this is how grpc_req_stream iterator exits. - return - error_logger.exception( - 'unhandled error in gRPC thread. Exception: {0}'.format( - format_exception(ex))) - raise - - -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. - - # When dispatcher receives an exception, it should switch back - # to console logging. However, it is possible that - # __current_dispatcher__ is set to None as there are still messages - # buffered in this handler, not calling the emit yet. - msg = self.format(record) - try: - Dispatcher.current.on_logging(record, msg) - except RuntimeError as runtime_error: - # This will cause 'Dispatcher not found' failure. - # Logging such of an issue will cause infinite loop of gRPC logging - # To mitigate, we should suppress the 2nd level error logging here - # and use print function to report exception instead. - print(f'{CONSOLE_LOG_PREFIX} ERROR: {str(runtime_error)}', - file=sys.stderr, flush=True) - - -class ContextEnabledTask(asyncio.Task): - AZURE_INVOCATION_ID = '__azure_function_invocation_id__' - - def __init__(self, coro, loop, context=None): - # The context param is only available for 3.11+. If - # not, it can't be sent in the init() call. - if sys.version_info.minor >= 11: - super().__init__(coro, loop=loop, context=context) - else: - super().__init__(coro, loop=loop) - - current_task = asyncio.current_task(loop) - if current_task is not None: - invocation_id = getattr( - current_task, self.AZURE_INVOCATION_ID, None) - if invocation_id is not None: - self.set_azure_invocation_id(invocation_id) - - def set_azure_invocation_id(self, invocation_id: str) -> None: - setattr(self, self.AZURE_INVOCATION_ID, invocation_id) - - -def get_current_invocation_id() -> Optional[str]: - loop = asyncio._get_running_loop() - if loop is not None: - current_task = asyncio.current_task(loop) - if current_task is not None: - task_invocation_id = getattr(current_task, - ContextEnabledTask.AZURE_INVOCATION_ID, - None) - if task_invocation_id is not None: - return task_invocation_id - - return getattr(_invocation_id_local, 'invocation_id', None) - - -_invocation_id_local = threading.local() diff --git a/azure_functions_worker/extension.py b/azure_functions_worker/extension.py deleted file mode 100644 index fcf8602c0..000000000 --- a/azure_functions_worker/extension.py +++ /dev/null @@ -1,254 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import functools -import logging -from types import ModuleType -from typing import Any, Callable, List, Optional - -from .constants import ( - PYTHON_ENABLE_WORKER_EXTENSIONS, - PYTHON_ENABLE_WORKER_EXTENSIONS_DEFAULT, - PYTHON_ENABLE_WORKER_EXTENSIONS_DEFAULT_39, - PYTHON_ISOLATE_WORKER_DEPENDENCIES, -) -from .logging import SYSTEM_LOG_PREFIX, logger -from .utils.common import get_sdk_from_sys_path, get_sdk_version, is_python_version -from .utils.wrappers import enable_feature_by - -# Extension Hooks -FUNC_EXT_POST_FUNCTION_LOAD = "post_function_load" -FUNC_EXT_PRE_INVOCATION = "pre_invocation" -FUNC_EXT_POST_INVOCATION = "post_invocation" -APP_EXT_POST_FUNCTION_LOAD = "post_function_load_app_level" -APP_EXT_PRE_INVOCATION = "pre_invocation_app_level" -APP_EXT_POST_INVOCATION = "post_invocation_app_level" - - -class ExtensionManager: - _is_sdk_detected: bool = False - """This marks if the ExtensionManager has already proceeded a detection, - if so, the sdk will be cached in ._extension_enabled_sdk - """ - - _extension_enabled_sdk: Optional[ModuleType] = None - """This is a cache of azure.functions module that supports extension - interfaces. If this is None, that mean the sdk does not support extension. - """ - - @classmethod - @enable_feature_by( - flag=PYTHON_ENABLE_WORKER_EXTENSIONS, - flag_default=( - PYTHON_ENABLE_WORKER_EXTENSIONS_DEFAULT_39 if - is_python_version('3.9') else - PYTHON_ENABLE_WORKER_EXTENSIONS_DEFAULT - ) - ) - def function_load_extension(cls, func_name, func_directory): - """Helper to execute function load extensions. If one of the extension - fails in the extension chain, the rest of them will continue, emitting - an error log of an exception trace for failed extension. - - Parameters - ---------- - func_name: str - The name of the trigger (e.g. HttpTrigger) - func_directory: str - The folder path of the trigger - (e.g. /home/site/wwwroot/HttpTrigger). - """ - sdk = cls._try_get_sdk_with_extension_enabled() - if sdk is None: - return - - # Reports application & function extensions installed on customer's app - cls._info_discover_extension_list(func_name, sdk) - - # Get function hooks from azure.functions.extension.ExtensionMeta - # The return type is FuncExtensionHooks - funcs = sdk.ExtensionMeta.get_function_hooks(func_name) - - # Invoke function hooks - cls._safe_execute_function_load_hooks( - funcs, FUNC_EXT_POST_FUNCTION_LOAD, func_name, func_directory - ) - - # Get application hooks from azure.functions.extension.ExtensionMeta - # The reutnr type is AppExtensionHooks - apps = sdk.ExtensionMeta.get_application_hooks() - - # Invoke application hook - cls._safe_execute_function_load_hooks( - apps, APP_EXT_POST_FUNCTION_LOAD, func_name, func_directory - ) - - @classmethod - @enable_feature_by( - flag=PYTHON_ENABLE_WORKER_EXTENSIONS, - flag_default=( - PYTHON_ENABLE_WORKER_EXTENSIONS_DEFAULT_39 if - is_python_version('3.9') else - PYTHON_ENABLE_WORKER_EXTENSIONS_DEFAULT - ) - ) - def _invocation_extension(cls, ctx, hook_name, func_args, func_ret=None): - """Helper to execute extensions. If one of the extension fails in the - extension chain, the rest of them will continue, emitting an error log - of an exception trace for failed extension. - - Parameters - ---------- - ctx: azure.functions.Context - Azure Functions context to be passed onto extension - hook_name: str - The exetension name to be executed (e.g. pre_invocations). - These are defined in azure.functions.FuncExtensionHooks. - """ - sdk = cls._try_get_sdk_with_extension_enabled() - if sdk is None: - return - - # Get function hooks from azure.functions.extension.ExtensionMeta - # The return type is FuncExtensionHooks - funcs = sdk.ExtensionMeta.get_function_hooks(ctx.function_name) - - # Invoke function hooks - cls._safe_execute_invocation_hooks( - funcs, hook_name, ctx, func_args, func_ret - ) - - # Get application hooks from azure.functions.extension.ExtensionMeta - # The reutnr type is AppExtensionHooks - apps = sdk.ExtensionMeta.get_application_hooks() - - # Invoke application hook - cls._safe_execute_invocation_hooks( - apps, hook_name, ctx, func_args, func_ret - ) - - @classmethod - def get_sync_invocation_wrapper(cls, ctx, func) -> Callable[[List], Any]: - """Get a synchronous lambda of extension wrapped function which takes - function parameters - """ - return functools.partial(cls._raw_invocation_wrapper, ctx, func) - - @classmethod - async def get_async_invocation_wrapper(cls, ctx, function, args) -> Any: - """An asynchronous coroutine for executing function with extensions - """ - cls._invocation_extension(ctx, APP_EXT_PRE_INVOCATION, args) - cls._invocation_extension(ctx, FUNC_EXT_PRE_INVOCATION, args) - result = await function(**args) - cls._invocation_extension(ctx, FUNC_EXT_POST_INVOCATION, args, result) - cls._invocation_extension(ctx, APP_EXT_POST_INVOCATION, args, result) - return result - - @staticmethod - def _is_extension_enabled_in_sdk(module: ModuleType) -> bool: - """Check if the extension feature is enabled in particular - azure.functions package. - - Parameters - ---------- - module: ModuleType - The azure.functions SDK module - - Returns - ------- - bool - True on azure.functions SDK supports extension registration - """ - return getattr(module, 'ExtensionMeta', None) is not None - - @classmethod - def _is_pre_invocation_hook(cls, name) -> bool: - return name in (FUNC_EXT_PRE_INVOCATION, APP_EXT_PRE_INVOCATION) - - @classmethod - def _is_post_invocation_hook(cls, name) -> bool: - return name in (FUNC_EXT_POST_INVOCATION, APP_EXT_POST_INVOCATION) - - @classmethod - def _safe_execute_invocation_hooks(cls, hooks, hook_name, ctx, fargs, fret): - # hooks from azure.functions.ExtensionMeta.get_function_hooks() or - # azure.functions.ExtensionMeta.get_application_hooks() - if hooks: - # Invoke extension implementation from ..ext_impl - for hook_meta in getattr(hooks, hook_name, []): - # Register a system logger with prefix azure_functions_worker - ext_logger = logging.getLogger( - f'{SYSTEM_LOG_PREFIX}.extension.{hook_meta.ext_name}' - ) - try: - if cls._is_pre_invocation_hook(hook_name): - hook_meta.ext_impl(ext_logger, ctx, fargs) - elif cls._is_post_invocation_hook(hook_name): - hook_meta.ext_impl(ext_logger, ctx, fargs, fret) - except Exception as e: - ext_logger.error(e, exc_info=True) - - @classmethod - def _safe_execute_function_load_hooks(cls, hooks, hook_name, fname, fdir): - # hooks from azure.functions.ExtensionMeta.get_function_hooks() or - # azure.functions.ExtensionMeta.get_application_hooks() - if hooks: - # Invoke extension implementation from ..ext_impl - for hook_meta in getattr(hooks, hook_name, []): - try: - hook_meta.ext_impl(fname, fdir) - except Exception as e: - logger.error(e, exc_info=True) - - @classmethod - def _raw_invocation_wrapper(cls, ctx, function, args) -> Any: - """Calls pre_invocation and post_invocation extensions additional - to function invocation - """ - cls._invocation_extension(ctx, APP_EXT_PRE_INVOCATION, args) - cls._invocation_extension(ctx, FUNC_EXT_PRE_INVOCATION, args) - result = function(**args) - cls._invocation_extension(ctx, FUNC_EXT_POST_INVOCATION, args, result) - cls._invocation_extension(ctx, APP_EXT_POST_INVOCATION, args, result) - return result - - @classmethod - def _try_get_sdk_with_extension_enabled(cls) -> Optional[ModuleType]: - if cls._is_sdk_detected: - return cls._extension_enabled_sdk - - sdk = get_sdk_from_sys_path() - if cls._is_extension_enabled_in_sdk(sdk): - cls._info_extension_is_enabled(sdk) - cls._extension_enabled_sdk = sdk - else: - cls._warn_sdk_not_support_extension(sdk) - cls._extension_enabled_sdk = None - - cls._is_sdk_detected = True - return cls._extension_enabled_sdk - - @classmethod - def _info_extension_is_enabled(cls, sdk): - logger.info( - 'Python Worker Extension is enabled in azure.functions (%s). ' - 'Sdk path: %s', get_sdk_version(sdk), sdk.__file__) - - @classmethod - def _info_discover_extension_list(cls, function_name, sdk): - logger.info( - 'Python Worker Extension Manager is loading %s, current ' - 'registered extensions: %s', - function_name, sdk.ExtensionMeta.get_registered_extensions_json() - ) - - @classmethod - def _warn_sdk_not_support_extension(cls, sdk): - logger.warning( - 'The azure.functions (%s) does not support Python worker ' - 'extensions. If you believe extensions are correctly installed, ' - 'please set the %s and %s to "true"', - get_sdk_version(sdk), PYTHON_ISOLATE_WORKER_DEPENDENCIES, - PYTHON_ENABLE_WORKER_EXTENSIONS - ) diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py index 292fe4857..69f2a5a4f 100644 --- a/azure_functions_worker/functions.py +++ b/azure_functions_worker/functions.py @@ -5,12 +5,14 @@ import pathlib import typing import uuid +from .logging import logger -from . import bindings as bindings_utils -from . import protos -from ._thirdparty import typing_inspect -from .constants import HTTP_TRIGGER -from .protos import BindingInfo +from .bindings.meta import (has_implicit_output, + check_deferred_bindings_enabled, + check_output_type_annotation, + check_input_type_annotation) +from .utils.constants import HTTP_TRIGGER +from .utils.typing_inspect import is_generic_type, get_origin, get_args class ParamTypeInfo(typing.NamedTuple): @@ -63,15 +65,14 @@ def deferred_bindings_enabled(self) -> bool: @staticmethod def get_explicit_and_implicit_return(binding_name: str, - binding: BindingInfo, + binding, explicit_return: bool, implicit_return: bool, bound_params: dict) -> \ typing.Tuple[bool, bool]: if binding_name == '$return': explicit_return = True - elif bindings_utils.has_implicit_output( - binding.type): + elif has_implicit_output(binding.type): implicit_return = True bound_params[binding_name] = binding else: @@ -91,7 +92,7 @@ def get_return_binding(binding_name: str, return_binding_name = binding_type assert return_binding_name is not None explicit_return_val_set = True - elif bindings_utils.has_implicit_output(binding_type): + elif has_implicit_output(binding_type): return_binding_name = binding_type return return_binding_name, explicit_return_val_set @@ -99,7 +100,8 @@ def get_return_binding(binding_name: str, @staticmethod def validate_binding_direction(binding_name: str, binding_direction: str, - func_name: str): + func_name: str, + protos): if binding_direction == protos.BindingInfo.inout: raise FunctionLoadError( func_name, @@ -132,7 +134,9 @@ def is_context_required(params, bound_params: dict, @staticmethod def validate_function_params(params: dict, bound_params: dict, - annotations: dict, func_name: str): + annotations: dict, func_name: str, + protos): + logger.info("Params: %s, BoundParams: %s, Annotations: %s, FuncName: %s", params, bound_params, annotations, func_name) if set(params) - set(bound_params): raise FunctionLoadError( func_name, @@ -151,19 +155,21 @@ def validate_function_params(params: dict, bound_params: dict, for param in params.values(): binding = bound_params[param.name] + logger.info("Param %s, binding: %s", param, binding) param_has_anno = param.name in annotations param_anno = annotations.get(param.name) + logger.info("Param_has_anno %s, param_anno: %s", param_has_anno, param_anno) # Check if deferred bindings is enabled fx_deferred_bindings_enabled, is_deferred_binding = ( - bindings_utils.check_deferred_bindings_enabled( + check_deferred_bindings_enabled( param_anno, fx_deferred_bindings_enabled)) if param_has_anno: - if typing_inspect.is_generic_type(param_anno): - param_anno_origin = typing_inspect.get_origin(param_anno) + if is_generic_type(param_anno): + param_anno_origin = get_origin(param_anno) if param_anno_origin is not None: is_param_out = ( isinstance(param_anno_origin, type) @@ -185,7 +191,7 @@ def validate_function_params(params: dict, bound_params: dict, is_binding_out = binding.direction == protos.BindingInfo.out if is_param_out: - param_anno_args = typing_inspect.get_args(param_anno) + param_anno_args = get_args(param_anno) if len(param_anno_args) != 1: raise FunctionLoadError( func_name, @@ -197,14 +203,16 @@ def validate_function_params(params: dict, bound_params: dict, # so if the annotation was func.Out[typing.List[foo]], # we need to reconstruct it. if (isinstance(param_py_type, tuple) - and typing_inspect.is_generic_type(param_py_type[0])): + and is_generic_type(param_py_type[0])): param_py_type = operator.getitem( param_py_type[0], *param_py_type[1:]) else: param_py_type = param_anno + logger.info("Param_py_type %s", param_py_type) + if (param_has_anno and not isinstance(param_py_type, type) - and not typing_inspect.is_generic_type(param_py_type)): + and not is_generic_type(param_py_type)): raise FunctionLoadError( func_name, f'binding {param.name} has invalid non-type annotation ' @@ -225,17 +233,19 @@ def validate_function_params(params: dict, bound_params: dict, 'is azure.functions.Out in Python') if param_has_anno and param_py_type in (str, bytes) and ( - not bindings_utils.has_implicit_output(binding.type)): + not has_implicit_output(binding.type)): param_bind_type = 'generic' else: param_bind_type = binding.type + logger.info("param_bind_type %s", param_bind_type) + if param_has_anno: if is_param_out: - checks_out = bindings_utils.check_output_type_annotation( + checks_out = check_output_type_annotation( param_bind_type, param_py_type) else: - checks_out = bindings_utils.check_input_type_annotation( + checks_out = check_input_type_annotation( param_bind_type, param_py_type, is_deferred_binding) if not checks_out: @@ -270,8 +280,8 @@ def get_function_return_type(annotations: dict, has_explicit_return: bool, return_pytype = None if has_explicit_return and 'return' in annotations: return_anno = annotations.get('return') - if typing_inspect.is_generic_type( - return_anno) and typing_inspect.get_origin( + if is_generic_type( + return_anno) and get_origin( return_anno).__name__ == 'Out': raise FunctionLoadError( func_name, @@ -287,7 +297,7 @@ def get_function_return_type(annotations: dict, has_explicit_return: bool, if return_pytype is (str, bytes): binding_name = 'generic' - if not bindings_utils.check_output_type_annotation( + if not check_output_type_annotation( binding_name, return_pytype): raise FunctionLoadError( func_name, @@ -357,60 +367,7 @@ def _get_http_trigger_param_name(self, input_types): ) return http_trigger_param_name - def add_function(self, function_id: str, - func: typing.Callable, - metadata: protos.RpcFunctionMetadata): - func_name = metadata.name - sig = inspect.signature(func) - params = dict(sig.parameters) - annotations = typing.get_type_hints(func) - return_binding_name: typing.Optional[str] = None - explicit_return_val_set = False - has_explicit_return = False - has_implicit_return = False - - bound_params = {} - for binding_name, binding_info in metadata.bindings.items(): - self.validate_binding_direction(binding_name, - binding_info.direction, func_name) - - has_explicit_return, has_implicit_return = \ - self.get_explicit_and_implicit_return( - binding_name, binding_info, has_explicit_return, - has_implicit_return, bound_params) - - return_binding_name, explicit_return_val_set = \ - self.get_return_binding(binding_name, - binding_info.type, - return_binding_name, - explicit_return_val_set) - - requires_context = self.is_context_required(params, bound_params, - annotations, - func_name) - - input_types, output_types, _ = self.validate_function_params( - params, bound_params, annotations, func_name) - - return_type = \ - self.get_function_return_type(annotations, - has_explicit_return, - has_implicit_return, - return_binding_name, - func_name) - - self.add_func_to_registry_and_return_funcinfo(func, func_name, - function_id, - metadata.directory, - requires_context, - has_explicit_return, - has_implicit_return, - _, - input_types, - output_types, - return_type) - - def add_indexed_function(self, function): + def add_indexed_function(self, function, protos): func = function.get_user_function() func_name = function.get_function_name() function_id = str(uuid.uuid5(namespace=uuid.NAMESPACE_OID, @@ -429,7 +386,7 @@ def add_indexed_function(self, function): for binding in function.get_bindings(): self.validate_binding_direction(binding.name, binding.direction, - func_name) + func_name, protos) has_explicit_return, has_implicit_return = \ self.get_explicit_and_implicit_return( @@ -451,7 +408,8 @@ def add_indexed_function(self, function): params, bound_params, annotations, - func_name) + func_name, + protos) return_type = \ self.get_function_return_type(annotations, diff --git a/azure_functions_worker/handle_event.py b/azure_functions_worker/handle_event.py new file mode 100644 index 000000000..450539ff7 --- /dev/null +++ b/azure_functions_worker/handle_event.py @@ -0,0 +1,410 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import asyncio +import logging +import os +import sys + +from datetime import datetime +from typing import List, Optional + +from .functions import FunctionInfo, Registry +from .http_v2 import ( + HttpServerInitError, + HttpV2Registry, + http_coordinator, + initialize_http_server, + sync_http_request, +) +from .loader import index_function_app, process_indexed_function +from .logging import logger +from .otel import otel_manager, initialize_azure_monitor, configure_opentelemetry + +from .bindings.context import get_context +from .bindings.meta import load_binding_registry, is_trigger_binding, from_incoming_proto, to_outgoing_param_binding, to_outgoing_proto +from .bindings.out import Out +from .utils.constants import (FUNCTION_DATA_CACHE, + RAW_HTTP_BODY_BYTES, + TYPED_DATA_COLLECTION, + RPC_HTTP_BODY_ONLY, + WORKER_STATUS, + RPC_HTTP_TRIGGER_METADATA_REMOVED, + SHARED_MEMORY_DATA_TRANSFER, + TRUE, + PYTHON_ENABLE_OPENTELEMETRY, + PYTHON_ENABLE_OPENTELEMETRY_DEFAULT, + WORKER_OPEN_TELEMETRY_ENABLED, + PYTHON_ENABLE_INIT_INDEXING, + HTTP_URI, + REQUIRES_ROUTE_PARAMETERS, + PYTHON_SCRIPT_FILE_NAME, + PYTHON_SCRIPT_FILE_NAME_DEFAULT, + PYTHON_ENABLE_DEBUG_LOGGING) +from .utils.current import get_current_loop, execute, run_sync_func +from .utils.env_state import get_app_setting, is_envvar_true +from .utils.helpers import change_cwd, get_worker_metadata +from .utils.tracing import serialize_exception +from .utils.validators import validate_script_file_name + +metadata_result: Optional[List] = None +metadata_exception: Optional[Exception] = None +result = None # Todo: type is coroutine? +_functions = Registry() +_function_data_cache_enabled: bool = False +_host: str = None +protos = None + + +# Protos will be the retry / binding / metadata protos object that we populate and return +async def worker_init_request(request): + logger.info("Library Worker: received worker_init_request") + global result, _host, protos, _function_data_cache_enabled + init_request = request.request.worker_init_request + host_capabilities = init_request.capabilities + _host = request.properties.get("host") + protos = request.properties.get("protos") + if FUNCTION_DATA_CACHE in host_capabilities: + val = host_capabilities[FUNCTION_DATA_CACHE] + _function_data_cache_enabled = val == TRUE + + capabilities = { + RAW_HTTP_BODY_BYTES: TRUE, + TYPED_DATA_COLLECTION: TRUE, + RPC_HTTP_BODY_ONLY: TRUE, + WORKER_STATUS: TRUE, + RPC_HTTP_TRIGGER_METADATA_REMOVED: TRUE, + SHARED_MEMORY_DATA_TRANSFER: TRUE, + } + if get_app_setting(setting=PYTHON_ENABLE_OPENTELEMETRY, + default_value=PYTHON_ENABLE_OPENTELEMETRY_DEFAULT): + initialize_azure_monitor() + + if otel_manager.get_azure_monitor_available(): + capabilities[WORKER_OPEN_TELEMETRY_ENABLED] = TRUE + + + # loading bindings registry and saving results to a static + # dictionary which will be later used in the invocation request + load_binding_registry() + + try: + result = asyncio.create_task(load_function_metadata(init_request.function_app_directory, + caller_info="worker_init_request")) + if get_app_setting(setting=PYTHON_ENABLE_INIT_INDEXING): + capabilities[HTTP_URI] = \ + initialize_http_server(_host) + capabilities[REQUIRES_ROUTE_PARAMETERS] = TRUE + except HttpServerInitError: + raise + except Exception as ex: + # This is catching an exception that happens during indexing while the init + # request is still in progress. The proxy worker will do nothing with this, + # but metadata will fail + global metadata_exception + metadata_exception = ex + + return protos.WorkerInitResponse( + capabilities=capabilities, + worker_metadata=get_worker_metadata(protos), + result=protos.StatusResult(status=protos.StatusResult.Success) + ) + + +# worker_status_request can be done in the proxy worker + +async def functions_metadata_request(request): + logger.info("Library Worker: received worker_metadata_request") + global protos + # Todo: should there be a check on if result is None? + global result, metadata_result, metadata_exception + if result: + await result + + if metadata_exception: + return protos.FunctionMetadataResponse( + result=protos.StatusResult( + status=protos.StatusResult.Failure, + exception=serialize_exception( + metadata_exception, protos))) + + else: + return protos.FunctionMetadataResponse( + use_default_metadata_indexing=False, + function_metadata_results=metadata_result, + result=protos.StatusResult( + status=protos.StatusResult.Success)) + + + +async def functions_load_request(request): + logger.info("Library Worker: received worker_load_request") + global protos + func_request = request.request.function_load_request + function_id = func_request.function_id + + return protos.FunctionLoadResponse( + function_id=function_id, + result=protos.StatusResult( + status=protos.StatusResult.Success)) + + +async def invocation_request(request): + logger.info("Library Worker: received worker_invocation_request") + global protos + invocation_time = datetime.now() + invoc_request = request.request.invocation_request + invocation_id = invoc_request.invocation_id + function_id = invoc_request.function_id + http_v2_enabled = False + threadpool = request.properties.get("threadpool") + + try: + fi: FunctionInfo = _functions.get_function( + function_id) + assert fi is not None + + args = {} + + http_v2_enabled = _functions.get_function( + function_id).is_http_func and \ + HttpV2Registry.http_v2_enabled() + + for pb in invoc_request.input_data: + pb_type_info = fi.input_types[pb.name] + if is_trigger_binding(pb_type_info.binding_name): + trigger_metadata = invoc_request.trigger_metadata + else: + trigger_metadata = None + + args[pb.name] = from_incoming_proto( + pb_type_info.binding_name, + pb, + trigger_metadata=trigger_metadata, + pytype=pb_type_info.pytype, + function_name=_functions.get_function( + function_id).name, + is_deferred_binding=pb_type_info.deferred_bindings_enabled) + + if http_v2_enabled: + http_request = await http_coordinator.get_http_request_async( + invocation_id) + + trigger_arg_name = fi.trigger_metadata.get('param_name') + func_http_request = args[trigger_arg_name] + await sync_http_request(http_request, func_http_request) + args[trigger_arg_name] = http_request + + fi_context = get_context(invoc_request, fi.name, + fi.directory) + + # Use local thread storage to store the invocation ID + # for a customer's threads + fi_context.thread_local_storage.invocation_id = invocation_id + if fi.requires_context: + args['context'] = fi_context + + if fi.output_types: + for name in fi.output_types: + args[name] = Out() + + if fi.is_async: + if otel_manager.get_azure_monitor_available(): + configure_opentelemetry(fi_context) + + call_result = await execute(fi.func, **args) # Not supporting Extensions + else: + _loop = get_current_loop() + call_result = await _loop.run_in_executor( + threadpool, + 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 = [] + if fi.output_types: + for out_name, out_type_info in fi.output_types.items(): + val = args[out_name].get() + if val is None: + # TODO: is the "Out" parameter optional? + # Can "None" be marshaled into protos.TypedData? + continue + + param_binding = to_outgoing_param_binding( + out_type_info.binding_name, val, + pytype=out_type_info.pytype, + out_name=out_name, + protos=protos) + output_data.append(param_binding) + + return_value = None + if fi.return_type is not None and not http_v2_enabled: + return_value = to_outgoing_proto( + fi.return_type.binding_name, + call_result, + pytype=fi.return_type.pytype, + protos=protos + ) + + # Actively flush customer print() function to console + sys.stdout.flush() + return protos.InvocationResponse( + invocation_id=invocation_id, + return_value=return_value, + result=protos.StatusResult( + status=protos.StatusResult.Success), + output_data=output_data) + + except Exception as ex: + if http_v2_enabled: + http_coordinator.set_http_response(invocation_id, ex) + global metadata_result + metadata_result = ex + return protos.InvocationResponse( + invocation_id=invocation_id, + result=protos.StatusResult( + status=protos.StatusResult.Failure, + exception=serialize_exception(ex))) + + +async def function_environment_reload_request(request): + """Only runs on Linux Consumption placeholder specialization. + This is called only when placeholder mode is true. On worker restarts + worker init request will be called directly. + """ + logger.info("Library Worker: received worker_env_reload_request") + try: + + func_env_reload_request = \ + request.request.function_environment_reload_request + directory = func_env_reload_request.function_app_directory + + if is_envvar_true(PYTHON_ENABLE_DEBUG_LOGGING): + root_logger = logging.getLogger("azure.functions") + root_logger.setLevel(logging.DEBUG) + + # calling load_binding_registry again since the + # reload_customer_libraries call clears the registry + load_binding_registry() + + capabilities = {} + if get_app_setting( + setting=PYTHON_ENABLE_OPENTELEMETRY, + default_value=PYTHON_ENABLE_OPENTELEMETRY_DEFAULT): + initialize_azure_monitor() + + if otel_manager.get_azure_monitor_available(): + capabilities[WORKER_OPEN_TELEMETRY_ENABLED] = ( + TRUE) + + try: + global _host, result, protos + _host = request.properties.get("host") + protos = request.properties.get("protos") + result = asyncio.create_task(load_function_metadata(directory, + caller_info="environment_reload_request")) + if get_app_setting(setting=PYTHON_ENABLE_INIT_INDEXING): # PYTHON_ENABLE_HTTP_STREAMING + capabilities[HTTP_URI] = \ + initialize_http_server(_host) + capabilities[REQUIRES_ROUTE_PARAMETERS] = TRUE + except HttpServerInitError: + raise + + # Change function app directory + if getattr(func_env_reload_request, + 'function_app_directory', None): + change_cwd( + func_env_reload_request.function_app_directory) + + return protos.FunctionEnvironmentReloadResponse( + capabilities=capabilities, + worker_metadata=get_worker_metadata(protos), + result=protos.StatusResult( + status=protos.StatusResult.Success)) + + except Exception as ex: + global metadata_exception + metadata_exception = ex + return protos.FunctionEnvironmentReloadResponse( + result=protos.StatusResult( + status=protos.StatusResult.Failure, + exception=serialize_exception(ex))) + + +async def load_function_metadata(function_app_directory, caller_info): + global protos + """ + This method is called to index the functions in the function app + directory and save the results in function_metadata_result or + function_metadata_exception in case of an exception. + """ + try: + script_file_name = get_app_setting( + setting=PYTHON_SCRIPT_FILE_NAME, + default_value=f'{PYTHON_SCRIPT_FILE_NAME_DEFAULT}') + + logger.debug( + 'Received load metadata request from %s, ' + 'script_file_name: %s', + caller_info, script_file_name) + + validate_script_file_name(script_file_name) + function_path = os.path.join(function_app_directory, + script_file_name) + + # For V1, the function path will not exist and + # return None. + global metadata_result + metadata_result = (index_functions(function_path, function_app_directory)) \ + if os.path.exists(function_path) else None + except Exception as ex: + global metadata_exception + metadata_exception = ex + + +def index_functions(function_path: str, function_dir: str): + global protos + indexed_functions = index_function_app(function_path) + logger.info( + "Indexed function app and found %s functions", + len(indexed_functions) + ) + + if indexed_functions: + fx_metadata_results, fx_bindings_logs = ( + process_indexed_function( + protos, + _functions, + indexed_functions, + function_dir)) + + indexed_function_logs: List[str] = [] + indexed_function_bindings_logs = [] + for func in indexed_functions: + func_binding_logs = fx_bindings_logs.get(func) + for binding in func.get_bindings(): + deferred_binding_info = func_binding_logs.get( + binding.name)\ + if func_binding_logs.get(binding.name) else "" + indexed_function_bindings_logs.append(( + binding.type, binding.name, deferred_binding_info)) + + function_log = "Function Name: {}, Function Binding: {}" \ + .format(func.get_function_name(), + indexed_function_bindings_logs) + indexed_function_logs.append(function_log) + + logger.info( + 'Successfully processed FunctionMetadataRequest for ' + 'functions: %s. Deferred bindings enabled: %s.', " ".join( + indexed_function_logs), + _functions.deferred_bindings_enabled()) + + return fx_metadata_results diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py index 4eeeea9d9..87546b5f2 100644 --- a/azure_functions_worker/http_v2.py +++ b/azure_functions_worker/http_v2.py @@ -8,13 +8,13 @@ import sys from typing import Dict -from azure_functions_worker.constants import ( +from .utils.constants import ( BASE_EXT_SUPPORTED_PY_MINOR_VERSION, PYTHON_ENABLE_INIT_INDEXING, X_MS_INVOCATION_ID, ) from azure_functions_worker.logging import logger -from azure_functions_worker.utils.common import is_envvar_false +from .utils.env_state import is_envvar_false # Http V2 Exceptions diff --git a/azure_functions_worker/loader.py b/azure_functions_worker/loader.py index ce96c1406..4d1acc6af 100644 --- a/azure_functions_worker/loader.py +++ b/azure_functions_worker/loader.py @@ -1,22 +1,23 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -"""Python functions loader.""" + import importlib import importlib.machinery -import os import os.path import pathlib import sys import time + from datetime import timedelta -from os import PathLike, fspath -from typing import Dict, Optional +from typing import Dict, Optional, Union -from google.protobuf.duration_pb2 import Duration -from . import bindings, functions, protos +from .functions import Registry +from .logging import logger + +from .bindings.meta import get_deferred_raw_bindings from .bindings.retrycontext import RetryPolicy -from .constants import ( +from .utils.constants import ( CUSTOMER_PACKAGES_PATH, METADATA_PROPERTIES_WORKER_INDEXED, MODULE_NOT_FOUND_TS_URL, @@ -25,8 +26,7 @@ PYTHON_SCRIPT_FILE_NAME_DEFAULT, RETRY_POLICY, ) -from .logging import logger -from .utils.common import get_app_setting +from .utils.env_state import get_app_setting from .utils.wrappers import attach_message_to_exception _AZURE_NAMESPACE = '__app__' @@ -35,34 +35,13 @@ _submodule_dirs = [] -def register_function_dir(path: PathLike) -> None: - try: - _submodule_dirs.append(fspath(path)) - except TypeError as e: - raise RuntimeError(f'Path ({path}) is incompatible with fspath. ' - f'It is of type {type(path)}.', e) - - -def install() -> None: - if _AZURE_NAMESPACE not in sys.modules: - # Create and register the __app__ namespace package. - ns_spec = importlib.machinery.ModuleSpec(_AZURE_NAMESPACE, None) - ns_spec.submodule_search_locations = _submodule_dirs - ns_pkg = importlib.util.module_from_spec(ns_spec) - sys.modules[_AZURE_NAMESPACE] = ns_pkg - - def convert_to_seconds(timestr: str): x = time.strptime(timestr, '%H:%M:%S') return int(timedelta(hours=x.tm_hour, minutes=x.tm_min, seconds=x.tm_sec).total_seconds()) -def uninstall() -> None: - pass - - -def build_binding_protos(indexed_function) -> Dict: +def build_binding_protos(protos, indexed_function) -> Dict: binding_protos = {} for binding in indexed_function.get_bindings(): binding_protos[binding.name] = protos.BindingInfo( @@ -73,7 +52,7 @@ def build_binding_protos(indexed_function) -> Dict: return binding_protos -def build_retry_protos(indexed_function) -> Dict: +def build_retry_protos(protos, indexed_function) -> Dict: retry = get_retry_settings(indexed_function) if not retry: @@ -84,9 +63,9 @@ def build_retry_protos(indexed_function) -> Dict: retry_strategy = retry.get(RetryPolicy.STRATEGY.value) if strategy == "fixed_delay": - return build_fixed_delay_retry(retry, max_retry_count, retry_strategy) + return build_fixed_delay_retry(protos, retry, max_retry_count, retry_strategy) else: - return build_variable_interval_retry(retry, max_retry_count, + return build_variable_interval_retry(protos, retry, max_retry_count, retry_strategy) @@ -98,8 +77,8 @@ def get_retry_settings(indexed_function): return None -def build_fixed_delay_retry(retry, max_retry_count, retry_strategy): - delay_interval = Duration( +def build_fixed_delay_retry(protos, retry, max_retry_count, retry_strategy): + delay_interval = protos.Duration( seconds=convert_to_seconds(retry.get(RetryPolicy.DELAY_INTERVAL.value)) ) return protos.RpcRetryOptions( @@ -109,12 +88,12 @@ def build_fixed_delay_retry(retry, max_retry_count, retry_strategy): ) -def build_variable_interval_retry(retry, max_retry_count, retry_strategy): - minimum_interval = Duration( +def build_variable_interval_retry(protos, retry, max_retry_count, retry_strategy): + minimum_interval = protos.Duration( seconds=convert_to_seconds( retry.get(RetryPolicy.MINIMUM_INTERVAL.value)) ) - maximum_interval = Duration( + maximum_interval = protos.Duration( seconds=convert_to_seconds( retry.get(RetryPolicy.MAXIMUM_INTERVAL.value)) ) @@ -126,7 +105,8 @@ def build_variable_interval_retry(retry, max_retry_count, retry_strategy): ) -def process_indexed_function(functions_registry: functions.Registry, +def process_indexed_function(protos, + functions_registry: Registry, indexed_functions, function_dir): """ fx_metadata_results is a list of the RpcFunctionMetadata for @@ -143,10 +123,10 @@ def process_indexed_function(functions_registry: functions.Registry, fx_bindings_logs = {} for indexed_function in indexed_functions: function_info = functions_registry.add_indexed_function( - function=indexed_function) + function=indexed_function, protos=protos) - binding_protos = build_binding_protos(indexed_function) - retry_protos = build_retry_protos(indexed_function) + binding_protos = build_binding_protos(protos, indexed_function) + retry_protos = build_retry_protos(protos, indexed_function) raw_bindings, bindings_logs = get_fx_raw_bindings( indexed_function=indexed_function, @@ -172,62 +152,6 @@ def process_indexed_function(functions_registry: functions.Registry, return fx_metadata_results, fx_bindings_logs -@attach_message_to_exception( - expt_type=ImportError, - message='Cannot find module. Please check the requirements.txt ' - 'file for the missing module. For more info, ' - 'please refer the troubleshooting ' - f'guide: {MODULE_NOT_FOUND_TS_URL}. ' - f'Current sys.path: {sys.path}', - debug_logs='Error in load_function. ' - f'Sys Path: {sys.path}, Sys Module: {sys.modules},' - 'python-packages Path exists: ' - f'{os.path.exists(CUSTOMER_PACKAGES_PATH)}') -def load_function(name: str, directory: str, script_file: str, - entry_point: Optional[str]): - dir_path = pathlib.Path(directory) - script_path = pathlib.Path(script_file) if script_file else pathlib.Path( - _DEFAULT_SCRIPT_FILENAME) - if not entry_point: - entry_point = _DEFAULT_ENTRY_POINT - - register_function_dir(dir_path.parent) - - try: - rel_script_path = script_path.relative_to(dir_path.parent) - except ValueError: - raise RuntimeError( - f'script path {script_file} is not relative to the specified ' - f'directory {directory}' - ) - - last_part = rel_script_path.parts[-1] - modname, ext = os.path.splitext(last_part) - if ext != '.py': - raise RuntimeError( - f'cannot load function {name}: ' - f'invalid Python filename {script_file}') - - modname_parts = [_AZURE_NAMESPACE] - modname_parts.extend(rel_script_path.parts[:-1]) - - # If the __init__.py contains the code, we should avoid double loading. - if modname.lower() != '__init__': - modname_parts.append(modname) - - fullmodname = '.'.join(modname_parts) - - mod = importlib.import_module(fullmodname) - - func = getattr(mod, entry_point, None) - if func is None or not callable(func): - raise RuntimeError( - f'cannot load function {name}: function {entry_point}() is not ' - f'present in {rel_script_path}') - - return func - - @attach_message_to_exception( expt_type=ImportError, message='Cannot find module. Please check the requirements.txt ' @@ -278,7 +202,7 @@ def get_fx_raw_bindings(indexed_function, function_info): for this function. """ if function_info.deferred_bindings_enabled: - raw_bindings, bindings_logs = bindings.get_deferred_raw_bindings( + raw_bindings, bindings_logs = get_deferred_raw_bindings( indexed_function, function_info.input_types) return raw_bindings, bindings_logs diff --git a/azure_functions_worker/logging.py b/azure_functions_worker/logging.py index adb5ff294..5591de7c8 100644 --- a/azure_functions_worker/logging.py +++ b/azure_functions_worker/logging.py @@ -1,25 +1,14 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import logging import logging.handlers import sys import traceback -from typing import Optional # Logging Prefixes -CONSOLE_LOG_PREFIX = "LanguageWorkerConsoleLog" -SYSTEM_LOG_PREFIX = "azure_functions_worker" SDK_LOG_PREFIX = "azure.functions" -SYSTEM_ERROR_LOG_PREFIX = "azure_functions_worker_errors" - -logger: logging.Logger = logging.getLogger(SYSTEM_LOG_PREFIX) -error_logger: logging.Logger = ( - logging.getLogger(SYSTEM_ERROR_LOG_PREFIX)) - -handler: Optional[logging.Handler] = None -error_handler: Optional[logging.Handler] = None +logger: logging.Logger = logging.getLogger(SDK_LOG_PREFIX) def format_exception(exception: Exception) -> str: @@ -34,74 +23,3 @@ def format_exception(exception: Exception) -> str: else: msg = str(exception) return msg - - -def setup(log_level, log_destination): - # Since handler and error_handler are moved to the global scope, - # before assigning to these handlers, we should define 'global' keyword - global handler - global error_handler - - if log_level == 'TRACE': - log_level = 'DEBUG' - - formatter = logging.Formatter(f'{CONSOLE_LOG_PREFIX}' - ' %(levelname)s: %(message)s') - - if log_destination is None: - # With no explicit log destination we do split logging, - # errors go into stderr, everything else -- to stdout. - error_handler = logging.StreamHandler(sys.stderr) - error_handler.setFormatter(formatter) - error_handler.setLevel(getattr(logging, log_level)) - - handler = logging.StreamHandler(sys.stdout) - - elif log_destination in ('stdout', 'stderr'): - handler = logging.StreamHandler(getattr(sys, log_destination)) - - elif log_destination == 'syslog': - handler = logging.handlers.SysLogHandler() - - else: - handler = logging.FileHandler(log_destination) - - if error_handler is None: - error_handler = handler - - handler.setFormatter(formatter) - handler.setLevel(getattr(logging, log_level)) - - logger.addHandler(handler) - logger.setLevel(getattr(logging, log_level)) - - error_logger.addHandler(error_handler) - error_logger.setLevel(getattr(logging, log_level)) - - -def disable_console_logging() -> None: - # We should only remove the sys.stdout stream, as error_logger is used for - # unexpected critical error logs handling. - if logger and handler: - handler.flush() - logger.removeHandler(handler) - - -def enable_console_logging() -> None: - if logger and handler: - logger.addHandler(handler) - - -def is_system_log_category(ctg: str) -> bool: - """Check if the logging namespace belongs to system logs. Category starts - with the following name will be treated as system logs. - 1. 'azure_functions_worker' (Worker Info) - 2. 'azure_functions_worker_errors' (Worker Error) - 3. 'azure.functions' (SDK) - - Expected behaviors for sytem logs and customer logs are listed below: - local_console customer_app_insight functions_kusto_table - system_log false false true - customer_log true true false - """ - return ctg.startswith(SYSTEM_LOG_PREFIX) or ctg.startswith(SDK_LOG_PREFIX) diff --git a/azure_functions_worker/main.py b/azure_functions_worker/main.py deleted file mode 100644 index 130e0e9ea..000000000 --- a/azure_functions_worker/main.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Main entrypoint.""" - -import argparse - - -def parse_args(): - parser = argparse.ArgumentParser( - description='Python Azure Functions Worker') - parser.add_argument('--host', - help="host address") - parser.add_argument('--port', type=int, - help='port number') - parser.add_argument('--workerId', dest='worker_id', - help='id for the worker') - parser.add_argument('--requestId', dest='request_id', - help='id of the request') - parser.add_argument('--log-level', type=str, default='INFO', - choices=['TRACE', 'INFO', 'WARNING', 'ERROR'], - help="log level: 'TRACE', 'INFO', 'WARNING', " - "or 'ERROR'") - parser.add_argument('--log-to', type=str, default=None, - help='log destination: stdout, stderr, ' - 'syslog, or a file path') - parser.add_argument('--grpcMaxMessageLength', type=int, - dest='grpc_max_msg_len') - parser.add_argument('--functions-uri', dest='functions_uri', type=str, - help='URI with IP Address and Port used to' - ' connect to the Host via gRPC.') - parser.add_argument('--functions-request-id', dest='functions_request_id', - type=str, help='Request ID used for gRPC communication ' - 'with the Host.') - parser.add_argument('--functions-worker-id', - dest='functions_worker_id', type=str, - help='Worker ID assigned to this language worker.') - parser.add_argument('--functions-grpc-max-message-length', type=int, - dest='functions_grpc_max_msg_len', - help='Max grpc message length for Functions') - return parser.parse_args() - - -def main(): - from .utils.dependency import DependencyManager - DependencyManager.initialize() - DependencyManager.use_worker_dependencies() - - import asyncio - - from . import logging - from .logging import error_logger, format_exception, logger - - args = parse_args() - logging.setup(log_level=args.log_level, log_destination=args.log_to) - - logger.info('Starting Azure Functions Python Worker.') - logger.info('Worker ID: %s, Request ID: %s, Host Address: %s:%s', - args.worker_id, args.request_id, args.host, args.port) - - try: - return asyncio.run(start_async( - args.host, args.port, args.worker_id, args.request_id)) - except Exception as ex: - error_logger.exception( - 'unhandled error in functions worker: {0}'.format( - format_exception(ex))) - raise - - -async def start_async(host, port, worker_id, request_id): - from . import dispatcher - - disp = await dispatcher.Dispatcher.connect(host=host, port=port, - worker_id=worker_id, - request_id=request_id, - connect_timeout=5.0) - - await disp.dispatch_forever() diff --git a/azure_functions_worker/otel.py b/azure_functions_worker/otel.py new file mode 100644 index 000000000..cadb3ca3a --- /dev/null +++ b/azure_functions_worker/otel.py @@ -0,0 +1,106 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os + +from .logging import logger + +from .utils.env_state import get_app_setting +from .utils.constants import (APPLICATIONINSIGHTS_CONNECTION_STRING, + PYTHON_AZURE_MONITOR_LOGGER_NAME, + PYTHON_AZURE_MONITOR_LOGGER_NAME_DEFAULT, + TRACESTATE, TRACEPARENT) + + +class OTelManager: + def __init__(self): + self._azure_monitor_available = False + self._context_api = None + self._trace_context_propagator = None + + def set_azure_monitor_available(self, azure_monitor_available): + self._azure_monitor_available = azure_monitor_available + + def get_azure_monitor_available(self): + return self._azure_monitor_available + + def set_context_api(self, context_api): + self._context_api = context_api + + def get_context_api(self): + return self._context_api + + def set_trace_context_propagator(self, trace_context_propagator): + self._trace_context_propagator = trace_context_propagator + + def get_trace_context_propagator(self): + return self._trace_context_propagator + + +def update_opentelemetry_status(): + """Check for OpenTelemetry library availability and + update the status attribute.""" + try: + from opentelemetry import context as context_api + from opentelemetry.trace.propagation.tracecontext import ( + TraceContextTextMapPropagator, + ) + + OTelManager.set_context_api(context_api) + OTelManager.set_trace_context_propagator(TraceContextTextMapPropagator()) + + except ImportError: + logger.exception( + "Cannot import OpenTelemetry libraries." + ) + + +def initialize_azure_monitor(): + """Initializes OpenTelemetry and Azure monitor distro + """ + update_opentelemetry_status() + try: + from azure.monitor.opentelemetry import configure_azure_monitor + + # Set functions resource detector manually until officially + # include in Azure monitor distro + os.environ.setdefault( + "OTEL_EXPERIMENTAL_RESOURCE_DETECTORS", + "azure_functions", + ) + + configure_azure_monitor( + # Connection string can be explicitly specified in Appsetting + # If not set, defaults to env var + # APPLICATIONINSIGHTS_CONNECTION_STRING + connection_string=get_app_setting( + setting=APPLICATIONINSIGHTS_CONNECTION_STRING + ), + logger_name=get_app_setting( + setting=PYTHON_AZURE_MONITOR_LOGGER_NAME, + default_value=PYTHON_AZURE_MONITOR_LOGGER_NAME_DEFAULT + ), + ) + OTelManager.set_azure_monitor_available(True) + + logger.info("Successfully configured Azure monitor distro.") + except ImportError: + logger.exception( + "Cannot import Azure Monitor distro." + ) + OTelManager.set_azure_monitor_available(False) + except Exception: + logger.exception( + "Error initializing Azure monitor distro." + ) + OTelManager.set_azure_monitor_available(False) + + +def configure_opentelemetry(invocation_context): + carrier = {TRACEPARENT: invocation_context.trace_context.trace_parent, + TRACESTATE: invocation_context.trace_context.trace_state} + ctx = OTelManager.get_trace_context_propagator().extract(carrier) + OTelManager.get_context_api().attach(ctx) + + +otel_manager = OTelManager() diff --git a/azure_functions_worker/protos/.gitignore b/azure_functions_worker/protos/.gitignore deleted file mode 100644 index f43e6c214..000000000 --- a/azure_functions_worker/protos/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/_src -*_pb2.py -*_pb2_grpc.py diff --git a/azure_functions_worker/protos/_src/.gitignore b/azure_functions_worker/protos/_src/.gitignore deleted file mode 100644 index 940794e60..000000000 --- a/azure_functions_worker/protos/_src/.gitignore +++ /dev/null @@ -1,288 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ -**/Properties/launchSettings.json - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Typescript v1 declaration files -typings/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs diff --git a/azure_functions_worker/protos/_src/LICENSE b/azure_functions_worker/protos/_src/LICENSE deleted file mode 100644 index 21071075c..000000000 --- a/azure_functions_worker/protos/_src/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ - MIT License - - Copyright (c) Microsoft Corporation. All rights reserved. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE diff --git a/azure_functions_worker/protos/_src/README.md b/azure_functions_worker/protos/_src/README.md deleted file mode 100644 index b22f0bb4b..000000000 --- a/azure_functions_worker/protos/_src/README.md +++ /dev/null @@ -1,98 +0,0 @@ -# Azure Functions Languge Worker Protobuf - -This repository contains the protobuf definition file which defines the gRPC service which is used between the [Azure Functions Host](https://github.com/Azure/azure-functions-host) and the Azure Functions language workers. This repo is shared across many repos in many languages (for each worker) by using git commands. - -To use this repo in Azure Functions language workers, follow steps below to add this repo as a subtree (*Adding This Repo*). If this repo is already embedded in a language worker repo, follow the steps to update the consumed file (*Pulling Updates*). - -Learn more about Azure Function's projects on the [meta](https://github.com/azure/azure-functions) repo. - -## Adding This Repo - -From within the Azure Functions language worker repo: -1. Define remote branch for cleaner git commands - - `git remote add proto-file https://github.com/azure/azure-functions-language-worker-protobuf.git` - - `git fetch proto-file` -2. Index contents of azure-functions-worker-protobuf to language worker repo - - `git read-tree --prefix= -u proto-file/` -3. Add new path in language worker repo to .gitignore file - - In .gitignore, add path in language worker repo -4. Finalize with commit - - `git commit -m "Added subtree from https://github.com/azure/azure-functions-language-worker-protobuf. Branch: . Commit: "` - - `git push` - -## Pulling Updates - -From within the Azure Functions language worker repo: -1. Define remote branch for cleaner git commands - - `git remote add proto-file https://github.com/azure/azure-functions-language-worker-protobuf.git` - - `git fetch proto-file` -2. Pull a specific release tag - - `git fetch proto-file refs/tags/` - - Example: `git fetch proto-file refs/tags/v1.1.0-protofile` -3. Merge updates - - Merge with an explicit path to subtree: `git merge -X subtree= --squash --allow-unrelated-histories --strategy-option theirs` - - Example: `git merge -X subtree=src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf --squash v1.1.0-protofile --allow-unrelated-histories --strategy-option theirs` -4. Finalize with commit - - `git commit -m "Updated subtree from https://github.com/azure/azure-functions-language-worker-protobuf. Tag: . Commit: "` - - `git push` - -## Releasing a Language Worker Protobuf version - -1. Draft a release in the GitHub UI - - Be sure to inculde details of the release -2. Create a release version, following semantic versioning guidelines ([semver.org](https://semver.org/)) -3. Tag the version with the pattern: `v..

-protofile` (example: `v1.1.0-protofile`) -3. Merge `dev` to `master` - -## Consuming FunctionRPC.proto -*Note: Update versionNumber before running following commands* - -## CSharp -``` -set NUGET_PATH="%UserProfile%\.nuget\packages" -set GRPC_TOOLS_PATH=%NUGET_PATH%\grpc.tools\\tools\windows_x86 -set PROTO_PATH=.\azure-functions-language-worker-protobuf\src\proto -set PROTO=.\azure-functions-language-worker-protobuf\src\proto\FunctionRpc.proto -set PROTOBUF_TOOLS=%NUGET_PATH%\google.protobuf.tools\\tools -set MSGDIR=.\Messages - -if exist %MSGDIR% rmdir /s /q %MSGDIR% -mkdir %MSGDIR% - -set OUTDIR=%MSGDIR%\DotNet -mkdir %OUTDIR% -%GRPC_TOOLS_PATH%\protoc.exe %PROTO% --csharp_out %OUTDIR% --grpc_out=%OUTDIR% --plugin=protoc-gen-grpc=%GRPC_TOOLS_PATH%\grpc_csharp_plugin.exe --proto_path=%PROTO_PATH% --proto_path=%PROTOBUF_TOOLS% -``` -## JavaScript -In package.json, add to the build script the following commands to build .js files and to build .ts files. Use and install npm package `protobufjs`. - -Generate JavaScript files: -``` -pbjs -t json-module -w commonjs -o azure-functions-language-worker-protobuf/src/rpc.js azure-functions-language-worker-protobuf/src/proto/FunctionRpc.proto -``` -Generate TypeScript files: -``` -pbjs -t static-module azure-functions-language-worker-protobuf/src/proto/FunctionRpc.proto -o azure-functions-language-worker-protobuf/src/rpc_static.js && pbts -o azure-functions-language-worker-protobuf/src/rpc.d.ts azure-functions-language-worker-protobuf/src/rpc_static.js -``` - -## Java -Maven plugin : [protobuf-maven-plugin](https://www.xolstice.org/protobuf-maven-plugin/) -In pom.xml add following under configuration for this plugin -${basedir}//azure-functions-language-worker-protobuf/src/proto - -## Python ---TODO - -## Contributing - -This project welcomes contributions and suggestions. Most contributions require you to agree to a -Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.microsoft.com. - -When you submit a pull request, a CLA-bot will automatically determine whether you need to provide -a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/azure_functions_worker/protos/_src/src/proto/FunctionRpc.proto b/azure_functions_worker/protos/_src/src/proto/FunctionRpc.proto deleted file mode 100644 index f48bc7bbe..000000000 --- a/azure_functions_worker/protos/_src/src/proto/FunctionRpc.proto +++ /dev/null @@ -1,730 +0,0 @@ -syntax = "proto3"; -// protobuf vscode extension: https://marketplace.visualstudio.com/items?itemName=zxh404.vscode-proto3 - -option java_multiple_files = true; -option java_package = "com.microsoft.azure.functions.rpc.messages"; -option java_outer_classname = "FunctionProto"; -option csharp_namespace = "Microsoft.Azure.WebJobs.Script.Grpc.Messages"; -option go_package ="github.com/Azure/azure-functions-go-worker/internal/rpc"; - -package AzureFunctionsRpcMessages; - -import "google/protobuf/duration.proto"; -import "identity/ClaimsIdentityRpc.proto"; -import "shared/NullableTypes.proto"; - -// Interface exported by the server. -service FunctionRpc { - rpc EventStream (stream StreamingMessage) returns (stream StreamingMessage) {} -} - -message StreamingMessage { - // Used to identify message between host and worker - string request_id = 1; - - // Payload of the message - oneof content { - - // Worker initiates stream - StartStream start_stream = 20; - - // Host sends capabilities/init data to worker - WorkerInitRequest worker_init_request = 17; - // Worker responds after initializing with its capabilities & status - WorkerInitResponse worker_init_response = 16; - - // MESSAGE NOT USED - // Worker periodically sends empty heartbeat message to host - WorkerHeartbeat worker_heartbeat = 15; - - // Host sends terminate message to worker. - // Worker terminates if it can, otherwise host terminates after a grace period - WorkerTerminate worker_terminate = 14; - - // Host periodically sends status request to the worker - WorkerStatusRequest worker_status_request = 12; - WorkerStatusResponse worker_status_response = 13; - - // On file change event, host sends notification to worker - FileChangeEventRequest file_change_event_request = 6; - - // Worker requests a desired action (restart worker, reload function) - WorkerActionResponse worker_action_response = 7; - - // Host sends required metadata to worker to load function - FunctionLoadRequest function_load_request = 8; - // Worker responds after loading with the load result - FunctionLoadResponse function_load_response = 9; - - // Host requests a given invocation - InvocationRequest invocation_request = 4; - - // Worker responds to a given invocation - InvocationResponse invocation_response = 5; - - // Host sends cancel message to attempt to cancel an invocation. - // If an invocation is cancelled, host will receive an invocation response with status cancelled. - InvocationCancel invocation_cancel = 21; - - // Worker logs a message back to the host - RpcLog rpc_log = 2; - - FunctionEnvironmentReloadRequest function_environment_reload_request = 25; - - FunctionEnvironmentReloadResponse function_environment_reload_response = 26; - - // Ask the worker to close any open shared memory resources for a given invocation - CloseSharedMemoryResourcesRequest close_shared_memory_resources_request = 27; - CloseSharedMemoryResourcesResponse close_shared_memory_resources_response = 28; - - // Worker indexing message types - FunctionsMetadataRequest functions_metadata_request = 29; - FunctionMetadataResponse function_metadata_response = 30; - - // Host sends required metadata to worker to load functions - FunctionLoadRequestCollection function_load_request_collection = 31; - - // Host gets the list of function load responses - FunctionLoadResponseCollection function_load_response_collection = 32; - - // Host sends required metadata to worker to warmup the worker - WorkerWarmupRequest worker_warmup_request = 33; - - // Worker responds after warming up with the warmup result - WorkerWarmupResponse worker_warmup_response = 34; - - } -} - -// Process.Start required info -// connection details -// protocol type -// protocol version - -// Worker sends the host information identifying itself -message StartStream { - // id of the worker - string worker_id = 2; -} - -// Host requests the worker to initialize itself -message WorkerInitRequest { - // version of the host sending init request - string host_version = 1; - - // A map of host supported features/capabilities - map capabilities = 2; - - // inform worker of supported categories and their levels - // i.e. Worker = Verbose, Function.MyFunc = None - map log_categories = 3; - - // Full path of worker.config.json location - string worker_directory = 4; - - // base directory for function app - string function_app_directory = 5; -} - -// Worker responds with the result of initializing itself -message WorkerInitResponse { - // PROPERTY NOT USED - // TODO: Remove from protobuf during next breaking change release - string worker_version = 1; - - // A map of worker supported features/capabilities - map capabilities = 2; - - // Status of the response - StatusResult result = 3; - - // Worker metadata captured for telemetry purposes - WorkerMetadata worker_metadata = 4; -} - -message WorkerMetadata { - // The runtime/stack name - string runtime_name = 1; - - // The version of the runtime/stack - string runtime_version = 2; - - // The version of the worker - string worker_version = 3; - - // The worker bitness/architecture - string worker_bitness = 4; - - // Optional additional custom properties - map custom_properties = 5; -} - -// Used by the host to determine success/failure/cancellation -message StatusResult { - // Indicates Failure/Success/Cancelled - enum Status { - Failure = 0; - Success = 1; - Cancelled = 2; - } - - // Status for the given result - Status status = 4; - - // Specific message about the result - string result = 1; - - // Exception message (if exists) for the status - RpcException exception = 2; - - // Captured logs or relevant details can use the logs property - repeated RpcLog logs = 3; -} - -// MESSAGE NOT USED -// TODO: Remove from protobuf during next breaking change release -message WorkerHeartbeat {} - -// Warning before killing the process after grace_period -// Worker self terminates ..no response on this -message WorkerTerminate { - google.protobuf.Duration grace_period = 1; -} - -// Host notifies worker of file content change -message FileChangeEventRequest { - // Types of File change operations (See link for more info: https://msdn.microsoft.com/en-us/library/t6xf43e0(v=vs.110).aspx) - enum Type { - Unknown = 0; - Created = 1; - Deleted = 2; - Changed = 4; - Renamed = 8; - All = 15; - } - - // type for this event - Type type = 1; - - // full file path for the file change notification - string full_path = 2; - - // Name of the function affected - string name = 3; -} - -// Indicates whether worker reloaded successfully or needs a restart -message WorkerActionResponse { - // indicates whether a restart is needed, or reload successfully - enum Action { - Restart = 0; - Reload = 1; - } - - // action for this response - Action action = 1; - - // text reason for the response - string reason = 2; -} - -// Used by the host to determine worker health -message WorkerStatusRequest { -} - -// Worker responds with status message -// TODO: Add any worker relevant status to response -message WorkerStatusResponse { -} - -message FunctionEnvironmentReloadRequest { - // Environment variables from the current process - map environment_variables = 1; - // Current directory of function app - string function_app_directory = 2; -} - -message FunctionEnvironmentReloadResponse { - // After specialization, worker sends capabilities & metadata. - // Worker metadata captured for telemetry purposes - WorkerMetadata worker_metadata = 1; - - // A map of worker supported features/capabilities - map capabilities = 2; - - // Status of the response - StatusResult result = 3; -} - -// Tell the out-of-proc worker to close any shared memory maps it allocated for given invocation -message CloseSharedMemoryResourcesRequest { - repeated string map_names = 1; -} - -// Response from the worker indicating which of the shared memory maps have been successfully closed and which have not been closed -// The key (string) is the map name and the value (bool) is true if it was closed, false if not -message CloseSharedMemoryResourcesResponse { - map close_map_results = 1; -} - -// Host tells the worker to load a list of Functions -message FunctionLoadRequestCollection { - repeated FunctionLoadRequest function_load_requests = 1; -} - -// Host gets the list of function load responses -message FunctionLoadResponseCollection { - repeated FunctionLoadResponse function_load_responses = 1; -} - -// Load request of a single Function -message FunctionLoadRequest { - // unique function identifier (avoid name collisions, facilitate reload case) - string function_id = 1; - - // Metadata for the request - RpcFunctionMetadata metadata = 2; - - // A flag indicating if managed dependency is enabled or not - bool managed_dependency_enabled = 3; -} - -// Worker tells host result of reload -message FunctionLoadResponse { - // unique function identifier - string function_id = 1; - - // Result of load operation - StatusResult result = 2; - // TODO: return type expected? - - // Result of load operation - bool is_dependency_downloaded = 3; -} - -// Information on how a Function should be loaded and its bindings -message RpcFunctionMetadata { - // TODO: do we want the host's name - the language worker might do a better job of assignment than the host - string name = 4; - - // base directory for the Function - string directory = 1; - - // Script file specified - string script_file = 2; - - // Entry point specified - string entry_point = 3; - - // Bindings info - map bindings = 6; - - // Is set to true for proxy - bool is_proxy = 7; - - // Function indexing status - StatusResult status = 8; - - // Function language - string language = 9; - - // Raw binding info - repeated string raw_bindings = 10; - - // unique function identifier (avoid name collisions, facilitate reload case) - string function_id = 13; - - // A flag indicating if managed dependency is enabled or not - bool managed_dependency_enabled = 14; - - // The optional function execution retry strategy to use on invocation failures. - RpcRetryOptions retry_options = 15; - - // Properties for function metadata - // They're usually specific to a worker and largely passed along to the controller API for use - // outside the host - map properties = 16; -} - -// Host tells worker it is ready to receive metadata -message FunctionsMetadataRequest { - // base directory for function app - string function_app_directory = 1; -} - -// Worker sends function metadata back to host -message FunctionMetadataResponse { - // list of function indexing responses - repeated RpcFunctionMetadata function_metadata_results = 1; - - // status of overall metadata request - StatusResult result = 2; - - // if set to true then host will perform indexing - bool use_default_metadata_indexing = 3; -} - -// Host requests worker to invoke a Function -message InvocationRequest { - // Unique id for each invocation - string invocation_id = 1; - - // Unique id for each Function - string function_id = 2; - - // Input bindings (include trigger) - repeated ParameterBinding input_data = 3; - - // binding metadata from trigger - map trigger_metadata = 4; - - // Populates activityId, tracestate and tags from host - RpcTraceContext trace_context = 5; - - // Current retry context - RetryContext retry_context = 6; -} - -// Host sends ActivityId, traceStateString and Tags from host -message RpcTraceContext { - // This corresponds to Activity.Current?.Id - string trace_parent = 1; - - // This corresponds to Activity.Current?.TraceStateString - string trace_state = 2; - - // This corresponds to Activity.Current?.Tags - map attributes = 3; -} - -// Host sends retry context for a function invocation -message RetryContext { - // Current retry count - int32 retry_count = 1; - - // Max retry count - int32 max_retry_count = 2; - - // Exception that caused the retry - RpcException exception = 3; -} - -// Host requests worker to cancel invocation -message InvocationCancel { - // Unique id for invocation - string invocation_id = 2; - - // PROPERTY NOT USED - google.protobuf.Duration grace_period = 1; -} - -// Worker responds with status of Invocation -message InvocationResponse { - // Unique id for invocation - string invocation_id = 1; - - // Output binding data - repeated ParameterBinding output_data = 2; - - // data returned from Function (for $return and triggers with return support) - TypedData return_value = 4; - - // Status of the invocation (success/failure/canceled) - StatusResult result = 3; -} - -message WorkerWarmupRequest { - // Full path of worker.config.json location - string worker_directory = 1; -} - -message WorkerWarmupResponse { - StatusResult result = 1; -} - -// Used to encapsulate data which could be a variety of types -message TypedData { - oneof data { - string string = 1; - string json = 2; - bytes bytes = 3; - bytes stream = 4; - RpcHttp http = 5; - sint64 int = 6; - double double = 7; - CollectionBytes collection_bytes = 8; - CollectionString collection_string = 9; - CollectionDouble collection_double = 10; - CollectionSInt64 collection_sint64 = 11; - ModelBindingData model_binding_data = 12; - CollectionModelBindingData collection_model_binding_data = 13; - } -} - -// Specify which type of data is contained in the shared memory region being read -enum RpcDataType { - unknown = 0; - string = 1; - json = 2; - bytes = 3; - stream = 4; - http = 5; - int = 6; - double = 7; - collection_bytes = 8; - collection_string = 9; - collection_double = 10; - collection_sint64 = 11; -} - -// Used to provide metadata about shared memory region to read data from -message RpcSharedMemory { - // Name of the shared memory map containing data - string name = 1; - // Offset in the shared memory map to start reading data from - int64 offset = 2; - // Number of bytes to read (starting from the offset) - int64 count = 3; - // Final type to which the read data (in bytes) is to be interpreted as - RpcDataType type = 4; -} - -// Used to encapsulate collection string -message CollectionString { - repeated string string = 1; -} - -// Used to encapsulate collection bytes -message CollectionBytes { - repeated bytes bytes = 1; -} - -// Used to encapsulate collection double -message CollectionDouble { - repeated double double = 1; -} - -// Used to encapsulate collection sint64 -message CollectionSInt64 { - repeated sint64 sint64 = 1; -} - -// Used to describe a given binding on invocation -message ParameterBinding { - // Name for the binding - string name = 1; - - oneof rpc_data { - // Data for the binding - TypedData data = 2; - - // Metadata about the shared memory region to read data from - RpcSharedMemory rpc_shared_memory = 3; - } -} - -// Used to describe a given binding on load -message BindingInfo { - // Indicates whether it is an input or output binding (or a fancy inout binding) - enum Direction { - in = 0; - out = 1; - inout = 2; - } - - // Indicates the type of the data for the binding - enum DataType { - undefined = 0; - string = 1; - binary = 2; - stream = 3; - } - - // Type of binding (e.g. HttpTrigger) - string type = 2; - - // Direction of the given binding - Direction direction = 3; - - DataType data_type = 4; - - // Properties for binding metadata - map properties = 5; -} - -// Used to send logs back to the Host -message RpcLog { - // Matching ILogger semantics - // https://github.com/aspnet/Logging/blob/9506ccc3f3491488fe88010ef8b9eb64594abf95/src/Microsoft.Extensions.Logging/Logger.cs - // Level for the Log - enum Level { - Trace = 0; - Debug = 1; - Information = 2; - Warning = 3; - Error = 4; - Critical = 5; - None = 6; - } - - // Category of the log. Defaults to User if not specified. - enum RpcLogCategory { - User = 0; - System = 1; - CustomMetric = 2; - } - - // Unique id for invocation (if exists) - string invocation_id = 1; - - // TOD: This should be an enum - // Category for the log (startup, load, invocation, etc.) - string category = 2; - - // Level for the given log message - Level level = 3; - - // Message for the given log - string message = 4; - - // Id for the even associated with this log (if exists) - string event_id = 5; - - // Exception (if exists) - RpcException exception = 6; - - // json serialized property bag - string properties = 7; - - // Category of the log. Either user(default), system, or custom metric. - RpcLogCategory log_category = 8; - - // strongly-typed (ish) property bag - map propertiesMap = 9; -} - -// Encapsulates an Exception -message RpcException { - // Source of the exception - string source = 3; - - // Stack trace for the exception - string stack_trace = 1; - - // Textual message describing the exception - string message = 2; - - // Worker specifies whether exception is a user exception, - // for purpose of application insights logging. Defaults to false. - bool is_user_exception = 4; - - // Type of exception. If it's a user exception, the type is passed along to app insights. - // Otherwise, it's ignored for now. - string type = 5; -} - -// Http cookie type. Note that only name and value are used for Http requests -message RpcHttpCookie { - // Enum that lets servers require that a cookie shouldn't be sent with cross-site requests - enum SameSite { - None = 0; - Lax = 1; - Strict = 2; - ExplicitNone = 3; - } - - // Cookie name - string name = 1; - - // Cookie value - string value = 2; - - // Specifies allowed hosts to receive the cookie - NullableString domain = 3; - - // Specifies URL path that must exist in the requested URL - NullableString path = 4; - - // Sets the cookie to expire at a specific date instead of when the client closes. - // It is generally recommended that you use "Max-Age" over "Expires". - NullableTimestamp expires = 5; - - // Sets the cookie to only be sent with an encrypted request - NullableBool secure = 6; - - // Sets the cookie to be inaccessible to JavaScript's Document.cookie API - NullableBool http_only = 7; - - // Allows servers to assert that a cookie ought not to be sent along with cross-site requests - SameSite same_site = 8; - - // Number of seconds until the cookie expires. A zero or negative number will expire the cookie immediately. - NullableDouble max_age = 9; -} - -// TODO - solidify this or remove it -message RpcHttp { - string method = 1; - string url = 2; - map headers = 3; - TypedData body = 4; - map params = 10; - string status_code = 12; - map query = 15; - bool enable_content_negotiation= 16; - TypedData rawBody = 17; - repeated RpcClaimsIdentity identities = 18; - repeated RpcHttpCookie cookies = 19; - map nullable_headers = 20; - map nullable_params = 21; - map nullable_query = 22; -} - -// Message representing Microsoft.Azure.WebJobs.ParameterBindingData -// Used for hydrating SDK-type bindings in out-of-proc workers -message ModelBindingData -{ - // The version of the binding data content - string version = 1; - - // The extension source of the binding data - string source = 2; - - // The content type of the binding data content - string content_type = 3; - - // The binding data content - bytes content = 4; -} - -// Used to encapsulate collection model_binding_data -message CollectionModelBindingData { - repeated ModelBindingData model_binding_data = 1; -} - -// Retry policy which the worker sends the host when the worker indexes -// a function. -message RpcRetryOptions -{ - // The retry strategy to use. Valid values are fixed delay or exponential backoff. - enum RetryStrategy - { - exponential_backoff = 0; - fixed_delay = 1; - } - - // The maximum number of retries allowed per function execution. - // -1 means to retry indefinitely. - int32 max_retry_count = 2; - - // The delay that's used between retries when you're using a fixed delay strategy. - google.protobuf.Duration delay_interval = 3; - - // The minimum retry delay when you're using an exponential backoff strategy - google.protobuf.Duration minimum_interval = 4; - - // The maximum retry delay when you're using an exponential backoff strategy - google.protobuf.Duration maximum_interval = 5; - - RetryStrategy retry_strategy = 6; -} \ No newline at end of file diff --git a/azure_functions_worker/protos/_src/src/proto/identity/ClaimsIdentityRpc.proto b/azure_functions_worker/protos/_src/src/proto/identity/ClaimsIdentityRpc.proto deleted file mode 100644 index c3945bb8a..000000000 --- a/azure_functions_worker/protos/_src/src/proto/identity/ClaimsIdentityRpc.proto +++ /dev/null @@ -1,26 +0,0 @@ -syntax = "proto3"; -// protobuf vscode extension: https://marketplace.visualstudio.com/items?itemName=zxh404.vscode-proto3 - -option java_package = "com.microsoft.azure.functions.rpc.messages"; - -import "shared/NullableTypes.proto"; - -// Light-weight representation of a .NET System.Security.Claims.ClaimsIdentity object. -// This is the same serialization as found in EasyAuth, and needs to be kept in sync with -// its ClaimsIdentitySlim definition, as seen in the WebJobs extension: -// https://github.com/Azure/azure-webjobs-sdk-extensions/blob/dev/src/WebJobs.Extensions.Http/ClaimsIdentitySlim.cs -message RpcClaimsIdentity { - NullableString authentication_type = 1; - NullableString name_claim_type = 2; - NullableString role_claim_type = 3; - repeated RpcClaim claims = 4; -} - -// Light-weight representation of a .NET System.Security.Claims.Claim object. -// This is the same serialization as found in EasyAuth, and needs to be kept in sync with -// its ClaimSlim definition, as seen in the WebJobs extension: -// https://github.com/Azure/azure-webjobs-sdk-extensions/blob/dev/src/WebJobs.Extensions.Http/ClaimSlim.cs -message RpcClaim { - string value = 1; - string type = 2; -} diff --git a/azure_functions_worker/protos/_src/src/proto/shared/NullableTypes.proto b/azure_functions_worker/protos/_src/src/proto/shared/NullableTypes.proto deleted file mode 100644 index 4fb476502..000000000 --- a/azure_functions_worker/protos/_src/src/proto/shared/NullableTypes.proto +++ /dev/null @@ -1,30 +0,0 @@ -syntax = "proto3"; -// protobuf vscode extension: https://marketplace.visualstudio.com/items?itemName=zxh404.vscode-proto3 - -option java_package = "com.microsoft.azure.functions.rpc.messages"; - -import "google/protobuf/timestamp.proto"; - -message NullableString { - oneof string { - string value = 1; - } -} - -message NullableDouble { - oneof double { - double value = 1; - } -} - -message NullableBool { - oneof bool { - bool value = 1; - } -} - -message NullableTimestamp { - oneof timestamp { - google.protobuf.Timestamp value = 1; - } -} diff --git a/azure_functions_worker/protos/shared/__init__.py b/azure_functions_worker/protos/shared/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/azure_functions_worker/utils/__init__.py b/azure_functions_worker/utils/__init__.py index 5b7f7a925..6fcf0de49 100644 --- a/azure_functions_worker/utils/__init__.py +++ b/azure_functions_worker/utils/__init__.py @@ -1,2 +1,2 @@ # Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. +# Licensed under the MIT License. \ No newline at end of file diff --git a/azure_functions_worker/utils/app_setting_manager.py b/azure_functions_worker/utils/app_setting_manager.py deleted file mode 100644 index 3d8ccbb45..000000000 --- a/azure_functions_worker/utils/app_setting_manager.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import os -import sys - -from ..constants import ( - FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED, - PYTHON_ENABLE_DEBUG_LOGGING, - PYTHON_ENABLE_INIT_INDEXING, - PYTHON_ENABLE_OPENTELEMETRY, - PYTHON_ENABLE_WORKER_EXTENSIONS, - PYTHON_ENABLE_WORKER_EXTENSIONS_DEFAULT, - PYTHON_ENABLE_WORKER_EXTENSIONS_DEFAULT_39, - PYTHON_ISOLATE_WORKER_DEPENDENCIES, - PYTHON_ROLLBACK_CWD_PATH, - PYTHON_SCRIPT_FILE_NAME, - PYTHON_THREADPOOL_THREAD_COUNT, -) - - -def get_python_appsetting_state(): - current_vars = os.environ.copy() - python_specific_settings = \ - [PYTHON_ROLLBACK_CWD_PATH, - PYTHON_THREADPOOL_THREAD_COUNT, - PYTHON_ISOLATE_WORKER_DEPENDENCIES, - PYTHON_ENABLE_DEBUG_LOGGING, - PYTHON_ENABLE_WORKER_EXTENSIONS, - FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED, - PYTHON_SCRIPT_FILE_NAME, - PYTHON_ENABLE_INIT_INDEXING, - PYTHON_ENABLE_OPENTELEMETRY] - - app_setting_states = "".join( - f"{app_setting}: {current_vars[app_setting]} | " - for app_setting in python_specific_settings - if app_setting in current_vars - ) - - # Special case for extensions - if 'PYTHON_ENABLE_WORKER_EXTENSIONS' not in current_vars: - if sys.version_info.minor == 9: - app_setting_states += \ - (f"{PYTHON_ENABLE_WORKER_EXTENSIONS}: " - f"{str(PYTHON_ENABLE_WORKER_EXTENSIONS_DEFAULT_39)}") - else: - app_setting_states += \ - (f"{PYTHON_ENABLE_WORKER_EXTENSIONS}: " - f"{str(PYTHON_ENABLE_WORKER_EXTENSIONS_DEFAULT)}") - - return app_setting_states diff --git a/azure_functions_worker/utils/common.py b/azure_functions_worker/utils/common.py deleted file mode 100644 index 963cd3c1c..000000000 --- a/azure_functions_worker/utils/common.py +++ /dev/null @@ -1,157 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import importlib -import os -import re -import sys -from types import ModuleType -from typing import Callable, Optional - -from azure_functions_worker.constants import ( - CUSTOMER_PACKAGES_PATH, - PYTHON_EXTENSIONS_RELOAD_FUNCTIONS, -) - - -def is_true_like(setting: str) -> bool: - if setting is None: - return False - - return setting.lower().strip() in {'1', 'true', 't', 'yes', 'y'} - - -def is_false_like(setting: str) -> bool: - if setting is None: - return False - - return setting.lower().strip() in {'0', 'false', 'f', 'no', 'n'} - - -def is_envvar_true(env_key: str) -> bool: - if os.getenv(env_key) is None: - return False - - return is_true_like(os.environ[env_key]) - - -def is_envvar_false(env_key: str) -> bool: - if os.getenv(env_key) is None: - return False - - return is_false_like(os.environ[env_key]) - - -def is_python_version(version: str) -> bool: - current_version = f'{sys.version_info.major}.{sys.version_info.minor}' - return current_version == version - - -def get_app_setting( - setting: str, - default_value: Optional[str] = None, - validator: Optional[Callable[[str], bool]] = None -) -> Optional[str]: - """Returns the application setting from environment variable. - - Parameters - ---------- - setting: str - The name of the application setting (e.g. FUNCTIONS_RUNTIME_VERSION) - - default_value: Optional[str] - The expected return value when the application setting is not found, - or the app setting does not pass the validator. - - validator: Optional[Callable[[str], bool]] - A function accepts the app setting value and should return True when - the app setting value is acceptable. - - Returns - ------- - Optional[str] - A string value that is set in the application setting - """ - app_setting_value = os.getenv(setting) - - # If an app setting is not configured, we return the default value - if app_setting_value is None: - return default_value - - # If there's no validator, we should return the app setting value directly - if validator is None: - return app_setting_value - - # If the app setting is set with a validator, - # On True, should return the app setting value - # On False, should return the default value - if validator(app_setting_value): - return app_setting_value - return default_value - - -def get_sdk_version(module: ModuleType) -> str: - """Check the version of azure.functions sdk. - - Parameters - ---------- - module: ModuleType - The azure.functions SDK module - - Returns - ------- - str - The SDK version that our customer has installed. - """ - - return getattr(module, '__version__', 'undefined') - - -def get_sdk_from_sys_path() -> ModuleType: - """Get the azure.functions SDK from the latest sys.path defined. - This is to ensure the extension loaded from SDK coming from customer's - site-packages. - - Returns - ------- - ModuleType - The azure.functions that is loaded from the first sys.path entry - """ - - if is_envvar_true(PYTHON_EXTENSIONS_RELOAD_FUNCTIONS): - backup_azure_functions = None - backup_azure = None - - if 'azure.functions' in sys.modules: - backup_azure_functions = sys.modules.pop('azure.functions') - if 'azure' in sys.modules: - backup_azure = sys.modules.pop('azure') - - module = importlib.import_module('azure.functions') - - if backup_azure: - sys.modules['azure'] = backup_azure - if backup_azure_functions: - sys.modules['azure.functions'] = backup_azure_functions - - return module - - if CUSTOMER_PACKAGES_PATH not in sys.path: - sys.path.insert(0, CUSTOMER_PACKAGES_PATH) - - return importlib.import_module('azure.functions') - - -class InvalidFileNameError(Exception): - - def __init__(self, file_name: str) -> None: - super().__init__( - f'Invalid file name: {file_name}') - - -def validate_script_file_name(file_name: str): - # First character can be a letter, number, or underscore - # Following characters can be a letter, number, underscore, hyphen, or dash - # Ending must be .py - pattern = re.compile(r'^[a-zA-Z0-9_][a-zA-Z0-9_\-]*\.py$') - if not pattern.match(file_name): - raise InvalidFileNameError(file_name) diff --git a/azure_functions_worker/constants.py b/azure_functions_worker/utils/constants.py similarity index 86% rename from azure_functions_worker/constants.py rename to azure_functions_worker/utils/constants.py index b916252cf..974c4d15b 100644 --- a/azure_functions_worker/constants.py +++ b/azure_functions_worker/utils/constants.py @@ -1,7 +1,13 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +# TODO: organize this better + import sys +TRUE = "true" +TRACEPARENT = "traceparent" +TRACESTATE = "tracestate" + # Capabilities RAW_HTTP_BODY_BYTES = "RawHttpBodyBytes" TYPED_DATA_COLLECTION = "TypedDataCollection" @@ -21,12 +27,8 @@ # Platform Environment Variables AZURE_WEBJOBS_SCRIPT_ROOT = "AzureWebJobsScriptRoot" CONTAINER_NAME = "CONTAINER_NAME" - # Python Specific Feature Flags and App Settings -PYTHON_ROLLBACK_CWD_PATH = "PYTHON_ROLLBACK_CWD_PATH" PYTHON_THREADPOOL_THREAD_COUNT = "PYTHON_THREADPOOL_THREAD_COUNT" -PYTHON_ISOLATE_WORKER_DEPENDENCIES = "PYTHON_ISOLATE_WORKER_DEPENDENCIES" -PYTHON_ENABLE_WORKER_EXTENSIONS = "PYTHON_ENABLE_WORKER_EXTENSIONS" PYTHON_ENABLE_DEBUG_LOGGING = "PYTHON_ENABLE_DEBUG_LOGGING" FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED = \ "FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED" @@ -42,12 +44,6 @@ PYTHON_THREADPOOL_THREAD_COUNT_MAX = sys.maxsize PYTHON_THREADPOOL_THREAD_COUNT_MAX_37 = 32 -PYTHON_ISOLATE_WORKER_DEPENDENCIES_DEFAULT = False -PYTHON_ISOLATE_WORKER_DEPENDENCIES_DEFAULT_310 = False -PYTHON_ENABLE_WORKER_EXTENSIONS_DEFAULT = False -PYTHON_ENABLE_WORKER_EXTENSIONS_DEFAULT_39 = True -PYTHON_EXTENSIONS_RELOAD_FUNCTIONS = "PYTHON_EXTENSIONS_RELOAD_FUNCTIONS" - # new programming model default script file name PYTHON_SCRIPT_FILE_NAME = "PYTHON_SCRIPT_FILE_NAME" PYTHON_SCRIPT_FILE_NAME_DEFAULT = "function_app.py" diff --git a/azure_functions_worker/utils/current.py b/azure_functions_worker/utils/current.py new file mode 100644 index 000000000..b9f304298 --- /dev/null +++ b/azure_functions_worker/utils/current.py @@ -0,0 +1,31 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import asyncio +import functools + +from typing import Any + +from ..otel import otel_manager, configure_opentelemetry + + +def get_current_loop(): + return asyncio.events.get_event_loop() + + +def execute(function, args) -> Any: + return function(**args) + + +def run_sync_func(invocation_id, context, func, params): + # This helper exists because we need to access the current + # invocation_id from ThreadPoolExecutor's threads. + context.thread_local_storage.invocation_id = invocation_id + try: + if otel_manager.get_azure_monitor_available(): + configure_opentelemetry(context) + result = functools.partial(execute, func) + return result(params) + finally: + context.thread_local_storage.invocation_id = None + diff --git a/azure_functions_worker/utils/dependency.py b/azure_functions_worker/utils/dependency.py deleted file mode 100644 index 76d4259be..000000000 --- a/azure_functions_worker/utils/dependency.py +++ /dev/null @@ -1,411 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import importlib.util -import inspect -import os -import re -import sys -from types import ModuleType -from typing import List, Optional - -from azure_functions_worker.utils.common import is_envvar_true, is_true_like - -from ..constants import ( - AZURE_WEBJOBS_SCRIPT_ROOT, - CONTAINER_NAME, - PYTHON_ISOLATE_WORKER_DEPENDENCIES, - PYTHON_ISOLATE_WORKER_DEPENDENCIES_DEFAULT, - PYTHON_ISOLATE_WORKER_DEPENDENCIES_DEFAULT_310, -) -from ..logging import logger -from ..utils.common import is_python_version -from ..utils.wrappers import enable_feature_by - - -class DependencyManager: - """The dependency manager controls the Python packages source, preventing - worker packages interfer customer's code. - - It has two mode, in worker mode, the Python packages are loaded from worker - path, (e.g. workers/python///). In customer mode, - the packages are loaded from customer's .python_packages/ folder or from - their virtual environment. - - Azure Functions has three different set of sys.path ordering, - - Linux Consumption sys.path: [ - "/tmp/functions\\standby\\wwwroot", # Placeholder folder - "/home/site/wwwroot/.python_packages/lib/site-packages", # CX's deps - "/azure-functions-host/workers/python/3.11/LINUX/X64", # Worker's deps - "/home/site/wwwroot" # CX's Working Directory - ] - - Linux Dedicated/Premium sys.path: [ - "/home/site/wwwroot", # CX's Working Directory - "/home/site/wwwroot/.python_packages/lib/site-packages", # CX's deps - "/azure-functions-host/workers/python/3.11/LINUX/X64", # Worker's deps - ] - - Core Tools sys.path: [ - "%appdata%\\azure-functions-core-tools\\bin\\workers\\" - "python\\3.11\\WINDOWS\\X64", # Worker's deps - "C:\\Users\\user\\Project\\.venv311\\lib\\site-packages", # CX's deps - "C:\\Users\\user\\Project", # CX's Working Directory - ] - - When we first start up the Python worker, we should only loaded from - worker's deps and create module namespace (e.g. google.protobuf variable). - - Once the worker receives worker init request, we clear out the sys.path, - worker sys.modules cache and sys.path_import_cache so the libraries - will only get loaded from CX's deps path. - """ - - cx_deps_path: str = '' - cx_working_dir: str = '' - worker_deps_path: str = '' - - @classmethod - def initialize(cls): - cls.cx_deps_path = cls._get_cx_deps_path() - cls.cx_working_dir = cls._get_cx_working_dir() - cls.worker_deps_path = cls._get_worker_deps_path() - - @classmethod - def is_in_linux_consumption(cls): - return CONTAINER_NAME in os.environ - - @classmethod - def should_load_cx_dependencies(cls): - """ - Customer dependencies should be loaded when dependency - isolation is enabled and - 1) App is a dedicated app - 2) App is linux consumption but not in placeholder mode. - This can happen when the worker restarts for any reason - (OOM, timeouts etc) and env reload request is not called. - """ - return not (DependencyManager.is_in_linux_consumption() - and is_envvar_true("WEBSITE_PLACEHOLDER_MODE")) - - @classmethod - @enable_feature_by( - flag=PYTHON_ISOLATE_WORKER_DEPENDENCIES, - flag_default=PYTHON_ISOLATE_WORKER_DEPENDENCIES_DEFAULT - ) - def use_worker_dependencies(cls): - """Switch the sys.path and ensure the worker imports are loaded from - Worker's dependenciess. - - This will not affect already imported namespaces, but will clear out - the module cache and ensure the upcoming modules are loaded from - worker's dependency path. - """ - - # The following log line will not show up in core tools but should - # work in kusto since core tools only collects gRPC logs. This function - # is executed even before the gRPC logging channel is ready. - logger.info('Applying use_worker_dependencies:' - ' worker_dependencies: %s,' - ' customer_dependencies: %s,' - ' working_directory: %s', cls.worker_deps_path, - cls.cx_deps_path, cls.cx_working_dir) - - cls._remove_from_sys_path(cls.cx_deps_path) - cls._remove_from_sys_path(cls.cx_working_dir) - cls._add_to_sys_path(cls.worker_deps_path, True) - logger.info('Start using worker dependencies %s', cls.worker_deps_path) - - @classmethod - @enable_feature_by( - flag=PYTHON_ISOLATE_WORKER_DEPENDENCIES, - flag_default=PYTHON_ISOLATE_WORKER_DEPENDENCIES_DEFAULT - ) - def prioritize_customer_dependencies(cls, cx_working_dir=None): - """Switch the sys.path and ensure the customer's code import are loaded - from CX's deppendencies. - - This will not affect already imported namespaces, but will clear out - the module cache and ensure the upcoming modules are loaded from - customer's dependency path. - - As for Linux Consumption, this will only remove worker_deps_path, - but the customer's path will be loaded in function_environment_reload. - - The search order of a module name in customer's paths is: - 1. cx_deps_path - 2. worker_deps_path - 3. cx_working_dir - """ - # Try to get the latest customer's working directory - # cx_working_dir => cls.cx_working_dir => AzureWebJobsScriptRoot - working_directory: str = '' - if cx_working_dir: - working_directory: str = os.path.abspath(cx_working_dir) - if not working_directory: - working_directory = cls.cx_working_dir - if not working_directory: - working_directory = os.getenv(AZURE_WEBJOBS_SCRIPT_ROOT, '') - - # Try to get the latest customer's dependency path - cx_deps_path: str = cls._get_cx_deps_path() - if not cx_deps_path: - cx_deps_path = cls.cx_deps_path - - logger.info( - 'Applying prioritize_customer_dependencies: ' - 'worker_dependencies_path: %s, customer_dependencies_path: %s, ' - 'working_directory: %s, Linux Consumption: %s, Placeholder: %s', - cls.worker_deps_path, cx_deps_path, working_directory, - DependencyManager.is_in_linux_consumption(), - is_envvar_true("WEBSITE_PLACEHOLDER_MODE")) - - cls._remove_from_sys_path(cls.worker_deps_path) - cls._add_to_sys_path(cls.cx_deps_path, True) - - # Deprioritize worker dependencies but don't completely remove it - # Otherwise, it will break some really old function apps, those - # don't have azure-functions module in .python_packages - # https://github.com/Azure/azure-functions-core-tools/pull/1498 - cls._add_to_sys_path(cls.worker_deps_path, False) - - # The modules defined in customer's working directory should have the - # least priority since we uses the new folder structure. - # Please check the "Message to customer" section in the following PR: - # https://github.com/Azure/azure-functions-python-worker/pull/726 - cls._add_to_sys_path(working_directory, False) - - logger.info('Finished prioritize_customer_dependencies') - - @classmethod - def reload_customer_libraries(cls, cx_working_dir: str): - """Reload azure and google namespace, this including any modules in - this namespace, such as azure-functions, grpcio, grpcio-tools etc. - - Depends on the PYTHON_ISOLATE_WORKER_DEPENDENCIES, the actual behavior - differs. - - This is called only when placeholder mode is true. In the case of a - worker restart, this will not be called. - - Parameters - ---------- - cx_working_dir: str - The path which contains customer's project file (e.g. wwwroot). - """ - use_new_env = os.getenv(PYTHON_ISOLATE_WORKER_DEPENDENCIES) - if use_new_env is None: - use_new = ( - PYTHON_ISOLATE_WORKER_DEPENDENCIES_DEFAULT_310 if - is_python_version('3.10') else - PYTHON_ISOLATE_WORKER_DEPENDENCIES_DEFAULT - ) - else: - use_new = is_true_like(use_new_env) - - if use_new: - cls.prioritize_customer_dependencies(cx_working_dir) - else: - cls.reload_azure_google_namespace_from_worker_deps() - - @classmethod - def reload_azure_google_namespace_from_worker_deps(cls): - """This is the old implementation of reloading azure and google - namespace in Python worker directory. It is not actually re-importing - the module but only reloads the module scripts from the worker path. - - It is not doing what it is intended, but due to it is already released - on Linux Consumption production, we don't want to introduce regression - on existing customers. - - Only intended to be used in Linux Consumption scenario. - """ - # Reload package namespaces for customer's libraries - packages_to_reload = ['azure', 'google'] - packages_reloaded = [] - for p in packages_to_reload: - try: - importlib.reload(sys.modules[p]) - packages_reloaded.append(p) - except Exception as ex: - logger.warning('Unable to reload %s: \n%s', p, ex) - - logger.info(f'Reloaded modules: {",".join(packages_reloaded)}') - - # Reload azure.functions to give user package precedence - try: - importlib.reload(sys.modules['azure.functions']) - logger.info('Reloaded azure.functions module now at %s', - inspect.getfile(sys.modules['azure.functions'])) - except Exception as ex: - logger.warning( - 'Unable to reload azure.functions. Using default. ' - 'Exception:\n%s', ex) - - @classmethod - def _add_to_sys_path(cls, path: str, add_to_first: bool): - """This will ensure no duplicated path are added into sys.path and - clear importer cache. No action if path already exists in sys.path. - - Parameters - ---------- - path: str - The path needs to be added into sys.path. - If the path is an empty string, no action will be taken. - add_to_first: bool - Should the path added to the first entry (highest priority) - """ - if path and path not in sys.path: - if add_to_first: - sys.path.insert(0, path) - else: - sys.path.append(path) - - # Only clear path importer and sys.modules cache if path is not - # defined in sys.path - cls._clear_path_importer_cache_and_modules(path) - - @classmethod - def _remove_from_sys_path(cls, path: str): - """This will remove path from sys.path and clear importer cache. - No action if the path does not exist in sys.path. - - Parameters - ---------- - path: str - The path to be removed from sys.path. - If the path is an empty string, no action will be taken. - """ - if path and path in sys.path: - # Remove all occurances in sys.path - sys.path = list(filter(lambda p: p != path, sys.path)) - - # In case if any part of worker initialization do sys.path.pop() - # Always do a cache clear in path importer and sys.modules - cls._clear_path_importer_cache_and_modules(path) - - @classmethod - def _clear_path_importer_cache_and_modules(cls, path: str): - """Removes path from sys.path_importer_cache and clear related - sys.modules cache. No action if the path is empty or no entries - in sys.path_importer_cache or sys.modules. - - Parameters - ---------- - path: str - The path to be removed from sys.path_importer_cache. All related - modules will be cleared out from sys.modules cache. - If the path is an empty string, no action will be taken. - """ - if path and path in sys.path_importer_cache: - sys.path_importer_cache.pop(path) - - if path: - cls._remove_module_cache(path) - - @staticmethod - def _get_cx_deps_path() -> str: - """Get the directory storing the customer's third-party libraries. - - Returns - ------- - str - Core Tools: path to customer's site pacakges - Linux Dedicated/Premium: path to customer's site pacakges - Linux Consumption: empty string - """ - prefix: Optional[str] = os.getenv(AZURE_WEBJOBS_SCRIPT_ROOT) - cx_paths: List[str] = [ - p for p in sys.path - if prefix and p.startswith(prefix) and ('site-packages' in p) - ] - # Return first or default of customer path - return (cx_paths or [''])[0] - - @staticmethod - def _get_cx_working_dir() -> str: - """Get the customer's working directory. - - Returns - ------- - str - Core Tools: AzureWebJobsScriptRoot env variable - Linux Dedicated/Premium: AzureWebJobsScriptRoot env variable - Linux Consumption: empty string - """ - return os.getenv(AZURE_WEBJOBS_SCRIPT_ROOT, '') - - @staticmethod - def _get_worker_deps_path() -> str: - """Get the worker dependency sys.path. This will always available - even in all skus. - - Returns - ------- - str - The worker packages path - """ - # 1. Try to parse the absolute path python/3.8/LINUX/X64 in sys.path - r = re.compile(r'.*python(\/|\\)\d+\.\d+(\/|\\)(WINDOWS|LINUX|OSX).*') - worker_deps_paths: List[str] = [p for p in sys.path if r.match(p)] - if worker_deps_paths: - return worker_deps_paths[0] - - # 2. Try to find module spec of azure.functions without actually - # importing it (e.g. lib/site-packages/azure/functions/__init__.py) - try: - azf_spec = importlib.util.find_spec('azure.functions') - if azf_spec and azf_spec.origin: - return os.path.abspath( - os.path.join(os.path.dirname(azf_spec.origin), '..', '..') - ) - except ModuleNotFoundError: - logger.warning('Cannot locate built-in azure.functions module') - - # 3. If it fails to find one, try to find one from the parent path - # This is used for handling the CI/localdev environment - return os.path.abspath( - os.path.join(os.path.dirname(__file__), '..', '..') - ) - - @staticmethod - def _remove_module_cache(path: str): - """Remove module cache if the module is imported from specific path. - This will not impact builtin modules - - Parameters - ---------- - path: str - The module cache to be removed if it is imported from this path. - """ - if not path: - return - - not_builtin = set(sys.modules.keys()) - set(sys.builtin_module_names) - - # Don't reload azure_functions_worker - to_be_cleared_from_cache = set([ - module_name for module_name in not_builtin - if not module_name.startswith('azure_functions_worker') - ]) - - for module_name in to_be_cleared_from_cache: - module = sys.modules.get(module_name) - if not isinstance(module, ModuleType): - continue - - # Module path can be actual file path or a pure namespace path. - # Both of these has the module path placed in __path__ property - # The property .__path__ can be None or does not exist in module - try: - module_paths = set(getattr(module, '__path__', None) or []) - if hasattr(module, '__file__') and module.__file__: - module_paths.add(module.__file__) - - if any([p for p in module_paths if p.startswith(path)]): - sys.modules.pop(module_name) - except Exception as e: - logger.warning( - 'Attempt to remove module cache for %s but failed with ' - '%s. Using the original module cache.', - module_name, e) diff --git a/azure_functions_worker/utils/env_state.py b/azure_functions_worker/utils/env_state.py new file mode 100644 index 000000000..3bc8d8f4f --- /dev/null +++ b/azure_functions_worker/utils/env_state.py @@ -0,0 +1,76 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import os +import sys +from typing import Callable, Optional + + +def is_true_like(setting: str) -> bool: + if setting is None: + return False + + return setting.lower().strip() in {'1', 'true', 't', 'yes', 'y'} + + +def is_false_like(setting: str) -> bool: + if setting is None: + return False + + return setting.lower().strip() in {'0', 'false', 'f', 'no', 'n'} + + +def is_envvar_true(env_key: str) -> bool: + if os.getenv(env_key) is None: + return False + + return is_true_like(os.environ[env_key]) + + +def is_envvar_false(env_key: str) -> bool: + if os.getenv(env_key) is None: + return False + + return is_false_like(os.environ[env_key]) + + +def get_app_setting( + setting: str, + default_value: Optional[str] = None, + validator: Optional[Callable[[str], bool]] = None +) -> Optional[str]: + """Returns the application setting from environment variable. + + Parameters + ---------- + setting: str + The name of the application setting (e.g. FUNCTIONS_RUNTIME_VERSION) + + default_value: Optional[str] + The expected return value when the application setting is not found, + or the app setting does not pass the validator. + + validator: Optional[Callable[[str], bool]] + A function accepts the app setting value and should return True when + the app setting value is acceptable. + + Returns + ------- + Optional[str] + A string value that is set in the application setting + """ + app_setting_value = os.getenv(setting) + + # If an app setting is not configured, we return the default value + if app_setting_value is None: + return default_value + + # If there's no validator, we should return the app setting value directly + if validator is None: + return app_setting_value + + # If the app setting is set with a validator, + # On True, should return the app setting value + # On False, should return the default value + if validator(app_setting_value): + return app_setting_value + return default_value diff --git a/azure_functions_worker/utils/helpers.py b/azure_functions_worker/utils/helpers.py new file mode 100644 index 000000000..4ac1c1468 --- /dev/null +++ b/azure_functions_worker/utils/helpers.py @@ -0,0 +1,29 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import platform +import sys + +from .constants import PYTHON_LANGUAGE_RUNTIME + +from ..logging import logger +from ..version import VERSION + + +def change_cwd(new_cwd: str): + if os.path.exists(new_cwd): + os.chdir(new_cwd) + logger.info('Changing current working directory to %s', new_cwd) + else: + logger.warning('Directory %s is not found when reloading', new_cwd) + + +def get_worker_metadata(protos): + return protos.WorkerMetadata( + runtime_name=PYTHON_LANGUAGE_RUNTIME, + runtime_version=f"{sys.version_info.major}." + f"{sys.version_info.minor}", + worker_version=VERSION, + worker_bitness=platform.machine(), + custom_properties={}) \ No newline at end of file diff --git a/azure_functions_worker/utils/tracing.py b/azure_functions_worker/utils/tracing.py index 0e08bf84a..62385ac8e 100644 --- a/azure_functions_worker/utils/tracing.py +++ b/azure_functions_worker/utils/tracing.py @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. + import traceback + from traceback import StackSummary, extract_tb from typing import List @@ -37,3 +39,18 @@ def _remove_frame_from_stack(tbss: StackSummary, 'filename') != framename, tbss)) filtered_stack: StackSummary = StackSummary.from_list(filtered_stack_list) return filtered_stack + + +def serialize_exception(exc: Exception, protos): + try: + message = f'{type(exc).__name__}: {exc}' + except Exception: + message = ('Unhandled exception in function. ' + 'Could not serialize original exception message.') + + try: + stack_trace = marshall_exception_trace(exc) + except Exception: + stack_trace = '' + + return protos.RpcException(message=message, stack_trace=stack_trace) \ No newline at end of file diff --git a/azure_functions_worker/_thirdparty/typing_inspect.py b/azure_functions_worker/utils/typing_inspect.py similarity index 95% rename from azure_functions_worker/_thirdparty/typing_inspect.py rename to azure_functions_worker/utils/typing_inspect.py index f5ae783d2..27ba19da8 100644 --- a/azure_functions_worker/_thirdparty/typing_inspect.py +++ b/azure_functions_worker/utils/typing_inspect.py @@ -13,11 +13,7 @@ import collections.abc import sys -from typing import Callable, ClassVar, Generic, Tuple, TypeVar, Union, _GenericAlias - -NEW_39_TYPING = sys.version_info[:3] >= (3, 9, 0) # PEP 560 -if NEW_39_TYPING: - from typing import _SpecialGenericAlias +from typing import Callable, ClassVar, Generic, Tuple, TypeVar, Union, _GenericAlias, _SpecialGenericAlias # from mypy_extensions import _TypedDictMeta @@ -40,14 +36,9 @@ def is_generic_type(tp): is_generic_type(MutableMapping[T, List[int]]) == True is_generic_type(Sequence[Union[str, bytes]]) == True """ - if NEW_39_TYPING: - return (isinstance(tp, type) and issubclass(tp, Generic) + return (isinstance(tp, type) and issubclass(tp, Generic) or ((isinstance(tp, _GenericAlias) or isinstance(tp, _SpecialGenericAlias)) # NoQA E501 and tp.__origin__ not in (Union, tuple, ClassVar, collections.abc.Callable))) # NoQA E501 - return (isinstance(tp, type) - and issubclass(tp, Generic) - or isinstance(tp, _GenericAlias) - and tp.__origin__ not in (Union, tuple, ClassVar, collections.abc.Callable)) # NoQA E501 def is_callable_type(tp): diff --git a/azure_functions_worker/utils/validators.py b/azure_functions_worker/utils/validators.py new file mode 100644 index 000000000..e5997dcf0 --- /dev/null +++ b/azure_functions_worker/utils/validators.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import re + +from .constants import PYTHON_THREADPOOL_THREAD_COUNT, PYTHON_THREADPOOL_THREAD_COUNT_MIN + + +class InvalidFileNameError(Exception): + + def __init__(self, file_name: str) -> None: + super().__init__( + f'Invalid file name: {file_name}') + + +def validate_script_file_name(file_name: str): + # First character can be a letter, number, or underscore + # Following characters can be a letter, number, underscore, hyphen, or dash + # Ending must be .py + pattern = re.compile(r'^[a-zA-Z0-9_][a-zA-Z0-9_\-]*\.py$') + if not pattern.match(file_name): + raise InvalidFileNameError(file_name) diff --git a/azure_functions_worker/utils/wrappers.py b/azure_functions_worker/utils/wrappers.py index 29f379da3..bd1aeb1bb 100644 --- a/azure_functions_worker/utils/wrappers.py +++ b/azure_functions_worker/utils/wrappers.py @@ -3,8 +3,8 @@ from typing import Any, Callable -from ..logging import error_logger, logger -from .common import is_envvar_false, is_envvar_true +from ..logging import logger +from .env_state import is_envvar_false, is_envvar_true from .tracing import extend_exception_message @@ -45,7 +45,6 @@ def call(*args, **kwargs): except expt_type as e: if debug_logs is not None: logger.error(debug_logs) - error_logger.exception("Error: %s, %s", e, message) raise extend_exception_message(e, message) return call return decorate diff --git a/azure_functions_worker/version.py b/azure_functions_worker/version.py index adb421530..77c039c52 100644 --- a/azure_functions_worker/version.py +++ b/azure_functions_worker/version.py @@ -1,4 +1,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -VERSION = '4.34.0' +VERSION = '1.0.0a13' diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 9b8b5673e..000000000 --- a/docs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -_build -_templates diff --git a/docs/Azure.Functions.svg b/docs/Azure.Functions.svg deleted file mode 100755 index e555956ca..000000000 --- a/docs/Azure.Functions.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md deleted file mode 100644 index 6364e4045..000000000 --- a/docs/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,38 +0,0 @@ -# [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) - -This code of conduct outlines expectations for participation in Microsoft-managed open source communities, as well as steps for reporting unacceptable behavior. We are committed to providing a welcoming and inspiring community for all. People violating this code of conduct may be banned from the community. - -Our open source communities strive to: - -- Be friendly and patient: Remember you might not be communicating in someone else's primary spoken or programming language, and others may not have your level of understanding. -- Be welcoming: Our communities welcome and support people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, color, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. -- Be respectful: We are a world-wide community of professionals, and we conduct ourselves professionally. Disagreement is no excuse for poor behavior and poor manners. Disrespectful and unacceptable behavior includes, but is not limited to: - - Violent threats or language. - - Discriminatory or derogatory jokes and language. - - Posting sexually explicit or violent material. - - Posting, or threatening to post, people's personally identifying information ("doxing"). - - Insults, especially those using discriminatory terms or slurs. - - Behavior that could be perceived as sexual attention. - - Advocating for or encouraging any of the above behaviors. -- Understand disagreements: Disagreements, both social and technical, are useful learning opportunities. Seek to understand the other viewpoints and resolve differences constructively. -- This code is not exhaustive or complete. It serves to capture our common understanding of a productive, collaborative environment. We expect the code to be followed in spirit as much as in the letter. - -## Scope - -This code of conduct applies to all repos and communities for Microsoft-managed open source projects regardless of whether or not the repo explicitly calls out its use of this code. The code also applies in public spaces when an individual is representing a project or its community. Examples include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. - -> Note: Some Microsoft-managed communities have codes of conduct that pre-date this document and issue resolution process. While communities are not required to change their code, they are expected to use the resolution process outlined here. The review team will coordinate with the communities involved to address your concerns. - -## Reporting Code of Conduct Issues - -We encourage all communities to resolve issues on their own whenever possible. This builds a broader and deeper understanding and ultimately a healthier interaction. In the event that an issue cannot be resolved locally, please feel free to report your concerns by contacting opencode@microsoft.com. Your report will be handled in accordance with the issue resolution process described in the Code of Conduct FAQ. - -In your report please include: - -- Your contact information. -- Names (real, usernames or pseudonyms) of any individuals involved. If there are additional witnesses, please include them as well. -- Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a public chat log), please include a link or attachment. -- Any additional information that may be helpful. -- All reports will be reviewed by a multi-person team and will result in a response that is deemed necessary and appropriate to the circumstances. Where additional perspectives are needed, the team may seek insight from others with relevant expertise or experience. The confidentiality of the person reporting the incident will be kept at all times. Involved parties are never part of the review team. - -Anyone asked to stop unacceptable behavior is expected to comply immediately. If an individual engages in unacceptable behavior, the review team may take any action they deem appropriate, including a permanent ban from the community. diff --git a/docs/Functions.Fox.Python.png b/docs/Functions.Fox.Python.png deleted file mode 100644 index a867d8d94b612c267ae2bcc7dfa53e7a59f1e77e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1153747 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%0A^do0SBs@2?v7bhe{Afp@e=# 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�<0M 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

yeoPmvM6bx$lo= zCVioa-poGciLYdEv&v8svyG`v7+Ijz*96-wEH5lbryxWgzc*7+r3fwNDR|S=e}R8r zrj4_2{8Z_$<(+wjBgL4UJ)4r>uUo{*(W0Dz27NWdJ2_!*l^zkMHZlJrc}uSa05ibP zkoW}{G}bLFAjjm|2^cJ}V`MnQP(!dp^4K0xnu-@bqZOEq$cHwT1*G*xeFp3w`nAOB zgHoP0CH~WHK#P9lu)fAIo2>bF-+J#yDh22t!96eXOKBkM|M<$&H`f&%H;@g2pdxp_ zc3V%QuTZyO+tIM7Mm(`_sFCh-b4fT*|7WQR-G?ERU*T{tr_P$n~ z1y1qf)lxAdXZyZjFJs?&|N3yvIp9Tf@SyOIBed@B&imPMzZi8@k( z4$%B9yP*4X^546&Ns&x1%C`mIvFhySIiSe+y%G)9!i{cLRaOl9hdb()_eoDJu<@xT zc2m91c+<_Ree9TbXg3uo{1_9~)%Ulb;Z{Wob?IN7{9E0m^i=pC;EMvf(>E%Bagkzn zMzh@eNcw`}<97LuF8VE zv;)zT03Nbcl_%&Mg~cap(&OFY)PqB=xpsF*xqP+`MLsm?KR>i*P}lZNn|SS!{XLQK z#MvM`JD_=MY?QS{Y{}sWj`tZYXwDT+8{>VDvAIi{a$&?){*_%oEdT+K=rlJwL$d!y zOL(Rk>2R@tkQPf87{zW4>@lo2UmKhLewHP zMu!Cxb1Klm7Ph&2Irhz4Mz}Dly3ymaVBJn#b2k`&B99M1L1V?!w z*Eo0YI?SjR%raItW=`d<5~A#9Ta1Obl6K5^tJ$maBW7OF660pOvIIa8aOU^pOCF@i zNM+K7z~^X|A>PL5DPp zNJw--XnSK~n(sEZ>}c;hXhHT6d}-HESe=&8m5%D26M1qAgTQu~)$x0|T|HTSDgR9c zBB_(JCC9^cdawk|c!QM#poS%hEF!=6mQ&4kB~?OXiupCTpKTwB=)g+Nm)B3yHZ~q> zQGd#${7dKA5u^(*{Hy(|ZKz$%ycae27S@dklRbr(`c9;m_&x^(OuCnbRA^O5f}QV2 zbzRu*f~pc5FWBpFGT=W*U%z96V@{@S8005oo~3ni;fDjptEV%uhIepC9q-$zRJF*r z<7U8MlG^Y zJGw$^aZCc}I}7jQr7YOpEHVn7H6KccWQl__%UgbLRmUAoC4&<;FmZ`+tJu0%-)k_v zM%jD$`Jg;k`ty=2Q^!Uoy%!i%14+NQXFyS~%J;!cA*Fu__iF)iy$G8m|7>Oil(#dhDgZPuts*wSgu4A}fM zjMC_P1K5ut!G&m$>F zQ5~IMun*0Ptc-R3+|3`+2I~QL(USN}g~R#|wQ>e-q5ORO;Rcl4JtD+B$q{?V2sr`X z1FZ4sUKpJ-G$5Fh>(A`6iy<-y!EVSoZ<5&cDBtr|`U`gAwijKiujyR4&1rh4!NLD1 z(iPl(Vd7#CM%&$N1{Acmv490clDZ9bfSQ`8({XSTOXB1oEI7Zrgm_7h!F>^w>TQxz z4i$8LgKh2)szVaj!P+b`SaBS}dsok&j;6*XS!Wx7f3U$C>VZ7kE2?`zVZPoO<8ev!r>s?{B>PRy6fNdom*u$q zWjgEB-fq5#AEfZB_oZkVdUx71o!Xo}#B5I{2VAWm_KlFsSsgt;a}(nIiZGK@m|DOu zVtSeaAt(JhzO?V$$-#n4d0)WMlVT503^|;fm z<}O;ieu>ay8g7}PwAR3wq8*gIqnbL*xZ(cQ=Xt?(w%y+9jy!pK?{1YZc3Rr?)pyV1 zCp1k@6>Z#kr(e^yz0C-7o_r)4l)RJ%NMrZ4R_d!@QcM6xt>+FFZ|d{$7b6)s?t3ux z4>#RQYc)p@R`F6N|L$a`5!A5vlL<_iwqqECiY0?CsIs`#_}YE@$*q+N7^`71RpGL9 zs=84Y4ym)j|6YFvU*W-dthVJU<~srriMle^#h%rlJ;!B1TH*9*QMJn2rsr2gM##f| zwzQJG5IYfd!J)5k@)~_r=E276a%=(U0U!&%B0P#FQFA^cpVc5dyTk)P0Az@R08aa7 zzB=XsoRFD{csgFdVlH1^xjRUH`djzT8w8WqZmvk#V_LC#&ZWQxQUXr@8_Lkjhpc)K zn^@I$UA_IXtG_l#!%?u=)T$u@x$!$ay?@(b+!-y{i^9jnusaUQ14T;UHwaF-j=zpunDfDyCV$2+w7rd^+`b4o zT=THv5)BB7+8(?t_(+ZkEw+>WzM9j(x9S@Td+3ojzXyr`R6eo7lXCS+tQ?zOp}`(e zPXASQD6<#7XP-v#_*d~ zfx$EYk+HOyyPfjeaSXV!@Mi2E!* zp(l!9G~T9V!=x?N*aZTq9dl}bF-e<^Bg|V0Mz89${T$>_LzJI&?80{w6QYyP8?XJo zY!DkC*cBE27@q5VIE&KE<~;0-6w7A1(`fLF!{tv)fDR#tan`2A4_6ov7A}5UdJiaG zmbwj7&}*YhA%O=BYzQ<3mL|cQ@R`f&aNX>&$Yj}U?-8Gw}RG%dqb55fIS*dC8aor z6$3#MbXHxTMTzZ-jKLLo9JAu+6ynwG%qvy0KL|vayx!T33lL)cSK>f|G%xvio6Sklu1R0wP=BDISYU1@~G~tKMA8kRBj#U}wb9yC;ca zb9<_nq5Yreo46!qT#^C?$s|7{P1gA&WWIiy36=G!Ygn{vAvmAYAo=V;Odg#@;<)#? zI05p30JwHa9-DJKi!xi0Aw!>;|MFAU-52qMrG4-CE+`jGdO#jbRg9R(k==ab4|rYri2V=F31ELq#~;6-6wJ2=J!N{!8`v-6^v97W_$O&!?=n0=o4}0nU&`c<-_@Rc)*E{L7Nh1p z$3fcN)KM3=&pG{D5JXLsl5_yZz8k#~3E3ANDsDdS3}U9r&5nhstl{k=GKK*Yk;y+o z9As~NY<&Jn(Ro_0+?u$)GF~s!kyE`b;Luz047fd#^ac`kuMa9i4-I+aE|6o}px1IS z2U&Q)dWx_IBQt&a_+ZI#Qz&mBVU7GlGMYpKzZiu0&;;Mi6KOqc4I8$Ek~@J zk_HwGgSg844~K<)$I*vlz3bSSH+?MY{Lp^Sm;=l^=5}W==TUxFdNP-{^&ij7_%D8- zA}Vk-*=0u8cHMa)_r12JjZ{?uY=jf8`NB{k-kxIkE*qTPH|Y?c3d2oD%qH){*#QhS zUgv8d3E{12aX?m=0wcciie(kY{LJh&FOnXZbV3E|$EHlMoQK^==J%B_zRqA_!(Z$C ziVL~tC!N^Pwb0q)IDWN!MD8 z@e-BrjWfjAu0=KaIyb&nE@MLf3w2H!=FhTK71?o{C%Qt;-^Kj8&}PdX?G{?V=77KG zvkX<5->z8}Ibp*Bw_5+_giY0TJowCn3iD>_MoGc;ztf;sE7;lQT$wAlpfS_!AFb#= z5yw<2H)H}_w%2vkvjH0wuHQzS1Jc1s9slmO&qPI@cX2cld!&qPPVOvmH8r?qr%}n< z^kuxcof8NeRyIsMn6esSX#{YM5-k=?A(#ts{knBkv@-mGq|ceFup}qQ79Mkwo_P!7 z3tZ)dH=VHWtu#qxQQBvmp0FEPKDuT0+Y21fg854RiCGXskpXgg?>jA5>QZ{y7wqEB zYPOj0Z@pfq4!4WB8%>I=a1n$jsOQhUFi1n>R=sz6+p7sAlPv7GXl{I8m`*ATIQrDD z?#B9@qanihZ1M{V&;s0DjPSQp5M1yUeS7#Z=w~3Nd{{+T8Z9w5At7vO6PL+pOwxN z5pd6>IVh!DORJocc^eAxKW^;th=XqcL69p9!ehJXr;V8OyDT#;5&~(ont8jG&3r_M zB@ZLQgt8kC@b5l7NON=cvbpU(m(;TIQ!L<;x!2M$HXd<#?BGv+#($!Lm0^0fogN4p zs~mHFi=Jb!o@T(RNm3iY%gTmRgsboXsznIY0QxXg)cxA0e2f9IeDdu|RKTgdD}0UU zqar%Xa^a1UEadpq{)(ND=K3>DGR4mtsfoaW%c-2$(8--&)=3g4|Fj38_JZ~6pkbEh zaW~YGh7-W4mI28r?jw#42G;q#Ro$1dI#*y!76pNp7m^+#%?4Dc-eup|`A+s%T-4RT zf(ioCRNy3LFCZzCFG*m0K;h}aghSw>FzaWqEbiiAMCCH3 zfmVLEO_YX)*7#^NIL)4r0dnVm(n z-`SK#ZajCefKP;wO5e$u*}OONShby6+?*f#w-^SY_9=^RNhtCMx*V|d&48}^94m4 z>Puk4t2|AkTjtaX5961EpsXp$uKbSwLQWpBv=&;>~~vi;SpP zVu6ov5dPGw#yR^vNLRCrW96M|ngt8d;jOOVJ~X17fSR_&B1`&KoNU&;h@Q$@QPKGe zCYr%;u|0bEr>%s;7$i)lkbz7iV~(3*DE`9xnLj+v#m^jG?L+a0T|n*z8f3P1y&;!O z<($B+*{e2c$e|9BO)&7o+!J$P&h@-zG_7lUtI`&u-%L~6w{cGM&w0eSk0hNW@}2V0 zFEdk00V|%ELLUiomoHAJ_rk(QWDd;$LlYW2VAT6jkU`OXg!!BbmIZc&9MkW?A#w^A zPi=Y)80WG^_}Y9=47}y5Hxl86evz^9a2tB`l0#O(N);SbnPxxbtJYsBx3p$lq-=Ly z38yp}*DTl$4^qsGwo7Mcgn>#&@%e}GtIjPC(tJ$8hs$TVe_Ze3$=VP&j_%LSdXR0b z0gIl>MjGOQg~7ck-a;w;xyMra7t7^BNV;I2*S_?`LmlutmOTaAzKE25YkRwl(t(6yH}CkRT|lGJ$vRb=d@H>53vEQA&r z`W{5GPb+$%$h!WsFd+~bQ}gr5Ar3{Db0YJX%*9mryktSR)n%Xg@4zpY?#Iy%KA_hk zLUHmC=o(T+;UkB!26I_UiDI1v{wq9e+7kEWcyet5{Gf4f#~PYxD{Xh1w?idKWza^C zN!*8vzDShg!az(ROzNSsMzRYr7yU=7INT*sICZrMX~>lf_KA%I0TxhKdtcLFB))ld zL-WiZLb7>M#D`(U@p@0Xsay3r0lQF{I4{bHul#NjMx@N9QC&_H*JYepOFH7*834qq z<=MrE@5}tYL7dp?yA2g?P^qlDO8B;T1rd#_lwc+bG1DT(@6f3;vKI+$n6|Ilpp_{! zxR73&3aO!W%LH~z0xySr%x)R#;Pns!pA6=F{_&{Fz^BGnmNxAr&R%A+SzR#>?$av` zFWiJ2PXuF2@^Z!5h?5x$DC~LF6ODA5EjF(TmB{KFS=E~iN{<^>SK-vh=V0T78=p`C6|+Ja%kIA_&lM(k~QvN7WSV$7DkJS)Gkl-;h~SzoW9 zAgn{DR&Zh8lE2y1v&w;dAH(2b7LVzfgnbx=umxl36mR><%qS&2lJ}Kivw#;zlJ<_@ z%CgDh@XuGm@Z#7$_#fO$?7Q!S!Di12A3yNXe)(ujpd*W967Ih9@l?_+u;tgyF`=sp{;5bsk`B)~8jk z^=4lm?wK_ax_vJQBJ?!Dv}XsM@v1idm%vY#ihS`xpbrxAbx%qSkWU? z9Fh0#1Z`yTSN7^OKMnG2iD#!oh{X>x`iHx&`Xk4r>VMcf6S`X;AI$W^2N1rajsPX6 z-V<}|rn9rsjdJ>ucjwjXb)4-wRb+j9@A?x53v7D=-j)U_mw8a8kP*JCa`M+WNIwe; z{qkDPcHRn@PTNx96`-p8Dk)C3lhaJ(!GmR^e#4zaO>{MlaA389D(Q%g_sLoY&uwMw zudm*aPE=jrsJPZlli6yWK0)=a`fk0w6(uj=+`hxxM}u%W4}E14_dFl$s-T12J`908t;zFP!qNLjVKS5Bi~CT6~{eHQ+V9$C3ut zxArBgDSAwBtqfIf>@Ryh|7CCJZZA-tCCf;foI6@YEVJS+9^=0o#6>BIzGq$ z#A85k!lV6g5E7tTN{7iZ!rR`$wbtpi%x4*pscL$}_O)1c^3*!La&>Rx&78=3*!4YQ zRW65GcymyK<%PF;d6XO*T0)q7`J)?c5N>p?&%0$4zwO`aK0o@t!0$>XDVu6WWH<-n zf_|?hM3uAcBn^1#4n?--Dr3DE0IHe@RZ>mcYJ0`wRVUO@yhpWrl?~*1`x)jiVjv@C zFYbFD?97VIu_{s$-cOMD&z=`Od%$!A&U%Q*Nmg9_H{g?mV!-P=8}=)NNnU}OPl>T7 zVBK&qvlYUej`7JKeB6>?YT%FOlOJ*gJ7(;ri9p-aLR`U!l2qAq($I$|+VRO+!O%Fw zCqG=d{9NEzMt^pwhgl{}teGN@K_{n&`|H@0gkv+mqfw<9^4$&&d_~er`?M%_^~dHQ z*AL<*LH@U%=^6SM_RLEaN7fl=3IX|@b5es)G=2ZsVGhyw@)fm>x|^Vu2H~TZb;h^?8YBeM>>k4s_(MxBmJ4Q{*;bzcOLV$Nk!5rdvQttiAuh zG>DxCEcf4kAfyGa9U^^bc4({QBS z;{grCj(M7d7EtLVP$ zCKn=f15cRtA>W(zU)?vtsVT4@xj2F3E|*8l=`LO!7Yx64Ve117!b&sB+b-_K(aeZB+7&Z7yx+&bsU((j6SF^{=Z{UocPa;`1cec=$;a@C&tQ=Kq!SfR^e#qiV`Vtvv8DtXzjujQ=?e;}vH>&J(3D0V#^8p;KIVo*K=D zf29uWC92xvlIT%GV0c!do`f_GVQO^pdut}=4>Jb%zRI}d%ph5adC|X5a&Ww;qwW03_IUOh)s{K6**kH7!4RCMe|Hx1kOrK{J0+DFujpq z|L_qk+ByDi>i&24CjlG6o7{Z}$f;M)4^;v2B*A>w8=e_)~Yuww@j0O>Mh zaw4;^)*u9|+E`+qilF`6Df)(*=L`y!ln_-%kmul~>j7(GNb?;+t#H~lJINXsjU>Fl z!#s{Mg^?i80ew8ju0QV8J)}#A=11JYU?2g-Jj{eapb_9`s={F00(AOPB3FUl-)w**)314 z-(uQmfG|zE$XT~B8touag4s+s0-{raf@j9~9Eibd_v(VcC`{#y)W-T+l{zx*;`>C| z$Azi1Yd>xR-S_WzgFa@zMUk6uAA{i!9|%qpO)&|0?fZq9?G`-`mcyaru`)I^F0GPn zAL4B9yzjsuKMAIQLN0urwx(4!@4Yo9X!{O?2=H^`vlL#+Dy8(VjbI%L-uXY(_nR1_ z!iZ_-9L-deVnai7;P2#HI`i`UWrdNsYG>ciehJ|2k`bm17ec|mv^i;BvHnvM=e?-u z3_U_<K#>d~#eFF4yB%BoqHN;9@_sk^1i{|iMTpX> zpN7~+hmV?I-Rn1KS}-2*SmZb~>9>P38Jb#M*;ild<;?)rYT2(pC-SJ2sBu1XX49O{=Di<0qbTcMki#L z7l+-Z2w}?`lRjou8?G4zO57NQ6KFE#pYoS(n3dm@QH;RWY$h#H-(DHJzIQJ%s-|sa z(Ffp;*y<%^@?%ez?y+X6_Sw>be+$av>IZXgyq2IQA)KP}P;cr2Z zVN0Lwtua4zQY7 z6_L<^axpYvC#Gs+*2;bUy^{LI5S|Kf&@i_qL!N4x_BwwS`W?15k%)_k0|&H zQZ+%Bb$ZGv9Tk>zyrxp9!yZ(viajS&yf5(Jcm+?QD6hN?3~X@4N|D|d3da|@;y&Ay zf0wiOMa>cP#SE4sB~(&i2M&**^YE*q_W1S4Mcp;lqnWEKgNllSm8qValWT{5R=@Ea zFkzbv88(XPDd^l>8aJ)!@qg~lQ>IKTGeP8-whd4Cgs?*=D!Ns17k|fTAu?ld4jO8R zlOc~`bWH$aHBWf%pVR-ZD=#px$=jaG(TBk_6>%ZgG!`af#-1Nl+j%WBog~ylNb%}O zb9aOqPhO`bA~FM{w%j#b#BD^{`n^V+UW;DTg*998oM(4Z>zL3nx9(?y+ON%^enH=I zJ1svd*&li@h*yslX8hcDx2BW7*=G(8m^VD3lIyR_*9iLV9a}eSkY(nVI5wsU#;}x4+vv}FZVK_#)FX6joqbfsqqt7kr^YpmSqkOFt6EO42Uw&g0 z8b^}{%|dKOIl}c6K7})plRm&Lby{u#$=@kfaqUFZw^OYN`B_NOTxWn>gNT1{BSB0_ zE3||X0c$%dv@ipPFEGPf4yI!bpK=jNdW$zR>{bqgWHJ^>zhV!2>!vM^#P3`V@xk%m zcNjXL{>BJ=zd2e?t0Dh+^31O1nN7{CPk;QuRnwJ4@8FRnrPx&_4XuMXVY7wxI`iEJ zP0>w{R$YGFo_}mwGRFAuLy?e>FU3&3=N*O`O+Mbox4Rb#)9V=`*w-~gl68`rxp4X3 zk`kyyrp$Z}|LE}H1b8QzhYBx3YL3&Sw^_^KLl7Z%4pj+o9QdC%P4TYABkUPF9Gip+ zL4yvy!GxiDkSE{A6M#`Nu1sEp3#1iKS&4M@i>h{C-`DV;usudcp=Oe)*mq2WrP!NhyFdZs zv~2C0so5L_$!0OaUe9_g*LKaqIe%6)6Y4O!&TCp5v4${gFQp%|UgH<)WQvi*-wE9( zp~*e`u!I&5r^85SV8fpsXVM^O+hpfIfZ-YERmLOiXiCMvl=;qff&E;D;}qa*_M@bN zPQ?b#!!$R;9PoRCnk6aDwztEChjWWKk5q5;zc>;Zmo%VbX-Dr=!tUS z1v*5D5@#_!K<|uaz?bQZgz1X-(cU-)YUD(SH@ogJcq6je57lwFNDh>Iy+hu zcjyS2HAQLhFbV*~dABkez@;r5bMN(Xd#+fyFe#6F(QysT|jkcjxz~FN;`T!XZ92HXHbL*SDtEFH}S#04Xw&j}E19AjJg8SmV)fRz?12B?K8L#C~NUvUz6a zwanW&5y+|BKMEnwFgdzTEqE%|3>h{3GlpERYB~fL1y*02P|}lAN-huPKk* z2l)hH@wg*!TZoK!E%sB2wwxlcu25{Gf+CxmNR=$L2czSP|KK$XkCb7<3^;)4 zp-wr%LqfH8u{oFq-?@Nnt7sV|cEik14r6xGK7>U9<9X_fs2EJNx$L z=sTBzQ+HzfDPKZF>5Z3Nt6ojjs(r5VF=s{%E&pu%K(%-0J0C^2G+O8FOhmqhOSa-I zuzS_**QA^{RiI?DdLKp&vE_@?&B|DPSVBUMP1!FuV-Fphl#O3J^f1$xF;9EOl{1Ub zu=zy~2q-7Jz3BinG9=;Ra^A9nS< zH26(H&TB2xV8-mTI6qLEH5XV@(5Tk9Whaiw8=J$x5)-jvh5bHhl@$s#fF)+ zx$5AnJG0i{yl%iCh9OvGE;Q{$7xGqw*|~yqqCGFhk#;Y>5`7EgSDePYCNCi8FW~h* z^A%PA?HOvlPYU5sA#frDDr|gW`J$1TGGTe@%tpUkcxOjO7XQS39lxI<+c$6768)Ps zm#5N?`|^GJ8JZ26DrQa7+FSdmr8UmQmN#8`XC|!yKD^Znn1|T|un&H$%Yc!1RIc$b z+t2hInt&R%F7>|It6e$JZNF6J{^M6jvxkGCisCW-_+y>s%`JC3bQf~g^G1Qwk~1ci zU+aQ!s?PQN)#Vf-)|hJ&Ay1FolBt1N`K3tqf;T*jpm>#N;)k~yCTRuFjwc>sCV<&p z>YRoYavVRx_VA=2Q*xvN^aDbrTWoxkg1q&M-GGN=-pQ|Z33#0iV=;W%P(y| zhse_eEx^uqTZg%1N^sDIV_qMAFE9dH1+_qq!t`Du`Vuj2AsJXEz6-{n$*mpznRzdB zG=AOYx0P&M9TeL-lHYSy;~NRzdKM+#qMIy98FwV4g2L65ep8I$`?jrTl%JpVnf#>0 z@v}N=Gb-#6+lAM&Rg90nS1Qmq&-qM0_^{10Vya`v+_NmO_n%}2e5-Y~nIseLjIoXW z?1oGMe}{q$7Y^W>PRbBv2}kWT3cLPg*2AG&FLlA?^d1zC7n=L+HztZ-zc}W4kp<2G zoapK_#0VhB_o}P4N`ewRCJW)|5}~a=m(u@&8->V*8MC;QDf^f*5n0&AM6#kP*f7ot zq@Kp_gLeph{iiqQ%HXP4+ggQ1)Se%dS+Ql_F=E?UMG6I?FD5{+bD%R!a`1 zedi#dUjNP1gTm;uDnkJ~o$S*HayKI@>E(TVA79&rQFrf^@2p7bqOKvC<}I;>4i9ra zNC`r6Z6JpIS5*G1S|dd?gxM20ZUeVZ7B)YE-7`N?P>jMOj}K1umK;!!l*a(no)VDx zoyA_RFQSEul^Cw|j!V%eC0?I)2Cs9bi@eG4FSa~!gM$m@7Z0xHUw2Sz-({_6on_Xl z{73U2RBD#4wVu?Z1pe}wAM)W3ETf1OG_oHbBA4pY;FZo?B-)Nq>Dx-`vs8cgd14mO z7X(h)HAr~u$pDcbKmEP>&xR+87`S?~#_-ras3SSG4>29aP1f$LABS0RVKJmz+0 zQga_}B8%r8r(2)8THY1r(jyJ7Ij0~s{==2Scvw~QwK2my`eON&QMJ5bZA@;JvK(le`T zIwtgOEdmU{{R#T|dI5=Y;9p{S*j2^mfAXxQ|C$H4hjr|=GASQ{#rL4)qW@UFt zEeVBKL1oAX7<0hFjuwBVJ-8b(HU|wThHV$FP5MrtnQ-w~h<`%nQ{UREma!ex)j?p( zp~i4qtL4~Az;Zf~3+EzossKDoqWT=}UFf&!IwJ-rl(dfZ&v1a%6xJF>r3DSCq;`m4 zLDLYOxgf>{*025L#|?JAu7n^d4AlCtDYKayuDUXZB~Ehw?Dsn&LB=4-aUeLy{rO=3f6IPOb#`s$%4eig}*g(!{cXjplCqKjFP8UqsdM`A*Ow^U~6s5Z1l}( zlgrB3qnX-;=z*UeBsoZ;1+;FuQB2g$A?bZ!VYxn=aYI@jo+}$0S_foCSwY015ARy7 zAoil{^Vks9-h-3{ZAhQlm&6)v9>Ho+fLq)EW=gnaC{lFm3jcM#BiqpLzPvX)AnRj{ z+76b*!Qp1#ug?77m?WXC^b9H5=K5D00j>b~!ZH{g8U&-3L`U~-L47}N^_ntP`F&V0 zM62wZ*KIdrgzX1>S&OpN_?=6z53#&s_1cgPqZHS7E}9h?V$DH9;+zyQS?n`*lLOJx zS&$ya#Rt|`4!D#R%w!N-{~up(9u0;6M~x%S~jGr1iJo7zERUx9Psl@u_1pE872ItB26t?|oc4r$^; z;y*ri_-S0-Sbbd$^2jGy;`qzs+R(W++@?rx&@xBX;iu)!g>T=!-OubYdO5qft(N`x z+c%F7Yu|!rl^s9b9%MjHRR1l!)NhV%=q9|)0pqK8W-v1$mqk4wqbEP3s!TjG^<->* z!Z23$@*!a!?N6c^=_#D_loN~O86_m!hBr@fW|Z)J=K98}q>4ICtHyysvU~6Y^~aHs zZySA4yoiB<$KH$N7oDYi(-jKKCMOYm1>b8f`ItL@-@Nja&_*638>)k_wJK{{tb4wP z)JX$wg=YBFrZ^>nYSd*uwMdSYUnDzD++alBwodgB$7kV@s_PK7vd9KI+0(j=91@(I z5h!}fX?Dh=zl10<$QYG?jZ$yQD9tAta?=ZL?Hy2YiOaciHs~EMa-M|b%hz^?$(K*R zNTK=<|2EAwvLrSQrJ$}+R8&-^&3_1puBh)rK7H^mgITR>^_r`1i*5boS~dx`*6(4A zKnW9Xz|lg;q!bh@(Nil#TUJpb3Wh+PQ*`-&tzLINTYf5^MGM30*Wt_pw|1vP4p?y# z+mO~_z()^#$fhMfpp?>>DWnX7M$4aDJS7T#no$i5qb40xbbH)T0A;dB1FgeT0vnh5 zo?y+G>E58@$HnGPDUWqz*H5`vq*?{_DkXmR|5o)R;Oc~qdin1{MoO3X(^RgGz*?$%=Rf)FMTFw zluz7#ccshc4Bojvm1A4p8$o9|Qul3P28Ll+JFrUUL`PlhdD#?6z3FR<16qP;k1iK z;A)55>vAPOsy5yx{=xc^mPe@clM($bOV~Q{Sqk>8&O70<=f;u5t?u{-V={2O=qH%39%E|9BT_44e`hIu3#PaMQd`w}H46@903=B6Y#=(DXpVst!?rR%5 z*dzAf$F^bOOc3{g29X}8yyDMpbl6OOxk%=|uZpQ=wEQq-VPKuyc#rzW;P=m^`($gK zCFmCzwK{{&K>8yM0VaBT9u8q6y6=S+wmWnSr=vTWhY5&{)8mwz#hFmOkIrOUW%J=- zqoUC_jSA4C3tWB2pN8T<`++|hPmXY7kX{Pt(68kld4!$3;x0vB%LR!aGt2)QM-OOj*ozke`$67ui z#H?Yfe`oM_r0uW!2dRtZOX5DLq<*%GAe5jTleWbov?k$L@^P<$Bz@Zh%7kHSy>V|q2&$123?V;0ViHeERyNqMa^XA2-L$Dd539{f9cw2BUHbsDJ<(Tt9U;-fEEWJ8TC0BE4hp` z{ktR?*oFxK2ExQ9@?BL7!d$BS)VcTAYAFsVGp_i!9W`Bz*K`fxd9R#L5)>|TX)eei zBaS)8%&nK9s+e@Uq6?qia$8(s9e6IEYRSMyVRezq`u^|Gzx{t6`d#wF8jXh;j`^Mq zclY<%*Gb5r`r>aXRLiA`E;D+ZlU9%(v>BC`Uwu_sL_om-D<%$xz}o-KV*qDeNNAEQ zL9cC&y&SyLx@osVZVNC2)Gv=l+ZiXb8NXzM zLBe*Oy{1x;`lM647R|a6P;mVp9F*6~gcF$B>`?Zq>t7oqlMcGGx^;%ybbVw~?S9M) z;e4&#c-zsMo+Hf(5J?enY~Tq6E?Mt-e12daQXdoEu@yFy577ut(!j@um{`DtKec`E zMahSEew40c!^>RV!%kf>UgN1u2>Xzi7YA}QsW z>1ouvGy6({(2lgt$)Ad@-Nre7iLUqhpT}K%!AumU7@=9gtrv;v)v68Ky>S}q_=Fc} z{2-8~Zht`(&OaaHE=+MisYyiki4GH*Q!R;^P*wp2#hyTg@MiLPx6VMh$a_?e2*Nw> zIcwfQOx9_mj@m;O6UeU3@ag?n)Chr;nqHsrVzjyP#=z5DU+Nmst>EB%R@c9k7l`=p zb)N;Udn8F{3(ayj_czOumDhaE^x(Ps1SLVUBwWhp9gRb_Oc90jNPqH%#hH=RURU+9G&44eiUDa=0 zyuwff&e1Q+)=sEiT^kB%?d6*e&b|5Hm=GWCyb63~^~%cI`2|j;y@3xGQrW{lm!}1i zhAvb#-GEz%1Y4e8yCdrCT!_!#@!&L@XhU-in_543@blfqASZ`SH|;rKmtiM5Z2kuR zfY+Qz-J6f7rRZL4IuMUbNbHCae&lQ6fWgUm!U)SeXp3~0={9eMmaVLu8&0*Ds}e;; zhH^;HO~7tri1dZLm1Y?cl+k5Ejh>Zy&AYTsi$8T%Ss;zaxhpm4b0NP2_CLe!C*4a5T%t*fbMZR5_ur;q8@@Ea z;Bi=if2fchmr(Gzc`nfsyy!h9JLmkmep&3-PXLdxO(E1pGa^8HcjxI;n_Sndl zPFL5jJrATQlY^#lk&)u6d2KSo`--17TS8B(&S;?fglPquqB-&_xik+}ZLzYWJV$_o zMd(@u8ynG0AM+0ZYCBk-RCHJLh`EC?@`;!S4%_%T{!7Yj9$$dg0RhHkN!l zx-pBoiVP~p@)id?!6{CEBV{~H2orSiwrrIett8g|c?@>QIF)PnxA`N0okfFJx_y}h zm!7S|N)OFxHZe5C9f2p1uN4yfjECO&dEC|M0J4i@VL@F#(INBzA5>KQIpZNG&IF%GbnW zg;YSCz5SfvrhvK7OkOWfgTAalB(Vhz|AxLm7c4pU$4=9o*{REKCijatI8AW93mG?^ z@S5rH9$e2U&4fB7aKokF8zs|FgeNak{;JqEm#G|~0u%lJqJma{3jQXibJ8jAN}HC; z88|V8F@Tn4st)BOGO0i1{%_*f$KO7%gJp|M*>iOLX88Dqy*>s-AM`TkZGXsiY> zEHh3??D(IEv4&rw^-)(9L?gu_ndTd)u~RLL*^vO>V;+GL$5)H}$>zK-YQ>odKt#lboH|?@C4#`` zRDTZ&f4lisV-D;B`NTjhjp?%yg6>J7H!HR^xt$J4L(WuLTAak0!Pf!kyIvK{zlK?f z@8UHrzlKv;?&`1acRuGWM<(Qrd~w>d54!&h%Jso{sDO(&bscLt3OAGmH3nzWXL3$P zVJq2}p3+(2w*bAgbrl0nJ5_y#D1g*ATy2xg`Pr_7uU)W%`fTdvPbDxYM|S@DL*>|(WM+DbdNiSki2~{DjsYmSgRiUa9!lP^ zet&rN3O0R@|8C!3dBx`=3^2?Dx}aDxNeWef>mqYY;l4eI?%$(#y^Q|N8Ojc`MSX%CPnG02AD{9w}K$p|%@Tcs(yi^5L}gLr*9=wQ4&! zvyYf&65R>K0~JVdVoO7V+#C5gWq+Z4A>Tsp=$aej1SX}Ws$CiX{iwy`p>rz8=529+ zJJHNU2f7>$4Q7E87H~tO!J@r$7DvZDNIxgAZ;kr-Ey zwBddxN!8v%7e2=Of&6{f#D3rYaP*og={Ta#7Zv0{&Gi&{y!4|hIyL?C#Jdy{;xaAq za)SEHX0@e@od%9Av1xMJaBF{8s?p5msUZg|^2cv3au6;e4;XdOK$fck?WBOLk)4@` z_!q6GravzT(ZY1ax^eD{JW#c-rD|cO))~M7)c@A73jec6hkpNgyB^NvYN00n2V} zao|(C;bin|e@|j>mUJEE}ofdsmTP{j3~$+Zm>Nal~}YGLfz; zfTbr{SjURk#=>R~j;R^gHw_IhPDkV)aTyc!Yfv<9#ssph5|L-qAb9(msY5{B*3!$5*TR#tCe^W!zW~@dlZWPcp>R@G@$O0Ss&Dx8Bcg z$f(M_Y9rq($nOo~~`W7Bb4wl!BuF?)!y&7A{RO z>M~#`>)v0Vm|!Z9+w9x-lJ|>0%~>D3MpN6wUThnvAhfhRa(?h>Lkr4GqgxLK_Oeh| z9yYy|+_`I*&Y1q;C$>nIWmq4PMzT-mMBD~!SI0e+APf^Zql2kH=%O74qWa4bl3z=> z05gH?n|c%D;_>gH$Nl%A&(8bY>_%8*Qfa;Hy=$*pj(nZ)VI-{uq zdE4oG3aWRh_l70WQ$hz}B&FyKs=eti48UIACEJ!W8q29R3HT~;O$Pg{2Mbf0O?yKM zc3*!q3@d$Vh-7R)D2TkRH$S~JqzfYz=cLoT9=-!=emDeOb~1wM2&9Y_ZFdj4a^5`c zAh{8E5fia0n(85f9Jn+AA4~r66YPR&cYPXpdTe?{qL*Y>+gAwk@}Q~p!+R%$nU-|a zdj>YU;_OgpRZK$z%km~OdByf}9D#*yW8BV5m2?7Iz4vuGvHJ31D#&zR?R+k+rufTm zqxn-`Y6)q-1qN{|P+~%^jUYZslQ<}y1CBi?z?}7$tqE;OsO-IeW(zm&^^YIKlB_34}&l7n>*0nH19QrZYFBQ%Whj@Lkt@9XC zI6l8BTx?OAS^XNFWXHe=Bc+cJ*5x1r28K(IwPeQip5^+x)e5vF%5)jwGzME5Y;nXmzDfYZ%_;pH}fa}eKh(L8@ zob(ZQhby={bu6p(fKBkWhW-d)+x%1bF{Z}k7>UoCVe6cE+cuAfAAcVISH@v;(iteU z%tXWIyvSBS*I6o-J2q=SJC1zqY=RuK$eDl_G2>I*2p&ezdwsiMbsdpPkEkv*^0t8a zXoy|Q%j|5O?NV|3hw1}Dp-0Ooz1*=oeIj)j%xV*Pv-MVFNYc;t{u4q|#T-p(E9xD* zxCmRlkfEm`gwOiY`K3_@p+QoWN*F|ef^DoR0u@1k)Bw`1ksgm*B)i}L_cT{%_*Jln zcK~wJ@o8P!;IawHpOs6K{rrSX%m@A5oZj7KI8R{Kb;`$tz9y_?J?MZ~mR}gFwh8hQ zAL7bZ+7?sUtPlHM)BYonVT2IW?JLKmAM^G3X=Dal;VNV}DYe?@2_cC?4s)z(FvWte zg73VJuAYETic9GtnKI-fji(1Z)8c;hj*KI8=eUpLg1sE1Ucw-$k*q4!NWOHxAS=P; z{=ZXM@!wM^nh#FpIfXHlE5Z4DBWp7pF? zE|!no7;kZs6t&MFH2L0CM)2$je)$wVnxaboD@7=@CvH1PILT5ccITiS#(mQ+8D5z}8QU&ens;r`XU@dYyr?r=T4w2G*|(6I?Du5k?-4 znr(*;pJ-etC|KqxWUBI|mRt8OlAXt^@Z!N$=_0d1{uTeIbQHUQfshbAc=6=^9ZtI) z3)Z`D;_<&eyMcP@X$4X_K(lC=r(jq?*U*%_s=#@w z(~sREHsqJB4RyG%=g;q=-z@>X`3 zj^W^`Q6_mlr0(uKXmfRgTX!rx+sWDy$Vy*L-Oz454qEo+L zm1fCm7}Ngz{8s@JOoC?Xs9bZP7~Y7b&mL8rS$$DF-GWO(E6D3oZJEf$$lt_H=YjC) zROd`!$!niyA_h%J?fMGT@SX&8`oaqfu3O~@!K$wn)Lnj5V4;ye?*C_{X9!@W=VD_{ zF;E1 z3f}jRrN&6yU4F6XmAQQ=A=+b4{j&cD2u(k25*dMHB6y4<*15rM^HvDQ8%e#~-i02R z74oJ<{g$VQp`fF)l~esX-<5f|Z(gP?Oz4jM&{3k3->u7BDBVcb21AvCfT1RO;=I4x zeBl2$vhKhI?v<gFUq()=~f)JO-pNlPLGp3Td! zPT>A~O*iwj*%XUpnWX05OM6qdHF1^J5|UMV4$GzXI`+fKdMnhWq6a%=^<_nr?{yz* zAMB8m-9Al%GVk|w`le{k?@DG#0gk_e78G!4{rTKempJU6Th!#wd&PV!^Z}G_gqOJG)uSBBa*7#bAImHVVoXL zN_Z!N>2;=(kL?^`P%sLpE;D_i!u~8=xZ-*MAjqkg^%v!xbVZiF&*n}Js|nkf^WY?S z3Nm=>U5;NH5xf&qv+fOF?B=XLywC!cS9*|3I`$){=-!UxgJpaTpe%zAzVB)l>`s0Z znTt!U)>H&xoYn^RRN3#T&kA>~cs|~e!^p=F5I$XG5P#*;DmVYx@%Ne3>gP>lv!VNP z*NT9Ls^${IEu(I;v7mczm*qRaRC>UnNDp$?X`>S`Y*{(~2vE!aQs1Gc%}B`XH-)3I z%U=w8_$Ld8d76p$2@?2p#Sn8RAPUX(m?zT}-POl9n3rSop%GcXvC@>bd+<2P%+55x z+dcRlInhL4z;pp8@)@%JGHE=oXfu5GMiX_Gw;`r}`yzejVe~=hsE1036Y=Ms*x%i^ zM=FmjwfKeQ0?c{zn1icTQ1sz>ydm?#J_kh`E-X$<e zDp)WPJ3e6Fz8N(M$D;l}Wz>R^sQRo)h>O@~+Yd>4!87L;!851V$TK(MC_qH(P#lFm zE&I6Gj&ku;Ao)H%-_lhsI z({*?*;`R{}rF+vJ^Fe*!Vgg~2o#<|?K3+E8@Hj4d>ZXCPq$a7@_)J8FYH1diQ?aLZ zes?RJXKB1-`jDr8(i?-qY`J+nU8^^mdM|ZOhk|idR8=1jC zlny5|6Dj+9>5XJ=OM|gwezu}*z4FZJGDYF)c7=<$rQB5LZEgcYT>HEerS|aEo7Bd_ zV_c=FmNkqgCkQtm*cW|&=Dt1b)5nx zSbB8S;C51|8-S$0d^5LlAWxdY4;DJ`9JwHR-jYbdLr)t+)2`A#eVH_zm%l^cPiS~D z*|cuAPQ!VdOH_&7*(ifmGYoU(qv$rf{IR<^^8H+uVn!v|>02~Akd<9On}R3lx}wul z`UG{1i2b{j(r)&v2R4^GUN(+th}umz6Arv7K2rnprjb`^C4=FKDx z0qTziSl`J((q1-k5g*!qTKKE@`^jkgJ(M>L^N9w@V<249ss=Z#K!0W2wL_^!Qd7LW z1B)-JIi@M%c^j#ZvpnCXyxHRLvE1YRcROoto-EaPPxC)~Wt=0?ShlQ<8uF#m(Be52 zX()v8iKckWH(AFOyTwq5g?^yO?iHlM?)XQ>xuR4c^QhrL?%1UBS@(-~;;_TT-O_#w zr5drVlz<^bMohbMQN{VtSLI)ChNIM09c3;^vu{5B!KV8oX`+_RJw+y3 z?(K3uIG1TB#lqoeI4lqk-cbaUL`hE*JQWuA-#w^yj;nRf*aJgjR5xX3&9`SrR;p zdf@7A4Hj;B0$%&Cvlwjp{fP%N?lO*~eXVuOZaCHI_3OXv4>TdoD!2+4Z_@ch%SQw; zbLGsO%Q4cJn?8=vi|Yt8Nd5NO>-0-nSUs(Vbr3J#CI~0U&)90{=-nt;ChUgtAc9PR(LKd zLx6a!G-p^FK9(rO2%Z@mU=_ptE31R`*QJ4-Mgc}L(8r*EToEcWo~}q_ptzMhkfg*t zqE62mmM2+E7!^fCQ^!JFvV->S&8&V|Cs$n7ROdMJ@b%n=Bf*!Nh@q*gAR#KVG7GBr zmn}D=z~(TcL#!o2ok}!tM*c6*I0+qDOs9+++^}-l*Efi<#NMmgQj0V%{|q6X3vo{O0(6|(A$9AiP?(rOrd_~gl1Sc6Yb*bc_J_WsLscOd{_>D|8ftfE^oO^D zc4J8h|7ouaSIcO@Yx4M3wlP*>^sS#g)%79F<`HP+phkkJ6fRrVk3&Y3df0fjLw-x* zG+D&yQ%T3v)co!`xBBJXn}CuDh$f;yB*=Lo7S4;GYk-F8&C-qAC{Q4|o{WAX-I zMadY#T%a+`U@WvxzZTRa^~tuI+(**Q=veF*cTIJJBCA7EOfh`KVP8Be_6QmpcryPN z_y>SuJ(;0;)KA@Au4OV;-&G7=na}#o8vz4tli1VY_52p`$RcrkZ zcVwJH`}#%-m)f;MRxX3^%t#cNkGNtKwT@$<1Fj0bWU za5jdJv@utSV9Ov}#D~6MpnMPVxs!Q+;zGm5DW$L&v|Yt2YBv&<@C*08oUU)B=z}4} zsh~hh_w-tr@#q=#TT@he1~JSibU)VqMp^uRwb)39H52ioe|`Jjr`B80O7+`XBfkoA z4mx>!q^|epx8k&G2B5z=M_Ht8=5scB72p9gdKa!j8MsOO2%lh)_5P>;rL%K0|;>2c3*r- zH1l`YSuCIU3!>P+ogyJ!V1O?iF||%)Hw;+qNfl<}IBDhp+8h<9T9~vp;x3ohZjKGu zL=-ryF#>(S$a7Ze3nkcnz0(abIa{*CknlEJ$NACa>%@=3UV$AzQPbJVpIZ47Z#eAh zd$6uP|6cdU@A^af>yraQYlr%F8N~A@q$AwMNrV9Cd5n^mll}`kzsz#L^}{woh{8hKPve~{-09ay zJk>X7h>MR2$8KL_#C4@v6?bdTZwJ5wzf?Rko0s|4Z|^jDA^e?m1DQqQ4Cd1{9@1&k zR%W2j_3drsVgngeZ{Pl%gZfzgRJfbs;}KLGp;Xxn4_ng1t1AdzGB%3**~BwfaY<8& zfm)ZrQN+jBAiXntyG-YxP`Ley~~^0Q3JME=yYL-7)j4S7~4$#00@NxXGqyZ@cx&>A8oA#ZkuUsQBPEr z3nP5$pRtPD(c{#g5q2FMKD=K9@let*%DtPy&MXQu>BQ<BsyJHD zsOl?VpOqsSK(A6rGy|%8>&O`XuWDpu;~E z|8zlVAGmTGc7EyGE$qLY)AROnhcxt0wZ@0f4irBI0l6Bk#cz&4t__A8mOF5DtF7Zo zPqg}!#hD^>(>YY;#`i1Ii6aTj#jJSMkIMnA7JM6oP!e0QFJ&&5e+3)9`xgc zTr{86BJsXkOAVARLkEG1XU~KCsZH^qVs_>+Ku}Z&A+oa?JVYiZKD2-mv+WY^Sh_s;oWmlSM96Si$Dy*v~S&0Bw+@A2_iTBIWS z+{AsqH>^G(#EH#|C+M`LVc@=VX86Ac$5D+kBk}k+d(=}r!$13$Opjfd!r127g^9ZG z@n%;VKiZ$qbqgL1#2$VpiASIy4k&M6F^xc#SrX%NP!3p2l|fVV>)96oK4}|dN_pU% zVHk%%_VVfT+PYo>=mK17dg0p17P8}|Q9oi?re&FR`S^&#$MK|^%P(HAxxKqW0G=_< z?z$rggi~ht4`&sbSSYIWRMoVTqJ<(ia_z2B`KwTYnKYt zep~beOFifM&B=-=sF3?Ue;B4?S%CO-<+ES*A&(7E6L5y1%+VaexiLhh2OLg$zc%Nt zmQDICe_|Yn4}*;qznePtK=mj7fGz@=y*xSKS!-}b=!Xd;?a1*_KXJ%EO<%RH04bA5Q_dr&IE?6L_EKxvBY2<^a1!?0t;3F zrSU&n%RTCB>OVoD)>$AVWiHaqArRHMux<5z!dDB^44J%$Xc|csAL3 zTMW4q$_U{ea9k;;&rOLUpS>nc0enjwp#!3^(wrT}u{jZxu<>*F`{wi9-LFmpo~obC zuTd({I^MDvJTT^f`24q2*~?lO*$7m#EGAJ6XEZ5Gs>p;2q?b+omTQfF>GS(sYPGz6 zWy?DR7;zVKcpZSGX^(4x?zQTF=GEo!DGlT!PteAl&B-*rf4bs-bz?>yP--iRS`mdG z;esd3L7s6IubJPfH02bKFkUhi9a-M9X$-3xQ&7Mg+*Xd1oQ#zW0`OT`NA2@rCv0mZ zNHgO6T6TnqUtAss1>7@#uF?+VjrmP%a6H(Xs@M87%gH7p=9RlbX0SM8nlKhrY929kCqN={Q zaHZ!1Ct58Kt6Y)>VnRI*r2z4Cje-{ALd~3 zODz=!gm#1AP=&*Q)6+*(+;Y~!D%!+%hZ}GzXA=sff$jIGHDL0(wJKz&LaqjYqKTG! z24g$`axy=vJP8XL^z^8?5?S$#4SP^y;Umzk!6N*cZ(hgYPe9&r!H^$@QtROvHx{Pp$lJNx;8}aS%Xo{|y0Cgn_UiK7jxloKh@~F=kz*UvtqcuKn>~ z1q&;6$W%CItLa&Qv%5x7XFB5H227r zLYVWiX(*F9;%es5|0%}UZ)AX*D4UR$JO@9>K1R^_JF9}CJwUF<1ZPw>mp*#W?Yf9@!{07*(Tet zRUKfbW@c?tM}Lx5C$2?|O_pgMoRH8|3Ad4weH2OA98+aISYEsx$Y~~d+Q@Ah zgSV^Oc&-G*YyqIA3+ht7(@Npvx%weUm-E2$BL-&uGQ%+7O@9a6E9)uY)D5*O56BuX zMDXAIqtHXADpT@TC5QYhNAEbdX#f@cmH6h1T(gfQpHkEOb(fS=vfi*RHV8hil+-5y z@&oV}*gzh(FqSPO4JgSpG>T%Zs%J|p_4vR|1die+2rCz-Ixfv$zD|eWDHW;AU;tSx z`1s*AdurGH`G>y+K)OqUjpB>>JLN*2D1*-^@1F9}lGsC|t2fb{fO`MfzU9Chff~Mr zria*C{epw%&Z^QTSb$m|L`O8zXdzjO7vrS+C3&^d=B?m!Yod6TdSXIBv%^UQ+om{J zz_Mf3bJkC{NFr&iI5qayoS}t1nNK2)n~iYLGiA_5?5Zx2oCVt(sPMEKsH(*=QSO@B zbU-P0g`=c#;0Eo>1?BtAgz)}P*M48ni;VAO|9uvGuZW>^q3eL?A-eBUhKvlCZY8&q z5YOZw{2#vY+(WH4m&v==ZuB^zf(qSNpR*mIoqd6L{Ld*cIH!6QY{nqjp@fQxMQu)DGvK$E> zGGRKR9s*;LtemX7eG`ik#uw|rfn%97rltI}Yx`LaieAsl*R1F$z~}&jGY+V#X1vJC zUxCp3wof9c&y{I=ZCwP33Y@={U$0h%&%j8f`IZ%=`$=Z_9p6Sq-DOFR+ieG{Wu51) z24=rMpxzVS`t|+_Xj=3|Jq`6c9RaE}Ncd;^!<}N9j~|e7@Uf(kx8L2lM`Wa9aa=cb zsN|5HM8E{^`R0$9KcX~`q<9v@gE3}sT3m%P2&x$xg3YycT+^~fQ|-?;?v;Y_&sX7 zlry^ZmD+_T1gqZezrfS3eq)$WIj2LV-}XHsae$@mFxJKL$VkyA_#c7i^mCgd$(_h3 zV4F^G)n8xy$N_&C{PLC+F$5V+^d{lcLDs?Rd>wbMOXa)|r70HlW^2+E6VQ*q5m|lG zULTT+(uM5u>iWZuN~`VcP=<`m*AacGyKXw`G)vRFG@RyK0xF4F$rIIq{K2cQZFRo< z;(?zC5Vm^fg@jD62LpkxsjyY#doJqFPHoWiO@Hs@wUcHe>yV5y3B+gG>N#rdR=|hx z1Uq~QE{tcOa9F}Kr9ch_4u&ZL%?|@f*uaQIEc6waBB+KRXG)Zyp}3iLIkgy-TMfVs zaSMNux=Z3+4^~1~5uHL0|AyXcTW#y%{S<$_9;9ai-`fkMD3GWuzmAJ!iGsl)1!9HMY(qzZs)HH4w@~v7Z1Yd}W~IUQQs)r|#eFdW9$r z{;pOXD!$t|J%xMK4I8B>s6VpcHrrIggV&)CKYY5F*K_tbj{_@m%6-&dl|+M(Olu(< zroHdnk>Q8l>dh&84(5^jTDKO?RnI587c3EL{LvD}vT$6D@#J{DaXxo#C2!y$<3xnr1k z$*s(0W!hj1Se|LS9S)-kwQfgw2**V_>>jDWbZbzXl`rgpUNnaS5?#Tb_2oP#iZe0H zLhjbea7sy=sx@7}Z$D%bp!lBNh*5bqC=>W!&a+c|abs3}p5me{0XAp9Xd zs*2v<#vH(ag(v%~j^r0Ah~nGM zC=*p+rj1+xp&7UDaOMbK4a5ctk1z_?*j&A1sL>9ix?VLfJm`cf$gW%Nb335^KtFmk zRD7fsY9NcYWNn#jI(;{X4GKmaq|5K#p(9Ob(4L=Ad%Q?#ozqqf7MF9 zZlASsz!gSw#F-UO;y^(vBfOV7p8xgW%xM_u*GTv29S+D_!>f6sO2~}9WRd$VkMGDe z?3bUq3-ksN1mGjU=YV0(v4LhUqi4}Ae@_E7{aV5m-(;uKF0xr?da;DA36LHCP;D?G zhqY?Dl@;QAOn*`>{Trk#CeGiwtC->VDYN=#=Udua zB3w`hrn{LPxhs29P@b=_R5f8@@plp!DVncir3bE`b7j($p(DJE&Jx3?M^KZGFWZtm7T@oOmcZEMUpyYL8*u!Ha<72=j+Vnl)fPOX0<$ug` z>!glN%xkVtx@WT*Fxwh6Vx9Mw2P!Lj5LSjp#`kWF}t65A(t^jbZElV17$W292Ai ztZ3Nh2x|F$FNgEqA~pcP@GA(Aun_bOGkx$qA@ISW-${iMSwunfe&b6AE*964M5BR|1QF4f20B1AP9->#QV=}Sw ziRp(`=fttlC>^OrK>2&rv@CMwG{M2~4Cw})DHwAoJf;(_5bC`(Y|Llgg#kM_A4Y)F5lzH~!9rI5W`sHA&4IT1B)cfzK)kFZo zI(1y=Kh{q>c2m&1c}H5B(yS|raxKB9#+UsDCeTnz31e8eG8mCgE8Ajon@)+lYLYbP50TGblno^{CFQOgT8J*d8gw zU{xB~D#hZPtOjb0Ls`0|Q*|UZp%RBuTePBadC4y9*+j$2&e)H_nPV1{3|Jj@^y8W4>!2McFOekWjNAKYyWGQXt;?39biu^ zIlm}@rk|5YKcJ{DHmr-d5tP`>KyE}C@SQ#cT|RV*{N+g9ULHxzMY@Is3&#aVEQFz&68!-5JDJR-gRRQwWHS<4_7(5KV|2?I0X|8E+(pg)#ynBj@}TmSNUjQ0hx{ z1j5H6IVj3pmSHu&PyCytMQ))YBdrH_!w2mS7lB(jr+sJUhy;~9DB-TypUbGvn7OQ-8u2JsiYx0bcb;i^2ccLVCmRqT(sxG zI#blZJs2zdS;U$NICh(z?h1nuyzapvR)61|n*({Xsnv4gyq0T8Id2t(p&jx$?_L5a zT)5y2Zi#p+`X#~}xm{b^g04GAj|T(#8lpbv^;aS)>7`8i9MmIR%>H5Qk^c*0I|7V7 zLzqc<(kf#5DZjeSB?szx=pQKJbGra!|3MSRBa%Q9fpbp^wLkZW&a)yqWsJ9y#j>xB-@cH`%^-Dv&&~dXJPUU z>(<35Q^%3QKsPJ2u1?LR(h40&8d9d$`F{m|m`x`F)El_=^0Tz?|Y7=$e@h+|ao0 znHJ{5k1sWZq@~(3JK)8j6vZ`E! zIZFOTr|`yMn|C=@?|rG6@T^J8yU!M=qYYYDOm0*?IR4O{~(<2)?<-Qn|X@Pz#8OzOCT_dk5Kg`eyEBL z2UH|v%(o2G{ZAG8$baw5PFkG$hn6<}i?Te$klmUxo= zAfU67wi>MrC%iYUS4JbQuDte}V4U!@K67QzdfVJiNJt#f^=X{$n^44h$Lk(m%)z>z zMwnLz6yPLWeL;aBkp=H8Myki=58Kw~TSnr6x6K0CrHmXvz=a76l&-6*J-!Mo!%t1I zwTe2T$lDf!z=?UVdoO4lBRJk6sddmm0I%8K%4rx0sRNmClu%pEvYY-=!?3Gg`LiGT zu>Qi`@cp6Y&OO7p-Qavl(eRB2GgW#T*e3EtiD3c@uu7*ZUR=XkmgzGQkDfO~47$hD zi1UH@r_7!H=JTx9*RHY*)1~~59R4VM|5>@?$X^W|M`_6Pyo6JU{ZY%<`u`#8EaRd~ z-?vY9DF{f40V*OnfOOb|BQqc!qQp?5j!2h)iiC?IAuveI3?;%)N{N6dB`|cbAx$xp{e{OT75H z1H#pRDzCo*qsy)mIw=-bRUIZ;@%*PXf7>zV`RNgX7kZzQH<13fnVtnM-^ySlr*Mmh zq(w(Hj_cmwZ0>kBYs_WD-_}3}QI1~!>2^?_L{3sWah92gs=PPE7lNb*E{$sMef~}P_UDeNcVO@RhQyvPn@yw~)i_8Nw#;X5)SPIpYFsMb+hRRs zqbjfAeShi|ojKlhI%VX*o@Is2J5<(4k_QAW{5Tbnd0Z zcDMIUT-`C8nig`%xUir4c0Y zoSZx~b~Ee^Cz-%pKu`9Q8dI?tF_|CVd8KRivrg+%TU-*sY%)m9BRh8ZlUDKVBug&p z`ql9sFg3s^t&H}fXR#>;M`y3`$nEB4t9)@0<`IYd6>krv<-zE{^uV7Q4-?<_=JJqu zOqPe_p1NhnP9MU)q%m|4yO7=;lXvzz=(E_k){e%%vh7he!&TpvNi z>tWt_sr!A``Qx&8{Nv{JQ#+bXc^RRU*I6vYt+NJL!E$syo!O+@5C*nrAA0wR6ry2)P7O{&h);H^GcihFzVMWXWE{u4mf3F@&TjrHWqb~^Ok*;*MQ^sId>p8nt? za`_*-9_)i+bdRG^bLsKFLV@(Zg@R@mcmtDmW+SYQMMew-wlmFc>HFi@D}a*(ETTE)KtDdu4rPFqSCe ziRZ_G%MLIjZ`#9P$NHt~<$)_#-dF5L+Qc)=1Iyv+Kj*;@iC-w#A40K|$CtlOv;3x6 zGCs~AJlfJSG>S~y%=Jd5Inm?Gc}!mftCOE@ojuY%U?ZHw=7Jmmv$*Kg=ScSGi-+hK1t++?ID4t?p4T69Dogxn+g=uB~uskTY)wDrD--TIr9kjyj-_4 zz$#G&`S^zBwL89vDPnW^_xb-$mSCXRZBqE99NiCyE@qg_Wku;sO6hEDI_oLUA&MH)BpMDFN^>zFWE{j>22 zIO_=c)xu~4_ff5wYttq5h{rkug+C{s2!zgL*KFPzL<^Jdv*dz|E|xu-6W%-cw7wVG zKGf|o-NrvhKEr0A?s={}>4dq=hcCH6pFt&j8R#*Agcj zmJ?|9xf46}>pt`zyrLk~?$0078@N$a*v+}&%k5b}NA74FYkkK*T(z&!RpDCVTqMr5 z|7Nv>OG;Ev(W?o0($XCTfu#O4>E!^suTQ`D1@+{&t)~hkZDgd3sa)UNq4{a0M#sc0 zf@382G;>GZsT1V%wL7?1dafno0TCxFGQ1Eyw|`le#0Pk^!qe61hL`1&-=}ZU?qg zjI$L{k(DS3n)s_k3XeNs;mGT1Ab9AV7Kzx~-W7Q5G<#i8ann{%=@yQ*Nb)uIcR24~ zpX`cra8&l(^j}l_k=PL=!j%{lG=JFxwMs)vlr$n|w_AE6Wrj=u*$19J1K>fydduEx zU)sTzFv`2LkP;5TI9=|+mHu;%>=cfX9_2RZ)SOO=q?+9FYBJsM9Z&-E{p`ESOv_;t z8QA6yUnABk^*r6ux2oLvB9-i0p+bWRx(vSP^4Brl@xal66Gw8s*E@_FYk=2bQAoTmrhrKyD{-ce2Toj+fPr~iH_Ebh@^)if8$+^GuDx}9uGh?jE6W3ySh zFNY4MgFMbtRpE$H$QB*hkMuZAoT3GL+r8{A7`G5+WvB~+zPm=(F%h=(5O!hCpDiGB z?rqUA9Qn{^sR7m;y^Hfd+77$G=WtjdJ^y(D2B3!CYcEO~voP}6Q=w(43@QAMt_G1h z?qb*8Dvmi1ukF6w-EO`V#(pPOF=2LQ{mIL{n21ih8fGy;1oEx8`t1SHrJ?D?u-?w* zl$~`xv1G0TTEQWM_q~|wG1dv@WAGt}Mwyu5g-=>ODz{QohK%9TO3*VDHCnuJK_2U> z2BB5mYd6dJQ9k`An2B%mP2AtH^wstptg)$xgvkq@E9*DC5hbtQawcQ)%=$$Id?vo~ z{+;1-{yoDxn!&5km@sk3BS+MdsD9W?@4}zU&&$z!NJbg5FQ8U(Kp>C#3XhYLt^tG&m4WCsl78#^F7mMuF7;ge z<_A8PBbCV-K7-Sp97vCXw3Lu>L0UZZj_2V>dpu4W1WbmhlTQe(e4;7#L*#MS^gRD6_GjL10FEh&6i#|Q-GQ1X3b56toV)E*uyF_$U<1)fmwh-!oP4v7)<#2 zAF)5pqlt*~Nccn>b^?1^C>%VxDb_FT59F3Ny6F-N2agx=)4B!GHI1jx_XV}NwMPj% zNUtLH;UvX3s;c#9@*^Cx(=3ZKEgAA!r>`Yanz6~-)^GM*>*-A1>$y2an)np3She!) zi*bKNuHslivqEWj@|EPO4nxx`wLX9LHiFC*0*`aN3kQ3+cz^prCW&Bi$JH^50E|33 zN>mp;l7~cidWl?cpxDE3{=K-uPB69n<`NK$H$Dp==BEEPj3_?eKu*6 zQCN=Tsp+F0L_ch7&K<$rLikZ)`=M0w?;z#&pFyes0_-woMh4h;cvQ+p^kLJI?3hU#zkW)A^Y?k`9(ER+?`UykAh zTQZgbY(y(xoFB6I_?;Xx9=SER7C6lmSJgK8wN1~t_L7=zmx;{T?pE1PlbDI}ZxRS? zjas4BJlJKC%xJ@ax&{kC z;aA#b^C`H^-y%+&uj6d$T;*5AMi@&>z}6S3*llbxy~`(hK-1P)!|ksCUi)wMfqk1X z*!yKoznBE>%Krea1JMj()Q)X=C*27hKP-u*fO|swbw3#HD?JhAM(B9r06c^vXQ(QI zKK!FYXUTzgFJ~3?@Y{Evt+_XQ{r-6E7rFaghh#7?io4O-h^MQHt$B^Hv}o_LAN5NM ze=KM>9I5&kVM+z{05T!JKSk^MpIe(L(ZBwfS)e@(^hxj(xgyhCScsolh{oXypEx`W zNT={c6eOgW}~& z+kOS^xGP~aj?of`ra}J|E5fP{i3N2brM|a8lg2f0izio7?j_AlF|<`Qv7MGIB&|b| z0qOEF;oNIWVxc9gi_E&u)8GW1V7kIU1`o0=osx_Q_m@hrC#-y&aCNvtT3R+>>$o|! zH}gp&UAk=&>o}@D0xzr2lQFYlj)!@ZeM6J?FPzJb&IG` z+C@(x))N`ArhhcuM4s9C3t$c#e`PhBRQvF!{0*Necrk$}tGT?1_|V%u-^%4^Y~ zDt0fj<>yEoGX;Mch@bnMBy%;#teSj?$s834>y}-{HAcal&J3suMp@L*q-M0U!C%!< z;6JM6IYk5$nenLtlGO{`sDpFNCq@OvhdOs@ix)=O;|}F?Q*$j6}*IhF;jXTP$d)%j4-gG8eVihXit`@x!) z<0@@?KOz;EtFe6(7z;z0^qb|hZYQig-24%H&a4Gf-mB-P zN!EO=F}{~@6VuA84Wn|DPNgUcpC3W&XJUoeDec`fbvL!`v7zF6a@JqV0N!a=!}EfO zThzNCn8Q{V%O1v-Ex12IMgT@fure0ws9@F`>=#k~nk0JzqD z;P)3ZxXVfP+=^CPos;q;YacAul&EW!M2_rV;-JHb(Te->q|je$oPQSBECDYrm>M5# zLAMx~9xu}nkCe~Kk*tAuLYMfLcFrv7Ou3(6X#RjtYi=|HH<#kv%3_clG4+n1dfJ z|M<7*_3(2n?>sI4W@krmD^h07mtp9e(sOeQg%K&@bxKBb+hnz#<=(XtnZ6gv{h7kc zHGe3c6+_o-zkUkvN^Nhul6*;Vddq%u+HqnBM&zfBi@5ldwjCet2eNi`%(v@)HmsK? zEN7nlBW)x32Lqe0AibO7d@z@PHZ0X`*lIH00{X_=pW-So$Z`-~(6|&4>DuTb4 zc2fX3vaj6wIrG%eav)??M=YMQih}NV`~d7|aLG7|JIp-tM=YsRPyGXX1FUu$VAamP z&4?`CUDI;6Divvtt%_4d!vKQ`D}a%igZqjIx2%U1_XX`u-gCu204>1jU2vWp>df5; zR{t5d=-{-GGuKt$Rl7-l@C$I3LW&-zBhzl;)F7@4S1ffhZX8SRT<(aE%4wbOm6P z5rYI%PU5k_{O9L;rj)NS7hU&8xSm*lqgZ4nWDYqoxIVP~cwRm6boJ)c>YbasjD4%l zX4)+Q#r_e=sU~Ce%eA+|mKS3pTco&PM&TAyqOr9Nk~$jjJ4AnU?xGvEYWhm@jv2T0 zg#l4BVK*YzQ&*}HN#ue(TqC1U$Prw>u_$NEY)My2rvt?#j8KZpSGZ7@X4x>AXlg6F z4%>ds$hq?G)9saleM?~j|4b(IuHAnzd2s*V+Y|XewT#ELw zPap0!F~X%>0O7D2ERCM9K>Hh5eFqN*l=b9+?K+c%XsVE%TycC`D0|B$d*JKOTwCEh zh@>lQ%Qif3%%B41!Y)W^UK|Q%z5T-~zx6`*^}xNF^?Kql_KC5@y(g~*zOfXGZ3V9l zHq1N9nR@T?+}#zHAx_yj!|UnIoxg_RP3&`f*kbm05za#o01PtGtl6+>lX&8RhYTUq zJrOTKb`@IG*3-cNj9UnNtvS};yzVgvG~^vK@GLb;Z*o#=ohjJ?|7^4Rx~;{a@9(d3 z4%s3d>$)UYbS!?5k7%q+O9VE-dB`m`R-hK`b%|%@d;6ce<#}pX=#8TR*qDN3`XLsfh~rhCErWHjNkVrHp=RTSX>6B(8n`S;qp zzE@a3fODig?RV0k{d{g+klRv9ae0eBZD;J?Fo*2F9rG&UES$mysEp;KO5@Ia*z8Yyf^_-sEDA>0m zj6<}>(3&CoWQH}VuaAjFArvT^osYv=asyQgr*>M9!W)50al_ZTa^a1mtl(rjePJJR zB*nYKVR5xxx2^UfC6#8jxD=;0N#j2^qjbc`*@TmP-uRPMII&ghS}-QdbH#?M0<~tr zO&x(jBZPe^K;3xIydfe@8q-(jawfbUvZ(AIrVu&swODV~XF<#H*M};}&d0XyU$1<` zuVlC^-t;EP$@w;VDqIEt@rAg!+{YZ&czGBoc$75%j~R$GlE_ERA5&@mh<~wZ$+(ExPqv>k8`TZ4d z{K^({k<`yo6VWF)p_X68?MLMyL5~?U7H#Ed=oNA?j0>H5dDbtlI`$uqnN!D-)S@k3 z{Lt|+D*BdG4MQ)P?dCyu1FVEA!8mM6 zB#c;)1&z4Y!{d~Y`#kvEurjIL41qo`n z#Pip1joTwaUnfXD$tryBT=$2jJ3vO4k* zN;?2huU6-Fsuia^6$x9el50cYGMp#9SA8it{(Ri^_udZ)nL5n}OM<$H*!>>!w8Q6^ zlxf`}o+36#(w{k7Ohbq<%BG5UB7AEqi=7-+pObjDGMw#yIj);rG;$gjrLXSSL=>F( z9WH+|C_{w#K>uFc*B97EY=r(*?CX8#m?41+t@kL%d#co>16-~pG=%^RIa^W(i$kiZ z3?q;AnKwEIf5o1B_A))MgIzxEN~F~#S?XW|Bs$xka+H@M=zJfm3pZa;VvWRGN)P{4 zW#;~)$`C=7X2iB(zG;_k^c40F(rnn9RxkKbCQx>#Th zUq2nZc>v;vVcqz-z>0{Juct{RL3b(NN@6S@FjYM*1CO9qi*@*Da$ojXlgY8 z3csHWIS6t99OyjoQ$#b83;DnX_mVE{4bBpNTNq!&9yI2#xIDrt3n$L9sgtdfE@_x` z^XNW&VvR@H{9n@JmBWghP;en4g47>@_{Z@1Y z@1&ryQ2>oc$C~yXp55OIL)88dHe1&0DQ{rP-Wu1b1S9?CT=4d+FqLvQ%<&5sTq|3K zblZpU$U1+vuotZ`w6fVM>oEJiO3r0mM}{bd?Z5bEBi|pmaAAO(;QA}>Nf`Dc)AnU~ z;3aX@5TxHvM?@XfB?W=GJE=N-Zzq7zjUs~IoZ3NwYr`DxoOAErh*f75~mmTYVUntpwDGOR?uo>fQaJ}k2#D;tj7 z^Q?U%GY%j`2}EeecgJ2&eY9wWm5>>GurcwuB?lZptNizbQ$CbU=?#+m=&$Y_=q~KK ztZXQ}Rr7voR_D&{7+?(}h^-I2E7gW+Wypykl#2}Hn%bH{yH_>UQn*!9&+jQjU$l|) zv1q~wV`CWj0{S-o)34=W-eMx9@rBb_}=o4Ih$?q=n7l~d+r;`)c(P~o$!;< z^&9EiS8Eb8w!Zh!S^uv>BmS>;0>LL_P-t$OeAXQyE;>WcQh#NK*^ft`iF4Y>3kERA zBqUOR-cx-_bTHZk_q+@}Eva+I_#Lnr{uw=4rgiLD7{$+>&@38?>qkC*geoGpBqV!t|7PyQ)WH6MYO zfmvFKmUu#Bw5X9vS_^Y}?vkgq_VA5>E>lqg4f(CPh^S0`?``qc4*>7s&V!yCA!fZn z;f_z`CjNzTB2{ayty8WB%?$Xya#Jfv?OW*}eBA-DHDo(q`cGh?FnynyuG#7doL+)9ej(Nb5LL#-J=KNKapdZNr87)HiQ99-5n*PSWmtcfPnFDt7UO?$@U$ zzy+6MAghBNxN7Y&BEskDYdh27GrBnYZ5pxL*TPZ9bn)whu(F4!5KLo?6l+z+aOLheiYc#kqirPs9 zP+KmJ?q}G2FTbU*n@?*p#_;ie3?um<^9q!nyt4g$w1$zBZ1#$>_WDN+lg|NBUlzU?dI z?K=9@1|T^~u}T2XL4m(AE2* zrN^_xHQ0RO1V0p5Fm*{7k&EgA*{V+$JLw{lKhK>gsGYCoAFK`Lr(Eg0sdYNs3eu;A zs=K}7$%}aiV&J0Y?ol*jVCWpjxm0S?=I5-y6OXy7ZauTD-(Fk~W@1hsW4PmN6jkE5 zkmYvgO|H;!^J~A{R%TIBsQWRUK3?^`*QqNOzNX&qm11W)`mcT}Xu7Bl)AH99b9~p$KApQcfyCMpziDb%2KyYsGh| z+c-4~ZnV$kPzuGc(lj+Icp@$br#9})T#ycVXNbUhdZkPh?Gspc zv1B@L?VEMTLn_p%F>~fPE3$CscqqR0!>2NnyfUa-@9)8HjLBNo|~8_IH^L96%s z*rAXVoXK09=|O1UIUk&QWc9Co;{WIIAM(|*lw`@x>|wX%T4+hQP$LZAh*qyDe) zo8_sB4}z7%owStx0ZUlj8TXGm_r11`;2kDp=Q|z9rB@oEtmjKYH8p)dNxgGzlseZhj`@lP7zZ-? zHZ1TtP2T%3G1PauI)DR^M8;l5?R7!TPih5rf8~p#>b>~!iRZ}GiIhQeE;QT;1N5;9 z%tXcOGycpTcz~jozdAZ|h?9Jb4rz0mk=XP1vwjB7Rdwo?Ikna4U9nG79Ro~PI#c;{ zNz!-Tq-$RtYlJi}nGyT)yR7=oaiz^d`|=^r7COu^9qcy)YemFzzBIsi1V zWFTs~Gn0+RGzo3Xcdvt}k^>Fspi4|oGCmyS%Z+ua6??@n)U^+nqQOinX-SAd7^Moy zymH!&!9CXuK|g08&pl%%%PWUqTKnG%ZIPZ&u4oi-jSuD*Hjr=Er|cov@~;Wfxrv$H zd0qXpxAd0%4^_dB^N#z`HTO3)dA4SIZVd$-T6%@PFUbjn=y!&EyQ#8ZvK+Whs*)WEnE`XG#KSZ$ly?-Wc zM$MoH`r)3Q8zQ{TU#`4|`H=#@KxKYk(J&3N%dgdl`dU1_Ha5#xJS_U4n<2?^@zu4@ zg9PCs!f)@z#!~UWr`whPoNiOp)9qSM=qS{{Jr1V~{ept1KSxc)+Pog_4tV1&7uVl- zD1AT6=8Bi7*!>An@A6lC&<}(%rfQS4Rzz<;rV_waH$J+u%O>k?%MN^*OsPAIU9Te4 zj~9pm7tb(Nfpzr$FW<^Ps~1)tcq86pM4Ub;jq-76$nRYBNEPyJkMG}Km4*e<%wgOh z)K2L_0g@LuV*jB*JGba6df*}7s&Kd4Ovw?aMqlj)w3{NZNdsaAHNWiCsd7p@Fv%m& zI9T-1T0G%5)Uzan*`+1~>SmpY9c9 z^ijUoz{C8ekApO(cP7uL5G-ztc;7rIvb8aJYivXxLT&4@A{VJ`YeTGGgg=XNNbwN0 zI}fhko{{&v*q^lyCRtyshEAbD9S<&?q+1S5H(k-ljDP^4pI^%&cSW{$Nz!7NHj)md}v?E`_uSp zU~CgLa92DdX73}@@)*e@h^j7&=bWNH4zAq8fz|+}XaKfU5MiL;)lsEAxyrmu1pA z*X=xdO(gt*ZAHUFS;2$81`Rfj2z*iXu4U&K_FK0#KXtj-_JUS{OHLaS+~wEmb`05x zK7w^IW!Cy6y{&T$rbAf-=rUu#*J=o0irS*8-8yxw2F|Y{kXH1>1gISvGOS@f6eO+l zlPNjTX5S_UYe=n%1jX7FJjnL)_*V!H$8gw7lbxN@_0 z%-+xpS|W|K7Jo@nYzL5{lQc+=B|jbXh)_Moh@5bd9LUC5l>;j{{ zu49|9QK#3EyH(B1p!RjYRSpDIaH74YvoTD{3CU-4YenbFxmJ3H`JV6n z=Q+f=xMO|7a@eQe$E_`f?d`1Yjl+;bb;y6L^=9nvtznoKR;DU+fS4#~RhXPfn^sFg zm|t*Z8Zx$NbpXY*@YMfZJI_q=?TPh$ud}XAPO-~4mKsgq>~}F0F!gQoJ(0dzPi6fD zv;T|rC-)dyP+BdFm;<2Z%>>G6XIbnrJFJX1YYS5nDW0`C)hDAy>7FSKl=UQ1py0|N zB=@DNOIt4qt-@kOOeo0_x{xd(6%CPq*+<{TYLt;keQKS=UB^CU<{|j~j z$_6Kj>;6c%TaY-!l2;B5!z!~TpVxwyn@8yi`(mFy&8q%nn*9$&_HnA^nHq>&Qv0R- z@U0)yZ~fwLBk6vKTrPfKj>ut!8hT5*EUAw8w(97XUO?Jdq_r1R+@3Ab=0`0%4wba5 z1&EWCQP&jR_~z5XkLQj$J|vLZd-n^xlk||U)^e*UIU5tVHFm%Gf5(~pe~z<6IL@?* zMA8}eDjOuCVUXxfsGa>Gh?b!v^O^RKW&=U~`PVLsbk=#0Pc4%;3OBNI)Y3;!L+_3# zEhi+w#Z09R6PvFxneiSn@O0b;5_~wxK%fzT()w3KqP^@4>sL$J)5Exfmr*Ql?{i!} zTs*1#Gf%&(;VJjt#eJ(Z4KjtTaomAR{%a%$SS;Wuf=6V?J8bAJ$h5`tqg7mliHx{v zBm?oYFES0t5(h=_9U)c-i*6rOJPq9XjO=wtW9pj0KAd!hJ+|ab>&IU3YV}|7l1e+J z;ZWD~)9R8mC$Y6Gswjl=`N{y3SKy2FB7RIhWVxs1J%!l`ll^nly0qA`{pf^yj*L23 za><2)Amh2ynho0-DqwW^IL)bx(ET#l=`$PrT>}19SRIS9Quq302FsNx%zf{FSrOba zbL;Gk#(N|0VJ?JI@T)(~q28B~%!C(mz}7X9(}!>2Hp^SsmUg!ig6$?FM*-;hQ{3(a>cN+-sh@m-1nuUWYM?B)y{|$ z^{sD>z$#UFdl~VI7RfVpslR?KXpQ^+V$9C#pR>MlYr9`o7L2azz{;!lMvMmZTgl*p z)=4kMvTA0T~i!BZ^D)EU>j3gYP0`WANEWH%mUVlSdKI^hbY$&L^ zsEAdDqbuR_b&A?euAb zEOKz-NuU4UDfS-=%42G4%(Lo2eW$^^95uZh8M2laV0Kf_lB>;97B&MO6I(Cw-Rj1Zt3cY4U2uDXzi-<+$f<3@?4-~!WlT;w4Bq)P31>=h*P16WD&b3@z5KrT3BuH7Y@aQi;c!dul`z47xMJz4D6MuJ>6mpr*e#2A#?^o4W5v zjlnhbB0QI~7ll3)fBxX6_-iEG&>nMa@lV$HyT{IIfAw0s|2XPifnK{=LKHgt{@MT` z#}7*5e*@SO%R!vdS)Jbus3gaCzM@nI%n)0{JBaQx_>UoL|DXYdnR~nltS&!QT@K^a zj#$F_$i53qy>+*qWe`<^KXTk+?APB5r7nxW+)}LBs-bf$%olQS1}t( zLGMITF2lxf1sqat3d!quk%n5gs8W?O$EQeb1JI-<^Jymy=FZslob zpqj(9=Rsb`2YUB*rDCzV$`mQ^rY3v7uXVeMnZLV}H$qWn`i0()M+b>&u~+?kWzkU? z^{Y|m@F^azI=E}to)$^XFlKnY`=im8zROaxyq_Dtf#BFXWAQm2d)k8D%X;I~_QteV zLxik$b}+NWqS>ThMQ3f~U23lgFs=d(M?MrZ%nar~u<-hoRDC$w!mFgc_p&2}`YyhX z@pC`plyC1*ZnamW3m|*QS6ynB$NJAu?-k}6jiGR@NZS1m7=sV6-ze)bFM=)MQ~36Q z1@(DuK;dG=hNpB8aYW!4z!TY#BW0@wd6Sys5pD$I2Tt84X`;+A5Y)R2RG(K6KGsLa z^w8U{QB^u;?fd)0f^R5jR-K9@>oalZO1-rET6M5?k1`g2VCM1bYH`j`M)BRLJn8~m z6TQ>>q-FeDzqo!?b3Yw*!fV#W#?#_Gp%&-W4G2z6=wOLT_#y>fbbo6&g{>k@b92{b zcjT^(BBV)Lvuj{~0RBYYHCFYzNhvxb3=JOHoW=uQ<+AwP;AvxiuGEm`iq=U=bO?Z3 z>OPRj9!^$-RzZ657q)N=9ZiiHjkU#K7QTAw=cN1OZJx0e!`-nqWtK5d5j1;A85{_~ z+&h)60rAoRMjU(yPtQqur!7W2zXPUekkD@#OK@bsLlCd^1Jt~gjeld2zO+F*YeX4xE-)ht&xB5Y|L7l%guW_jXkwNaU2X_epltG@htf5*>(RW z{3fyVN4%wa+nvc;>T8$%cw%8&=h6xM~EhIZp_@DQgPeU-b^Ih49 zlQ5U0g;2iTU`3{#r|vzmlEcJy2!pD<2s%|)mgy|}P)(ip&_)Qf1x(4WdfL!iLT~qp z+(a6(q^DFN(8$`p(Op%?)`^7?TJK!Qh>O|mNx)f7Kz83_dP*NAJsez518IEjA4UhN za1Iv9bTcOhe7+;Nd_NqLK>I*+Q%Gr_3b)oodO%unMu-(VXb85 z3QaS??Vu(kiMih-@VRhpp(RWHF`;}fyRRID0*QL96PJi*aIhpQ`BDAf73T8a1%W#taN~qi;SrZyI2*# zd^i=bnbL|fw4LxIiN9AA{wuvL{zrN{4Np?_tHkgIAZdPOMEeloIWcrw`yqCo{Ihe` zlE`k3w&~2IP&EALa5N>qcq>;RLVXF&sD6x12(xib#&p+ zcut@^+$s#E9I%2;sN+2T9!7Hx0~S4!BMiqggvvx>=Zh$*p{ zol4=Z&9ljdp8%TuDwHxagU}J|P5|AFgPW*dhO2%WMuFGqBj-`Y{kXzIk9@f$aF$(2 zn{*}{OX_6@aqu^lMQ0;TLq=2~{noyU1&)FAEqrL>7{7_sQ{St-hV2fNvksu{$_M4| zLwYzH8acB^bMTnlN!mQdZ?-fv^aYplWSxx)Wy!<@RoB`v7&%Yt^pl)_Ts_TAWIfM0 zgm)v91Gh@c>r5{Ay+u#cIzzWk{yr3J8+t_yYs|oE&O|N=6L(V<+#NlhTrBVZ3gcFS zeJ$E@G<#|&$-`r4LF%;{_y7YF-3k|OTXA^?zvN61gS2d3t^bEZe>zj+bl9sLX{zqf zXkSMLdgO3&k{Qr<@F`xP^^#irMpzsWY3*v(Mwa=qMP{aM*e_JB&$eluO?dpHc;zjex|vF7EqPZ&5uwdBTN}8ZGi^ZBEFaWeQ-_$(TSnMg zxf*)>)Vt;>V|vf;v5@#W>b}|k?x&jnzMl>pgXMx7`L0f-=3R?A450{$XLhN}$mJDX zEIl`o-IdyZK~l*qRf|f0GW$Pm*X1*mou#sZ5%mmg7Zh7tQ;$Q6Pp%w2sp<~a5{c1e zj>_%Hir1|hXDVRTxyaM51Un|s+?MQ8i8)4eymE#VZ8pGPm-Q^c6bL*{3P$0V?WLHri+^RmNQ z;F~a4)WLob`mBqf4Q5!dxyp}5WzWBTuiD1B4~KbdqEbRd_}x%aZ__7xGdNm3A^`38 zx~lN7oh9)a{N1_v-lqzVtkiy^5~!!NSAKouJPeny^0Y*Rr0tRKb8>nT=7m%8`Co)W zNnAMfo<7R^o@j@yf8^VCCAh5@LyXuLqh2~jugI^Go>zbQ2IajZezRhymb6B)34YM# zAu+kC`qGd&=e3IWG8IN$x29alB4fwzjBq3N^ct(`{yGR7w%?!!TWcR<#yr}~{?rPWo-bkSfq(e$&< zq#qlGHKjZ|2lmxlu&v@jw}44Z{-v+@xDM9rRN0LdT?5<0%e7A#=Fhq7W25-dd5?9l z)c|n)><}cwSpCT{v-BvP8e2F{W{i9Ab`1{k`G942hn`2*7IOBNUgu(-bxNZ$N~*5D z&9nTuTDmYDVG?kNPTy3K9(*XT1zN+Jr{@?5TbtfTR%!VykQXIN(#T!N_AE?yI#Dp! zwnO<%%BD{QP7NJGO;!jI5l;4)cC)EpvWy0FXN>r%T|I>&LlA{*q)@sCl#4*+H2ustB}=hjIZrY`xE^zr9xTz#s4Mig(x z_Vdx3tf_eK2T+v@?U4`Vun$;J36tLPT`4a<6eO_ZIt>v{NIkD}i+`Qw2aQ$`RRX_eGF6prZb=JwR-g`F;ST>9zD59nr*ze{UUli< zTl-g%i+QHLbhtksPR$S>&iNv3is{J}aHuJ+GIrUda@&xr$|iDwg9UF+*CChB?Pp!6 zF+oM8u8YPq+4~Izh;Dza{s)FV&-CmmyxjRlKLCh34b@TBNUnp|yLis0G=7vx+}qjh z}lOQM_vwbWy7s)uov;Ixm(f$#mxv}-T2sf7{r)3$(g{GjeXFJ=2UY2 zprgnP;%)-?vma5RASnS)BFZ(iU5wQjp@@SM5u_b@vXdSe(^P>a0|l!5u5JcdS+0Kt2esOl~5<%Qfa*>lg%g$DGuH96%* zVG$r%_?~1Twldt3!51mN$5h;;(UNZ8QfZl!u7Oq4wjs<#cFakIY1u?7koykNETC+}gfV9z#Z324Kl?c0LaUA8KwzUp_+p5-M;gly=Zpho$Y>}#B2nZ2q4~u{@|eGSt-IZ(=628~BvCeX|`xwZ*KQ>hSX?bj(O z*5H50b5W-{V@r)w^On|`Zl2yger8Q>`0`+3WAA$>sFP0cHLMxoF_B8@hZ)W_SExT* zyI%1i^|@BSc(j6KYal>botVCJjZ+lg=?Z{9gRdsO6eacKeS4OCQ z8+#BukNQ_S?mo-bwYthy#t#HWh9bBEOv0Hbbfy8wR?9$QjGw?Wm^m|6zt0I5Z=92L zk2=9^6e1DMEL@N27F=t4CpFPHw#}#HnU_f|nktUV#B_^Q_{{mS5jLjbC$KZ5qW4d~ zue2p`C&!A2*r7qYeZZw`>~;nel3RTB^nV`1_rHv+#y28=EVEAq&fh%#wvzR7tqnuO zgA+)m6-#6&1?BLA<$-^gKze59kEa+k>#o5HoGD3?&}vPwB^aa+pcS z6zHGpyzn|%``+1ntTtWWb5Dbbv}tiLXEEKL3#DE9e#A$2=Ed@f@?0Obr8`bv%Kjf; zZy6Q!_J@7bT}lcL$U#8`1cU*k5t|xf0EZSO25ANmq)VhMkdUqkQW#QNL6j0^0EZG$ zkU>hipFN)Yd2yfrdY-j-ch-61?BCwsxUTE-xQ9n`06^ELLn4@K$O*SkyaMNz5q6`oibPnQP85pfSvqFzP@z! z)}VQ4ec(pxvt|Difa>XG9U~TxSp>PmfzriEL-)wV2^P+hXZ@)^h4p)Ln3z{Bfe8lk zGTf%({IU8>?q&YL+MDk8w|9H8BAr$7x#@@}816d3dq{5ZPpsQ^4_bb+wNqZ;?@9ah zKbpu%VEN#|rAEH*JBnYT!FqJNg#k~IXP=!OcA_)W=6?7G=5=4H2F6#c|l)p)SnZPo5{ohU2=#oX(do>9BwR?;P!+ zRK2Y6$K~k=!X2<1e>|JcXX-H`;w6cC%n62T$D-fDB4#z7pqcp$zxL(F(aCe+rE?J{ zA7xNubxOlx3(H7B^kMVfR7zs?dn}{3`gD*qZ>4wruO@^cr8{`-oE-GqM`{4D9oq-0 z324N1c%oLH7D1`^+B8N_PB2Digt(uh++$+rV0h1~K{FPSm}m!= zc$7#O(VDv7b63Q$L&qW3b(`^K;%TS#xwWQFuIe_u%Aa8yVOQFMsQ_sgC)`>c>c#ZTfTk4(~43 z(32VDxr%H_3<`ua|3Bxg#zdMi?sIFsSYPJ-bfl@_f@SftCj#ON=asMAceJ5F#^;#~lV;+s z!n3(#SXvp5oH#mUrq$ZAxkOa1#bbMJdV9aANt6cTkH5-e&~u+K+kP(E=c&{Wq4)z} z8?s?KTt5^iTSFEfd}Q&Ygy?g?_jL_lK^-&|thocJu{0c4i?$i1(ln0McybUd(Km%x zUf&}91CTM)tN@V#oxYV-`GL#irJ1d0_S}{(XgMPO>}&D}7A~y_JyF@Dia-C$t@#R% zJyh{Eh`I(sTxx784Vd8|B}LCr57FJ(_iW2(a}nNrF7ncr^+yK0YHOe88PzRHks@2o!FOJIq*f7t=q25B@ma009#4VpB+8_T7wE~6!wpQ%#Jv9 zYfUwr>uSA0o6hX%*vh!xM^yi6EFFg%eOpg5iNd+K;&aPLCX6_tevPt>Lrh0OvQE*l zrfEY21*@X+GAET09C{Qq>BL{TO zJ@H@(mY8ld_{_llx?oop70V4+A{w!20-{C%jWgW#T?^;mQSPZrQN;Vpr{yS_Slad| z3gI<*>x0`UBTSuo6QuE$j2eef?2?+Hn;bu4t)Pimho3N1*wM#!I zk=918xK`I>Q?a(Ep{^EIvf^M_dm*i|GMZ#;TE>V~vS7f90hdyWq8O`Jk$!Gqmbb%g zU3?LmWM3JGcY8vz(vx;C0LeY;VF=~ad*lEyumpp5XU1V2dbNnDism2-)Z`n&y<_x~ zA(~KnBDj3;S%n*>f>~D!U;mSI&#o+K1+^3+IkUK3-1(~s~)|uqA^=fBo>xsbP|_dzsdXk(RA~-B_xpRV(@YPQ~XtEwHeT% z(gHGFr)7|TTjXq3jT_;hF`TWT`a}cOHqg4gEWg0op?0C8Ym+bOTv}riSV|SkLS0Id zqUTBj0@cbRy9p^^7d1u?xF}n}mTMK;2a(C=|7EJo8EP!C= zp4+R(LHoP`_x1`l`1)zS_%ORYMC666H@Q}%ftc%`m0|% z{-!Lk^Ecx{qaRg;qgU%vZ%x&^;e>CvTWirc@51+6AN4-NGpfzMo0}k0gU20}I z=cK6hWvv0`=}FV!6f|}FA3J(hv_l=)u$K>lo@-Y)ma{Kd?gkYT2$%9M7^8oX8FW(@ z(DDkyGroC5oIC$wtXwT$v7`t@w@cq3HdCAG+9U76n#*Ig@F~DM?5MCz;tegYg2BYor04Kihs2{wP6k;=E! zwLB%nyqz*BpdX3WApU=>4YgbdC@b5Uu`o>J&rFO7eVU3oL&oLrxnVyDX{<7!D4L>mah53J;QA+XTL7ZhK)z=Fyf%4RL^O;cN^c^PDI&J?RNA(mHmz zL!R&NizvYGAA(w%FaXdD8xp+-(-b1dSm8>$Ql`>l$R|f1*U*jOxnP-8-pp)GyaJ?Z zw++7mbAgtnHcibBK)-bc76VGs19P54%$@|6N>A9soQ|hY#yqTWH8JD@DI@(c#c@e{)Fm{1MnC4O{xMR=qD|p8b{?df3i{bqu*= zpsfA-J;Uu>vKFxagk~S?vHR8Jf~$aAd5Hl|9h=*+H~@;UctGl<2LWM8^K;#19fGM;B zidN}*!-tae!b@|zcN?9w{M^idpB6c*g`XIG)l*THeQ0AI>UQP8{rJ9sVw8uFFqOPS za+J_<7UN`izoO#jmt9B>Gg}k+(y8|yNPAcOJYW4%3%%gLCF8yjv$X}MT$SI>$=b-F zTT6kzG*?O4ZnC@W%oH?vB;nrq{n(^GdQUM;OvUB>RjkFpkfPynAps*HFVa08a z9JPF5IN&ezb3Uh)zPd#7sv(;5n?dnQ(Z2hD5y1?iXS58_64si@m5r|{kSmHXgMzRg z(&0|leABye>P)K7X+(Z zp`hs~>{SXKv%^DJ`+aHt1=TBX#*zZMg0H;>8{&``b0=~I=vngYYpCF?YkAF}1ZF^L z5eqM5D)bsL*>`WbfL-=EX<{?GY5ZFGUGP$sl`l4c?G6n8p_25ep{naZ9O>{(rGw^H z+1X;nIP?PqCRz)`UyaB>J1Uy4H58NC=kmw`CKYA);&xPF`Zq+unZ_g@_2j1xuJq6F ztNYOnw&qGBY0hqSE87j0;r-vX7pwyAcQ8#toMYIdT3Zx(1)I$*zG}$O;%Ki}Ep;Af z$lO#iU#|>o)+pT?Lw5qcD!yA09IR7SEsN=sMkbWd-XzhtA9~sGdsf-^~!1BkRE?`QDnAEe`$+Nd31eTKJ!;sDLtSfJ-wb z-`083Or?MG{T*V5^bpIiPj1F_VjW0+2^U7Uz-@sVwrAQ5``1Cz8a4Wc>7>6M)TQ)@Xvt_d;*jhtgnJaAtMS58qawup z#NXic$(4Vy?N*{qc>EGOc9;z~ox|B(O2wsrnkOIeUv!}LeS%PXi_VB7BCLWVGtcuHJ*Fzp^86i!(MhBXqadu(4Qj>xVf}wVC4D~=)8y)NSI>(dV51 z6}GBpgHb#c{`9rw1Pd3XYk&!2i4VX*7rh4}kWaDE(!|}<*!~i9BTk%AMG_=ZNU?)y zXfFN?HR8|r+$)x}i3`sMpWQHEir#pciiErZejgB%=5WjBu2&TQ&YL}!sUQc<-T+^g zXd*3LBJn^vx326ihs5!^QWUY-ljW-V3}xl`rfZ^tex1%(Dr23_N`3E~Y1b5R@Z2y$ z$$~CmhCi=koA!LOFV)jlOZMyCj$(I~fBL9CF5ob@c}wqXEE-CqCr$iwM`M%;mJ8xR zYUHuSPXbt3PcV6k58hqqLJGr5e*gT|xlX=H01>Z4V1UksHODn&ksiXD`$Itbb!`T# z;iG_u%%zI-rF{^eBlf%RE=Jmmom;I5C~YTrfufl`RR?*+)b027xjP3!_e|Y5N|UPA z7r&gebJG>`tA}(U?A)Xa6(3bayq%tJq9K$QAD8Y_k7j<%676K&CO7GT9pGc z($?W{e!R3YC~{`V!XD)|o%fL2?0b}gne@`NbZ#80e%B&+JeXA>E0lqSGs)zGh6Uy2 zB6pR*g#7!k!AGE21PefRk%K)6S&xxf*@wTlnwto#i3A}tHC>ssbI$}vQOciA%M96* zRhacA+Z@QIKr{0ug)-qBH0p97Q+Cf|oF+=n8peDkB9D1kI1EgSvs94sv6$`3B|;oX zF~t`x5h74pFSoBk%QY)z(wQkUO}m8uri-Kr1KrH$jgnHHOwWHHO^;{!vG*g;@YkurxoVAv`iR)a7K_~_a1(drhdgNhvz;O z_NP5G+v0cP-w4@*7N2l`+aP8NMV5h6un=T@GVcHrU*u%wNL%{x5%*gUt8l@GmrZs@ zFYO-jT$NDzbrDF1Gz*opLk)U--XVMb0S=lskJzIGz?M>w6J%*HeX_EReNiox_iM-G zN%H=;S6ONKoF%)-9Rmd;i!sMlAT@*_5nEaW?h^aXv-IQG3#C*2wKq)AmHcNm&$(|g zYbagj(O-FxIm?u7Kdrq(}Mt=1Brz)*C~32mKLKM zHg>p_Kav(#!s}W*W%u|bu;-5G<@dHq#t^@+)TuwkxjDbBt({bPC-DqVW2<_w4LS0u zw*2OHY-8@daAmCN)9rBsb?0x8O4kn-=MRqVK4{HBJjKzbtv#6ABMWyBds}C#%tnp- zW|4-}cx~Zt5KhKXt=9K>WaTU#>et#rH)8)0ndNY&l(WVkbtm--cWX5+% z-0JfrAcO?Y=TQ=)&RVImrc{O0+jLaX7E{tE^&nkP;3EQ2o}GjrV~Eo=yhT@ms^0gN z(^d29e7@#C-=;N=Xn?|b4i(*M20DACb>7`3!Qj_#U0-cXN);FTH+~hJ)^lSqjvo=f z*y1zes3!k!AGY@2himp3=)3-X7`KQXH7c=-_u`>O*yYjJc{@!huz>r5_hv&bLu>|Uu9c38B3Uq_i(v&^ zCc63wOi=N};{71X$gR-hbeGjc+K)VAJCFnn^0#!H8p#$+8ASNy2IQ04$71kl) zz(OBQva*06os`|!V6nRD;*!)91`MZ($20fdI>thSw=`x#`>lZJDqs#g+Q@F=bF&0N zFJLzR3~iT8z&6ol&2)gKys97;IgaU`)=W6EDrv0-IsE6w0uytaN&WRC2ljaEqNyPb>i!6g>X!ru9At6 zUx;Z@Ho-r`iS9Wtf+osM6~E;G(I!3u_3(8DurQYS4#=?gaxX}-2o81TW!IX53_5gj z_L-(UTH@_Lm5l~|tJfSnvc5CPtSawac?S}!^gIK@YEP|8%=K_RQAG`1W2vAW5RUOq zn8=Beh&I9N@>%26qYzgHqWq z`qmpE1Ma(Vcq;J2B{t4rmj@g!p=x0}!Ez;0a|MNh1zsdYtsvDn$) zJSwHN!w(Ir5U$#}^;~CqQ5l$`KP?h}NrO@Kbu!RE4jCN&XL3i4a_Lk67oaI$}Bu90N$-g~y78ZM0<2{h0?e-hKga|8bzS^qELSZwh zDNNn2c%Jclnu2A@&R7%pJ8i9u@6J^d6on?K%Rw0_QC!+6Wd#@v076}=24DQ2mnKyk zh%X)}`kK$Tm65EP?8&tgIKLfI4aDU2=}-apb4x?^$jd)2DffuW;;a0SUyecn7y)S= zd8teO&L3+G~zSD z?}0Qutc`~wNE1Pm%H6=QgD_`@R1p`%=+9W2w2s)`52DKD{jWns24EzeWb)haj3)qig27saQhR%&1 z2tgQaRz^P$AZSxGP0!X(B=Um?Z{K&n99OFR59f#usWNgq(W1_O3vE*-tBlRfvyrW_QXBiKas=tgY~`Tmju zaWL`skj9R7hLmO0bCngYAM0;`nDlGUZ;9W7_T)mhm^4y^BXa9fv^=5FEcoQWHOkP0 z!NKOg6I?IK)TAG`K;=_C{ZEs921rGOv$m1`lwVO0|Hx?$0=F8J-~`Q8X78+DwDxTVPW|HlXGS<=;oyUmrY8^ z`6ps>?OMx+FcMvA(sqdn?*{Lypo6H8(!}*>tdOm&=FthBtjDC%m1;V8(7R9(we>L= z@YIxj?@^OQo;mPYIe+W$p;W~Eraon$VW0+pzgz9{E>FaNrH9Rb;vUW=|8Ri$kiF1{ ze&Ph9op($m8R?j$&$_w8n^%4{zGZ8iOzo83=JgzRIH%OlAQqpfc8&^9pet8wO)&%b zpVA`16TSmsKY%5m3?j9daoaGwuXRUJakbYXF;6}yI!b`QcuSEoX0Lp z=|*n7-~m?S7ecgsz#L#jFrKV4NMp7vIRT$tH90ZMDknVMp&7F2kS4z_C!qKcEnF&I zmyxaPE=3t0QLyx%pc?g)4Xu@H9Bc`HRY{{a zIdb$)0eKMGHID7mCVEQ)LnaZ3VO{C5sjig8b{VAM1{oH&^}t6KMF`Y^_0b}|B;QK? z!RPjjno3qnS0coy-y|lT<3eS52arx*b7L-4j7}s{VS;GjmP3wi%W25B0DSp{<+1u0 zP1#Z+F2%h}k=x8EWS_0{uBQmw>5t2yoIvn@Fg+EU_xxQb2!FPW!+xmWV_0vft41vm zvT^#LD$RBU7L$7b$wH(vYx1}k`;CmVOiu>O@s5*@tt<9T%a$)#&p<^qs6>`T=zU7q z@&?wa+944!mo>9@zu5)Gra#lE(1s@{l{a|>@|j35%R=SZc}#C!rA@||6kI42AAZHl z=oXC`G)iU8`gyvzyjBL#4fP_vzcj>pFO8jOFp9;E#K^6|#f> z&TPBh{!ibsO6gl*!|ihn6}x_&3K!ia!()CW6TcW+=WtHq8#bE%Q~6!fh!M;+LR;h7AIs*+W!|QeH)GH$plx+y zG0TiBO{9JgleYwma8v>t`DZwKwTd-V>fn(N>9Mp+1>bQ1XeG(yd$(~S@tzeh0@svfq~kI-gmCJ0 zT8q&SdPBaS{jjawGkLH7@Sei&5J$R+&{3`Cc#+#Wn>$}3zY$aXJAT?uGrHs~UWzEK zKN3PGa(5uDLT3Rap8Js=L@E*^mMaQ<;aV~RHBoZx(ss2u=~fJ=Ul$o0aX$YRE#noR zppJ=)R$(>jR2P~W9LZUYj^It2U^)W=;4-FC22rVkB0-nr{b$k{|CTUP|69UP1|%$a zi0|@H>D%$ReE_33W&KK8Z-x+z!6pu*ohtG-V&CTC-qe^%=up-`bI$?aS;6s|RxlzB z1eKu&vea-Ok%B>vGJ*AooiJ_$v6^a0AsiJ;Um1YwvLF`QGw+{#n%VXBrQ*80em2kO zjGeHMK@AAsS+^*qd4*F5s>6_zg zdxEdTZ#DkTybXKw`vhRmLddOI^ks$mdoMnb3YX%5MuDQ)(w&9VzW-#83`qbZ_6ro4 zYctp+msWGYZ=R=Oi`1`a`=nR`JXdm;(-8Y4E05zi#c+Lx`$yAqeqsGvr+v6kHK(4C zhRl$b+IU2Z1|$xrFY0>E4poDRS3FIyD|sUC(Wbam8HHjja8C-?=Y2cErki(HLw8p*4oJrmlH4J*h}J%7wl5{=RJ{ zd54M;f?xp1I8RG%PF;xr$)o0+g34SV`kT{cs)~6c`7r~C=-`6Mn-ny#rG$uRef>!b z^PTXj6EisSl{&DWpY#OTvxrJZSq^z?X82uuNKP;*6QPX>-3awkwrvLxWWkh(PKZ;TtMR zq&Qhd;|eIpfy1Z}HSwy&CF&bRgAL6v`~;d8cC$h$^d6i6Lp;KhZ&|n*x#HO?GeVcf zH&IB@Nr?;g58Q|yU7+?62cAJPF&H*e7!-p91S))O{IrTFAI>iN&`R@ISk;f z2G$PKe&nr88?1GS;+{I=v$r=zOe9#wF?GL^<@BV(u}u!6SJ%EU5_iU0+pF3CmK7HK z|1B#_pM$a@8mYQ7@uPXrU19<7kg!|#bpX~BR2GSO6r@e{KM)F)8!tIXG`#N-oBjC|) z7T0dOz^1jYPWuKAOR-Q(7CXRUUEPl^$=e^QR*{uHt<)ZHtB-}_=Z3gcUmqU+P)F=k zMM|SwT^z!l#*D0G@)3EMs%HuNtHCnucDrU;HkV4aFp+-inpg*4%+sN#!t?y)uAjAu z1%t*BCV@m-59K=r$fMEkBUKK}<;xcy>t~b{jEpm(6VH$>nCo$Lh0A@mb;h=xUKd?~ zK83xEs7U2nrVSjocpI@gv8$N}iSV_Mj$;R{}l%x=x4;C$6U@iObA( zb}Rme9X|G&19*g4y`ZO*K-x}a70E%271vV_Y7Qz_LrA&1N~-|Yu2o=pp7jW)<5?$P@?A(-22ZED<7YwgTsXU@6mB*hQ$IT)M>LD9DK!L*I!Ty1cV7pXx`D6ujr} zC^iFj{eV96MX#8gZ+98~RwAYUv*yEsyi1d!434WHBlYcK+%}~Wi9a9#rUWoDGfu+4 z4#J;PL(W|oG<(J5rUY)8o3#2MZTHT!D`q4Dscs69O2%FXMeA4?QKm#(g9?D3zftC8 zNvM9$6?D@k3CqZFn#tkDG%K*Lg~wkK=>)Vbv_8}C%_m@v!jE--O#M?>GbVr55G`yJ z@1Z2TI=^o}Y0~+t?8ELnxuuDr!**+e-6baxZ#%wGg@FbA@!~Vc$>FTKq{;ZLu~$uv za$e{$4Qu!3$alIG;U>y`)FKr`sqC%0ok=pgZ-jz?fI1wPpd$_nQ+8=06BfdXO}Rl7 zq?gUVKyYR=&g>wp-H^p&_@)Bnz3>eVE;u|-J)hx5;C}MiRKDc6rPt>(q7qZtL#!o_ z58E<~Xv{KcMz-8->YQBhS1J)vxM2gC@cC>Wyh*DXy-K2|0u<`fDKFuRz2$1iPJ7!{^x zetTM-YfD3kW+KE#bY*Ta>LPLtfBW7>zfF1hL~p#0WJutT!5VwsEkpGMEUp>vjT zqzqO$z+iQ7)@2J|RWdk5HE9y6}{6tyNjKz{Kirn!m+J z?f+aI@PCms?cXbZ`l-fEov6Bs|5!oVUJ_;?p2oU!;Z5Z=N3AniCqbdA22r{5BCqU> zFaiwOuUHNmXPq39L16oC5N`v!6a`qM^K_g3uX2r&EkQ}_#=`+EgQ-|efjVSuPveQN zx^`&mtbw3qEZQ~yl1|ZzZQc$c--aH2&O`Mmh1=?AGq}>31e0DqOs zOqrB?N>XEkcZ)6ianQC`z3clkOCeu>U+~u^fW7Zo>SD(mu^6$tdRaF} zn7ewDuOOVD-IXdXzn6(wO8iTvsY_S6JqXBf>9+5T>G*Sf{TpC{mh_FN51T>W zi_);3KwagcXL_*gq5UOpw^`UoH;+t#q9z-j)-!^JtNxf}o9iwKRTT7YRhUw=4)J9n zym`ZcDs zqj!?OdXm_#Nk?M}MuoZw-%mIoLwaNKSrPHXPnRpkz1XM1@~ymn zHn2N_4AES`+8y@{0U-mZRPYjvrO{pohD(WvBUfhq#USu zPCE+#jM_5aXzx7t8{x2=mOnIfw#qcTaxE8~3pnyjAc1Tb-yLz@>GxRh2}2++w+gTV ze;B7;y~ney-BSvlR8r|0JZe*|oGhIBQBNIAa_0!X=wm~Xr z_u!qV*AeG{k*x^f04${w<)3L)qh2N5d;xIGm|C)c(6jsoXOgKHs^-EOsfd;?E&PS^ zNJA^Phws@*j?Yq%1_t%_6ak-V_J zbxHON6@GtSE;?wT`RBEuTF{6yOyIBML4sEdXT7DXf#m`|bJjZj`Gn0gC#5@& zS+Tq@R3zqM(Oy6AjJS~#>J-W}1vb9<7}@+nNBZ?!^lb}_qf(Y}gox?u`lr~J;aDTA zq_Pgb!AcQ(>#hevBeb^CK>8BAy6*Sqc*lX<&_i-}2Ld0WSyu}stL~Pd6?df@Q9G`t zn_uGZPspmA<-)&XLmdp@#D3-3+xy8}Kl%z!9{$PB+8cnV!aek8jw{)uBa||~?NZeu zcArhV%YnOYCA}5(GTV4JaG_guhW3{9*0rcpA}-ermdj0PO&Pn5@#c2;O!(fFtV`08 zrQ-29b@F;;^yW=8W%2j$p=|ighlX~jaK-@AmkL`>t;E*-Fye9t@@AXL>qsI}qH>S4 z%)4>$;`s>Fq1FKC`Lk{%c8m2M4;0rAhIJE-mMh@#D>=1qOd#B%_?I7$P%CNMQa%#D z+4gA*-IVc;g%#WMJHrxeb(TdO^6GP^)_-yBjj+Bvzc{eov$I9lnzKJWb|6I*Kl)0I z?V24Mq#|BmStOj)v;HKDe~`8Jxcou7N)D-<*T@O)A%}A4&uIvjnoMb)O2Fu9x_1$Z zPU`iIVDR^ljK_?lE1CU^^T+xV`dXCVIG83fE}x1>MWp<|;P16u*C{hk#`tZ$%_;r0 zd=%mFW6?ULb7Sn>Ig!Y`(R1iM!Oe;0JCmEQF->#-pv-a#lAiv9Y)ZW;{nH7v8{t;e zD0<5b3zarqd~2F0l%7Y(cxUpeY8$dQa=g!zcxqF5lasYNlE|sKLlr!O-+Z;xexorZ zxT6z_+`LX!r7yuK?&kO|u8v~7yT3nsKTMN9wn_*0$ky5%&&HPp0iF=Og*S1)j*J$wX3cIuikym0)+aNGF7 z@pKGZsox3H&tCYN5-CF)hX90CnI8l0>+7OQ(%$Ati(us&VZ^L`Gn0`V{^8J4Vcp~; z0cY#N7Q=vudjirqTe3?BQ$D`?lg^F)o>>dsWlD!>p2H10XInMJ(Gyiq$U};_Hl$h% zdm;*Xp;$2zDPPPtimAq6q+Vypq{*QciT{w_J(`NZRP4~1_*-eVFU^wYOeCci2_g~~ zcheS*Z+}+7pDe!Q7o47fSATG&BInp<;`ZMIarmDDQ3nphQQP=+GH~Xe}~P+G@F8xsVl1)1Y@>x=3wx)yaKEjP0ASul&$+eUqu zY2%+s=S1U5B&UUS>gx3J_m*B_enx&JG<(F6zbjZ4IN}JAj4^k$IU_|wkG`7_R@*Pn zQ;{a0uI^SQuhs}xW|XPMpGP2|%4K6rLWPc=8KO=X3Jso#BK@`T8$R4}o9Rs3b}!{& zeV%Dzuq-iJ;v*`eRsJpb2AW!F4*d;@SD_hh$ksyg%RvWFyl~eKWymoie-8H%2aWl@ zp!xhqPgGL2AzhqN1%s8fm)*H$KKESN&)BuFqr+c96^SK{1v6X6qs0wzxR+M#MCzy5 z6mT;2?^p2MyIaPGdr-Akc{RDTI4IWKr1R#9jW3~n`43D)-ezmY&krW2_2;#29Ryt5 z88;@YIqx-V+}o26-g)H1S;kEYwu4Rv``1p3$V{m00ia}BRQIANP&P}YsSw}kNn4lj z$!^{iDsFjigDHEaA~|5P?G}TP$&}lKtlUG6G`_6x*;Q_GUy)kBjQZN$o0d^#^7>XP zCfk$>3!l}2jRJACV&*7#GQt^$i^_UEF!3)iOq^Y){_+b{FSuc}clB-`903_K(4 z(p(RFcdJ*lDfa+Ei#Up&Uxt&*oAs=r{ODQ73;4DHwkUXbtF{zInXAQ~^7Bz72|^ z3x3b`432JN_4iaKRSw|TF-3`AUwk*`n0@`bO&hC&FB3gNQxQ`+`dLkUUz&p@pKMk5 zQo|%G^!=ACZ)og1{T|L71ST-|oEP0bZ*I?D_CmJyo#WaB3kV~iJQqS_;TGV+~MTE{$iy|6EG z`7mACxt-u`A^m}v`hmQqF8Ql?!D>1u8S4Hp{SH3PLQrIvh@2N z@ab9GXf2KEq8CT5&t#;XGO=A#DRyiQ{slFVgX$1OUfYpJA5y-4)%S!&s6BEdb6nz2 zp!GeDWnoG+@T4WY{hCo2G7Je$1o(^E7no&x^v=3+`Iha^<`i--Q#rML_a^tzU5tBW zICA~YChrV`@qF1j!3OB`M%OWTwb%?9MQdY+` zaOKB`Y!`S8ZaC%SgGn%A6ni&zQuCF~6U6NxrGs?$`|mAJpse@&8X?Cz*omt1XH!dX zO35W#$#Y)^Yb-d|d~*;~zWYV_dQWW8DznJCahQm+wkaD4!?8XN z!%bS1F83kyZytGYsLOd*Za#J;6htn3%b9~2b|Hn(siWhS89`P{?GgB)`HX)o%QT7; za4V3cd{vRse8_Z;!FqOxmPLMTmc({}mssqejOIR%^<-m0VDz~~0iRRY+7^5pvWQ65 zsWP*hta9R=ocL{f>ygj)XAr?en@o)K%tA1mD7|tVvnPvFo6H-1{z-#uGSjmr^7p1S zmcI#n9EB0OJoW8Q)gz}ZmHZ*YG&yJ_@xu!gvX=4$TzVBrMrFvMI*r_Oq>k7pp#NUx zwEuU(w+-HRo0+A2)MPfsrg?4h@nh<`w94fliy;V1iz6+OQq+U5x=C*m%8w~%Odxsi z7>&dIUiQ2LTK$-h19o+xaX0OTou_ zl-DXP@}?^>$qa`#+mkyz<8tAtqdM=Gx)OLxwkR^04?~R&KVxnEjOZdgBAV?-gR#C_ zbZMjWWJO(weo{!*2ls=Co&6_&Y7u43=B{)j{YP$}ys8Xd<5ILylRZpZgF!77eC@apyaQUOc(H>NTcS^2zh#tWqe#$m*p|3v$VBouzmOdT4~fX3a>ygb%; zr%&ZROrY$|n(WeUj}-FYqpyLV0%<}F=k_3XFP9#!EBS%QQrS4Kr7#)nQEMxb44!Fe zH5*P9y|P+^KJYdpFMiIOV{ITjz5wcWKBM`*b%M1|Tp$^gXc}zBl5cM0*5p6+)7P9 z8{zAX-C~H7{=Je!>$(AxAovBmwTx_qsqH;U;(~USaTyEtC%Vz1(->v+6oTe!WwS{9 zWNGUeb18f3W@zYatb+d@kD!8Q*yhjbyRq!cH(eb$i#c2OWvzF{XSL%>g!~MuFM<5N z%8rsS1V*V&CaRipU$-D; zMa^O0cceZ8o$11>jP?we|De6K^t+yS|Ab+|`GMo&9d`4kHIL^f%STWRqy)@L7U+lT zcNJVVV3Pfi-9atV`u=>>Pvp%0gAer#IQiy!1Vpw{g>?8y)5(t`l?&Xi9T(O(eSSH7 z7A~>Z*&DYf$HsEz%2b@vX*jNXrox%tKNjJa&&N(4aUq@gdVdSX@sOe^Zz?8=JVyecICVEPr^*!tVeqT>=f$uPTmSbY+%@3l` zRMx4PiF=(H1vzVS=oaidjHCE`U zTW;f*o;!F4Q%J9p{MW1g*q_qw%Nd!@!N&$uC$Rbltf#gQR3W`JHJjAet#_Vm2t}S^;_*QHh}M`Fj10+1Uu$FTCkVQjzF4t`AWQ}-o1#<=ezMXW;PpH78 zLDdRFxl8M0VEcW>NF_ewMcx?f0K*1orubmir2reYUv3&yYYHh*R~ zZ&A-!UQWB?Cem9p)%stZOKq~_Zfu`z+g z;A@GRCg%m!DiN%2FRfmFJ3>?TLE_|vHu5`&uJg#S7~QSDew^@Wjj-CIhbpKCrOT{X zNIO|dq#fB;uc8b?4Wv6aZq0b_0aHW%7coXlk=V*Q(9{|2^u{i<89EsaFT)kJq+DUX@t3|Ys~>i=Qu&BLKy|NrqMRH7ASN$XM8kYwy3WgTV=#uk!{Ez8)) z8g(ez>;_q9#*BR|V=ILtp_s9ZEjh@@7Gqz2_jEqj_qyI)zyJKl<$m4I=VN<5?~z95 zGiL6;#viII2x>dm-2*4aIoZ^1v$i8ys4x zIx=ZR6r%>-|2vz)KWgYbP(!yBJq5~zCZ+?Kt$w{r8^e(wTPvEDj}km8iEyhVLh!t; zdt%Uj(hYx6DITS`{hYJ`R(hNA0(zCnJUp%SPC(Y^$P zLD&oLv9uzg$E)19q!OLDhsXn8E`%EKPNJ*}5$v>D3mos&pFbXIR{f);GRK?KnsxaU z50AxMBA=_+7l^SIAEHQE4$*7ZD4!a`J1>*bD+g~t&42Lq>h?w(%v~k=#T3C*-qD6j z5OpyGW#ijl2YI+>d}NKkM19~KP{;dx)Ck&Xk$ofjUxhuHpp7I%67q{^dbmwo{t<*| z`AX?2^R|?t$fBXs#L=5w>aH}aLKF9%^zaV0S|hA95w`Z8I(?>~tFUUeg=&TYQr7Gz zeA|;0%F<8I+7N5IYX1UvEM-MOsDOVnlHFjA4nZ$3aj1mujT9jc!~7V=XC zekLRaAlOUM^ZZSkSXjCjL09!ameU;W4Xd`L|8ObX{fsg_k_#R{!sADa<{gM(d2?8l?T4o8q8+U@pa|unIzu8Yjdp- zvOLhT_7lj`aQ4%j0s+1;L@W(<@9#17Q#^*&B<7f?@mq8zcc-QPBo_ioZPw)gH?r_F z@xeO^b|*}Hd+bbZg9-_u{PCCI@(UClwfY2QYDUvg9S-OTE}xyXb?3GV1pT2S?>?&* zBoSfBdO)7Sl}VZpwU!$m>4JrvwntwWk-ENSG;dE(T^1t@6pVH5lsBmvEre~^BbA-6 z@d)*Fdys;DWQU$w9==E4eg0%-cu#eHf{$^Cx;}q|G2HM}RD*OfCu!neijDrv^KQeu z<62u*5$!usAATs8Jj=Hdm0X(BBMa*(TcvQOt&T#_X;El~TRs|f2KOLEFdO1qma7;$ zCU&Z&NpV-BPf!DG+$BigD6(An@-(7ZNpkjcp)Ry5{_?7^M)|Hf<2E4Jd#F$90i`-o$wO^n4`z_*U7bM_b%0A*+TIg7svG$d)`JWRRmERcacbJo?<8b8< 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 )3d*TR4{LREJ#T24C>P~^w!O}|%F@EqDH?$R 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

  • >Qw38eKD1 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
    hQyltZ{bv;u28n5I8b8&bs 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 str: - user = req.params.get('user', 'User') - return f'Hello, {user}!' - - -The annotations are optional, so the function may also be written as: - -.. code-block:: python - - def main(req): - user = req.params.get('user', 'User') - return f'Hello, {user}!' - - -Logging -======= - -Azure Functions adds a root :mod:`logging ` handler -automatically, and any log output produced using the standard logging output -is captured by the Functions runtime. - - -Context -======= - -A function can obtain the invocation context by including the special -``context`` argument in its signature. The context is passed as a -:class:`Context ` instance: - -.. code-block:: python - - import azure.functions - - def main(req: azure.functions.HttpRequest, - context: azure.functions.Context) -> str: - return f'{context.invocation_id}' - - -Bindings -======== - -Azure Functions for Python supports the following binding types: - -* :ref:`HTTP and webhooks `: trigger, output; -* :ref:`Blob storage `: trigger, input, output; -* :ref:`Queue `: trigger, output; -* :ref:`Timers `: trigger. - - -.. _azure-bindings-http: - -HTTP and webhook bindings -------------------------- - -The trigger binding is passed as a -:class:`HttpRequest ` object. Output bindings -can be returned as a ``str`` or an -:class:`HttpResponse ` object. - -Example -~~~~~~~ - -``function.json``: - -.. code-block:: json - - { - "scriptFile": "__init__.py", - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] - } - - -``__init__.py``: - -.. code-block:: python - - import azure.functions - - def main(req: azure.functions.HttpRequest) -> str: - user = req.params.get('user', 'User') - return f'Hello, {user}!' - - - -.. _azure-bindings-blob: - -Blob storage bindings ---------------------- - -The trigger and input bindings are passed as -:class:`InputStream ` instances. Output can -be a ``bytes``, ``str`` or a :term:`file-like object `. - -Blob storage trigger example -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -``function.json``: - -.. code-block:: json - - { - "bindings": [ - { - "type": "blobTrigger", - "direction": "in", - "name": "file", - "connection": "AzureWebJobsStorage", - "path": "file.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return", - } - ] - } - - -``__init__.py``: - - -.. code-block:: python - - import azure.functions - - def main(file: azure.functions.InputStream) -> bytes: - return file.read() - - -Blob storage output example -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -``function.json``: - -.. code-block:: json - - { - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "blob", - "direction": "out", - "name": "file", - "connection": "AzureWebJobsStorage", - "path": "test-file.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return", - } - ] - } - - -``__init__.py``: - - -.. code-block:: python - - import azure.functions - - def main(req: azure.functions.HttpRequest, - file: azure.functions.Out[bytes]) -> azure.functions.HttpResponse: - # write the request body into the output blob - file.set(req.get_body()) - - return azure.functions.HttpResponse( - content_type='application/json', - body='{"status": "OK"}' - ) - -Note that in the above example we use the :class:`Out ` -interface to set the value of the output binding. - - -.. _azure-bindings-queue: - -Queue storage bindings ----------------------- - -Queue storage trigger bindings are passed as -:class:`QueueMessage ` instances. Output -bindings can be returned as a ``str``, ``bytes`` or a -:class:`QueueMessage ` instance. - - -Example -~~~~~~~ - -``function.json``: - -.. code-block:: json - - { - "scriptFile": "__init__.py", - - "bindings": [ - { - "type": "queueTrigger", - "direction": "in", - "name": "msg", - "queueName": "inputqueue", - "connection": "AzureWebJobsStorage", - }, - { - "type": "blob", - "direction": "out", - "name": "$return", - "queueName": "outputqueue", - "connection": "AzureWebJobsStorage", - } - ] - } - - -``__init__.py``: - -.. code-block:: python - - import azure.functions - - def main( - msg: azure.functions.QueueMessage) -> azure.functions.QueueMessage: - body = msg.get_body() - # ... process message - # Put a message into the output queue signaling that this message - # was processed. - return azure.functions.QueueMessage( - body=f'Processed: {msg.id}' - ) - - -.. _azure-bindings-timer: - -Timer bindings --------------- - -Timer trigger bindings are passwd as -:class:`TimerRequest ` instances. - -Example -~~~~~~~ - -``function.json``: - -.. code-block:: json - - { - "scriptFile": "__init__.py", - - "bindings": [ - { - "type": "timerTrigger", - "direction": "in", - "name": "timer", - "schedule": "*/5 * * * * *" - }, - { - "type": "blob", - "direction": "out", - "name": "$return", - "queueName": "outputqueue", - "connection": "AzureWebJobsStorage", - } - ] - } - - -``__init__.py``: - -.. code-block:: python - - import datetime - import azure.functions - - def main(timer: azure.functions.TimerRequest) -> str: - # process timer event... - # put the current timestamp into the output queue. - return f'{datetime.datetime.now().timestamp()}' - - -.. _azure-bindings-cosmosdb: - -CosmosDB Bindings ------------------ - -The trigger and input CosmosDB bindings are passed as -:class:`DocumentList ` instances. Output can -be a :class:`Document ` instance, a -:class:`DocumentList ` instance or an iterable -containing ``Document`` instances. - -CosmosDB Trigger Example -~~~~~~~~~~~~~~~~~~~~~~~~ - -``function.json``: - -.. code-block:: json - - { - "bindings": [ - { - "direction": "in", - "type": "cosmosDBTrigger", - "name": "docs", - "databaseName": "test", - "collectionName": "items", - "leaseCollectionName": "leases", - }, - { - "type": "http", - "direction": "out", - "name": "$return", - } - ] - } - - -``__init__.py``: - - -.. code-block:: python - - import azure.functions as func - - def main(docs: func.DocumentList) -> str: - return docs[0].to_json() - - -CosmosDB Output Example -~~~~~~~~~~~~~~~~~~~~~~~ - -``function.json``: - -.. code-block:: json - - { - "scriptFile": "__init__.py", - - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "direction": "out", - "type": "cosmosDB", - "name": "doc", - "databaseName": "test", - "collectionName": "items", - "leaseCollectionName": "leases", - "createIfNotExists": true - }, - { - "direction": "out", - "name": "$return", - "type": "http" - } - ] - } - - -``__init__.py``: - - -.. code-block:: python - - import azure.functions as func - - - def main(req: func.HttpRequest, doc: func.Out[func.Document]): - doc.set(func.Document.from_json(req.get_body())) - - return 'OK' - - -Reference ---------- - -:ref:`Azure Functions for Python Reference `. - - -.. toctree:: - :maxdepth: 2 - :hidden: - - usage - api diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 831c830da..000000000 --- a/docs/make.bat +++ /dev/null @@ -1,36 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build -set SPHINXPROJ=AzureFunctionsforPython - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% - -:end -popd diff --git a/docs/sharedmemory_existing.png b/docs/sharedmemory_existing.png deleted file mode 100644 index 6b59e03d8f4263772f8a5641da7457bd1768494b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50289 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

    a}K#~hW{HEfS!PwdlEG^Fj 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

    ;w#iY+3)M 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 diff --git a/docs/sharedmemory_new.png b/docs/sharedmemory_new.png deleted file mode 100644 index 174b04e71a0e2626fd4c8b0308494e1391df8a84..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53205 zcmbTeby$>L`z}0yfTDm3h|&fU3Mk$2(#;T}v{DiSA|;Jr07@tzDLpXc&>bSuB@IK@ zAl+Txx`%gv`?rt%-{v@;;~8P*zSq6hx~}Uy&+A+hsG%lLNdgTY7@6=XDFuuHZu z7y;>JBJi8e^BE5CKRicG`TxN3y66_cKQ38HsY=0M1)*0@UJ`Oa00|x81R+N#_erL2YP8?4;f7-lBES36DE=mB6bCX36%7r***4UZQ0$~XG1 z2?6fabsp$pp`TZud`WRHBs;F>VkD;LuN}oWqS)n+{1f%({`^Eq)yR)U+uYU3Z3YS{d${_@I^`Ix-QE zrA_+vMX7C!hk1t$y2c`1?dQ@2vr7Z1K(u}BVNSHGG5&px|Q?KtgZR{+LYk@8nGuhnaXY3sRyLqJV>E*8Mb2`(7MZ3FaL+d7WBlB+RJlbi$jD9q-KGCWoPB zEV-8lM9;S2Dd~&b-flEDhnTJ@&&;)}(1y@{00Kl|!$L#AuyQk$MCO22oV4=p)d}CD zUtfPvr5##yL{Cp3Qp}HO+o!mHw})bgd{Sx1SRatk931+OBIk+rUo3M3);SD3KUqli zGr$my&mFJsFJIX@rYT80drq|fw5NcaL7DU@$2bAIS^vp|sgp~&@AomowWCXA?dx@) zHe4Tj)BbMnb45ELbh(Q=V~vmhS&5ufHZ&`p@FE-qO<2W~u8R-q>AtD@ef)1>V0hj5 z;JoYH=i|Nzyz*c+am95#^yRzrofoV6PdH+BtrJ+`eM(0YTI1BvX|h%j2CzQ)3&Sou7yERt!Gzw>b37 znrDW*8mV+`;pWQ6$l2^qD6fGuCi>Z_f*sDfleZ#dx)j^N#ICEDfe_^xk4c&*CcgAv z_L_Zk;iGe6yZu^w=S(UCeKM<4mqJTI)ceoN2a#So+tw@l_5)cuE(W(Uz4yt*50dC@ z;{1*?x+mVv27fz_w+pP@?>%FL627D?9LsM@seQQ7K;C(BxV_pLV>j%v^Y4b6bhe4| z0k&h3*ZK~;pXd{&ctbknFb&L&{$t|_k99GT!BWB~Dx>uEyUoP@#nr6Av;r<@&5TnW z6Y2UGlhFrdlipSI$G&R!NX!E9X{Jv%1nO&>6O&M;lhtmHA|V(?zVDAGy^nM0+s7&# zSD#NhMue*gw}zzpQ6NT3ozeV5x%vVkWaoANYVa*qN6TE<8bNl9r~kaLkC(VP+dS$J zf!gzv{r0bK+q=(gcC{z@ZQdk{- zHT9{YmEP<)kHfQjFxsrXH_~phkZ_}J^h@~@QISp`elM^c;T9RR^lvBOa{Ns1l0SQX zvLN9S&sIy>^3!#~s<~Ds6#p)Br2B@zH2dj|33B6mY1RW-F7b9}g2*DXPksvQF)334 zqL_Vg+%@SulAw053N$t1NI*4NQeff~$#aimj8I)59N=B<+{B;2Pcmx^7f^Bs_ zw&jjx{LAKAlCNm8NZTQ7t&E(u1U18rJ-8iLBkPvdi9!;UuI)N`EGEIa#Y($2bf}uxYIf6SsUvT07UneuRA9 zD@x1j6!S4|yM58a7W( zNN7m!aQ{Kt#i~nD5#DNRE7M86$s{fqd2RAxJKvB2g&RTcl2i=IlLP1MO8o;l!)C>1 z-DZxl=T6)wXU9wM+JdR?|BiRIS&n?R@$M2DYhHPBfS?h5w_(w&*DNr=?$f$8;QI`1 zglLum0a^QJP)FCW7p>rtxBo)m;840M?LH^p$cY35)qD*!vV4P; zlKx?JgRia^_o=08WvIDmki)Hg{v*&#NJwb6)_=$@ZS5-4KG2D@3!~+;p2I4Pn}&*6 zL}eoO+WV7W9h_Awn*IOK3zLtMQl=urQxE$$^3YxITO=feQI2~fmQ}iqeS7R-NpY6+ z_Jm5sj5NpmJ}K^hm5A%?2o-h=c@da?2I^Dte}an#OM+2NAeY%T*CN8rLO-MB<_Tbd z2ffO!lU`-?EVG5}oJGfJu%96%dpgHCNfGvz%;P8C2j^N|86-vuS(7!`q4Vf29YoQO zh4WAy!w!K$j>cTw_fH6O(;OGNWSgay``#+wk2`OMHdz=2{}0V+)v2c6w0XfF1#D?c zZj-tjzB4V`Z0D+TazqrGwcZq|0bq;M+%fjqJJS_VsJdT!*RA}f?G6SrL)#oeZ!_7Q zC^6ajVr#1gg&2IBTKWQ1w3ivjZY7?%kF05LZf;xC@dId(GB4Nj^a>-jdgAJEfoV^v zl{km|D4q6LLC>B|Y>3Ts#Zh`uPT>UMJmK1xjT|f;>!Ck>KJG+&g&v*xoj6=(=$;8d zFPvNQCZ1P^m~T{Z*)g6zC1dod(XA|fq;ekW?InC`dNy{&l}nfttn&HxQO=JW&wAZU z=p+o=x6cKX`ndhSpH66vTvROu_C@vS;G1F2?6S!3k3I@)$}+!#W(0-`xID~st3M<# zYe_r%S?9^J@j1JrbRX_-bA`qGHwhy?xwqep%(?p<2jx-!p!a)5P2n&h?rrScSuz($ z{?9e$|5up*|IQh-tW59j(@2sX&wSzP;Ho=$<$bz4)Y$}J7`INAwu2*XXm9cGXqkvl zcly+^$S9-2hTk8ru0F4ip?4Xsw=EfXmi~6WO?Ec&k#20dYTPZ-&R9O4J2})v%`hXg+w2_v*XG0*!8;e+Sh-N>0QTq=-y4emzDM>B3*e`wOZ<^4BgK+ z=%X)bm^6d;!Ar;eao15;=M8zfm$3r&b|Io$3L-0&%XxNrou>!0q1XXkV?K2CRuH|- z0mxb^b>62c012w@E)9&wFBVu2u+zrF!{i#y&-NwyDxGZ{`{D92fT2XMf>QWpI?lfK zseRpPDLtSl787m@^v9Ji8$Myb@^n4%Iczt!)gXqJh@AwVUy`hRA;F^qQ8sSjGU?^& z*dt3FLq;zg4Sq4Gsyg0_RPOxOlZx1om!c)K*G@q`7C)FGa9qwUi>*69UB`lCSA<7H zXx&x_3hP2FF{9^Sl~~?P);Fo<_}~tbwf9X3{tSab*%9y&)4lN}?Ju^UoKRIH*MZaU zSbQa&D7NZrGVnRwWz&svKR?~?M8Q)#?uUxL;;h-x6j=dj$j;gw<;6hgM_CQ$LyLPI z4NFo$W1o8j#(vG~VES{q>-t1#Y&bq2hhFg;EpF-6N!BO4PX9D$f5EMOe6QZ0@Zi_r zahQ>zO<92dsQ2A+^C&x=fs?TcOa-Uuo4~8orLDfow!;{MO~c zmL7RV#AuVy$wmXhIa_4Cx=$6i^e0kRXqn{lDzG%$zX-^qJ=SZ6O0~jCm)^!sTj4^9@%N4wU5RlAR7MkMF(Rs>9y~y1r3v_>_$EwzWCuB z#pst_zyhp@OMcE6+x%Tju~w>DDQv$r3(?b>Jxn_Xo3mk+ONmZ+|F3)+>aJ42N-!?3 z($y2~gk9Hc2hqm%UBb(cN2$vE_`oiWi{m*aAtC7i6~mmtW96yi?<*|j+PH}^ykx79 zoS7RI>&?V({boO2rN%V5=$BZV>y_GC zWpiiJE}O-BA1#&+u@KtKm^FR9Q`*cDX;iUZdu+?cp%W97I#Oggy1ojsJ#RCkPqi{c zrF_R^ei2c1%`|>NT)7cRcL?|>!laR$nBFNYF53wr{hsaU%OLB{qm8EaUmXXVQK^VH#AW zWYf^-xsHHt5^e zm@7A-9{hnq4pU}t|Je+s9bDEGXaNUPa<7eU5Vv-3Shz&>Ghd;u)_lZ-^~N}<`WJSrf>$LQij z{ueT?YMKDbkO6x>5(>^6!zOyvYP6jH7ix!VG zX`?tu7ub4YM&67B_pCdDZ<0m;o>?u{$$mCs(uqe*XVL_aM4#`B=h0$Dson%gD$0ht zWYC4%+Mn57lhL%IDdE}%6%C{Lm?S0MYj5NZ`Jh7k501`qhuM{2mt4IP6_7x3H?ta-3(zFfbWnv~Rg9HVm? zMOXrm$H?>Y_ay-;);maC(Vrtx8cu5Y&aB)4vZl#sCOnRhJ@C5a5^^clChw`oO2OwC zL5GwP@ZoVvrl-bj*p~aDyq$3ZWu~Mcm3@iB!HdGsoBa9x$KjZdaOv7CYs!6)kzKQ7 zzubxedpGV`uHHyq)BEqJh|W-nv)6fbXYNG}WFo^ZVM~L#Hs_zAR3?A}JZ99)wkCFU zr!F4bS#lpmAVtryz2)0|L?)BYx87iZu&mZ_mbg*7s!nK^8F+3smRjP(&6ZpE{8VF9 zx&|+mhfoVh5ap9d>_eR|OCv=Y)5bLB$=g!zY5CrIfz2Lm(HUET+YB1smxr}Y$|l^i zIdry9w!25O?!vqsK^Bj3>=J4wLh#~n_m<4BbU)33f`k8Z;0vvKr1$eo<%^C^Y3uV89N&H;o#+cgpzuC<=;$ zP7wC0KZTtQ*nNB1Sy}VN6u`d*kkA_?-NEy~-1kw+dMtwBp&%sj7pOKE#=)V>N=;S~~rM(qaez2t8%nF!Pb zr!Q@TTs8zg*>rKMqwD#trqH5g!c#vjPSjK>Ei=+|atD10lcDxS37^PoCtnyF+ zbpz;aOa%HUs|Y^7n;g;x!U8Vi&br|CeXK8`h+`VRJUn+tX$Us2&TM<3;484W2e#d3 z^f@UII+*hB1hTLJiSJfYVozJ-qc~%q)Bmp2h{o@Sp&FGq_22hI^4G)A>XWT@fS>u- z=%IVUT6DllwLC&szXBx#JAuc_70IqsIFJJUjmZ$)HZWBDsAb~GV6OfUZVEy20gtE- zuGx}c?0wi+1MP6%9VU}-76{somOSy@D)T&itA#ca`ybD(5j9a_Gvu-ki^w3CIQiR; zHlD139?iG#KKSIbJOit}2@m^yGD)|T54aTP1b&}ddx)j=2tPPnNW@h#B3m#YX2ci# zB~2e0ydxV&k>IuWVjF<;xW+Hkqj3$1-bbwfbaC=O$L*w$fsMsy`ZxRu+D0LCZ&yA| z)R$){5(SMKW-k5fVN>ndk&WYQus|%ER(kZf%P7#ox_$UTwSNL|N9c&x{`j~7tWa@P zp3iP<*mHM4my;0sY+K08?WZ~#8YbC-ry!eH?5&L8Xo9Yh8`5f|khz|QNe;p$AYlkz z%tNoguWCm%h~Pb|Q(F7#mqzZ3%#NV`#}SYT{!%5pF2G~a@#Lq7E0`qk!z6Zj_)klk zVz{mTZNPR*(X{-w|I%Rrsr!>YlSH;q-Y4pF+#aNhKMd zX_yIN08qE$d&X-OW@J>l+te3EwW0Br9BwbdGe6x)ziQi`h;Oops4 z!_5IfFIhE4joR_CUSm4zk391}UbSorWwfu_N&t}ij?mjQ1RWhT;@!~*TsRRxHi zhU~tARYyzNod95V04f9F1cN2}{)V_LCDPkZ{+j|8|NeXP10|1&v#SgQ8Gr!F%cEIP zDkL42A+(vB3s)~)9f2N5@&%Zyt5620bXt4`0sJVx@+^?9JwB>@*;fB{Mt-(eRu}Vy zt9BUq;8~fSA#NMW1=CbqBsY4zxW|I~V~(9h9;@bgl}qg4K8MmTDTf_dlZ)|u9k%fWAfQ7 zyCu%6g_cmMmj_{PSx_1RlfezRl;eNjgU>&|0Vbv31hBo)p=Os80i#NN5{` z(Hxq9T=cKY2(BZCl>L=XmkU6t%T<+ML@o9t!_}vIqyOU*M}V0lqUvBVSUNAtq0OBT_)TtL2QeQfMRozzgYRe z=mKu?OE$u@pK(B#wZave^Dr9l`+ooo@&i0sa*^O+*Mq@$vp@lW^mz|rro_zpX{AW^ z|M7_cUs4T+ztYgW`xY{RF@d{KUA;&Y5x4N--lq9>QX6}|1RQH!6VTNQ>;-M2`5dp~ zJJqN8qA)qjizV<0 zs9JXr!>T}9xX9wTKc~|Q^2t=b?zsnY_@w8_W;1S4tJW&BFO-LVP}TfEX^(-xO$|Yq zJ*y#<43ly$51Z9DQXAVQc&rI+1G-sG2|N6-X9NBQi=YDA31v`GNQ%*DBWlbvuB%<_ z&kXm7qVZBoly?~ei7EoutkrmU$+l1ikCsnd)Df!^E;#B&bLR!0kI#G z3|eFmH6e1G0lM1LJs+p8cp;0%ASzBi5lTVft(NbXxZ1eL~y|w}*2aAbnHwz#qVL^zw zW};a8U4k*i6-!y!21>8QC1*$LdZzjsidB%udvAH4{bi0x>V!&mGla}%6U02gRAtM` zLP+3C>3)6#8IR3SUJ1Hr8c=ePfn;d+q!Hr9MgOHZWJ!4UJ4ENLq>gx4opxhOl=~gt z*I{0hDY91u_+s5{zeDW5AY-EGK~~QzLMv^gBClF0-pk16H|kWq>B?MO{E89HbdFzz z<|DKWln#%8@Mc{nA1Q|e0fqNWD0nL2wV<)Xvn-OnTfpK_gTs&u@#^73l-^tOG@_kCoU{o2|gA#cbFG}A8f zDJv~L|5K-4MV1%O0#q{rwE_Ax4%^`e1CG$aqYR=C?bcfpD&3gz2U#xH;SYa*p#w6N>&kXJ@kovv% zwdnlpcr@-efH%r1F zG!oi*D*j{;bEni9-O?$0&^3@Es-vzM7zH@dFw!ifOLq4;au>>yl9ku<1Cs?N-ZkKW z2dEdJLJ+f+r8YD(^4isbD9Xr9fiKitr2^d~b9C&5sp0cD84YZEedrwbjbW*wF0|1$ZZIUdq znAa!erl8W$j=^g!MX@{(S?ww@+>OYzD5VXE&R@c%RI^LWwy(?vb6#LU2=V~p&I9Q% z%Ch`yf9u)4yTu51L9#8W!C|RK?|kW~9S^zahOgUN#exbDuK!D#$e~O8^2Njc(63%N2_-8d_)H+08wV0loW$8?s0Aqh*f41_hYQit@wJm6 zk(%hH3~f#ih3B8E!DLaPDh!q!WuUN@Ms)Cx+Rkb!r?345GvG3MHNt%mBG+*^*c>B& zCGIxl;pix6W>}uUQ(z(VFCBN$FM8QP6s1{>RF1;f?UD=sV=4mpVYFB1VzMIy0AfqG zdruCSRS~FrAgCAjKy`@*jHSv|U$4YEl0(rEW0iqR%Mf(~Fs^Bjs07$z$}6B5NjlMB zW^X4VXRx(@@R^hrt9hM8JCL0*M!YNQ<}Sn$AhZiPcpzo1BSCERS-9q3fI>_7HBzT@ zin^3CmvF$$3?Eb=n}MuIL<1J6H!f@e6aWX@4{tuKTCc@z0Z5mo2UX|U&QZYqkbJ3F zpx~BQK+(#?096@C&7|;uBD@ z9p@u;D=z?^2Q-0F#PnuBvE~4`d7&E1+geKM+;qnhpdD!3Vqn}V%(g!sznB4pG#e~w zOfQ5sVQ-C)!5_zL%<2JG?Etc{>DhMTId1=G|B(fn_cZ{mk;{UiKs``N6dwoBq~U^O z*rj|>{V@RMt{AWg6`YR2N=oQ?fx4KC0ZArWt2_7Ru^D{KW8OvyaJoh-c|>U886}jI z(m_aAjFp3^!VLn*B)2u3QBYec=Xt^J{!FcAAU6C^8bm!>hg8h}-txW)1%r9uecL6p5DoN#9v9VXW_>_TeZ4e`v@9$w5E5c6>n6frhc7kcBqTzLeCl^&;$0BFH+z$a3z`qKAk_Cb%WLa1*Y z=jfx;b^n)UsyQaD;zLLESn)xOr>rm()FDHlN7CFve#!O~Hp!oio8qC$6VT)2wXfd% zq)gbRYoI(_eRi~*4zx(z<^~yy7rV2L3*Q zY2V*JMt7tq{D_RWdkMz$0`5jGX5zM5GSrbI<*$;V_Op$$n2pmX>`}x5rC5uKw%!5I zWAjbtl_}`akU|&&GKqP!Bs*A_zhHT7>?-DOIWA>pfIT``uAOuOG4g_|!%+D#66C6w zyEz){D!$a05gG$Ve#_sRVn&HMfmf!-C7V7C?3V##Qt?S2_xa8>n6J}ite8`c5um1- z&vIi)$HoBgvCHeHijrty2DrCu)-u7ZDa4;LDTc=~#KW>f;`~s3-`WwP?6Z5TqoYs5 zKu_^0l2KhuQEVl@;hqnZugVdq37t=y>1HR)je%B7+7CquKle3A#1Vm{B+y`7UA9d0 z8Vq?at+ad7I_zKT87-wS0%l}Ln@iQ?c>#r6VdwOnXFA24heY8VeguB{}%9O zE~Zp+lsRezF3L$STOttj$B8$$I;i&IdG$*t@VAa=~jlZ*g8{$bjmu3RhnJR4SUe} z>S}5rm++hgAr=*EU$grIOie0qEvNX1N4mL!bM>x=!-a_A zPyK3jVilby zA!|fm376gvqSXu{bt^!J$6twi0ckbjQ{Mf`yw`Ntj6<7mTW0QP8+de{$Jx;XFpVcW zZ;Tp@B}#IpN;FS)dm%Stwc@)`rEph;KWLWdx|>Lwku&FhZZ8I=c%#A7&1My=1fTUX zp6JA@T-|8n_8wj@7QIsStBgKKn-|`0dQsS`j1*~%cyV-Zfe&|>B2^b{L>t9vxTLAI z4{EfX=ynIsYAe6+wscXCSZs~crGHN46MDk z?@;^HlaaSwb8|<#rE-RO3)Nel;iILfN2Ur-J_>o>*smw+WUMw>E)w6y9-7NkM}rj^YUIQ9 zuz<=4CpRXDHAGDz|E94l%Vt=LP-d?ayZi(fAR+f#jH`|eJH zvCq3I(YgdCn_jS%ix1<*#}`fU8vb4PVDm zRNYA<#GbGaRsvSMFS)uxW-ZsZK|?qi;LtG~{er9r0%LFdS0hQNMuD=2E3v~w-e|}w z092|GqTU?|tr~Y{M}U7Z39*pTq*6WNr~3mz5bqs!RjU?N(saOp<^~YY%DIbdnyK+Y zRdr($5v*Nl_iID&_~CpOt*d$+n>8JJ<39+{6I<5olb=L`zUSS-O|i-Q&t5ilhxmUV zCuZyTFnl@;GI7unI)lAuKUu2pw^ETk1Rv>cMy$g8VJ^>m~rCSG4x(lP^tSHzom!<%gvbiq*L zIw#ZQK2L!DA#i%UUxr6#i1ImFWZ4h=Gni*+;~`qL*+h$g!KGq>(Prdo=9!OWXRP_T zEBwq4?*pjI!v*$cT;o84FmHy(6c=7z^Z$2lyE0r*`mGvZ;uw&XnQaxg48d%Z%ya-J zS%UFVja@(L8=@oh7bz@B^b(^Qt4S)6{VBmGCL|kEGO?|A*apC87>9=p#f1J;XC>s= zG-bhc4Z6wqe(N>5yKkc9#H)57Bj-e$>mVWZk3>%s1m z^wYoRxd~@%Oq!bZB<668S6?6dPjXP_L=w1PoK@8?M3 zJxv3}SA^@=0pK&qeErjpKU)`FD$sW*Crg(2Zsy89*-Q4tlwQ78jSX46m`8c#zg6P+ zJB`A;&kij;8r((^Wh3T>uH(+7dXY*(|5Eapa2l}DyJGT;JV)K1~%7zqMq~4}ZlGpX-ZDcrA%hvV* zf}B0{6wY8ko*lt*WhiCC?ah4m4Ig0VNdxXSepE91)5^Q*PJQ3LTpoVl0zmgDJKYhS z>kDMBoycFBnO7e9S7P@aC^q^6)NO6X)nMuNQdl0+WLJiAUtXL_Oj=x2*~t8gFQ}&; z#>E|H`$k^s%BebQIJMCaZq8cOD99n<@US0e;6*#FkAjjgZ)9FO2W(u-JFLg_urzZUdeG7;slS}d(n3u< zSjm@&`O4M`%&yNO-v2%EKD4frjiDp_`n&O~RK;;vnQa|jPn^^`{tiWO zKpKVaF1t?Se>mc}fN4mzRn^*b9)OgRVBT2)l0npX3e@pKGSPaDvb6q{eMW5@8PMBV z#eJ!b4s{xi;t%j_y=H7I9E5*OO7AoG{o1tP!DQ!5hGLo)QcWsBy=Z`OU$1T+^zRT?YI;mDOXsPq@ z5#4jmCkinVUexT?xe0i9Fji8ZQ>FP>maM@wbOn61=w0JH`S%yD2eD{a*cOvsdiVK( zQEmi-_g}5p6Lm$;0ZYZ=^;%4HJWyiS27YiCNO;F4>PB#F)+d{YKP~r;tld1kJ}=g9 z)$r>*!FYV!zvoEz>M({#{w$rlNw}w_xfzBW3%Sh-h^JYce<3r_0%F1R4 z=eqYw0Go>+BoZhDVV=X2_9Atyy+`z(vjg<;tqF z><(IvwpMp{Eu88JsfYJCfNOLOn{|6fMkZJ>+xhs-_;WB6rW?C-6~NUqoKAQK4uF{# z-|&IO0e@Kxx-vi)qD+b3o1Z=0UVALhK!HmxRaH#29sywxovZQ9cY9RKHRih87 zOihcj8m}ygGj+rmIUDZ-B_h_JOt=M!S%0B^ z@#}A-wveQX1&8)rGAj_Ei{+B&l+)kx1e$CnSFm0=D4777YtKy5tAV4d&EVbiN0(E` zf-(YU!5z1pV+AhS0DK1PmgizdZ0}O74JxJp(&7dOQKAvsmaX5J(*d=& z!|CHpLmJE^teVRUe&Ln!hA_H}m{S)GJkzHl1;&ggpfB{I zCyD5TvBxT(2sjSfzzbmNkDLnNXSUTH4-nv1k;K07#*^grW_;fP4&2TqJ1-AX5!yiz zl;A0i53rSBL000R(bkmH$>n90Ox;ZW%wSEx2Q14)hR*h>48&+ntNfnZ&+ zJE#vCBvx?8jl#}>qh>B3TwQ#G|4mL7oZZ3yVEPL6|1}wJ|3C&20zT_WT&p}Yfa3pn zqSn1b*+^C<`4w;+u2iYN+HG3}XiNVMh5L}N9m=SSpo35WY!4m4NNmU+RtZu^4A^H? zt}$9ac6XMCy2S@T$Q_#3`w{NZNPzRhpnFaNjs!HLA|R`MCy0e5%`j!kUm8Trq7m{N z4-A1UPgy~3RtZi!voJDUA#aPC^P_t7OvbK`VeJyKFcI0>eehYaUTXOgH{|sOSs(>VET962Zw_FKjg-0XT8j+s zVvMVzxQy*UWKkS{mLn-;U8fv#k>~(mp$MkTejH=%Emwahx4V1!Gf+G+Q{<2h&X+FD z8lJ`A3|2S}$dVP>+PrN^tlLQ$Y9lV-q;xbmgbGMo6C{0Kw*;G_Wc40<(v2a_dnEC! z!zoEYTQo1-r}*AXN$zv%u^(qT@2rjvVJ-a*A2IRqQ>H z0+uNx3p76N@L5N2yiENL4#2{l^Tmuq^<`aT)jfB_4J7o}{S(!-Swv#oz9+BtmOi*& zji3oiebwPzYhb-j4D%J-9`M__{-kAr&rdr4Paw5jA9FnQrjjFlqOfxoeYTm~3ua6= ztGAK;?+;g8#&TZph!C|=fEE7y6QBv;gBLI5pe?cTKS^@`n?+8aD1Juw~L(WDb4JuWn#1q!bq6I{O{zH50%NEZ3)m`+q?}+Qv-*bR|lO9hMp%j zZ}h9BFr-}?{nTSKf5VU5VrPT--S|+6UgA}wS+4|{<;|Tt=YKN=;p7gU$6ob!vz*>= zAK#H}*ZiwUUf}ngL+V-@Efi1)z#8~p7XeD=2u`4uYyXMiJesB@PYm3Ux@_zttdRJn zt@^`Z?=xmGS#}$&bWGxu_<<&OBcbL_ztVEByAoB`oZIy2>xt?LdM-#G#e+${^+UQc z8}!N^UL$-Ca`>{Q_pEZu51GXMoAZh%C89bcbcd_tc`P|;r`JfteKi&7I0Q~$S>?`n zG2m=L3xQ#!6Rsccr3YFXCk@DZQG|)t4)2b1_;Kr-Uuk*}e&@@Kax0}Shf+JIjdMCE zqvg7wBUlXV1*n_sBnBV0@3OnyOt*1Y+>ZD`P7_%ogK#)znjVzJ z%w35rgv_m(G?6TR>YuoYG< zzh~noy+M-)3%-<*U2I!LmZm#RIk__yScW=P?Ewppy)%q~rX>NeO_t8e3~kM@s;9xiD)%^T5*}73W^N3mLYA`s22d!SW5?4{ z&=nT|`t{q)7C!eZWQU@lR&pMR(py_pPnSd zWdY4=Y0AjoKGk#Gw_rk2q~zec8G1_Z&WNm)gYDSo)PrTAYWWJ_PW$a4P?U3l7k*zvI8I}_AtnMFx~KZ6smv_Opl0y zI*d23g096|HuHrjAzYE4BL}62kmvPZCP&aeAJycCJZZ|ZZjimLQ~SebP|?eUxaF7s zRu+>kPDMp^aC=!O5!?Zd@iyHJVLFE1JEC}DMxfM^DCndDqq6(}>hS>}QK~iWyS9eU zB_Q)xIvY=K<|G9o_B3v#Y>U=%=|D3!NJXkvRHyKPt!;&PE$q+bRpi z`ly6O$L*-X3djMs3EAkH!NkFo`W>U^xJPiwX}y~m zqDWnNr=P2uf%IjpymWqT-JWuLs4%CuI0oEt&VMGa^^E4RyT=QmL=BPr^-Ylu*8+t#h!$%dLxwxU$?-Ib9(q^;r|R>B;= z@LdNlo4pz4rTy?C zWq3nNTYc09T1#r5B!1r9UgV+ibXRm^oxfXbo0uoHZ3S!7nU-MmCCLM7H!UEt%fUeQ zOHh`wdaxa?_b$0iI&9*zgnsn<0E2|yhQv@MWtsNJ9VQj7v;wy*P`0s6QXgX;#Ti9c zII!u*O8C_Nl-~~eL3^h1W;BAq$R}m@Ltn%4)`>8mn8I>@WzEc<{9-*~t699d1GO53 z;!R5oapX7mI32zTVos5J!LM#Yp7ab8y~6imM|=rGU3>P4xiaB+^{TOl|INk^nqXk~rVNJ+X%cqmtrug!cWzkHDyPo!e$4a_|JivnE*}dDy z*Pd~FwFNIDRTSqyucJggyPxT;1|YpFg2NP%2TtlpL>jO+yUn4J$M?`@t<;|5dZn1EG}ho!e5XP10)8 zv)fesO&|325NZ$Te)Ud$QOfw@C(ue+!l_uE%V5rs>Id4sb18)e;a&;Jy!E%ae%6P* z(H9LAmREQ%Mx<7`{ZNt{ID_-GNoF8FHE10O09FZ@zz@`SrV6Ivt}plk?B6tdYd;V? zufO~AeV9pLTrWx4Kx&6E?wMMc7t0@_fe293#vqmkXPcbb+vi9o_`p&Ngysha(UiYmZDp&3SkBt=jkeT6Z!)jfi4{AD_?2D27 z?nyLzomZ`UA!}mVvO`!MCozFM<`aq?9=N4Z`ynJ_Q9JNI{9yI+?-*X9dR+ehGrYDq ze0p~GY?T1CNbai&c%vJYtwxigKlZA{_riMOxbL~_X|I}Q_O<-a$ot?QTeUttwias( zJKQm|dO^~!@TBiD2x8^E*c_8!e0pyN#tjV(S#a)I;Dk4_RCT^y!J*izewUqItWsO#z zWLH+`4>u8D$p*pqUUUEpsqH5+dTZ#69I!+5-zDZ|8C{@Wa6Q@h35$Ef35yBeJ&~Vo z6RN_aTpn}m=7Z!@((7da4mJVeU-Mo2 z-TT;UecB(kpXM=j)bl*|eP3~2ztf8@D<+>oGPi1b%MB4i)H84LqnVC!jR((e?chB- zb^J{8?AKutGJ#=TjPz;f+qA(%mQ7#&4M29F1pSY+kyRazbyoAE`Yvxup>%^DFCoRhcNCI}L6^sl z*+X-m@D7zPbU77y&D#^>z2)uRgNCmE?00FhC^UeZvWK2xALGyDL^yFbPX<5XHHI`{ zA%6bkl~|}Xq%6K%8)eJCD#5avv;iP9$p+ZZ?$8TZne7xzKl3p3)!haTSu$E0I3WZ) zwl-FylK@83lU(Ot<}mv;H^D99hPdHj(KGaP7u4irB9-BArx$adXiGrjXt&!6HM2`= zg0n$*QLY{BgZJO1B<=+xD>H}ZVQ3diia#iTChuqnSY-0kBpR*uYSxI?=$utdb{yEL^+!?;#w zwTLISQTc~+lFgzuPO7LU6|_{P8b1=g9eQm=55ilfV8+Ysr1b3jT`u{_HJ}hxS3?X| z5;nxH+!3(nCuw+H)#CjlT0v0u7bRJt2PYyRul=L8iU;3P;KYP~#CR+<`J*{m-Pn%( zuLUO+LU}yCQ&yjk<*rf;^_pFsCQ7@kR#jCLz3=(RJ^frn^Jqs0nieh#Qx3>@KD%sC zBilB-SL#)B8NjVxNV2S&ySTLa=9}}8{<^ri<@6I3v7R~~S`u1TN0`Qf z--v79!te6tg`)0JjU{`dSaASr`BIO~=7;Q-@bw>!}s8 zF8Vw;SJn8%DSP^Wkz$UO^`qfhF)~xM?F-*L*Bu`aB|nc{;tMuNk&vrtc|a7n>1?~{ z-+d^YSvO+5=eW4saK+15p2_t+EP~ZdxH>TlhS1A1kw^M_tl|{uym`w(EZ79ibL+L` zSsTb4#?^3IkTl%Q4$TLs$Fjxq+9xq?;n$&FX7zhq=A+J`doWTxJ^#M)CXAbX2?7q1 zQJp@Nqy{qb6q0t^xiF|57Jn_7ymE4gJ~fDv^||}|l7~<|@X+87yRa$8k=q zhMDBv--vGVRlWS&tv}&V#w=>e27&AeUw7+c=FqG>FHA*F=!ai3cQZ`aD=%`>bgjz$ ziTGE!E1&!hEnb+cKD5nrkKdv*XT(X{dsqk1lcav?0v+G9Xflh;HXS>g|W>s z-`L;2u67Nl@oqx<4`*GP?#ocnx)FRtD&YU>RClO#J-oX9Qb=&}s_9 z$F`5XUfiN|Ju7>giQR6;@4G#q_gvt_Rb$O)+*_ z(x{B_PRa?+dz0D4!zY*o$Ed|QSQ09{4Xin9j*Rltb*kY9nK0*0yKa6QpH@hEJXDid@#yf*_zIKmFrc8v+5TBuwxP3wf}A@s zaZ;wNwDaTT$amMuR+#C1KAJ5iDjX%se7vs|Ix#{O_CbQJEAY9ega)Wh; zr>VQ!#hPzYLfI$%eI%;}UKmD;t_`T^pV&v&Iu}p4Pu1=mKdU`tzfvr+v8{SUT;*DZy*oJyWR`xH5&dZJV5z)vq!6f$I}~O5+b>RgQ1S+`rP4vh5{N-QqH3 z0-4+*NH~22{}MKJuwK1aK)dCqD7bU@4m;P%JuEWCGDWeKBHMARUqeSgYO6A~B{q8v z-@1qsZ1;@v)Gp}Pu&#F;=$?%+@T0&#@ni`|ShRH8bky}Nk~f!%QmIxT2uQ?w?ko(t z;Gtjr##8)v9cM{w{GCttY0Z8fTN72cTkx>x^Bt}cTs59p-{Y0NmbK}4;_%jW zd465J6Ni~)uq)*-k?v{)dzxUDekCF4hlHmZsf482zNVD#@lbHjlKn$LMmWs4Rrc5% z(`|qQaJoKAncv2CU@d0QvRBHpw8rtUNx-QdHvjIUS9u!e&Tdq!Z0@m9GB>+?3ui7V z`=Qj^{d>0dM2Sh$NiXD+4UG+#C7{al$U+@S(B=i*h!68dAJ%?7_Yk5V?p z{#@s5GOYmRwso$@T7Sq1>Gp0zcI?ALn_rQGZ#+?KDwk%_30>@~UF_KIIhY5@`AtS6 z(dbNLux^2?+I5Xrk07@gamSafPo9J63l5-fzFjRmPp($0xOO#OOG__Ty&U$q{j`^Fl+vdh?&O^HryCD&k@n3pT#p zt&Kg(B_cGS?KbbeF5tP_tEwW6GmOx7K$D*UlkU?~4(v^N?uUz54fL|mfbKV6+M;Zw z5vVa2;0IHiSf`7(BHTrUTr(#;tG@AR7E%`vjI7@Ch!&FK+umOFOAiJ|KY8tO3dy&jbfUvA0%risw?#5?JaE|FXD zWs8Ft1;ulaOxem2pA1ppi;E<#(HQScjT6`*d0BWO@wru7EtJm7!ZvDDI7zi^#hVi2 zUM8|kL+y;jRL(1DkJASem#oX2XRSLlxj4_`!r2Rh6C4WFNBTGqCYkR}4wuWZ2rv4= z!}HZIm;zs(O`m+PKunQVtFGN4$#S=L$Nc&mqp4L7Zfll}C31fmhHL5q|ExUtYk;fq!8>hj*ixZ^7@}%yC%vMo0{%` zGQtsrL&{yz@2E{OLu|Y{fa=6BxuHtL2ol%k9u<|V(l~+{?}T_`)B^stKq?xrIKA;u zw+=fhcHwaz(0}M0qcf$X_9=80mZpC<6AzRnW>Bl!O@B>Q4BkOaPnUz6GXi;wELmty zs?#k+X-(hSzR{h>#D5<6(bu&K#fj&kz|Klye2K35_CNZ%JK zH3kJ?Phn<)9JzZypDCAgSHgy{`m~ey)v22R^Y-w78CraiM9nB=@J=jDM7x~=>bVfN z;}5*AZH!g=?6HwZQb}YaF4}LIYIXkRYPz_rX{hlkS;(`>wu`<}+cn4a`E1)c-NMgL z)~?#|*>Doie$QZhxGc57H*NZIX!#poHSI?l!ZvHBv}W#x1J^0tSBjd!onocgeo1yN z(F>8^yIEp}X0=z=7uV_D9vmI#GT*J85O4~A*1NGD)Du(;*o`-80?^4xv7p1^`soH& z`f}4Rglil=dxP^tLOU<#E#A97u0-5)^`uC>s&OI_^~0c?Dk8dYN|C_h zYBIv&oC*nkLB4f*Y+i^&Mnvix=xwyU5LMgxPTFs;o&7Oj9*uu(u^8YyFw&Ei3rCKJd++C&zlB#xJSGejOCrpo}k(9k9bN zUa9Hq74OHcI~5}kzbdH99(QA>RMlpF0u)Vt$_+%+KDIEp1N7e+&AW70R8=LB2BD|K}bkx?VI79x& zjLFvdzDcUQnV?a2dMl~e`CHA9!$KtCS7DF-jOOhsniGXwr>Sdq`Et67!2sa$j9^ut zZFm6gogAC7#=JaPf~|=7_dZ&B5cV@YlI2g_i;en2i11d8BO2zsNb8eZSjujGI3?POTe7Pb>Zjm z*e>t$y}Pj+0`Ob=;(A)&Nb4zDe~11il5??T#u6p+omlN)kbe~C9k(UVCd&kuwCCMGH-R`?_}tDXBX0)vYJ4D zeTil!{o>$-O!ba<`o6C5f{Jgy+m{}UcZmuIc`|!lEmV0OWYbyoFt}Vip=Hf+?T+lU zuMKu{$hUoSfB#tOs3r6E0C~@|N#_?_1QRUs*)%aDbc@b<7iJtDz!MA|(Fzd5C&MNM z6Nf)Jm?hFAO`wj;X}($Nuw|(u%SNYYap*KR{2Fd1`!L$@fCK!u8MAgIwexdEcIFz% z>-ccQk_xRLk>bQ*x1}@_pa&rhAy@r=gI5&w+UdfY+4`0BOBQIlM5H;Kl?~esJ6&!L znr)5?q~BZdr(nv;)XDvbU>Pal zO!0dt;_%ZD3KlFk*3R_Up&~JScX$}D_vYO4HE)f(-wQmdM(Fg6s@d;EO0m4xw>WR1 z>v0Gg%zOTB*`^*OXVn6j&Fu_1E|4pTQ;}#)0WdG@{q-`MN*{So-H(K-f3QIxK`SVE z9vYhN(3*ITXsCq_{&{mxluR!{a}&~#c-a0h%$wR5eGxRIeH_-@ao~SP0ucxlWv4A+ z#3@-VNzqAFQypoyJciQL&#p6L*as!jbTIa~@Klgc?ORA>kPbrw1`m=xJ+gL3vFa&O ztUA8Y@-}tk%SFon)W8u6XD_59@BAQ8gFo|3aEmXGKo40ERC$%h0bv+<;g%0c(B{1% z9nOwjP64gf7|h}3YbHR0U`8!J_~d6&pB)~(sGvq_7B5fx`~S5xezyUHZ%s(ohuCS% z_kJ9Ri1I)}z0avK;s?LRpsGE1ZijF zoi8sU(PTi939U*Ayv={t^Y@NP(kC^=v;X|Eedw2E{9l^d=vUum(TvA_RDiC&8!f6L z3k}47vsg_)>d60At`I`}rz1_eX%*-pY3_q$2j6M}th=$S@)_ufz@LzE^t{<1BoxBp zp&LVC*YMve6~wl2C3l;xv3tYtH00470O=!}RYC4FuVPs=n{j>0zwr zya;*#0Z;~~>Ai6Ta?>naqXNi)cB^SJr#U24fmDv{>dmH#wd zrdES%!|k1R)++&~g&*{zd_WM2y$2O&U5l~5QcwE5h<)5c< zB>^6Z3TKdIMj-_UNRWFOVF(-=D*t?RCd=>lHr$e1JL?nQj!RH$H}}%8W+Tp%Rv-eU z&=8EN|BId!X+_Wv)h@KqykybIo(~)&dyp@dYoI&%7ztidFEj%<^pbVIP#KU7Pu*aWfPm!J$$j4JJ{JRa+*X3M>dQgvud$9Q5>$^$U1$M&J3rH5zXW5BX z2S6Q3jUSBF6S_}vwWaVNHX&WjSW~nqRN2PWz671s{g;>)Nl)Radx+o@|86^RiTpPT zRqXIJz}s1Mr6RQqh_CR@>9?z6;x2TkzW_bCjBy0OFt@H+H6g;l$>D)x7D#nn zei<`<4^PGoI?1k;2Zyd*K#)2_3mRZ7_1&N(-7c30JLCRR=fMHUDz8!w0+6Pp9g@9h z7me}SS{*}UW5C5py#W~=iE@Fk0YH!2i&%S0?g-Z=CIL1LRxR4EiiN$%Sofj(Kj%lw zVtvUXLfTSHR(=Rh$KGaO(U}wL8lFn#q))_>=ZP9sEEgbA9 zK2<^CZH1hT9s;rA4yx0F7RmI9{7y+ueb3c9eUIl54v^V>P6~kv1<56|+Ez1&*T?gl zEnf{`AvAo#doi+xn6#NSpjb>lFVblJ$r5tePQ03g3kO!kOH_%!=)LjgoifA+zg7T_ zZJ+l0FIZ(LhJvGx)ewLy0%i$~y(IXyNBwCU$lKeJtAI1&)ULa)&Yfkk#65tLMOJ# z02|7HJ$?f%3=+f&RTG-#6Jkw67k(<-?C(+ZXUQE5&3H)XoUjc-eR%#N5 zSpXy<_da$Ly1$5^ji+J=FHoTP=cUWX#O_ot5iIqny#pl2D-?U0U~o9enC0&$y-kS@ zlQHWr$%Xi~1Raw2D|DD>czRohBRn1-6Nvh3HjALjh?Ij(nO$yuX}SsJ>qrf^)Nij6 zK_7^g^M}z5HJ-a5e-dDEpSzz1aw;Wq)o14os``Jye1r+a_Q|mDxcS}cz5a=JFgoe` zQE;K6o`ZRaDC$!2)04jQT~FxSt(qYF4z@W=Ll>TCUdaDmkt9iqOK2HE&FU!ZsBy<= z==m-HE|^z6`RYZ8O6=33Q}@h)gpa^-mtST#Yjq{y8jGTSglp?hs9_o!zLc~@)s$;` znD2qdE`^KJ$oo`pg#bl|p{VWrRkg1>sohu=yUS%vkyIqlXmOtGEeg z$-m#>jUpX(dKgeTWjn zBLVGZ&96iby(LqXsiv%Ba*jcMR7zyt3n$?>ecY|(r8awEzC%T#H@6rzRoFjfOPsO! zXV&!&MijfcfZXXY+4;Hh3!XqbO{Kzq#bXX~x})k3SJ^*dxqx$wiH!G{N6dXjQz z0qlkn*F{9}=>=lP^g3eh2a)}#f(@K7nA!vVzJ3^FMpGJ&>hWoULgZE<^yD{Qz&rru zK&u~z87K!*N}SskE|cGe0Ri>fhIgPZkqnkuW9b=LcR8ZjA1B-~a}9ZsiBCn3H<_Y* zHLb-6x1QQ&;Es@L*exY@kWePoK%DAd+3OMiF3>e#M+ie_#*SWcgJBcn@&bA#nhW zt9)%aVj#fjRTI2JMgOp`ib}imU}kaRy?DYrl})xHoB3`Yp8mmLgBPEMm)zrJG8rZk zww1}9I=Fg=*s)c}Rq_~KLoRyb~3Y2fSimMq)$Myu~Q*dR=2Qynwp1En7|o9vZ^6_b^XY`V{aVV^=i+& zO+;TVLv1*CDY=ykV`X#od3$vQ9jHYFi*q`48?Yuu6+}bG%1W>HH2D*v(DENBH$$Y# zN(EsUm~I5i1rq2PZEb>w*?vp;BHAq!+N9{*snqt@6dNTBsgs|nr8`YUX{kXidq2=n zpgViw#Y@!kob2&^b$+>4Vmc(GckITme=^9bH@@{pZZ7x zSHHmgU}2+7-@$7=WrZxm9#WqjrX>))1!zQiioJl+fWI@Q&O|Oa`3!k#NL?NYd5T1-goe{bhSVILC9jO#Im3p?7#s6 zM$o14DIa38uMEC<4Qja41>g4b$F}p^nE$l@IPGiZ+Bsi1Smm)^=G;~=8^z#NiN$LY-VL!N_U0fG)hc;)NohpD@5s7O2mGXDpN2>3n-Az#ALVfBl{^|QpKfKo) zeye+R{vAI9%re>(dl5}7fg}Gjhqh+g|tZV8;L&sUB zMK|@R6J0~>xL`GGo)(TyaGePfo=h#+3mNWRrG7OyaHd_5gb93<_z-2ea-zj}oo+Q1 z3Jp%W-JB<@$~sqmk4y_`-l1U(7x;Q8dD1XlpC9xh@z`@8+^FmnHAV5pw~Xs=tQOYe ziXOy!+9+OvyF$+D1-(fOvqi)vH=|4e4vM|U3_CjG%+9!ae>SBqU~f$_y3y^2?`jSp zI8q6Gk3JvnO*im0nUg8$K?P#)e+uy^=NLBKx*7lRT)zSf-Y`LlIc zSCX+Rhmn!Q_ztfP5Ca!9Pl3qcg9HJAg4!=|13?nxksy&q5UuG_V;cydC4iJ{i2I?$ z5v}2s@UKWBkqQM4?H-;J=8k!lWr$&n@De$Axzw}AusP(q)u-M8N0l;U54sk;N%J64 z_!H=XiVcITf>Up9>I^cQx&#nDVD#pE286j4)k_Bo%hTciYlN4DH@jF0=1TAH*D1Jpa2$fMsyg2kU`@;ka$?3Ibak+ zh){%7A31;B5M>Jp%&k+#eGNS!(h#*`FvAxuAZD(uJyhnuGbGl*vlx7GUeDU?>Bj19 z&^o{rCm{YO;UN+GyX&;KXd?0EcyY7+t65C-HPk-f!ajr1iSM4CX|7?eL2WclGzWIB zN{#0c(xH<7=(b)SDf%ZpPW5(Wk?Mt1vsEp zZiDsTSm*xVzS({e*lc*A0(W;d2dKktS4o;!Rc1714|t=qZoj5_vTeXncQ_f793!17 zSDr)6-!b_0A?3gv#|jDeeAc@}-S!tuUGu^OEh<S7RsY;&-uM=)~kF5J# zKw4n(?cIIEizcLlg7%8ptXYuxwpQr%&+HdA~ks?&U-DTP{9bHNfg|t#7JW+E;&_ z(}k3#Z{vHoRbr{GP4yMqv~7SWgC{BlHaMrb4*u?q;l(qdle`6io^E{I zORIb|gzB7c^L*J$dfA&Li6iWUebw&xjO`e&%->**_e39lx7aOkC}5B3nJwP74qF0z zFJaZ;Eq~K&0@&}?urM@DRiEXO?f^ckZh>bdZTp7j&}PwE%lg7MQ?(mgAN&tZyCtog z1DwvTowtxtnYezme_1B<#-mu~4T-iYbFt9Li@=6s5tOKYak$^w!zL=ZW;Q`QW3@tN z{mnLwO6=m7AP0g*Ro|xRuDX43N-&g!?nWf)a z!i#tPdj!MS_IT~ka~YUKr#L&IsQmo;_ig8FVBt4aoh|+5v**_ej6r})E;NgK zx7+4SL54-L8o!|i#sJHcf)zWNamofmJ$b49M2nDa+eOLalfZ@@Y$f*hN}4V zb@z8?Ew<*GTzTofQV;D$+|;1WHD6R`tgDIZMK!z?D6Q}ihQ6^TO>ADl=cQIYpUbOt zzi`rGJ0a+K#IO11Nlsdw0#6OPA+erHPE_eD!VhGarOl{!I{ympg@ViUb#PfR*?HE& z{tCX{j44e`5{;}^^)8vujX#?G$aST%?!a5Jkb_8FpejdF6r3B6$X-)b(_}W#lqR~R z?;Gt%@cUMwrW?+#S*p4iGRM0Sa#b@yBLdgIv|_oVas7B0l<_s5O8ecTLA_I*L~kHiFqBN)?m1uCljowuCrg5&KSdN@DB7f+ew1$61eKWl(FfsUE{S;?#;BrIw0v>8dP{yY zZ=tFp(JeMUl@P8NXwFh-vvy6yc_zleJQ|n|Uvrxuep@1cq>>BNEqSICjR*tAOd>S?^J{h&8YRTZ2_)LbD#m+sR;?8|Y?LX*8-{}E?{=;c zR!>KI5=SKkKUiFnW;^C!ByRQfg5bxOi2WPX<)j!QP6V^A66a_lS+HUGg~4kg)5diW zie)jmr4`%)fiqnSw0NA{LA`6;?VYQ584ia}mFEnm`-+Q=$TQ-7&JeBvO?`eyFXt6q z3(I>roqBU4QWqZ5eouSgPq6*nDbN5N;_<=FGqA7FQj-UllCW%V)y~#9PA$R#++Y6V z^i6qQ#e9{C?>jHD{oXF`(Y0?fscY!izGA+_gw_5ziz9u`Q#Y?j(R>%M=&Yx()bNq2 zWF=Q>V^@uf)>ZD=D3h86TJYx8lUvETaQMqk&DHE05^1M88)JyVjfj+qwEl%;xYsz_ zzv}*Cd*YDrYgtNDy;I0iAoat;^hEP$hhK|KX4U)%n{Pc1()NM==v}^|P=XnYK%$M@ z9Tz-r5L7%&#ZwPB^xla$yvc$JSWf!n7Zz4i)9uo@4ct|))D7|&6wKayw6qEAx%@Bf z%URQ^`_CTs?|$~Q{g*mt!Fif3ubWeIU!WELd_ebO$5t9&jN<8um6bc@Z?}h`q@3KD z!GN7EIDmy5`DgvnOdkVtEh?ix#$}Z$o&6jS^JaHe@I4C`xa2zuc%-Us8CBB)uQc4axP!$$ zQ=(CDX)@yYk6=reXqniK`3)!Y)*_jvb8GsWcI`r6y)*Tv%Bi^J&9_`0%5JiNPi^e5 zhl1m(@L0|0RN>J@kDnfPbr%)wrr2jn+|qujD-0zhllNqM?B>>4PoKbT7e^(ZIN&i$ zD4NSF$dvl`;?-=AtqlZ;6%N4H3r}VchW+2|Hh!GEsV<3ecVeGEb+VG<@%6u8_=VsMMaGYWD;+-NX%Ux1e%M{@4gRW`8N zy@+Tz4o&fLyd#1wmB{^$n!8SRrOWF#A#K2>_+y z;z=b5QrHx#p<9>=Nh=)27Bp4|`JuusX&9&Slx6?4gvDSkxa!)beRA z&MRGUbZ?JeF!V+}Md zyR3Iiymufy_SC}A1$Y5tp-S8r5rY%yS5T|IUJO+x%>Uw{ksN!_l8v+*P*o{$)+t;0 z{-g<+^7;ypN*{1}r2o}MR7Sl&^(f+0{1a|ogdHm#i?VEl<7-NdjJ5h--7HK}UyUl7 z*dj5c3(QL=`118^Xzfc5mO-mS8O9-jCHiNE6(DjTxdELE0eZ`Kpur=G8vM}=)+IGa zk@Eq~FJmFgi0Xz$1{qFh(W=4w*#S=v!!&jyH9JDg9|w z;c-0jX#h1ZnkALoAXOg@Hz003vbaUmnU@Fz50V0J7nGG_oH0n^gqU1=A)k5*V~eD3 zLW#bK>IpH3bH{kRR-cpt9S+$Nlin7Mj6?OU8!E9)z?8H`wN&fMMXdrQ z8GIq37U`8Dqytq5k>>%yE=Zb=C9w|Rg{!2Cx*sLp_Vb_5lG^hmD?_-ZVLYT*;sm%M z2|!!y5~Sdlfb5 znSA8KQk19++972?x;4S&9ZiPUo&IxOD3Dg|MFFzIxeDm>U*J*WZA^M)eF(~qLIn^B z@bcEmNE19&q&NQl=da@sjsHcKdH35$8UhmSfh&v#AF?fVBM*Dq{^_q7ek^Y9-w415 zx-EHcVIJOLroZ<*X!Bo?ZWR2CBve9uYc(c8dc?z?lNya?M6j?S#3?`8#{{1!DUNge ziaex5!H#>d!6|T1_Y)1d;WFUp3Vn`!hrYmg=YI(E~8cypi-^Mj++wkbmlr5gD5 z!c9F-KxKY~XgI#$rUG!z8GXZVkO%XjihSl09f{x!TZH|r_YYLj$5<(#(sxUVkScXM zya3HN!v%RP68=II9Q7K(5^`2e0JmSB&8iB#&+-BBSFXW&nPdm|_&#K5Veg=00{B%)`Qm1_kydRKl(Bngn zQv*eK9i5YRVaXw9xn!97NBV}M0eZ*f0nl(FT){FGE7;kCbMK4=o({?%5Ud z0q8w^Q3EWLe;Z(qjbPMjitLLZ8-xGs(RG_HEGnM`*9-XNj-;?+zdFjqF2jS0FW+tP zxCE+Vu+&*~ND3bCfa-hw=8Vc~E5xi3ykn0V2>^&JaiateJhTlxt%UQSXqo+Rwsz|n zc(S0UXcP}8d(r*jeN4hH;q4+}RhDu{uA!I&WBr6g96Dl%e2|MLQ~-e-J}yoaBqoq? zL1xGY;2}mI{(2$OUU4yn;v67eopUWZjNom>nEIddj`vX!|0MtO3wM@OUxCbKBvyf3o90{z))xpGj*{@L`Y$Yj8Xdz zg}MIP*}wph2mq@bTck@8z$1&9zU08NZ3-;04tBV!P$|JHe1an>$dD$Flw?TmvuIEB zzXul?4;JEUR-NZ;?!x0)a*?VO^OX0)|~@$s7vAP|gc- zv2nHgztFzV{%i*$)Z#fwREp#uXQyL_df@JtcACKoJs0JSj1Ld9d!Y$qspuB*^~M(k z!`lb2fMsaWe*N^YYfmR=NlcLStROk!Lu1~zIrg6bvD|t3g#~k^H1T0>gXv9vJ^dz^ex}X$UBlJGS$CJcOQb<^Jqj^gA(6BBbI3!@%@s zTO%ynIhyg_Lg>-BVg`l08O@KKuuY1*24NS|qqM|lKKvlka^jE_C4olq8q`?su7UZsKbj7u*8gh;yRbUK|Va4``=1Q^jBdi}&NwaD6G=kXYM>5^Sf=r4B1 zIY_AFa77xHvBdA{i*F0D;Lifl38G?JYfXRd$m`6>pq-TQC-m5spY$|%Ph2xf+1!`> zNP-7fcgp?OrYOCohihh$|Hiy;T~CKQUu#S`^zqpl3UJvtPV)o{HB9wc+O1)d58p#n zECpNh*XVdVs8)D3oE2wyl5_X|ALGBL`%}RiTgq2p&#s@2aMTG{(#n$^&>Y@Pc~axB z!i1-|f$ohjs%0-`Q_L9(XTI?kWZv|$75Cuo8nk!M;bd%8O^ZgR0dMJ(-#tzSkPls^ zK{nKpr3l(dl;a&m=dQ<3nu+TX&2|R}GsAvg9INeHe=AB`P zhkak#=4+F*-9LvR4*7*yFDZ^Xwts25Xn1_4O^<)h*(E{C zE$zGGbmYN(W~Uh%RSnNoyrSMnd_vn#Vl*fz4Q2SqZHX+BrDzbi|Jq0sj`tBj*)6_9 zt}+3WtZMgVE!4b(C-Av>FhGhFz2nC|wCck+uH&LGf5lPuuLrGd*feWeC?A2}X8Gmz zbI0HbdSUE)ocumQO$t5M)S586s)g?|t1j>ozaT4^vMNubaLx>={$aZV-|Zwg16HN0 z?WnuY-^lxbJi?NlX9umRk^35ksU>^+`Us?{o${h~4-?q`E#pchB$-Yzk6oQZ=iIA) zTrpn(j=-+)0@|v{@Fl_L5cinW;p*Ny%g&5*dBN-Z!>|1o^zZLkEDGTy=UuolbmVE`O*X|O zt0KdwyIj3J>_^NfG== z|D1ANdZLAnl;@4Lpn$0?#?owf_QsHRz&6xpA$&qsf68Ru{fqB*(taH(hU2=0!V)3n z1PEIq- zCrQ76o1B$yf4f7b`u)dwVT`|Hl{>%Mh7`%JAF-}W25+Hga<2x(_2c5qu9=Q0Wy~yz zZMmFH%TM~jfx@D182VvF*+2^#hj#2tn{)tj?Lg_#7$j@wI;81Yxj7G;lnE9xQ$e!J zXF#oRpM%^-^&<2$^)=`cqc0lyt-|&T;hCiMF(^VH3a^Jt0vyy;amW!p$KB|?#-sG> zomKBqvX?=do`?crh)^F_gD!0Vin{4xhBWguD1i3XtoHQK!9RT~^>G1GnBGZGWVgbG z_k=o8IS001!op3dD_GNU9ZV}PBJz^D_Xa66RqgcExTW)2IVj;fdEK`HjapzB9ZIv5muCi)NwGjQ(oVvi`K6w)wic+SdJme! zsP7Dqrx$uW1&C`W9=)7>ao%oLKzlJYp|Pe#Y3} zLcOThgdzHM_WHTTmJO*!mX=jhx@IrE;VH2_k1VMltwpJ_8!QV(jLW-{;?H+SI`t~( zh)BZ>1iR!Vm13My(u;E3!}$klBE433Q`X#pzN@XUe9m5>^0_ z!Vk)JC%CGpVxsz^NdVt}4*U9pBocvx2CUzTxXgEAqf$w0sHv3km!5qSKkA`dQ-6Of zYHZR9+k0I7o^Z~{Zrvl8%nG;+S;GNCO2(n-Gy|odO+?7Qc6pSp?^M_heoj4Kms7Nn z$4N22q(2yrRlfvlr*{I9h<6TkmMhsXHBa(h2bwv7Q+1c6>N8UED5<7VI|kaGOQe(? z$)m`<7hZ}uXCYY?MA}5bmM(m)&Yjedb1vu}#6*Iem8- z87!WCqd#h(yqgY-yVSQUrmS;e@M3GIJrsf+S0To z1J|_$5~(zIOzA~;0&64qaE+=JB{CB?+$GD<^F?NK+r2F_|b=IaT1>=Z&y#m&zoq(_P2qpbeOt?Xrcg7!Fe~ zkJ8eWOo?ZO2cGm0bc?C*vtxIhL4Hrde<0F-jNPZ>+=nG)9l$Ar5GN1klvPeLgdNt3 ze46TN=7-6z*Xq0fmIXNKYYp%f*8$>LF!lWX!z`T&vJjB`65`yS$cMMI)6DR}|2l0k zE;kX?Qrt2^$#R;ysB$FZDbON*OP9Q2uIBna-Zs8rnm}0mO$>Vgi1X6Jn&Q4(lmYB;A61DGJrH#-KFg0BXG* z;XbJR*#sDplS*IcQec4JiSg}`(VetF|I8aMzX4h0R?D@mFkBHmGT zELl`qbSO!&sPrYrDG8+}V#+x+#?{yG%P54fREjYrpM#8lb6x z2q~I8`(U_5d(jJl)`ttMsI2Gxv=k z0BK(5pB}QhN$mO%@55>F?((tz-|<1RB^u9PWVgIvYkinUD(!hcZSNRHl36(5hESmt z9-XRxWm%jz;^pd<&M``=numW+m%cYVDaJ;w6?%B@F7xOD{AZ12jc=cek0waXb|@gQ zm0#Z^$pEh8{C(~)G)xM^>M;b6=7Ao+o?=y}(9A zvERM*Ok|OUPm;YF)!VSs$95|g6$G1Y))d_VHSy&n-)@{1wHbNeB+fkEVON=|z`ysv z31{dt6PS%UKh8qT0qubw!i5R#P$xxxbvk8on(++{l=~5KgN{|9i0iO{uzcVWr;B>` zw`VJoAwDhC7OCl`cJ3mgRf&qpD@N14H>C*InBE(&&V23?4^}*sbnxfNe^%T+j}aPJ z)x1VvBX06F=ho7MGjA6I_92@7$V^*;JWewerk+}rIYh`$(YYuT35Oj$Um4nb(4%I_ zMKPT-5exQV6BekdJg%adP-~tvx~y4~#ya#ojGpD1)&+L9v|VNO(k8$ThwB!0PV8eP z;c=&3+$m5J?-M}o;M3zcgIfkrvEj$sL=O@ z^-2yC1Nocw8(mj4-xes>IHs=KCZXW%+sQa)+HmUX4}nk{#bp1>1{{(BZz^mL8Z@A) zLTPmZK61yv*9_ML`XgF4s`a$#gUM=A7JR}?nx9n#{+th5(01o{DuGr&Lud3}?|C9b zX;H{O6%y;+2i|_>shWb;)^9*4{2dQuxcu}WNak%7#-o34 zg|U$qs?^D5FIPMBxyi+keY{Ba6yG8z=xI>_;|e^EY0;z;9O?tsq+ZF6I8iX$wuC1U z=7fmIA_!_H7<9eiJ>i5knIC?*6)eWjC7?q}XsmUi!ZTQ7IvcQp=Ha*HDs zbLmQCtm!x3Feh#;6<0^1xynxE&OHtA1Q;^3#|YZIhG8y!6v_GWL;Eo&aE~(-bd;sU zas2H3c1n(^D`*R-XD2rgvg+z&^fWeNNL(@QGPPFxn2@e>%BHMOS71e#I5$!ZGf#to zf`evRCEuxa0>;o_i#c`o2AC@qW;>pG!%&9=6w4T|N=-XVI7R%}y#bair#!aby2+p09TA<<|QQAD&J>l9a9veC|+!)5O>B zo$;#+!VQbQ+a@U-W5ZbGiD$>NBqnRxU(4<>ad(Wd1ZOFwZ`ZulmvU@YSnU_3*JIZCWo~YTHyjy*5 zrFkM;vdy;d@%qWWd$?BPt~$$rv2vGhy<-Iu^3IfRSA`jSI~qOAYfad6H24+%(8>6k zI?61%2J_8cKUWc}V*i=ov72Y)xu&JSx~O+M+@+zFJ7GXNS8t%Gk3-1@0R7WbH`)Ib z<5Yjp2GW;m-%0WI+re#7{Ykxx{WqJ9D7k4TyIoFtYYcgCiFP&4P+sb#;W|&<>(*`x z<%6Z8S#M;?PT9;Qy)V8VtADP9Z>i{!ZbPznFk$dFU+{J#!OP3Mt|~pEpE&n8K2!!% zJp0u|`6L9@XAE~5lSbRh9iR~T2wy0p>AxfNS+nSA#LR-V)hak>o88b%c}I9V?G@9L zbh*z7!J_!O_;=TYE_~-mJ-9%KFa9OONJmAXTuawV#cA!qshn>ynldwxyqNLqo1rFW zi;b`1vT8cTm0)8Zeu%oNzaA9gsmQrEcT{qsa(ipN=xiz!qN89O*6hh3!TWpSC0hR> zbm$vDpl#UYdk#Da)y`ADe2^gx5)C(RNScp><-IHBAPXd{|00J}Zm|Nb%Do@Fu1yoKx|`iJHfO}xEP)1rhWjpqH|bRvZPB&3 zJ^5WH2@~Jmu#eag$A=2}3~y21Y3AlL+4{yedIt&jgta$Q8g+I*r8NW15cY)U{zzX0 zZs*5a!v}fXW(v!h9%Zm{-J8ZQryEy={g2kZJP^vZZNDUqqU>Z}L-wqpWHbo_)GIe?Kr zq7k%0pdAakJq5HD7}FB?)N&8Y%dq#RL64f8Ckm9M4nZJ4)pI(m>bBMuuJI>I{Rz)n zeqKfzdI9^ogdoa3V4x#eQNFVO~|z08=q9@(Xla|XIN*{Syt>Ui|`$bQ2s+&pxj z^f0tWX8NWsUT4whaQ$DE>60NDD-wJ5o_-eT%eE7I_fTAjH2JV9>b|W)uXyhShB)M_ z)1gnLKC`DQ1sfXifkhGU~qDz7KJ}x%+N+t}xGWpvya6akOR8Kf#S# z6l#n_FELTU$**3@epE_P_K^gXl>my|0T>|EpIMWVL5(+#|NCzapeOA9cX1!y?H+9{ z$r%@1=~?&ue8l!O=F<{Sg~}@>g32!eTdK}_rNm=_G7$_xM318wEN+ZfkN0cwy>_Bw zm{^M5YP;=hJ=RhtAeOyQEto-iOE8>1*w{LkgZQ;;tj$C2aGpK6&b=DES*o{`hrizG z{3YLpxDL01IgFcjns=$8uTT0i2Rx3GG!n&T*8E;2&QQ{0`HQng9OtI}(cyukQO2_G zviB$l{n-+h;n;c1)(?jdL&9aTobWhYBD)yxm-@}(`-4E(nHmExTgs+$J&@V+wt#Ys z8SHJ@jF{)0B{{_az5tHot^aQ2-lvR(25b544WJGU_dxit*aMNvSDQnU@qles;mYqb z4}reiyJLW6y43*H?DG~u1Q75x^S0c1JNBghONSHfn+OKxz|W71Z~Fp`$T63zelQG7B%%Fm7Q8K7_UD+7TnX2G?*s({!?(}SCN!W@xiA-rGd_(z?%vEw*bH* zfb84j1d4Y&9xnhENPW8o>Lov0?gCnsjDciTzl$;N&T0x2u6V!mo+F2`gWe5EyGmVY zAa{{0s%d^*0+UJp(t0?nnwH)gL3@Gcb=a&;D;X~V@0L;BB~aDxhkyVqF7SHY&F-ge z(d+nkD<*`ZUVzf}T=k1#l1bd05!kKJDcJ*t6^zb$Y+fM!`|&rQwOa81)y$HItnYnh61WHCBP}E=Co3UqZ{~%mZr~H1 z=g+ukhlUIXF0c=+8y0f9%v*BKUAmw9$ zi6mznsHOvM4f{WY491gykSmM@q_7I28HgDb#imG5`aWtLGr9j$5|eh__c20#rSa;6 zl2o@;IeeSNe@u5PpUTY1WYYfb?k6E^SHs*L_%3v-O80GI0i z=uti%+^9nw&1&TjadSz(>vh_QGvHMNPj74tNCg9El6#TXVS0i=Ffa$(WT?=XrMMiV z(6|s7AA9dbl4xaE?1cyEV5`H?s1~=l`fnCmZO-X*rGfP$jg+K6sRXEntDZuHnuKfz zH9*TW=v0fpp+?PNNtSuWGkU(=J7WEN>P(Nw)tlW{dmVrT=J?hV*$k*K%eMecR(k({ z-9}$Cq#0^i-FZpluf348-H7F37;I5X&D?j+J=X;mIVW7-%noSqnP*vgMIa-K-^ zhcoAV%47Njc7(P{-))&9F2Y4kBAY(4oFdm0PT?Svaxo`}sp#Wc;AnbLF{@y2bH<=d zmuBVr>KLp%YZ=_~=VPiYzG4=XffRxCN>8Xcf^YDI0Jqz>qU{I(p>nG9p5X2(X;~{# zao9-R5h-c}zE6YBo|IViRx|VY3hH;s)0eOue|CzfjBmhgu*tw-xrWtQvwf_qd6PYd zwTQKMM#zw^$j6hMO*4qU3x{Ubvt0TCCoc8jZq)=6z2K`c#&~{ALPb@S?y|w ze9f0}V|OCltf_QwouKw`sLs=5;QKOd3iusqY`=@OH3z(aVQ};%5*{Ze+m(Jz28+8P zPe=!5K4>Bfm7bGv7a5;9PRCAPtc*Zq0PrtJqeT5>Gycu$-ZF(c4aRBWiJTc}t&P9e)+ME*XrBq^xdT zL+D*Ra|~;~5sV`Fe5-4x%5x>Hi=bzm2)2uAAPa^_KHnA^4>YMahm82P85(2L@Lo=# zxv#>-1)BT`S}-v~V1UpJAZhmmzNbo6Z*x%JiV+~XT0kVHSQP?xNqC$fU41o8(lqi6 zKj|l}#aX~OYAgZ+jFHf7GxGytKpko=g2d6lyCiD`=}JgE4r;&D#;GO*3dsjZL>6a& zIu1wHSDnMFgsxYj4){Miv}jloUf;-+a$!D6hIJ{s{V@)Aeh{Fw)FzI((H0(_mQ`Z6 zwU{#{HbE-=E;ir}$Ki;6T^a1_>sv9dx}q-gHDV2@*WIRv)pK+K@3j`g2x@`|L`-w^ zJ_tJv72&#HNeUI;P!5=KBc{7$5jN=+?G2_64Bf1EXd-SfTA0&G5CO$to$oec7z9!tLMb0R&sW zrQ_&%9K{f)xV2N&1gvI~ujtkpVS6uGRl(xveZ#&TC4U2MMYUq0`HEA|iKt`eT+!eU0bMZtEoOki$D!ODZ?elg%JD|vMD|9{7s>5Y5 z(|+MZyW1@C)V8m+zS6h25enJU{ZJU8>Tz3|z&FT)yo|@a_?DT}x(Z*7#hh$^p**U*-bd&1r9aLw{VTpuUVfQmf@krd)PXAlI ztfa<+hY!2+_Z`&4V>p~S;U3%%>HF2uy>WhlOZ8{7k z#}$H2JI}~I;fbsZl6gV##dl9J=l|kq54U?Dx4TZ4?eu;oV}yI}!UC$j1F_;FxGF2T z)pJlm&#vfofw9v0=&3R(DdJVxg|Yg%RpElCDJ-seT(^io+J&Pcf)QMq3tf$I)UBTLbnwnX2DL>qz&CvvYb^5Xh z7{(aHY++&Hc+n1F%!Rm!jv9hSV2y4b zDzi4brPAWtz1EmPyh%*QORsr`&cE{4{P6|M3dRYVR|{j~43&ofv>ALi*r)eae9s}X zL)`h2WnTgkGkATcu6Dgo6+y~SKSCQ3r*9(4V%-9ZsQA|Ex7H(~5Y&5;kiSU3Sp;?l z_!vT{6m(CB$?XQMu!Z~ooc9-g5FpeHHh`i@?i*~WJcTxt_!p&Jl%;HhUSP7AchnPB z7#rDC4Tzb8rLESJiW@@pr2*ki=CbWlas-#dvqGep<>Ux*OcYYqy}|xrApD|1#g&vu z-zv8hwXK^vzl^9fTJ*_LQCV9?9E#?ov_lASSUW%~;c3uR*4(gH4v~aCjFb*TzPn2> zdDB|-(b7bvN{P~mMsyVD^pw|5N8EI!zEwBtlP&>+$_$zH_n4QoHhQ;Ci1u!i9WXFu z%eh8vFODY}mwdG2B;!b38&1aP1gY37XA>h$;^;QmKFu!I{@C1HXAUp$oh=&MQHzTF zK52C7)sTyNJcwcpQ|IKYU~6xDOxy&)4B9wknh0oag7X8;G)ybd7vfA+B3=^=#NRN{ zgC;g>ErR+*{a2E`+rr*k3smmpV`TKm&$Z&-cU;14NFmUPQz^b^8b}v?lf>!0T83w- z$mcLWbE_u)CT0bmKqh$q2WUI{6EO7e?_^bz$HxbAbls>9s1W{c{(bUPwh3Ksb10)i zEJz~rtZ$S8rm5(cwr{%KVYLm{)3iWH7`rQIzPtPwkd*o|KoZ2I^93@@jVQTJ0^3h+ z)h#++C780st9>5S`yQ@2O5kk}!Fmx&;^xBUwR~CN{VNX89xnDt3HcIGFM?QBo(y9R zO-X55+|A6d!h4ss`!Y@L9hTEsTuIKj3s@UW?k^YDc+zZ*Rf;#n&kHs%^MpEXGOO&%4GNO?mk^b5|+FcpHw8<>~H# zD7-sOZGqLfzTCZpF)Hv&E4}#0tZI;1TKmX%r(9z(1JO`;xQMm@>uT_yC;%P@7lLt> ztH#rAmucW*(L4tx?IOT0oN$B&w@$jIz`(!~=x??gHTy`D+pgnQVbZgt9Al9`*&BLU zktw74cjlH^azXqFCx+?wzos|l8L0#9YhE**F6Y90h9D6qoc7LVF^g5y3!MHCC3jhTUw@40AO;4lV`&_OGULVH_@w1>aPC;`!*I}o?JZfy-S+O z&-3(GJ#nozSG|}c%D7|fr}DL4C8}%+Q}b=Xk7%uYMCOe&*wpc}{!VX@eheA@TB8dFkIf_nsB3XD+clcK`rFYzdAZ(aZRRzd=s@E=d(qt!z z8qdmi*u`8++?-nkHJfsLMQlE7MQoowJ`=YN9|Xgm`nP~Hz#sw?(dCzgl;zSDfo9g| zLz8-|)#{B?93J7(j9253;%#@+D|n6K#$Fwx&zve1GKJozsEiPDN#KwwSp>7FZ_dO$ z0ww1TW)5WgPaM_lOMwYP1#wG4wxwdKl`L-tF!7ly+)i(%ub@IRRq8!PEr3IUi{^!Gz5i7l`EN@J2h@*bw%#??$j*9^a5|)p)sRU^Nm8D)w zvfkDHo#{V($za;Zt>Z6Xc<7C9-jO%eqpyf}P3>i=u;yg~k`a@MuD3Te}`BvxW-?%+Uw(H`jNE*!vj{^J%?_eC8Nv5D?k?i2; z=*z&=X@qX1U*?LCiEV0qUC1y1?xmKr&;0oNv;wTRlv$}xKeMwh1Qt}G&C`2dtH7X4 z+_S@l7`IoV4%%~y?-eZ=y1BPn52SijPA73;&wvZ-58d90wZj~IUtiiqerY%u#x5Vd zQgt%3nv6{^4DX5Aw_8kmvz4_d>XNFt_w zrqh~^>pgnwfxOiACW|4vbbU#V)C&!0>h?QwvN};QGd(p*u0wUeE}MhL3^uN>(sw^= zpT^oxdqoe@c2_R=NcE5n2dwjM;l~ovWIXE@q$O)5HuBTq8xC)sCtcrtW4yap`!f!w z{^!ls|BQR*T`8a(y0%QyqlM%kuNT(3JU8iL7fx-E5$?Gkz5Jc; zkeq4v|1S0`J9YhU~kSS%H zYCtIIfAr{no^dnFNjU%2eITg~0oQ!|Qsvwy(C5`aUZ45f)%qFAJH2f@bSteW_B=vT z(E3sSa-w?X07Rw@Q~Je6&D?D(C#I+eY-&F8Y!L-rAIi=kR6H=iqA9gO_2rYaPs@%3t z=5Cf&Brjn$(>@UJ-|AWfOsm>SeX8LoCpEdIY$*r7JLU#{%sSV1ZqM<-5%(c#&E6KL zF+oSF=s7^Ut80*l){DB>^fHT~L6i&YE5N}#kolAU*UeE5EY$?rSC{jV8+|M7w7&H% zm9oMnx9)&u?v66XX<_z0vy}3!z-53kZ9%^Pqm5Is-k=gXM?_Gx%he+1X&?+Xrx>w7K zHOSi_^!Ea_r}hXtQ*s!288D0(YH;~SWPFE6_b#Wl@=1sR`vPh+Fan@)&s%*nn)I?3 z*NX}f$%*kSU6h#v`#b9f<1l&hQo!WkvSyvs^@0SHaVd=fr_SiVYUfGc_qY?UOP=i` z>}uQdp+@EUVKw*20Z?+h7e()}_QrBz<^Dv;des+^V*Xn_f?A7mK8ODL<7)mr+jy3v zDU?1#JKy@!_%*AFTODtuJkDuH^xsexe!szhzw)>9{N%Y^JvAh1ayOK z#Ho;8BuGlGHIyjEgvKgr%1Ha|dlt|tG9|<0h%OTp$mU%oUjZU~@vz4w^ULAw;n{b> zVTc>=<0l;^-n;X{rBVOR`8UZOObY3b-zC)2gog`r2_c4C!nFftHI$bd_Vh}}#FRyt z=2Sar(!`MwuLiQj=prwj6t~k(lQw$nG#H-(3q1VQo=z~<1x8LU;M%;}-OKqv#sQ3FS7d{9`FsGfk( zyAA!vvVo}qwsN>^n@z5k(Cj*uf!t|v5iNczrx0J7{1?>a(^L&pu zzx__A@%;AQ+2CEX^2-WrDsyTFFU6gV5va#=nEsW3$9$&*-piOABMx~rSE>v0=L&C5 z1)O=T@S(996w>H-?%CFoafGb}2RvhMDwfTQFDLOl924;k=hfQbH91a*HEqrY8QEuk z0!}~F7z$KQzblbzw>ctz@T1h`)=H~Wr|~F&RV>Hwp*?P*)lp@lgMgM9T-G1A1sH-~sG=&|J~F7W5JzrVpu zRZJJH`b%C0jQ|P z?74<$mp$^)$$UUo&Vf^7u*I7)gh5zY^+?r ztX}tA1mteguKFLmtlt_1G1E^4h#0eFvq<@wx|00zri2TEZ#*GqAS5!Ya-KCpzZq@6 zl$_yr-XIyq4HHAxH2E^)cj{F3c-Mi07NBaCtPKa?{B^9QSVqmi-~3wZ!W51tAV^tS zT5_*Xm04!%zSa;Vx%X+?ZnCIX85$|%1+*-N))=q!-r!-E>@TEph5J36{-hT%E%i07 zpFmmPFi<;FHsd+5mC*)B8+)_G z%x@c$Ni1(oBUS=qWg=PG;}MtgE{g1io(Q(Kmns}vF7TH@&~ofN58yh z;wBe{2@<3+X<(WIf(wp+_8+q3*f|bswABlteLlFf+&{V*!n>UF7wCOqFz&2a{DTAD zuY0Q*5LF%Y5)~m@ZnvH(E;i9i^!NUHTkucX^BUBtZ*aZjb)7I7OWgyRo~!BFKH#u5 z52`@h`}wPh^7n)4`7zp7_lii2hZ-QfQ$#@1o+r2MRmbD+bN1dq(q8!|Z|9^iyqAU5 z*7Bs)us3GwqKb6?W1%`yS|C+G6Q2Q2%{CR zKkX{hTEy>C5`4!_k1RL<^zInuyG!}p1=Sv5qjLDGC|bqJ@(svYHcUaisRUMeRZ3$- zat%26hibL`1xG+R;DISwoG#yGq3)+&^S~>*iT_nDdkwGsoDyaQ@r>?FGtGXZa>Fs( zdL%bzda(INtKZrO>b+2|7HdT7>jrK+f7m9#7aKpK2V7hf+4Hj0y-dYt{T1_&kQPHp zx58*Ou|v`023}ft&#EVLwm5KsCNw>{0F2mG4}Nrv0oBh5?5~)vG#SaoLT5rX(HO1j zGSIliT@&c4&`6%^J9;u+u0FeM^CiG5MHNC8NTr%7Oc31erE}B5!)R+})Y5lzkV?2* z$;)m816(;gWt+w#^FFi%gEId=*7p8XMERt#Js*akpEOj2O?6D+^~#i8z*Fb;)E zY3_Mmd@N9KVv1LTQ-!BM?*RBVF~4m*aljd?iDppktDh`S7t6BKs7766Ql3?hS^>rB zIa{C$fCkc1H0A&-I25*H6zHM)utuk<`;+O{9uu`s|5Ao3jtv<0&cBkpue}JwV*POx zXlZ)e%$FneqtzLxT?B@UKddb5c_>-%d_3(ovF#fBgsqa6-{b-}%X^YdJh#!yKM6D# z78CD0A31)%;Tjg0o7Iub9$GG7ed7YjP7=~Cy{cURzj?&xH{*NvjwJSfjvbM zYd-~NEWEq5Fp3d~zC3lE!yFjc)351A599MGMXuxvK`5figQ$`CBsJ971xG}{xb?E3 zTUoP{9X&0(OHUe3_d3(vfY{S=)||n?}EIlQ(IaVIhct zAz^COIsz%vJn`%V6eVy??kous8w3AFEj>a*uOJ=>4ex!j!e>F;OI4|<%#;E+_aDkg zY+7UyhI?+~__M;S5RoAqNVwyu-{k^K;kgod1g-99AVB9=$%!n6-Uu_5r8?lceNpqS zL6KLp6R{qhm!cMGe^USqU3Nz>40e5d`y4DZqhltNGs;uM0}s@6_~MQ`1g)#2kUUT) z3Jk~*9w5$yr(6w^$NAelCJ#8hTxr6cFGnb;=T3K=cUu@F4B zsg+nL6rx?jb7RB7lfm3GbZ`LngZ>W`%AUvJZZO#-7~Rlojh-GUA~a&OZ`L2w+-7?| zBlMMrl25#KgJ;Z*dPrLnrBgm)erH!_f`sk?D<7!0m*Gs3 z3rFJK7chQs$lC>EI4{P~t81W62gEH#l$rUNrdWU&bKSyxe~NI0x&27>Vj6xPdu`*W zFUBu^Q(BZM!clMGh3|memPLz`T|C)dj?nP4ULM52SMa>PJAC*{@+E@0j)6iZG|zPg zaeV>y0<5S99;KNQ_=6=zqQnjpug%Uv3N?w6`T1tW@S#g*0Z_U;mQ5 zwMMt}(tTlQ8+A|3+6Bzv=`%4)pQ;KR{&#{?T3WwP12b2!^>%KuyT*?5T;pjQA`ZQj zFg<=k4l1s^?k=^T%LOT2%09XLqP`&^3U)384tc2;_<>E@^vt3ZsAiE^uGa+wjjl&4uT-BUV(JJ61;dS zp2zX-zJ*cc)h5UR5XXtJ{k=%wahKbUO#y#D`a^ujaXMQ64@@l}#t%;1_*b-m&R`UI zyh8yYru8?K``R`Mt__j&*xsS*1Xut4JT5a$Q!G}K~#KfyQf}R@# zT#$R_>vdY~qlfNn7l&q!O;ib2%Jc@YX3!lIqtgzH;~ZREp6mG6t8t7&v=`hBArhFV z{hu%hk24hcZD#rBhX41Ea%%O`(nb@(8N`Ht%MbbX;35Z^Wari*3#y#jJ|_aN11G6m z^l=2?gcs0($MUb&75d}xM}HYtpaszVu`OVhTC~rR0w0>A%Rev=9RBAe`0u|7dM$q+ z_1D(H2|x7>JnMt~Vv#1R*|yV9;DhXraAj?!5(VqP{|7khtq}kK diff --git a/docs/usage.rst b/docs/usage.rst deleted file mode 100644 index 52337420c..000000000 --- a/docs/usage.rst +++ /dev/null @@ -1,5 +0,0 @@ -.. _azure-functions-usage: - - -Azure Functions Usage -===================== diff --git a/eng/ci/code-mirror.yml b/eng/ci/code-mirror.yml index bc010ab3f..46edc6bf1 100644 --- a/eng/ci/code-mirror.yml +++ b/eng/ci/code-mirror.yml @@ -1,10 +1,8 @@ trigger: branches: include: - - dev - - release/* - - sdk/* # run for sdk and extension release branches - - extensions/* + - dev-* + - library-release/* resources: repositories: diff --git a/eng/ci/core-tools-tests.yml b/eng/ci/core-tools-tests.yml deleted file mode 100644 index a62683f69..000000000 --- a/eng/ci/core-tools-tests.yml +++ /dev/null @@ -1,26 +0,0 @@ -resources: - repositories: - - repository: 1es - type: git - name: 1ESPipelineTemplates/1ESPipelineTemplates - ref: refs/tags/release - - repository: eng - type: git - name: engineering - ref: refs/tags/release - -variables: - - template: /eng/templates/utils/official-variables.yml@self - -extends: - template: v1/1ES.Unofficial.PipelineTemplate.yml@1es - parameters: - pool: - name: 1es-pool-azfunc - image: 1es-windows-2022 - os: windows - - stages: - - stage: RunCoreToolsTests - jobs: - - template: /eng/templates/official/jobs/ci-core-tools-tests.yml@self diff --git a/eng/ci/custom-image-tests.yml b/eng/ci/custom-image-tests.yml deleted file mode 100644 index 95956683a..000000000 --- a/eng/ci/custom-image-tests.yml +++ /dev/null @@ -1,26 +0,0 @@ -resources: - repositories: - - repository: 1es - type: git - name: 1ESPipelineTemplates/1ESPipelineTemplates - ref: refs/tags/release - - repository: eng - type: git - name: engineering - ref: refs/tags/release - -variables: - - template: /eng/templates/utils/official-variables.yml@self - -extends: - template: v1/1ES.Unofficial.PipelineTemplate.yml@1es - parameters: - pool: - name: 1es-pool-azfunc - image: 1es-windows-2022 - os: windows - - stages: - - stage: RunCustomDockerImageTests - jobs: - - template: /eng/templates/official/jobs/ci-custom-image-tests.yml@self diff --git a/eng/ci/docker-consumption-tests.yml b/eng/ci/docker-consumption-tests.yml deleted file mode 100644 index d1de145a0..000000000 --- a/eng/ci/docker-consumption-tests.yml +++ /dev/null @@ -1,37 +0,0 @@ -# CI only, does not trigger on PRs. -pr: none - -schedules: - - cron: "0 10 * * *" - displayName: Run everyday at 5 AM CST - branches: - include: - - dev - always: true - -resources: - repositories: - - repository: 1es - type: git - name: 1ESPipelineTemplates/1ESPipelineTemplates - ref: refs/tags/release - - repository: eng - type: git - name: engineering - ref: refs/tags/release - -variables: - - template: /eng/templates/utils/official-variables.yml@self - -extends: - template: v1/1ES.Unofficial.PipelineTemplate.yml@1es - parameters: - pool: - name: 1es-pool-azfunc - image: 1es-windows-2022 - os: windows - - stages: - - stage: RunDockerConsumptionTests - jobs: - - template: /eng/templates/official/jobs/ci-docker-consumption-tests.yml@self diff --git a/eng/ci/docker-dedicated-tests.yml b/eng/ci/docker-dedicated-tests.yml deleted file mode 100644 index 518b03392..000000000 --- a/eng/ci/docker-dedicated-tests.yml +++ /dev/null @@ -1,37 +0,0 @@ -# CI only, does not trigger on PRs. -pr: none - -schedules: - - cron: "0 11 * * *" - displayName: Run everyday at 6 AM CST - branches: - include: - - dev - always: true - -resources: - repositories: - - repository: 1es - type: git - name: 1ESPipelineTemplates/1ESPipelineTemplates - ref: refs/tags/release - - repository: eng - type: git - name: engineering - ref: refs/tags/release - -variables: - - template: /eng/templates/utils/official-variables.yml@self - -extends: - template: v1/1ES.Unofficial.PipelineTemplate.yml@1es - parameters: - pool: - name: 1es-pool-azfunc - image: 1es-windows-2022 - os: windows - - stages: - - stage: RunDockerDedicatedTests - jobs: - - template: /eng/templates/official/jobs/ci-docker-dedicated-tests.yml@self diff --git a/eng/ci/emulator-tests.yml b/eng/ci/emulator-tests.yml deleted file mode 100644 index adb1016f3..000000000 --- a/eng/ci/emulator-tests.yml +++ /dev/null @@ -1,46 +0,0 @@ -trigger: none # ensure this is not ran as a CI build - -pr: - branches: - include: - - dev - - release/* - -schedules: - - cron: "0 8 * * 1,2,3,4,5" - displayName: Monday to Friday 3 AM CST build - branches: - include: - - dev - always: true - -resources: - repositories: - - repository: 1es - type: git - name: 1ESPipelineTemplates/1ESPipelineTemplates - ref: refs/tags/release - - repository: eng - type: git - name: engineering - ref: refs/tags/release - -variables: - - template: /ci/variables/build.yml@eng - - template: /ci/variables/cfs.yml@eng - - template: /eng/templates/utils/variables.yml@self - -extends: - template: v1/1ES.Unofficial.PipelineTemplate.yml@1es - parameters: - pool: - name: 1es-pool-azfunc - image: 1es-windows-2022 - os: windows - - stages: - - stage: RunEmulatorTests - jobs: - - template: /eng/templates/jobs/ci-emulator-tests.yml@self - parameters: - PoolName: 1es-pool-azfunc \ No newline at end of file diff --git a/eng/ci/integration-tests.yml b/eng/ci/integration-tests.yml deleted file mode 100644 index 6f8f69d9e..000000000 --- a/eng/ci/integration-tests.yml +++ /dev/null @@ -1,53 +0,0 @@ -trigger: # run for sdk and extension release branches - batch: true - branches: - include: - - sdk/* - - extensions/* - -pr: - branches: - include: - - dev - - release/* - -schedules: - - cron: "0 8 * * 1,2,3,4,5" - displayName: Monday to Friday 3 AM CST build - branches: - include: - - dev - always: true - -resources: - repositories: - - repository: 1es - type: git - name: 1ESPipelineTemplates/1ESPipelineTemplates - ref: refs/tags/release - - repository: eng - type: git - name: engineering - ref: refs/tags/release - -variables: - - template: /eng/templates/utils/variables.yml@self - - template: /eng/templates/utils/official-variables.yml@self - -extends: - template: v1/1ES.Unofficial.PipelineTemplate.yml@1es - parameters: - pool: - name: 1es-pool-azfunc - image: 1es-windows-2022 - os: windows - - stages: - - stage: RunE2ETests - dependsOn: [] - jobs: - - template: /eng/templates/official/jobs/ci-e2e-tests.yml@self - - stage: RunLCTests - dependsOn: [] - jobs: - - template: /eng/templates/official/jobs/ci-lc-tests.yml@self diff --git a/eng/ci/official-build.yml b/eng/ci/official-build.yml index 568fdf16b..2621689d1 100644 --- a/eng/ci/official-build.yml +++ b/eng/ci/official-build.yml @@ -2,8 +2,8 @@ trigger: batch: true branches: include: - - dev - - release/* + - dev-* + - library-release/* # CI only, does not trigger on PRs. pr: none @@ -49,30 +49,7 @@ extends: - stage: Build jobs: - template: /eng/templates/official/jobs/build-artifacts.yml@self - - - stage: RunE2ETests - dependsOn: Build - jobs: - - template: /eng/templates/official/jobs/ci-e2e-tests.yml@self - - stage: RunEmulatorTests - dependsOn: Build - jobs: - - template: /eng/templates/jobs/ci-emulator-tests.yml@self - parameters: - PoolName: 1es-pool-azfunc - stage: RunUnitTests dependsOn: Build jobs: - template: /eng/templates/jobs/ci-unit-tests.yml@self - - stage: RunDockerConsumptionTests - dependsOn: Build - jobs: - - template: /eng/templates/official/jobs/ci-docker-consumption-tests.yml@self - - stage: RunDockerDedicatedTests - dependsOn: Build - jobs: - - template: /eng/templates/official/jobs/ci-docker-dedicated-tests.yml@self - - stage: RunLinuxConsumptionTests - dependsOn: Build - jobs: - - template: /eng/templates/official/jobs/ci-lc-tests.yml@self diff --git a/eng/ci/package-worker.yml b/eng/ci/package-worker.yml new file mode 100644 index 000000000..27587a55e --- /dev/null +++ b/eng/ci/package-worker.yml @@ -0,0 +1,37 @@ +trigger: + branches: + exclude: + - '*' # Don't trigger this pipeline automatically + +# CI only, does not trigger on PRs. +pr: none + +# Does not run on a schedule + +resources: + repositories: + - repository: 1es + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/tags/release + - repository: eng + type: git + name: engineering + ref: refs/tags/release + +extends: + template: v1/1ES.Official.PipelineTemplate.yml@1es + parameters: + pool: + name: 1es-pool-azfunc + image: 1es-windows-2022 + os: windows + sdl: + codeSignValidation: + enabled: true + break: true + + stages: + - stage: AggregatePackages + jobs: + - template: /eng/templates/official/jobs/aggregate-artifacts.yml@self diff --git a/eng/ci/public-build.yml b/eng/ci/public-build.yml index a8456b721..26f1b6625 100644 --- a/eng/ci/public-build.yml +++ b/eng/ci/public-build.yml @@ -28,6 +28,14 @@ resources: variables: - template: /eng/templates/utils/variables.yml@self + - name: codeql.excludePathPatterns + value: deps/,build/ + - name: codeql.compiled.enabled + value: true + - name: codeql.runSourceLanguagesInSourceAnalysis + value: true + - name: codeql.sourceLanguages + value: python, powershell extends: template: v1/1ES.Unofficial.PipelineTemplate.yml@1es @@ -36,18 +44,8 @@ extends: name: 1es-pool-azfunc-public image: 1es-windows-2022 os: windows - - sdl: - codeql: - compiled: - enabled: true # still only runs for default branch - sourceLanguages: python, powershell - excludePathPatterns: deps/,build/ - runSourceLanguagesInSourceAnalysis: true - settings: skipBuildTagsForGitHubPullRequests: ${{ variables['System.PullRequest.IsFork'] }} - stages: - stage: Build jobs: diff --git a/eng/scripts/install-dependencies.sh b/eng/scripts/install-dependencies.sh deleted file mode 100644 index d1b953642..000000000 --- a/eng/scripts/install-dependencies.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -python -m pip install --upgrade pip -python -m pip install -U azure-functions --pre -python -m pip install -U -e .[dev] - -if [[ $1 != "3.7" ]]; then - python -m pip install --pre -U -e .[test-http-v2] -fi -if [[ $1 != "3.7" && $1 != "3.8" ]]; then - python -m pip install --pre -U -e .[test-deferred-bindings] -fi \ No newline at end of file diff --git a/eng/scripts/test-extensions.sh b/eng/scripts/test-extensions.sh deleted file mode 100644 index 7166dc8e4..000000000 --- a/eng/scripts/test-extensions.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -python -m pip install --upgrade pip -if [[ $2 != "3.7" ]]; then - python -m pip install -e $1/PythonExtensionArtifact - python -m pip install --pre -e .[test-http-v2] -fi -if [[ $2 != "3.7" && $2 != "3.8" ]]; then - python -m pip install -e $1/PythonExtensionArtifact - python -m pip install --pre -U -e .[test-deferred-bindings] -fi - -python -m pip install -U -e .[dev] \ No newline at end of file diff --git a/eng/scripts/test-sdk.sh b/eng/scripts/test-sdk.sh deleted file mode 100644 index 649a58a2c..000000000 --- a/eng/scripts/test-sdk.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -python -m pip install --upgrade pip -python -m pip install -e $1/PythonSdkArtifact -python -m pip install -e .[dev] - -if [[ $2 != "3.7" ]]; then - python -m pip install --pre -U -e .[test-http-v2] -fi -if [[ $2 != "3.7" && $2 != "3.8" ]]; then - python -m pip install --pre -U -e .[test-deferred-bindings] -fi \ No newline at end of file diff --git a/eng/scripts/test-setup.sh b/eng/scripts/test-setup.sh deleted file mode 100644 index d062021dc..000000000 --- a/eng/scripts/test-setup.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -cd tests -python -m invoke -c test_setup build-protos -python -m invoke -c test_setup webhost --branch-name=dev -python -m invoke -c test_setup extensions \ No newline at end of file diff --git a/eng/templates/jobs/build.yml b/eng/templates/jobs/build.yml index dd422f4fa..0efdc13b1 100644 --- a/eng/templates/jobs/build.yml +++ b/eng/templates/jobs/build.yml @@ -10,18 +10,14 @@ jobs: steps: - task: UsePythonVersion@0 inputs: - versionSpec: "3.11" + versionSpec: "3.13" - bash: | python --version displayName: 'Check python version' - bash: | - python -m venv .env - .env\Scripts\Activate.ps1 python -m pip install --upgrade pip python -m pip install . displayName: 'Build python worker' - # Skip the build stage for SDK and Extensions release branches. This stage will fail because pyproject.toml contains the updated (and unreleased) library version - condition: and(eq(variables.isSdkRelease, false), eq(variables.isExtensionsRelease, false), eq(variables['USETESTPYTHONSDK'], false), eq(variables['USETESTPYTHONEXTENSIONS'], false)) - bash: | pip install pip-audit pip-audit -r requirements.txt diff --git a/eng/templates/jobs/ci-emulator-tests.yml b/eng/templates/jobs/ci-emulator-tests.yml deleted file mode 100644 index d2ab3ce87..000000000 --- a/eng/templates/jobs/ci-emulator-tests.yml +++ /dev/null @@ -1,100 +0,0 @@ -jobs: - - job: "TestPython" - displayName: "Run Python Emulator Tests" - - pool: - name: ${{ parameters.PoolName }} - image: 1es-ubuntu-22.04 - os: linux - - strategy: - matrix: - Python37: - PYTHON_VERSION: '3.7' - Python38: - PYTHON_VERSION: '3.8' - Python39: - PYTHON_VERSION: '3.9' - Python310: - PYTHON_VERSION: '3.10' - Python311: - PYTHON_VERSION: '3.11' - Python312: - PYTHON_VERSION: '3.12' - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: $(PYTHON_VERSION) - - task: UseDotNet@2 - displayName: 'Install .NET 8' - inputs: - version: 8.0.x - - bash: | - chmod +x eng/scripts/install-dependencies.sh - chmod +x eng/scripts/test-setup.sh - - eng/scripts/install-dependencies.sh $(PYTHON_VERSION) - eng/scripts/test-setup.sh - displayName: 'Install dependencies and the worker' - condition: and(eq(variables.isSdkRelease, false), eq(variables.isExtensionsRelease, false), eq(variables['USETESTPYTHONSDK'], false), eq(variables['USETESTPYTHONEXTENSIONS'], false)) - - task: DownloadPipelineArtifact@2 - displayName: 'Download Python SDK Artifact' - inputs: - buildType: specific - artifactName: 'azure-functions' - project: 'internal' - definition: 679 - buildVersionToDownload: latest - targetPath: '$(Pipeline.Workspace)/PythonSdkArtifact' - condition: or(eq(variables.isSdkRelease, true), eq(variables['USETESTPYTHONSDK'], true)) - - bash: | - chmod +x eng/scripts/test-sdk.sh - chmod +x eng/scripts/test-setup.sh - - eng/scripts/test-sdk.sh $(Pipeline.Workspace) $(PYTHON_VERSION) - eng/scripts/test-setup.sh - displayName: 'Install test python sdk, dependencies and the worker' - condition: or(eq(variables.isSdkRelease, true), eq(variables['USETESTPYTHONSDK'], true)) - - task: DownloadPipelineArtifact@2 - displayName: 'Download Python Extension Artifact' - inputs: - buildType: specific - artifactName: $(PYTHONEXTENSIONNAME) - project: 'internal' - definition: 798 - buildVersionToDownload: latest - targetPath: '$(Pipeline.Workspace)/PythonExtensionArtifact' - condition: or(eq(variables.isExtensionsRelease, true), eq(variables['USETESTPYTHONEXTENSIONS'], true)) - - bash: | - chmod +x eng/scripts/test-setup.sh - chmod +x eng/scripts/test-extensions.sh - - eng/scripts/test-extensions.sh $(Pipeline.Workspace) $(PYTHON_VERSION) - eng/scripts/test-setup.sh - displayName: 'Install test python extension, dependencies and the worker' - condition: or(eq(variables.isExtensionsRelease, true), eq(variables['USETESTPYTHONEXTENSIONS'], true)) - - bash: | - docker compose -f tests/emulator_tests/utils/eventhub/docker-compose.yml pull - docker compose -f tests/emulator_tests/utils/eventhub/docker-compose.yml up -d - displayName: 'Install Azurite and Start EventHub Emulator' - - bash: | - python -m pytest -q -n auto --dist loadfile --reruns 4 --ignore=tests/emulator_tests/test_servicebus_functions.py tests/emulator_tests - env: - AzureWebJobsStorage: "UseDevelopmentStorage=true" - AzureWebJobsEventHubConnectionString: "Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;" - displayName: "Running $(PYTHON_VERSION) Python Linux Emulator Tests" - - bash: | - # Stop and remove EventHub Emulator container to free up the port - docker stop eventhubs-emulator - docker container rm --force eventhubs-emulator - docker compose -f tests/emulator_tests/utils/servicebus/docker-compose.yml pull - docker compose -f tests/emulator_tests/utils/servicebus/docker-compose.yml up -d - env: - AzureWebJobsSQLPassword: $(AzureWebJobsSQLPassword) - displayName: 'Install Azurite and Start ServiceBus Emulator' - - bash: | - python -m pytest -q -n auto --dist loadfile --reruns 4 tests/emulator_tests/test_servicebus_functions.py - env: - AzureWebJobsStorage: "UseDevelopmentStorage=true" - AzureWebJobsServiceBusConnectionString: "Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;" - displayName: "Running $(PYTHON_VERSION) Python ServiceBus Linux Emulator Tests" diff --git a/eng/templates/jobs/ci-unit-tests.yml b/eng/templates/jobs/ci-unit-tests.yml index 5ff54888c..00ea36731 100644 --- a/eng/templates/jobs/ci-unit-tests.yml +++ b/eng/templates/jobs/ci-unit-tests.yml @@ -4,18 +4,8 @@ jobs: strategy: matrix: - Python37: - PYTHON_VERSION: '3.7' - Python38: - PYTHON_VERSION: '3.8' - Python39: - PYTHON_VERSION: '3.9' - Python310: - PYTHON_VERSION: '3.10' - Python311: - PYTHON_VERSION: '3.11' - Python312: - PYTHON_VERSION: '3.12' + Python313: + PYTHON_VERSION: '3.13' steps: - task: UsePythonVersion@0 @@ -25,17 +15,6 @@ jobs: displayName: 'Install .NET 8' inputs: version: 8.0.x - - bash: | - chmod +x eng/scripts/install-dependencies.sh - chmod +x eng/scripts/test-setup.sh - - eng/scripts/install-dependencies.sh $(PYTHON_VERSION) - eng/scripts/test-setup.sh - displayName: 'Install dependencies' - condition: and(eq(variables.isSdkRelease, false), eq(variables.isExtensionsRelease, false), eq(variables['USETESTPYTHONSDK'], false), eq(variables['USETESTPYTHONEXTENSIONS'], false)) - bash: | python -m pytest -q -n auto --dist loadfile --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch tests/unittests displayName: "Running $(PYTHON_VERSION) Unit Tests" - # Skip running tests for SDK and Extensions release branches. Public pipeline doesn't have permissions to download artifact. - condition: and(eq(variables.isSdkRelease, false), eq(variables.isExtensionsRelease, false), eq(variables['USETESTPYTHONSDK'], false), eq(variables['USETESTPYTHONEXTENSIONS'], false)) - \ No newline at end of file diff --git a/eng/templates/official/jobs/aggregate-artifacts.yml b/eng/templates/official/jobs/aggregate-artifacts.yml new file mode 100644 index 000000000..ff665e899 --- /dev/null +++ b/eng/templates/official/jobs/aggregate-artifacts.yml @@ -0,0 +1,31 @@ +jobs: + - job: "Aggregate" + displayName: 'Aggregate Python Library Workers' + + pool: + name: 1es-pool-azfunc + image: 1es-ubuntu-22.04 + os: linux + + steps: + - task: DownloadBuildArtifacts@0 + inputs: + buildType: 'specific' + project: $(System.TeamProject) + pipeline: 652 # official build pipeline ID + buildVersionToDownload: 'specific' + specificBuildId: $(BuildId1) # exact build ID + downloadPath: $(Build.ArtifactStagingDirectory) + displayName: 'Download Artifacts from Specific Build' + + - script: | + # Assuming each branch build produced a wheel or tar.gz, merge them. + # This can depend on your packaging logic. + mkdir -p $(Build.ArtifactStagingDirectory)/final_package + cp $(Build.ArtifactStagingDirectory)/dev-*/dist/* $(Build.ArtifactStagingDirectory)/dist/ + displayName: 'Merge and Compile Python Packages' + + - publish: $(Build.ArtifactStagingDirectory)/dist + artifact: azure-functions-worker + displayName: 'Publish Final Python Package' + diff --git a/eng/templates/official/jobs/build-artifacts.yml b/eng/templates/official/jobs/build-artifacts.yml index 631115b4c..97c70daa1 100644 --- a/eng/templates/official/jobs/build-artifacts.yml +++ b/eng/templates/official/jobs/build-artifacts.yml @@ -1,242 +1,48 @@ jobs: -- job: Build_WINDOWS_X64 - pool: - name: 1es-pool-azfunc - image: 1es-windows-2022 - os: windows - strategy: - matrix: - Python37V4: - pythonVersion: '3.7' - workerPath: 'python/prodV4/worker.py' - Python38V4: - pythonVersion: '3.8' - workerPath: 'python/prodV4/worker.py' - Python39V4: - pythonVersion: '3.9' - workerPath: 'python/prodV4/worker.py' - Python310V4: - pythonVersion: '3.10' - workerPath: 'python/prodV4/worker.py' - Python311V4: - pythonVersion: '3.11' - workerPath: 'python/prodV4/worker.py' - Python312V4: - pythonVersion: '3.12' - workerPath: 'python/prodV4/worker.py' - templateContext: - outputParentDirectory: $(Build.ArtifactStagingDirectory) - outputs: - - output: pipelineArtifact - targetPath: $(Build.ArtifactStagingDirectory) - artifactName: "$(pythonVersion)_WINDOWS_X64" - steps: - - template: ../../../../pack/templates/win_env_gen.yml - parameters: - pythonVersion: '$(pythonVersion)' - workerPath: '$(workerPath)' - architecture: 'x64' - artifactName: '$(pythonVersion)_WINDOWS_X64' -- job: Build_WINDOWS_X86 - pool: - name: 1es-pool-azfunc - image: 1es-windows-2022 - os: windows - strategy: - matrix: - Python37V4: - pythonVersion: '3.7' - workerPath: 'python/prodV4/worker.py' - Python38V4: - pythonVersion: '3.8' - workerPath: 'python/prodV4/worker.py' - Python39V4: - pythonVersion: '3.9' - workerPath: 'python/prodV4/worker.py' - Python310V4: - pythonVersion: '3.10' - workerPath: 'python/prodV4/worker.py' - Python311V4: - pythonVersion: '3.11' - workerPath: 'python/prodV4/worker.py' - Python312V4: - pythonVersion: '3.12' - workerPath: 'python/prodV4/worker.py' - templateContext: - outputParentDirectory: $(Build.ArtifactStagingDirectory) - outputs: - - output: pipelineArtifact - targetPath: $(Build.ArtifactStagingDirectory) - artifactName: "$(pythonVersion)_WINDOWS_X86" - steps: - - template: ../../../../pack/templates/win_env_gen.yml - parameters: - pythonVersion: '$(pythonVersion)' - workerPath: '$(workerPath)' - architecture: 'x86' - artifactName: '$(pythonVersion)_WINDOWS_x86' -- job: Build_LINUX_X64 - pool: - name: 1es-pool-azfunc - image: 1es-ubuntu-22.04 - os: linux - strategy: - matrix: - Python37V4: - pythonVersion: '3.7' - workerPath: 'python/prodV4/worker.py' - Python38V4: - pythonVersion: '3.8' - workerPath: 'python/prodV4/worker.py' - Python39V4: - pythonVersion: '3.9' - workerPath: 'python/prodV4/worker.py' - Python310V4: - pythonVersion: '3.10' - workerPath: 'python/prodV4/worker.py' - Python311V4: - pythonVersion: '3.11' - workerPath: 'python/prodV4/worker.py' - Python312V4: - pythonVersion: '3.12' - workerPath: 'python/prodV4/worker.py' - templateContext: - outputParentDirectory: $(Build.ArtifactStagingDirectory) - outputs: - - output: pipelineArtifact - targetPath: $(Build.ArtifactStagingDirectory) - artifactName: "$(pythonVersion)_LINUX_X64" - steps: - - template: ../../../../pack/templates/nix_env_gen.yml - parameters: - pythonVersion: '$(pythonVersion)' - workerPath: '$(workerPath)' - artifactName: '$(pythonVersion)_LINUX_X64' -- job: Build_OSX_X64 - pool: - name: Azure Pipelines - image: macOS-latest - os: macOS - strategy: - matrix: - Python37V4: - pythonVersion: '3.7' - workerPath: 'python/prodV4/worker.py' - Python38V4: - pythonVersion: '3.8' - workerPath: 'python/prodV4/worker.py' - Python39V4: - pythonVersion: '3.9' - workerPath: 'python/prodV4/worker.py' - Python310V4: - pythonVersion: '3.10' - workerPath: 'python/prodV4/worker.py' - Python311V4: - pythonVersion: '3.11' - workerPath: 'python/prodV4/worker.py' - Python312V4: - pythonVersion: '3.12' - workerPath: 'python/prodV4/worker.py' - templateContext: - outputParentDirectory: $(Build.ArtifactStagingDirectory) - outputs: - - output: pipelineArtifact - targetPath: $(Build.ArtifactStagingDirectory) - artifactName: "$(pythonVersion)_OSX_X64" - steps: - - template: ../../../../pack/templates/nix_env_gen.yml - parameters: - pythonVersion: '$(pythonVersion)' - workerPath: '$(workerPath)' - artifactName: '$(pythonVersion)_OSX_X64' -- job: Build_OSX_ARM64 - pool: - name: Azure Pipelines - image: macOS-latest - os: macOS - strategy: - matrix: - Python39V4: - pythonVersion: '3.9' - workerPath: 'python/prodV4/worker.py' - Python310V4: - pythonVersion: '3.10' - workerPath: 'python/prodV4/worker.py' - Python311V4: - pythonVersion: '3.11' - workerPath: 'python/prodV4/worker.py' - Python312V4: - pythonVersion: '3.12' - workerPath: 'python/prodV4/worker.py' - templateContext: - outputParentDirectory: $(Build.ArtifactStagingDirectory) - outputs: - - output: pipelineArtifact - targetPath: $(Build.ArtifactStagingDirectory) - artifactName: "$(pythonVersion)_OSX_ARM4" - steps: - - template: ../../../../pack/templates/macos_64_env_gen.yml - parameters: - pythonVersion: '$(pythonVersion)' - workerPath: '$(workerPath)' - artifactName: '$(pythonVersion)_OSX_ARM64' + - job: "Build" + displayName: 'Build Python Library Worker' -- job: PackageWorkers - dependsOn: ['Build_WINDOWS_X64', 'Build_WINDOWS_X86', 'Build_LINUX_X64', 'Build_OSX_X64', 'Build_OSX_ARM64'] - templateContext: + pool: + name: 1es-pool-azfunc + image: 1es-ubuntu-22.04 + os: linux + + variables: + # Extract the version number from the branch name + pythonVersion: $[substring(variables['Build.SourceBranchName'], 4, 2)] # Extract last two digits from dev-xxx + + templateContext: outputParentDirectory: $(Build.ArtifactStagingDirectory) outputs: - output: pipelineArtifact - targetPath: $(Build.ArtifactStagingDirectory) - artifactName: "PythonWorker" - steps: - - bash: | - echo "Releasing from $BUILD_SOURCEBRANCHNAME" - sudo apt-get install -y jq + targetPath: $(Build.SourcesDirectory) + artifactName: "azure-functions-worker" - if [[ $BUILD_SOURCEBRANCHNAME = 4\.* ]] - then - echo "Generating V4 Release Package for $BUILD_SOURCEBRANCHNAME" - NUSPEC="pack\Microsoft.Azure.Functions.V4.PythonWorker.nuspec" - WKVERSION="$BUILD_SOURCEBRANCHNAME" - elif [[ $BUILD_SOURCEBRANCHNAME = dev ]] - then - echo "Generating V4 Integration Test Package for $BUILD_SOURCEBRANCHNAME" - VERSION=$(cat azure_functions_worker/version.py | tail -1 | cut -d' ' -f3 | sed "s/'//g") - NUSPEC="pack\Microsoft.Azure.Functions.V4.PythonWorker.nuspec" - WKVERSION="$VERSION-$(Build.BuildNumber)" - else - # this is only to test nuget related workflow because we are setting nuspec here - echo "Generating Integration Test Package for $BUILD_SOURCEBRANCHNAME for testing purpose" - LATEST_TAG=$(curl https://api.github.com/repos/Azure/azure-functions-python-worker/tags -s | jq '.[0].name' | sed 's/\"//g' | cut -d'.' -f-2) - NUSPEC="pack\Microsoft.Azure.Functions.V4.PythonWorker.nuspec" - # Only required for Integration Test. Version number contains date (e.g. 3.1.2.20211028-dev) - WKVERSION="3.$LATEST_TAG-$(BUILD_BUILDID)-TEST" - echo "No Matching Release Tag For $BUILD_SOURCEBRANCH" - fi + steps: + - script: | + echo "Branch name: $(Build.SourceBranchName)" + # Extract the last two digits (minor version) from the branch name + version=$(echo $(Build.SourceBranchName) | sed 's/dev-\([0-9]*\)/\1/') + minor_version=${version: -2} # Get last two digits + echo "Extracted minor version: $minor_version" + echo "##vso[task.setvariable variable=pythonVersion]$minor_version" + displayName: 'Extract Python version from branch name' + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.$(pythonVersion)' + - bash: | + python --version + displayName: 'Check python version' + - bash: | + python -m pip install -U pip + python -m pip install build + python -m build + displayName: 'Build Python Library Worker' + - bash: | + pip install pip-audit + pip-audit . + displayName: 'Run vulnerability scan' + - publish: $(Build.ArtifactStagingDirectory) + artifact: build-artifact + displayName: 'Publish Build Artifact' - echo "##vso[task.setvariable variable=nuspec_path]$NUSPEC" - echo "##vso[task.setvariable variable=worker_version]$WKVERSION" - displayName: "Generate Worker NuGet Package for Release $BUILD_SOURCEBRANCHNAME" - - task: DownloadPipelineArtifact@2 - inputs: - buildType: 'current' - targetPath: '$(Build.SourcesDirectory)' - - task: ManifestGeneratorTask@0 - displayName: 'SBOM Generation Task' - inputs: - BuildDropPath: '$(Build.ArtifactStagingDirectory)' - BuildComponentPath: '$(Build.SourcesDirectory)' - Verbosity: 'Verbose' - - task: CopyFiles@2 - inputs: - SourceFolder: '$(Build.ArtifactStagingDirectory)' - Contents: '**' - TargetFolder: '$(Build.SourcesDirectory)' - - task: NuGetCommand@2 - inputs: - command: pack - packagesToPack: '$(nuspec_path)' - packDestination: $(Build.ArtifactStagingDirectory) - versioningScheme: 'byEnvVar' - versionEnvVar: WORKER_VERSION diff --git a/eng/templates/official/jobs/ci-core-tools-tests.yml b/eng/templates/official/jobs/ci-core-tools-tests.yml deleted file mode 100644 index 3e8a9b622..000000000 --- a/eng/templates/official/jobs/ci-core-tools-tests.yml +++ /dev/null @@ -1,35 +0,0 @@ -jobs: - - job: "TestPython" - displayName: "Run Python Core Tools E2E Tests" - - pool: - name: 1es-pool-azfunc - image: 1es-ubuntu-22.04 - os: linux - - steps: - - task: UsePythonVersion@0 - displayName: 'Install Python' - inputs: - versionSpec: "3.10" - addToPath: true - - task: UseDotNet@2 - displayName: 'Install DotNet 3' - inputs: - packageType: 'sdk' - version: "3.1.x" - - task: UseDotNet@2 - displayName: 'Install DotNet 6' - inputs: - packageType: 'sdk' - version: "6.x" - - pwsh: '$(Build.SourcesDirectory)/.ci/e2e_integration_test/start-e2e.ps1' - env: - AzureWebJobsStorage: $(LinuxStorageConnectionString311) - AzureWebJobsCosmosDBConnectionString: $(LinuxCosmosDBConnectionString311) - AzureWebJobsEventHubConnectionString: $(LinuxEventHubConnectionString311) - AzureWebJobsServiceBusConnectionString: $(LinuxServiceBusConnectionString311) - AzureWebJobsSqlConnectionString: $(LinuxSqlConnectionString311) - AzureWebJobsEventGridTopicUri: $(LinuxEventGridTopicUriString311) - AzureWebJobsEventGridConnectionKey: $(LinuxEventGridConnectionKeyString311) - displayName: 'Running Python Language Worker E2E Tests' diff --git a/eng/templates/official/jobs/ci-custom-image-tests.yml b/eng/templates/official/jobs/ci-custom-image-tests.yml deleted file mode 100644 index cb08f8a5b..000000000 --- a/eng/templates/official/jobs/ci-custom-image-tests.yml +++ /dev/null @@ -1,34 +0,0 @@ -jobs: - - job: "TestPython" - displayName: "Run Python Docker Custom Tests" - - pool: - name: 1es-pool-azfunc - image: 1es-ubuntu-22.04 - os: linux - - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: $(CUSTOM_PYTHON_VERSION) - - bash: | - chmod +x eng/scripts/install-dependencies.sh - - eng/scripts/install-dependencies.sh $(PYTHON_VERSION) - cd tests - python -m invoke -c test_setup build-protos - displayName: 'Install dependencies' - - bash: | - python -m pytest --reruns 4 -vv --instafail tests/endtoend tests/extension_tests/deferred_bindings_tests tests/extension_tests/http_v2_tests - env: - DEDICATED_DOCKER_TEST: $(CUSTOM_DED_IMAGE) - CONSUMPTION_DOCKER_TEST: $(CUSTOM_CON_IMAGE) - IMAGE_NAME: $(CUSTOM_IMAGE_NAME) - AzureWebJobsStorage: $(LinuxStorageConnectionString311) - AzureWebJobsCosmosDBConnectionString: $(LinuxCosmosDBConnectionString311) - AzureWebJobsEventHubConnectionString: $(LinuxEventHubConnectionString311) - AzureWebJobsServiceBusConnectionString: $(LinuxServiceBusConnectionString311) - AzureWebJobsSqlConnectionString: $(LinuxSqlConnectionString311) - AzureWebJobsEventGridTopicUri: $(LinuxEventGridTopicUriString311) - AzureWebJobsEventGridConnectionKey: $(LinuxEventGridConnectionKeyString311) - displayName: "Running Python DockerCustom tests" \ No newline at end of file diff --git a/eng/templates/official/jobs/ci-docker-consumption-tests.yml b/eng/templates/official/jobs/ci-docker-consumption-tests.yml deleted file mode 100644 index f1a4a23f1..000000000 --- a/eng/templates/official/jobs/ci-docker-consumption-tests.yml +++ /dev/null @@ -1,71 +0,0 @@ -jobs: - - job: "TestPython" - displayName: "Run Python Docker Consumption Tests" - - pool: - name: 1es-pool-azfunc - image: 1es-ubuntu-22.04 - os: linux - - strategy: - matrix: - Python38: - PYTHON_VERSION: '3.8' - STORAGE_CONNECTION: $(LinuxStorageConnectionString38) - COSMOSDB_CONNECTION: $(LinuxCosmosDBConnectionString38) - EVENTHUB_CONNECTION: $(LinuxEventHubConnectionString38) - SERVICEBUS_CONNECTION: $(LinuxServiceBusConnectionString38) - SQL_CONNECTION: $(LinuxSqlConnectionString38) - EVENTGRID_URI: $(LinuxEventGridTopicUriString38) - EVENTGRID_CONNECTION: $(LinuxEventGridConnectionKeyString38) - Python39: - PYTHON_VERSION: '3.9' - STORAGE_CONNECTION: $(LinuxStorageConnectionString39) - COSMOSDB_CONNECTION: $(LinuxCosmosDBConnectionString39) - EVENTHUB_CONNECTION: $(LinuxEventHubConnectionString39) - SERVICEBUS_CONNECTION: $(LinuxServiceBusConnectionString39) - SQL_CONNECTION: $(LinuxSqlConnectionString39) - EVENTGRID_URI: $(LinuxEventGridTopicUriString39) - EVENTGRID_CONNECTION: $(LinuxEventGridConnectionKeyString39) - Python310: - PYTHON_VERSION: '3.10' - STORAGE_CONNECTION: $(LinuxStorageConnectionString310) - COSMOSDB_CONNECTION: $(LinuxCosmosDBConnectionString310) - EVENTHUB_CONNECTION: $(LinuxEventHubConnectionString310) - SERVICEBUS_CONNECTION: $(LinuxServiceBusConnectionString310) - SQL_CONNECTION: $(LinuxSqlConnectionString310) - EVENTGRID_URI: $(LinuxEventGridTopicUriString310) - EVENTGRID_CONNECTION: $(LinuxEventGridConnectionKeyString310) - Python311: - PYTHON_VERSION: '3.11' - STORAGE_CONNECTION: $(LinuxStorageConnectionString311) - COSMOSDB_CONNECTION: $(LinuxCosmosDBConnectionString311) - EVENTHUB_CONNECTION: $(LinuxEventHubConnectionString311) - SERVICEBUS_CONNECTION: $(LinuxServiceBusConnectionString311) - SQL_CONNECTION: $(LinuxSqlConnectionString311) - EVENTGRID_URI: $(LinuxEventGridTopicUriString311) - EVENTGRID_CONNECTION: $(LinuxEventGridConnectionKeyString311) - - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: $(PYTHON_VERSION) - - bash: | - chmod +x eng/scripts/install-dependencies.sh - - eng/scripts/install-dependencies.sh $(PYTHON_VERSION) - cd tests - python -m invoke -c test_setup build-protos - displayName: 'Install dependencies' - - bash: | - python -m pytest --reruns 4 -vv --instafail tests/endtoend tests/extension_tests/deferred_bindings_tests tests/extension_tests/http_v2_tests - env: - CONSUMPTION_DOCKER_TEST: "true" - AzureWebJobsStorage: $(STORAGE_CONNECTION) - AzureWebJobsCosmosDBConnectionString: $(COSMOSDB_CONNECTION) - AzureWebJobsEventHubConnectionString: $(EVENTHUB_CONNECTION) - AzureWebJobsServiceBusConnectionString: $(SERVICEBUS_CONNECTION) - AzureWebJobsSqlConnectionString: $(SQL_CONNECTION) - AzureWebJobsEventGridTopicUri: $(EVENTGRID_URI) - AzureWebJobsEventGridConnectionKey: $(EVENTGRID_CONNECTION) - displayName: "Running $(PYTHON_VERSION) Docker Consumption tests" \ No newline at end of file diff --git a/eng/templates/official/jobs/ci-docker-dedicated-tests.yml b/eng/templates/official/jobs/ci-docker-dedicated-tests.yml deleted file mode 100644 index b76b1f883..000000000 --- a/eng/templates/official/jobs/ci-docker-dedicated-tests.yml +++ /dev/null @@ -1,71 +0,0 @@ -jobs: - - job: "TestPython" - displayName: "Run Python Docker Dedicated Tests" - - pool: - name: 1es-pool-azfunc - image: 1es-ubuntu-22.04 - os: linux - - strategy: - matrix: - Python38: - PYTHON_VERSION: '3.8' - STORAGE_CONNECTION: $(LinuxStorageConnectionString38) - COSMOSDB_CONNECTION: $(LinuxCosmosDBConnectionString38) - EVENTHUB_CONNECTION: $(LinuxEventHubConnectionString38) - SERVICEBUS_CONNECTION: $(LinuxServiceBusConnectionString38) - SQL_CONNECTION: $(LinuxSqlConnectionString38) - EVENTGRID_URI: $(LinuxEventGridTopicUriString38) - EVENTGRID_CONNECTION: $(LinuxEventGridConnectionKeyString38) - Python39: - PYTHON_VERSION: '3.9' - STORAGE_CONNECTION: $(LinuxStorageConnectionString39) - COSMOSDB_CONNECTION: $(LinuxCosmosDBConnectionString39) - EVENTHUB_CONNECTION: $(LinuxEventHubConnectionString39) - SERVICEBUS_CONNECTION: $(LinuxServiceBusConnectionString39) - SQL_CONNECTION: $(LinuxSqlConnectionString39) - EVENTGRID_URI: $(LinuxEventGridTopicUriString39) - EVENTGRID_CONNECTION: $(LinuxEventGridConnectionKeyString39) - Python310: - PYTHON_VERSION: '3.10' - STORAGE_CONNECTION: $(LinuxStorageConnectionString310) - COSMOSDB_CONNECTION: $(LinuxCosmosDBConnectionString310) - EVENTHUB_CONNECTION: $(LinuxEventHubConnectionString310) - SERVICEBUS_CONNECTION: $(LinuxServiceBusConnectionString310) - SQL_CONNECTION: $(LinuxSqlConnectionString310) - EVENTGRID_URI: $(LinuxEventGridTopicUriString310) - EVENTGRID_CONNECTION: $(LinuxEventGridConnectionKeyString310) - Python311: - PYTHON_VERSION: '3.11' - STORAGE_CONNECTION: $(LinuxStorageConnectionString311) - COSMOSDB_CONNECTION: $(LinuxCosmosDBConnectionString311) - EVENTHUB_CONNECTION: $(LinuxEventHubConnectionString311) - SERVICEBUS_CONNECTION: $(LinuxServiceBusConnectionString311) - SQL_CONNECTION: $(LinuxSqlConnectionString311) - EVENTGRID_URI: $(LinuxEventGridTopicUriString311) - EVENTGRID_CONNECTION: $(LinuxEventGridConnectionKeyString311) - - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: $(PYTHON_VERSION) - - bash: | - chmod +x eng/scripts/install-dependencies.sh - - eng/scripts/install-dependencies.sh $(PYTHON_VERSION) - cd tests - python -m invoke -c test_setup build-protos - displayName: 'Install dependencies' - - bash: | - python -m pytest --reruns 4 -vv --instafail tests/endtoend tests/extension_tests/deferred_bindings_tests tests/extension_tests/http_v2_tests - env: - DEDICATED_DOCKER_TEST: "true" - AzureWebJobsStorage: $(STORAGE_CONNECTION) - AzureWebJobsCosmosDBConnectionString: $(COSMOSDB_CONNECTION) - AzureWebJobsEventHubConnectionString: $(EVENTHUB_CONNECTION) - AzureWebJobsServiceBusConnectionString: $(SERVICEBUS_CONNECTION) - AzureWebJobsSqlConnectionString: $(SQL_CONNECTION) - AzureWebJobsEventGridTopicUri: $(EVENTGRID_URI) - AzureWebJobsEventGridConnectionKey: $(EVENTGRID_CONNECTION) - displayName: "Running $(PYTHON_VERSION) Docker Dedicated tests" \ No newline at end of file diff --git a/eng/templates/official/jobs/ci-e2e-tests.yml b/eng/templates/official/jobs/ci-e2e-tests.yml deleted file mode 100644 index b3ff4c57d..000000000 --- a/eng/templates/official/jobs/ci-e2e-tests.yml +++ /dev/null @@ -1,148 +0,0 @@ -jobs: - - job: "TestPython" - displayName: "Run Python E2E Tests" - - pool: - name: 1es-pool-azfunc - image: 1es-ubuntu-22.04 - os: linux - - strategy: - matrix: - Python37: - PYTHON_VERSION: '3.7' - STORAGE_CONNECTION: $(LinuxStorageConnectionString37) - COSMOSDB_CONNECTION: $(LinuxCosmosDBConnectionString37) - EVENTHUB_CONNECTION: $(LinuxEventHubConnectionString37) - SERVICEBUS_CONNECTION: $(LinuxServiceBusConnectionString37) - SQL_CONNECTION: $(LinuxSqlConnectionString37) - EVENTGRID_URI: $(LinuxEventGridTopicUriString37) - EVENTGRID_CONNECTION: $(LinuxEventGridConnectionKeyString37) - Python38: - PYTHON_VERSION: '3.8' - STORAGE_CONNECTION: $(LinuxStorageConnectionString38) - COSMOSDB_CONNECTION: $(LinuxCosmosDBConnectionString38) - EVENTHUB_CONNECTION: $(LinuxEventHubConnectionString38) - SERVICEBUS_CONNECTION: $(LinuxServiceBusConnectionString38) - SQL_CONNECTION: $(LinuxSqlConnectionString38) - EVENTGRID_URI: $(LinuxEventGridTopicUriString38) - EVENTGRID_CONNECTION: $(LinuxEventGridConnectionKeyString38) - Python39: - PYTHON_VERSION: '3.9' - STORAGE_CONNECTION: $(LinuxStorageConnectionString39) - COSMOSDB_CONNECTION: $(LinuxCosmosDBConnectionString39) - EVENTHUB_CONNECTION: $(LinuxEventHubConnectionString39) - SERVICEBUS_CONNECTION: $(LinuxServiceBusConnectionString39) - SQL_CONNECTION: $(LinuxSqlConnectionString39) - EVENTGRID_URI: $(LinuxEventGridTopicUriString39) - EVENTGRID_CONNECTION: $(LinuxEventGridConnectionKeyString39) - Python310: - PYTHON_VERSION: '3.10' - STORAGE_CONNECTION: $(LinuxStorageConnectionString310) - COSMOSDB_CONNECTION: $(LinuxCosmosDBConnectionString310) - EVENTHUB_CONNECTION: $(LinuxEventHubConnectionString310) - SERVICEBUS_CONNECTION: $(LinuxServiceBusConnectionString310) - SQL_CONNECTION: $(LinuxSqlConnectionString310) - EVENTGRID_URI: $(LinuxEventGridTopicUriString310) - EVENTGRID_CONNECTION: $(LinuxEventGridConnectionKeyString310) - Python311: - PYTHON_VERSION: '3.11' - STORAGE_CONNECTION: $(LinuxStorageConnectionString311) - COSMOSDB_CONNECTION: $(LinuxCosmosDBConnectionString311) - EVENTHUB_CONNECTION: $(LinuxEventHubConnectionString311) - SERVICEBUS_CONNECTION: $(LinuxServiceBusConnectionString311) - SQL_CONNECTION: $(LinuxSqlConnectionString311) - EVENTGRID_URI: $(LinuxEventGridTopicUriString311) - EVENTGRID_CONNECTION: $(LinuxEventGridConnectionKeyString311) - Python312: - PYTHON_VERSION: '3.12' - STORAGE_CONNECTION: $(LinuxStorageConnectionString312) - COSMOSDB_CONNECTION: $(LinuxCosmosDBConnectionString312) - EVENTHUB_CONNECTION: $(LinuxEventHubConnectionString312) - SERVICEBUS_CONNECTION: $(LinuxServiceBusConnectionString312) - SQL_CONNECTION: $(LinuxSqlConnectionString312) - EVENTGRID_URI: $(LinuxEventGridTopicUriString312) - EVENTGRID_CONNECTION: $(LinuxEventGridConnectionKeyString312) - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: $(PYTHON_VERSION) - - task: UseDotNet@2 - displayName: 'Install .NET 8' - inputs: - version: 8.0.x - - bash: | - chmod +x eng/scripts/install-dependencies.sh - chmod +x eng/scripts/test-setup.sh - - eng/scripts/install-dependencies.sh $(PYTHON_VERSION) - eng/scripts/test-setup.sh - displayName: 'Install dependencies and the worker' - condition: and(eq(variables.isSdkRelease, false), eq(variables.isExtensionsRelease, false), eq(variables['USETESTPYTHONSDK'], false), eq(variables['USETESTPYTHONEXTENSIONS'], false)) - - task: DownloadPipelineArtifact@2 - displayName: 'Download Python SDK Artifact' - inputs: - buildType: specific - artifactName: 'azure-functions' - project: 'internal' - definition: 679 - buildVersionToDownload: latest - targetPath: '$(Pipeline.Workspace)/PythonSdkArtifact' - condition: or(eq(variables.isSdkRelease, true), eq(variables['USETESTPYTHONSDK'], true)) - - bash: | - chmod +x eng/scripts/test-sdk.sh - chmod +x eng/scripts/test-setup.sh - - eng/scripts/test-sdk.sh $(Pipeline.Workspace) $(PYTHON_VERSION) - eng/scripts/test-setup.sh - displayName: 'Install test python sdk, dependencies and the worker' - condition: or(eq(variables.isSdkRelease, true), eq(variables['USETESTPYTHONSDK'], true)) - - task: DownloadPipelineArtifact@2 - displayName: 'Download Python Extension Artifact' - inputs: - buildType: specific - artifactName: $(PYTHONEXTENSIONNAME) - project: 'internal' - definition: 798 - buildVersionToDownload: latest - targetPath: '$(Pipeline.Workspace)/PythonExtensionArtifact' - condition: or(eq(variables.isExtensionsRelease, true), eq(variables['USETESTPYTHONEXTENSIONS'], true)) - - bash: | - chmod +x eng/scripts/test-setup.sh - chmod +x eng/scripts/test-extensions.sh - - eng/scripts/test-extensions.sh $(Pipeline.Workspace) $(PYTHON_VERSION) - eng/scripts/test-setup.sh - displayName: 'Install test python extension, dependencies and the worker' - condition: or(eq(variables.isExtensionsRelease, true), eq(variables['USETESTPYTHONEXTENSIONS'], true)) - - powershell: | - $pipelineVarSet = "$(USETESTPYTHONSDK)" - Write-Host "pipelineVarSet: $pipelineVarSet" - $branch = "$(Build.SourceBranch)" - Write-Host "Branch: $branch" - if($branch.StartsWith("refs/heads/sdk/") -or $pipelineVarSet -eq "true") - { - Write-Host "##vso[task.setvariable variable=skipTest;]true" - } - else - { - Write-Host "##vso[task.setvariable variable=skipTest;]false" - } - displayName: 'Set skipTest variable' - condition: or(eq(variables.isSdkRelease, true), eq(variables['USETESTPYTHONSDK'], true)) - - powershell: | - Write-Host "skipTest: $(skipTest)" - displayName: 'Display skipTest variable' - condition: or(eq(variables.isSdkRelease, true), eq(variables['USETESTPYTHONSDK'], true)) - - bash: | - python -m pytest -q -n auto --dist loadfile --reruns 4 --cov=./azure_functions_worker --cov-report xml --cov-branch --cov-append tests/endtoend tests/extension_tests/deferred_bindings_tests tests/extension_tests/http_v2_tests - env: - AzureWebJobsStorage: $(STORAGE_CONNECTION) - AzureWebJobsCosmosDBConnectionString: $(COSMOSDB_CONNECTION) - AzureWebJobsEventHubConnectionString: $(EVENTHUB_CONNECTION) - AzureWebJobsServiceBusConnectionString: $(SERVICEBUS_CONNECTION) - AzureWebJobsSqlConnectionString: $(SQL_CONNECTION) - AzureWebJobsEventGridTopicUri: $(EVENTGRID_URI) - AzureWebJobsEventGridConnectionKey: $(EVENTGRID_CONNECTION) - skipTest: $(skipTest) - displayName: "Running $(PYTHON_VERSION) Python E2E Tests" diff --git a/eng/templates/official/jobs/ci-lc-tests.yml b/eng/templates/official/jobs/ci-lc-tests.yml deleted file mode 100644 index b5229d901..000000000 --- a/eng/templates/official/jobs/ci-lc-tests.yml +++ /dev/null @@ -1,42 +0,0 @@ -jobs: - - job: "TestPython" - displayName: "Run Python Linux Consumption Tests" - - pool: - name: 1es-pool-azfunc - image: 1es-ubuntu-22.04 - os: linux - - strategy: - matrix: - Python37: - PYTHON_VERSION: '3.7' - Python38: - PYTHON_VERSION: '3.8' - Python39: - PYTHON_VERSION: '3.9' - Python310: - PYTHON_VERSION: '3.10' - Python311: - PYTHON_VERSION: '3.11' - - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: $(PYTHON_VERSION) - - bash: | - python -m pip install --upgrade pip - python -m pip install -U -e .[dev] - - cd tests - python -m invoke -c test_setup build-protos - displayName: 'Install dependencies and the worker' - # Skip the installation stage for SDK and Extensions release branches. This stage will fail because pyproject.toml contains the updated (and unreleased) library version - condition: and(eq(variables.isSdkRelease, false), eq(variables.isExtensionsRelease, false), eq(variables['USETESTPYTHONSDK'], false), eq(variables['USETESTPYTHONEXTENSIONS'], false)) - - bash: | - python -m pytest -n auto --dist loadfile -vv --reruns 4 --instafail tests/consumption_tests - env: - AzureWebJobsStorage: $(LinuxStorageConnectionString312) - _DUMMY_CONT_KEY: $(_DUMMY_CONT_KEY) - displayName: "Running $(PYTHON_VERSION) Linux Consumption tests" - condition: and(eq(variables.isSdkRelease, false), eq(variables.isExtensionsRelease, false), eq(variables['USETESTPYTHONSDK'], false), eq(variables['USETESTPYTHONEXTENSIONS'], false)) \ No newline at end of file diff --git a/eng/templates/utils/official-variables.yml b/eng/templates/utils/official-variables.yml deleted file mode 100644 index 1654d7e6c..000000000 --- a/eng/templates/utils/official-variables.yml +++ /dev/null @@ -1,4 +0,0 @@ -variables: - - template: /ci/variables/build.yml@eng - - template: /ci/variables/cfs.yml@eng - - group: python-integration-resources \ No newline at end of file diff --git a/eng/templates/utils/variables.yml b/eng/templates/utils/variables.yml deleted file mode 100644 index 6361d2d19..000000000 --- a/eng/templates/utils/variables.yml +++ /dev/null @@ -1,5 +0,0 @@ -variables: - - name: isSdkRelease - value: $[startsWith(variables['Build.SourceBranch'], 'refs/heads/sdk/')] - - name: isExtensionsRelease - value: $[startsWith(variables['Build.SourceBranch'], 'refs/heads/extensions/')] \ No newline at end of file diff --git a/pack/Microsoft.Azure.Functions.PythonWorker.targets b/pack/Microsoft.Azure.Functions.PythonWorker.targets deleted file mode 100644 index 887dc8d52..000000000 --- a/pack/Microsoft.Azure.Functions.PythonWorker.targets +++ /dev/null @@ -1,43 +0,0 @@ - - - - <_PythonWorkerToolsDir>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)../tools')) - - - - <_PythonSupportedRuntime Include="win-x86" WorkerPath="WINDOWS/X86" /> - <_PythonSupportedRuntime Include="win-x64" WorkerPath="WINDOWS/X64" /> - <_PythonSupportedRuntime Include="linux-x64" WorkerPath="LINUX/X64" /> - <_PythonSupportedRuntime Include="osx-x64" WorkerPath="OSX/X64" /> - <_PythonSupportedRuntime Include="osx-arm64" WorkerPath="OSX/Arm64" /> - - - - - - - - - - - - <_PythonWorkerFiles Include="$(_PythonWorkerToolsDir)/**" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" /> - - - - - - - <_PythonWorkersRuntimeFolder>@(_PythonSupportedRuntime->WithMetadataValue('Identity', '$(RuntimeIdentifier)')->Metadata('WorkerPath')) - - - - <_PythonWorkerFiles Include="$(_PythonWorkerToolsDir)/*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" /> - <_PythonWorkerFiles Include="$(_PythonWorkerToolsDir)/**/$(_PythonWorkersRuntimeFolder)/**" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" /> - - - - - - - diff --git a/pack/Microsoft.Azure.Functions.V4.PythonWorker.nuspec b/pack/Microsoft.Azure.Functions.V4.PythonWorker.nuspec deleted file mode 100644 index b3ce47d0c..000000000 --- a/pack/Microsoft.Azure.Functions.V4.PythonWorker.nuspec +++ /dev/null @@ -1,45 +0,0 @@ - - - - Microsoft.Azure.Functions.PythonWorker - 1.1.0 - Microsoft - Microsoft - false - Microsoft Azure Functions Python Worker - © .NET Foundation. All rights reserved. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pack/scripts/mac_arm64_deps.sh b/pack/scripts/mac_arm64_deps.sh deleted file mode 100644 index 2d70bafad..000000000 --- a/pack/scripts/mac_arm64_deps.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -python -m venv .env -source .env/bin/activate -python -m pip install --upgrade pip - -python -m pip install . -python -m pip install . --no-compile --target "$BUILD_SOURCESDIRECTORY/deps" - -python -m pip install invoke -cd tests -python -m invoke -c test_setup build-protos - -cd .. -cp .artifactignore "$BUILD_SOURCESDIRECTORY/deps" -cp -r azure_functions_worker/protos "$BUILD_SOURCESDIRECTORY/deps/azure_functions_worker" \ No newline at end of file diff --git a/pack/scripts/nix_deps.sh b/pack/scripts/nix_deps.sh deleted file mode 100644 index 2d70bafad..000000000 --- a/pack/scripts/nix_deps.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -python -m venv .env -source .env/bin/activate -python -m pip install --upgrade pip - -python -m pip install . -python -m pip install . --no-compile --target "$BUILD_SOURCESDIRECTORY/deps" - -python -m pip install invoke -cd tests -python -m invoke -c test_setup build-protos - -cd .. -cp .artifactignore "$BUILD_SOURCESDIRECTORY/deps" -cp -r azure_functions_worker/protos "$BUILD_SOURCESDIRECTORY/deps/azure_functions_worker" \ No newline at end of file diff --git a/pack/scripts/win_deps.ps1 b/pack/scripts/win_deps.ps1 deleted file mode 100644 index a7be372e7..000000000 --- a/pack/scripts/win_deps.ps1 +++ /dev/null @@ -1,18 +0,0 @@ -python -m venv .env -.env\Scripts\Activate.ps1 -python -m pip install --upgrade pip - -python -m pip install . - -$depsPath = Join-Path -Path $env:BUILD_SOURCESDIRECTORY -ChildPath "deps" -$protosPath = Join-Path -Path $depsPath -ChildPath "azure_functions_worker/protos" - -python -m pip install . azure-functions --no-compile --target $depsPath.ToString() - -python -m pip install invoke -cd tests -python -m invoke -c test_setup build-protos - -cd .. -Copy-Item -Path ".artifactignore" -Destination $depsPath.ToString() -Copy-Item -Path "azure_functions_worker/protos/*" -Destination $protosPath.ToString() -Recurse -Force diff --git a/pack/templates/macos_64_env_gen.yml b/pack/templates/macos_64_env_gen.yml deleted file mode 100644 index 90a3578d7..000000000 --- a/pack/templates/macos_64_env_gen.yml +++ /dev/null @@ -1,44 +0,0 @@ -parameters: - pythonVersion: '' - artifactName: '' - workerPath: '' - -steps: -- task: UsePythonVersion@0 - inputs: - versionSpec: ${{ parameters.pythonVersion }} - addToPath: true -- task: ShellScript@2 - inputs: - disableAutoCwd: true - scriptPath: 'pack/scripts/mac_arm64_deps.sh' -- bash: | - pip install pip-audit - pip-audit -r requirements.txt - displayName: 'Run vulnerability scan' - condition: ne(variables['pythonVersion'], '3.7') -- task: CopyFiles@2 - inputs: - contents: | - ${{ parameters.workerPath }} - targetFolder: '$(Build.ArtifactStagingDirectory)' - flattenFolders: true -- task: CopyFiles@2 - inputs: - sourceFolder: '$(Build.SourcesDirectory)/deps' - contents: | - ** - !grpc_tools/**/* - !grpcio_tools*/* - !build/** - !docs/** - !pack/** - !python/** - !tests/** - !setuptools*/** - !_distutils_hack/** - !distutils-precedence.pth - !pkg_resources/** - !*.dist-info/** - !werkzeug/debug/shared/debugger.js - targetFolder: '$(Build.ArtifactStagingDirectory)' diff --git a/pack/templates/nix_env_gen.yml b/pack/templates/nix_env_gen.yml deleted file mode 100644 index ae3cf4330..000000000 --- a/pack/templates/nix_env_gen.yml +++ /dev/null @@ -1,44 +0,0 @@ -parameters: - pythonVersion: '' - artifactName: '' - workerPath: '' - -steps: -- task: UsePythonVersion@0 - inputs: - versionSpec: ${{ parameters.pythonVersion }} - addToPath: true -- task: ShellScript@2 - inputs: - disableAutoCwd: true - scriptPath: 'pack/scripts/nix_deps.sh' -- bash: | - pip install pip-audit - pip-audit -r requirements.txt - displayName: 'Run vulnerability scan' - condition: ne(variables['pythonVersion'], '3.7') -- task: CopyFiles@2 - inputs: - contents: | - ${{ parameters.workerPath }} - targetFolder: '$(Build.ArtifactStagingDirectory)' - flattenFolders: true -- task: CopyFiles@2 - inputs: - sourceFolder: '$(Build.SourcesDirectory)/deps' - contents: | - ** - !grpc_tools/**/* - !grpcio_tools*/* - !build/** - !docs/** - !pack/** - !python/** - !tests/** - !setuptools*/** - !_distutils_hack/** - !distutils-precedence.pth - !pkg_resources/** - !*.dist-info/** - !werkzeug/debug/shared/debugger.js - targetFolder: '$(Build.ArtifactStagingDirectory)' diff --git a/pack/templates/win_env_gen.yml b/pack/templates/win_env_gen.yml deleted file mode 100644 index 2eee3411a..000000000 --- a/pack/templates/win_env_gen.yml +++ /dev/null @@ -1,44 +0,0 @@ -parameters: - pythonVersion: '' - artifactName: '' - workerPath: '' - -steps: -- task: UsePythonVersion@0 - inputs: - versionSpec: ${{ parameters.pythonVersion }} - architecture: ${{ parameters.architecture }} - addToPath: true -- task: PowerShell@2 - inputs: - filePath: 'pack\scripts\win_deps.ps1' -- bash: | - pip install pip-audit - pip-audit -r requirements.txt - displayName: 'Run vulnerability scan' - condition: ne(variables['pythonVersion'], '3.7') -- task: CopyFiles@2 - inputs: - contents: | - ${{ parameters.workerPath }} - targetFolder: '$(Build.ArtifactStagingDirectory)' - flattenFolders: true -- task: CopyFiles@2 - inputs: - sourceFolder: '$(Build.SourcesDirectory)\deps' - contents: | - ** - !grpc_tools\**\* - !grpcio_tools*\* - !build\** - !docs\** - !pack\** - !python\** - !tests\** - !setuptools*\** - !_distutils_hack\** - !distutils-precedence.pth - !pkg_resources\** - !*.dist-info\** - !werkzeug\debug\shared\debugger.js - targetFolder: '$(Build.ArtifactStagingDirectory)' diff --git a/pack/utils/__init__.py b/pack/utils/__init__.py deleted file mode 100644 index 71c835333..000000000 --- a/pack/utils/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Needed to make azure a namespace for package discovery -from pkgutil import extend_path -import typing -__path__: typing.Iterable[str] = extend_path(__path__, __name__) diff --git a/pyproject.toml b/pyproject.toml index c7f41970c..ef11c7f5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,45 +1,38 @@ [project] -name = "azure-functions-worker" +name = "test-worker" dynamic = ["version"] -description = "Python Language Worker for Azure Functions Runtime" -authors = [ - { name = "Azure Functions team at Microsoft Corp.", email = "azurefunctions@microsoft.com" } -] -keywords = ["azure", "functions", "azurefunctions", - "python", "serverless"] -license = { name = "MIT", file = "LICENSE" } -readme = { file = "README.md", content-type = "text/markdown" } -classifiers = [ - "Development Status :: 5 - Production/Stable", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Operating System :: Microsoft :: Windows", - "Operating System :: POSIX", - "Operating System :: MacOS :: MacOS X", - "Environment :: Web Environment", - "License :: OSI Approved :: MIT License", - "Intended Audience :: Developers" -] +#description = "Python Language Worker for Azure Functions Runtime" +#authors = [ +# { name = "Azure Functions team at Microsoft Corp.", email = "azurefunctions@microsoft.com" } +#] +#keywords = ["azure", "functions", "azurefunctions", +# "python", "serverless"] +#license = { name = "MIT", file = "LICENSE" } +#readme = { file = "README.md", content-type = "text/markdown" } +#classifiers = [ +# "Development Status :: 5 - Production/Stable", +# "Programming Language :: Python", +# "Programming Language :: Python :: 3", +# "Programming Language :: Python :: 3.7", +# "Programming Language :: Python :: 3.8", +# "Programming Language :: Python :: 3.9", +# "Programming Language :: Python :: 3.10", +# "Programming Language :: Python :: 3.11", +# "Operating System :: Microsoft :: Windows", +# "Operating System :: POSIX", +# "Operating System :: MacOS :: MacOS X", +# "Environment :: Web Environment", +# "License :: OSI Approved :: MIT License", +# "Intended Audience :: Developers" +#] dependencies = [ - "azure-functions==1.23.0b1", - "python-dateutil ~=2.9.0", - "protobuf~=3.19.3; python_version == '3.7'", - "protobuf~=4.25.3; python_version >= '3.8'", - "grpcio-tools~=1.43.0; python_version == '3.7'", - "grpcio-tools~=1.59.0; python_version >= '3.8'", - "grpcio~=1.43.0; python_version == '3.7'", - "grpcio~=1.59.0; python_version >= '3.8'", - "azurefunctions-extensions-base; python_version >= '3.8'" + "azurefunctions-extensions-base; python_version >= '3.8'", + "vtest-sdk" ] - -[project.urls] -Documentation = "https://github.com/Azure/azure-functions-python-worker?tab=readme-ov-file#-azure-functions-python-worker" -Repository = "https://github.com/Azure/azure-functions-python-worker" +# +#[project.urls] +#Documentation = "https://github.com/Azure/azure-functions-python-worker?tab=readme-ov-file#-azure-functions-python-worker" +#Repository = "https://github.com/Azure/azure-functions-python-worker" [project.optional-dependencies] dev = [ @@ -48,6 +41,7 @@ dev = [ "flask", "fastapi~=0.103.2", "pydantic", + "pycryptodome==3.*", "flake8==5.*; python_version == '3.7'", "flake8==6.*; python_version >= '3.8'", "mypy", @@ -68,16 +62,15 @@ dev = [ "pandas", "numpy", "pre-commit", - "invoke", - "cryptography" + "invoke" ] test-http-v2 = [ - "azurefunctions-extensions-http-fastapi==1.0.0b1", + "azurefunctions-extensions-http-fastapi", "ujson", "orjson" ] test-deferred-bindings = [ - "azurefunctions-extensions-bindings-blob==1.0.0b2" + "azurefunctions-extensions-bindings-blob" ] [build-system] diff --git a/python/prodV4/worker.config.json b/python/prodV4/worker.config.json deleted file mode 100644 index 548822af9..000000000 --- a/python/prodV4/worker.config.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "description":{ - "language":"python", - "defaultRuntimeVersion":"3.11", - "supportedOperatingSystems":["LINUX", "OSX", "WINDOWS"], - "supportedRuntimeVersions":["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"], - "supportedArchitectures":["X64", "X86", "Arm64"], - "extensions":[".py"], - "defaultExecutablePath":"python", - "defaultWorkerPath":"%FUNCTIONS_WORKER_RUNTIME_VERSION%/{os}/{architecture}/worker.py", - "workerIndexing": "true" - }, - "processOptions": { - "initializationTimeout": "00:02:00", - "environmentReloadTimeout": "00:02:00" - } -} \ No newline at end of file diff --git a/python/prodV4/worker.py b/python/prodV4/worker.py deleted file mode 100644 index 021fa3f03..000000000 --- a/python/prodV4/worker.py +++ /dev/null @@ -1,68 +0,0 @@ -import os -import pathlib -import sys - -# User packages -PKGS_PATH = "/home/site/wwwroot/.python_packages" -VENV_PKGS_PATH = "site/wwwroot/worker_venv" - -PKGS = "lib/site-packages" - -# Azure environment variables -AZURE_WEBSITE_INSTANCE_ID = "WEBSITE_INSTANCE_ID" -AZURE_CONTAINER_NAME = "CONTAINER_NAME" -AZURE_WEBJOBS_SCRIPT_ROOT = "AzureWebJobsScriptRoot" - - -def is_azure_environment(): - """Check if the function app is running on the cloud""" - return (AZURE_CONTAINER_NAME in os.environ - or AZURE_WEBSITE_INSTANCE_ID in os.environ) - - -def add_script_root_to_sys_path(): - """Append function project root to module finding sys.path""" - functions_script_root = os.getenv(AZURE_WEBJOBS_SCRIPT_ROOT) - if functions_script_root is not None: - sys.path.append(functions_script_root) - - -def determine_user_pkg_paths(): - """This finds the user packages when function apps are running on the cloud - - For Python 3.7+, we only accept: - /home/site/wwwroot/.python_packages/lib/site-packages - """ - minor_version = sys.version_info[1] - - if not (7 <= minor_version <= 12): - raise RuntimeError(f'Unsupported Python version: 3.{minor_version}') - - usr_packages_path = [os.path.join(PKGS_PATH, PKGS)] - return usr_packages_path - - -if __name__ == '__main__': - # worker.py lives in the same directory as azure_functions_worker - func_worker_dir = str(pathlib.Path(__file__).absolute().parent) - env = os.environ - - # Setting up python path for all environments to prioritize - # third-party user packages over worker packages in PYTHONPATH - user_pkg_paths = determine_user_pkg_paths() - joined_pkg_paths = os.pathsep.join(user_pkg_paths) - env['PYTHONPATH'] = f'{joined_pkg_paths}:{func_worker_dir}' - - if is_azure_environment(): - os.execve(sys.executable, - [sys.executable, '-m', 'azure_functions_worker'] - + sys.argv[1:], - env) - else: - # On local development, we prioritize worker packages over - # third-party user packages (in .venv) - sys.path.insert(1, func_worker_dir) - add_script_root_to_sys_path() - from azure_functions_worker import main - - main.main() diff --git a/python/test/worker.config.json b/python/test/worker.config.json deleted file mode 100644 index a0b0ad3fe..000000000 --- a/python/test/worker.config.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "description":{ - "language":"python", - "extensions":[".py"], - "defaultExecutablePath":"python", - "defaultWorkerPath":"worker.py", - "workerIndexing": "true" - }, - "processOptions": { - "initializationTimeout": "00:02:00", - "environmentReloadTimeout": "00:02:00" - } -} \ No newline at end of file diff --git a/python/test/worker.py b/python/test/worker.py deleted file mode 100644 index e2ef12d22..000000000 --- a/python/test/worker.py +++ /dev/null @@ -1,19 +0,0 @@ -import sys -import os -from azure_functions_worker import main - - -# Azure environment variables -AZURE_WEBJOBS_SCRIPT_ROOT = "AzureWebJobsScriptRoot" - - -def add_script_root_to_sys_path(): - '''Append function project root to module finding sys.path''' - functions_script_root = os.getenv(AZURE_WEBJOBS_SCRIPT_ROOT) - if functions_script_root is not None: - sys.path.append(functions_script_root) - - -if __name__ == '__main__': - add_script_root_to_sys_path() - main.main() diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 3fdb69c81..000000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -# Required dependencies listed in pyproject.toml -. diff --git a/tests/.gitignore b/tests/.gitignore deleted file mode 100644 index 3e4ede76c..000000000 --- a/tests/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*_functions/bin/ -*_functions/host.json -*_functions/ping/ diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index b45b30f61..000000000 --- a/tests/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Bootstrap for '$ python setup.py test' command.""" - -import os.path -import sys -import unittest -import unittest.runner - - -def suite(): - test_loader = unittest.TestLoader() - test_suite = test_loader.discover( - os.path.dirname(__file__), pattern='test_*.py') - return test_suite - - -if __name__ == '__main__': - runner = unittest.runner.TextTestRunner() - result = runner.run(suite()) - sys.exit(not result.wasSuccessful()) diff --git a/tests/consumption_tests/test_linux_consumption.py b/tests/consumption_tests/test_linux_consumption.py deleted file mode 100644 index 09f10fd91..000000000 --- a/tests/consumption_tests/test_linux_consumption.py +++ /dev/null @@ -1,383 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import os -import sys -from time import sleep -from unittest import TestCase, skipIf - -from requests import Request -from tests.utils.testutils_lc import LinuxConsumptionWebHostController - -from azure_functions_worker.constants import ( - PYTHON_ENABLE_DEBUG_LOGGING, - PYTHON_ENABLE_INIT_INDEXING, - PYTHON_ENABLE_WORKER_EXTENSIONS, - PYTHON_ISOLATE_WORKER_DEPENDENCIES, -) - -_DEFAULT_HOST_VERSION = "4" - - -class TestLinuxConsumption(TestCase): - """Test worker behaviors on specific scenarios. - - SCM_RUN_FROM_PACKAGE: built function apps are acquired from - -> "Simple Batch" Subscription - -> "AzureFunctionsPythonWorkerCILinuxDevOps" Resource Group - -> "pythonworkersa" Storage Account - -> "python-worker-lc-apps" Blob Container - - For a list of scenario names: - https://pythonworker39sa.blob.core.windows.net/python-worker-lc-apps?restype=container&comp=list - """ - - @classmethod - def setUpClass(cls): - cls._py_version = f'{sys.version_info.major}.{sys.version_info.minor}' - cls._py_shortform = f'{sys.version_info.major}{sys.version_info.minor}' - - cls._storage = os.getenv('AzureWebJobsStorage') - if cls._storage is None: - raise RuntimeError('Environment variable AzureWebJobsStorage is ' - 'required before running Linux Consumption test') - - def test_placeholder_mode_root_returns_ok(self): - """In any circumstances, a placeholder container should returns 200 - even when it is not specialized. - """ - with LinuxConsumptionWebHostController(_DEFAULT_HOST_VERSION, - self._py_version) as ctrl: - req = Request('GET', ctrl.url) - resp = ctrl.send_request(req) - self.assertTrue(resp.ok) - - def test_http_no_auth(self): - """An HttpTrigger function app with 'azure-functions' library - should return 200. - """ - 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("HttpNoAuth") - }) - req = Request('GET', f'{ctrl.url}/api/HttpTrigger') - resp = ctrl.send_request(req) - self.assertEqual(resp.status_code, 200) - - def test_common_libraries(self): - """A function app with the following requirements.txt: - - azure-functions - azure-eventhub - azure-storage-blob - numpy - cryptography - pyodbc - requests - - should return 200 after importing all libraries. - """ - 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("CommonLibraries") - }) - req = Request('GET', f'{ctrl.url}/api/HttpTrigger') - resp = ctrl.send_request(req) - self.assertEqual(resp.status_code, 200) - content = resp.json() - self.assertIn('azure.functions', content) - self.assertIn('azure.storage.blob', content) - self.assertIn('numpy', content) - self.assertIn('cryptography', content) - self.assertIn('pyodbc', content) - self.assertIn('requests', content) - - @skipIf(sys.version_info.minor in (10, 11), - "Protobuf pinning fails during remote build") - def test_new_protobuf(self): - """A function app with the following requirements.txt: - - azure-functions==1.7.0 - protobuf==3.15.8 - grpcio==1.33.2 - - should return 200 after importing all libraries. - """ - 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("NewProtobuf"), - PYTHON_ISOLATE_WORKER_DEPENDENCIES: "1" - }) - req = Request('GET', f'{ctrl.url}/api/HttpTrigger') - resp = ctrl.send_request(req) - self.assertEqual(resp.status_code, 200) - - content = resp.json() - - # Worker always picks up the SDK version bundled with the image - # Version of the packages are inconsistent due to isolation's bug - self.assertEqual(content['azure.functions'], '1.7.0') - self.assertEqual(content['google.protobuf'], '3.15.8') - self.assertEqual(content['grpc'], '1.33.2') - - @skipIf(sys.version_info.minor in (10, 11), - "Protobuf pinning fails during remote build") - def test_old_protobuf(self): - """A function app with the following requirements.txt: - - azure-functions==1.5.0 - protobuf==3.8.0 - grpcio==1.27.1 - - should return 200 after importing all libraries. - """ - 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("OldProtobuf"), - PYTHON_ISOLATE_WORKER_DEPENDENCIES: "1" - }) - req = Request('GET', f'{ctrl.url}/api/HttpTrigger') - resp = ctrl.send_request(req) - self.assertEqual(resp.status_code, 200) - - content = resp.json() - - # Worker always picks up the SDK version bundled with the image - # Version of the packages are inconsistent due to isolation's bug - self.assertIn(content['azure.functions'], '1.5.0') - self.assertIn(content['google.protobuf'], '3.8.0') - self.assertIn(content['grpc'], '1.27.1') - - def test_debug_logging_disabled(self): - """An HttpTrigger function app with 'azure-functions' library - should return 200 and by default customer debug logging should be - disabled. - """ - 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("EnableDebugLogging") - }) - req = Request('GET', f'{ctrl.url}/api/HttpTrigger1') - resp = ctrl.send_request(req) - - self.assertEqual(resp.status_code, 200) - container_log = ctrl.get_container_logs() - func_start_idx = container_log.find( - "Executing 'Functions.HttpTrigger1'") - self.assertTrue(func_start_idx > -1, - "HttpTrigger function is not executed.") - func_log = container_log[func_start_idx:] - - self.assertIn('logging info', func_log) - self.assertIn('logging warning', func_log) - self.assertIn('logging error', func_log) - self.assertNotIn('logging debug', func_log) - - def test_debug_logging_enabled(self): - """An HttpTrigger function app with 'azure-functions' library - should return 200 and with customer debug logging enabled, debug logs - should be written to container logs. - """ - 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( - "EnableDebugLogging"), - PYTHON_ENABLE_DEBUG_LOGGING: "1" - }) - req = Request('GET', f'{ctrl.url}/api/HttpTrigger1') - resp = ctrl.send_request(req) - - self.assertEqual(resp.status_code, 200) - container_log = ctrl.get_container_logs() - func_start_idx = container_log.find( - "Executing 'Functions.HttpTrigger1'") - self.assertTrue(func_start_idx > -1) - func_log = container_log[func_start_idx:] - - self.assertIn('logging info', func_log) - self.assertIn('logging warning', func_log) - self.assertIn('logging error', func_log) - self.assertIn('logging debug', func_log) - - def test_pinning_functions_to_older_version(self): - """An HttpTrigger function app with 'azure-functions==1.11.1' library - should return 200 with the azure functions version set to 1.11.1 - since dependency isolation is enabled by default for all py versions - """ - 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( - "PinningFunctions"), - PYTHON_ISOLATE_WORKER_DEPENDENCIES: "1", - }) - req = Request('GET', f'{ctrl.url}/api/HttpTrigger1') - resp = ctrl.send_request(req) - - self.assertEqual(resp.status_code, 200) - self.assertIn("Func Version: 1.11.1", resp.text) - - @skipIf(sys.version_info.minor != 10, - "This is testing only for python310") - def test_opencensus_with_extensions_enabled(self): - """A function app with extensions enabled containing the - following libraries: - - azure-functions, opencensus - - should return 200 after importing all libraries. - """ - 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("Opencensus"), - PYTHON_ENABLE_WORKER_EXTENSIONS: "1" - }) - req = Request('GET', f'{ctrl.url}/api/opencensus') - resp = ctrl.send_request(req) - self.assertEqual(resp.status_code, 200) - - @skipIf(sys.version_info.minor != 10, - "This is testing only for python310") - def test_opencensus_with_extensions_enabled_init_indexing(self): - """ - A function app with init indexing enabled - """ - 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("Opencensus"), - PYTHON_ENABLE_WORKER_EXTENSIONS: "1", - PYTHON_ENABLE_INIT_INDEXING: "true" - }) - req = Request('GET', f'{ctrl.url}/api/opencensus') - resp = ctrl.send_request(req) - self.assertEqual(resp.status_code, 200) - - @skipIf(sys.version_info.minor != 9, - "This is testing only for python39 where extensions" - "enabled by default") - def test_reload_variables_after_timeout_error(self): - """ - A function app with HTTPtrigger which has a function timeout of - 20s. The app as a sleep of 30s which should trigger a timeout - """ - 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( - "TimeoutError"), - PYTHON_ISOLATE_WORKER_DEPENDENCIES: "1" - }) - req = Request('GET', f'{ctrl.url}/api/hello') - resp = ctrl.send_request(req) - self.assertEqual(resp.status_code, 500) - - sleep(2) - logs = ctrl.get_container_logs() - self.assertRegex( - logs, - r"Applying prioritize_customer_dependencies: " - r"worker_dependencies_path: \/azure-functions-host\/" - r"workers\/python\/.*?\/LINUX\/X64," - r" customer_dependencies_path: \/home\/site\/wwwroot\/" - r"\.python_packages\/lib\/site-packages, working_directory:" - r" \/home\/site\/wwwroot, Linux Consumption: True," - r" Placeholder: False") - self.assertNotIn("Failure Exception: ModuleNotFoundError", - logs) - - @skipIf(sys.version_info.minor != 9, - "This is testing only for python39 where extensions" - "enabled by default") - def test_reload_variables_after_oom_error(self): - """ - A function app with HTTPtrigger mocking error code 137 - """ - 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( - "OOMError"), - PYTHON_ISOLATE_WORKER_DEPENDENCIES: "1" - }) - req = Request('GET', f'{ctrl.url}/api/httptrigger') - resp = ctrl.send_request(req) - self.assertEqual(resp.status_code, 500) - - sleep(2) - logs = ctrl.get_container_logs() - self.assertRegex( - logs, - r"Applying prioritize_customer_dependencies: " - r"worker_dependencies_path: \/azure-functions-host\/" - r"workers\/python\/.*?\/LINUX\/X64," - r" customer_dependencies_path: \/home\/site\/wwwroot\/" - r"\.python_packages\/lib\/site-packages, working_directory:" - r" \/home\/site\/wwwroot, Linux Consumption: True," - r" Placeholder: False") - - 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 using http v2 fastapi extension with streaming upload and - download - """ - 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/' - f'python-worker-lc-apps/{scenario_name}{self._py_shortform}.zip' - ) diff --git a/tests/emulator_tests/blob_functions/blob_functions_stein/generic/function_app.py b/tests/emulator_tests/blob_functions/blob_functions_stein/generic/function_app.py deleted file mode 100644 index 77e9dc596..000000000 --- a/tests/emulator_tests/blob_functions/blob_functions_stein/generic/function_app.py +++ /dev/null @@ -1,471 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import hashlib -import io -import json -import random -import string - -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.function_name(name="blob_trigger") -@app.generic_trigger( - arg_name="file", - type="blobTrigger", - path="python-worker-tests/test-blob-trigger.txt", - connection="AzureWebJobsStorage") -@app.generic_output_binding( - arg_name="$return", - type="blob", - path="python-worker-tests/test-blob-triggered.txt", - connection="AzureWebJobsStorage") -def blob_trigger(file: func.InputStream) -> str: - return json.dumps({ - 'name': file.name, - 'length': file.length, - 'content': file.read().decode('utf-8') - }) - - -@app.function_name(name="get_blob_as_bytes") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="get_blob_as_bytes") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding( - arg_name="file", - connection="AzureWebJobsStorage", - type="blob", - data_type="BINARY", - path="python-worker-tests/test-bytes.txt") -def get_blob_as_bytes(req: func.HttpRequest, file: bytes) -> str: - assert isinstance(file, bytes) - return file.decode('utf-8') - - -@app.function_name(name="get_blob_as_bytes_return_http_response") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="get_blob_as_bytes_return_http_response") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding( - arg_name="file", - connection="AzureWebJobsStorage", - type="blob", - data_type="BINARY", - path="python-worker-tests/shmem-test-bytes.txt") -def get_blob_as_bytes_return_http_response(req: func.HttpRequest, file: bytes) \ - -> func.HttpResponse: - """ - Read a blob (bytes) and respond back (in HTTP response) with the number of - bytes read and the MD5 digest of the content. - """ - assert isinstance(file, bytes) - - content_size = len(file) - content_sha256 = hashlib.sha256(file).hexdigest() - - response_dict = { - 'content_size': content_size, - 'content_sha256': content_sha256 - } - - response_body = json.dumps(response_dict, indent=2) - - return func.HttpResponse( - body=response_body, - mimetype="application/json", - status_code=200 - ) - - -@app.function_name(name="get_blob_as_bytes_stream_return_http_response") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="get_blob_as_bytes_stream_return_http_response") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding( - arg_name="file", - connection="AzureWebJobsStorage", - type="blob", - data_type="BINARY", - path="python-worker-tests/shmem-test-bytes.txt") -def get_blob_as_bytes_stream_return_http_response(req: func.HttpRequest, - file: func.InputStream) \ - -> func.HttpResponse: - """ - Read a blob (as azf.InputStream) and respond back (in HTTP response) with - the number of bytes read and the MD5 digest of the content. - """ - file_bytes = file.read() - - content_size = len(file_bytes) - content_sha256 = hashlib.sha256(file_bytes).hexdigest() - - response_dict = { - 'content_size': content_size, - 'content_sha256': content_sha256 - } - - response_body = json.dumps(response_dict, indent=2) - - return func.HttpResponse( - body=response_body, - mimetype="application/json", - status_code=200 - ) - - -@app.function_name(name="get_blob_as_str") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="get_blob_as_str") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding( - arg_name="file", - connection="AzureWebJobsStorage", - type="blob", - data_type="STRING", - path="python-worker-tests/test-str.txt") -def get_blob_as_str(req: func.HttpRequest, file: str) -> str: - assert isinstance(file, str) - return file - - -@app.function_name(name="get_blob_as_str_return_http_response") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="get_blob_as_str_return_http_response") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding( - arg_name="file", - connection="AzureWebJobsStorage", - type="blob", - data_type="STRING", - path="python-worker-tests/shmem-test-bytes.txt") -def get_blob_as_str_return_http_response(req: func.HttpRequest, - file: str) -> func.HttpResponse: - """ - Read a blob (string) and respond back (in HTTP response) with the number of - characters read and the MD5 digest of the utf-8 encoded content. - """ - assert isinstance(file, str) - - num_chars = len(file) - content_bytes = file.encode('utf-8') - content_sha256 = hashlib.sha256(content_bytes).hexdigest() - - response_dict = { - 'num_chars': num_chars, - 'content_sha256': content_sha256 - } - - response_body = json.dumps(response_dict, indent=2) - - return func.HttpResponse( - body=response_body, - mimetype="application/json", - status_code=200 - ) - - -@app.function_name(name="get_blob_bytes") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="get_blob_bytes") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding( - arg_name="file", - connection="AzureWebJobsStorage", - type="blob", - path="python-worker-tests/test-bytes.txt") -def get_blob_bytes(req: func.HttpRequest, file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -@app.function_name(name="get_blob_filelike") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="get_blob_filelike") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding( - arg_name="file", - connection="AzureWebJobsStorage", - type="blob", - path="python-worker-tests/test-filelike.txt") -def get_blob_filelike(req: func.HttpRequest, file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -@app.function_name(name="get_blob_return") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="get_blob_return") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding( - arg_name="file", - connection="AzureWebJobsStorage", - type="blob", - path="python-worker-tests/test-return.txt") -def get_blob_return(req: func.HttpRequest, file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -@app.function_name(name="get_blob_str") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="get_blob_str") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding( - arg_name="file", - connection="AzureWebJobsStorage", - type="blob", - path="python-worker-tests/test-str.txt") -def get_blob_str(req: func.HttpRequest, file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -@app.function_name(name="get_blob_triggered") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="get_blob_triggered") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding( - arg_name="file", - connection="AzureWebJobsStorage", - type="blob", - path="python-worker-tests/test-blob-triggered.txt") -def get_blob_triggered(req: func.HttpRequest, file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -@app.function_name(name="put_blob_as_bytes_return_http_response") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="put_blob_as_bytes_return_http_response") -@app.generic_output_binding( - arg_name="file", - type="blob", - data_type="BINARY", - connection="AzureWebJobsStorage", - path="python-worker-tests/shmem-test-bytes-out.txt") -def put_blob_as_bytes_return_http_response(req: func.HttpRequest, - file: func.Out[ - bytes]) -> func.HttpResponse: - """ - Write a blob (bytes) and respond back (in HTTP response) with the number of - bytes written and the MD5 digest of the content. - The number of bytes to write are specified in the input HTTP request. - """ - content_size = int(req.params['content_size']) - - # When this is set, then 0x01 byte is repeated content_size number of - # times to use as input. - # This is to avoid generating random input for large size which can be - # slow. - if 'no_random_input' in req.params: - content = b'\x01' * content_size - else: - content = bytearray(random.getrandbits(8) for _ in range(content_size)) - content_sha256 = hashlib.sha256(content).hexdigest() - - file.set(content) - - response_dict = { - 'content_size': content_size, - 'content_sha256': content_sha256 - } - - response_body = json.dumps(response_dict, indent=2) - - return func.HttpResponse( - body=response_body, - mimetype="application/json", - status_code=200 - ) - - -@app.function_name(name="put_blob_as_str_return_http_response") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="put_blob_as_str_return_http_response") -@app.generic_output_binding( - arg_name="file", - type="blob", - data_type="STRING", - connection="AzureWebJobsStorage", - path="python-worker-tests/shmem-test-str-out.txt") -def put_blob_as_str_return_http_response( - req: func.HttpRequest, file: func.Out[str]) -> func.HttpResponse: - """ - Write a blob (string) and respond back (in HTTP response) with the number of - characters written and the MD5 digest of the utf-8 encoded content. - The number of characters to write are specified in the input HTTP request. - """ - num_chars = int(req.params['num_chars']) - - content = ''.join(random.choices(string.ascii_uppercase + string.digits, - k=num_chars)) - content_bytes = content.encode('utf-8') - content_size = len(content_bytes) - content_sha256 = hashlib.sha256(content_bytes).hexdigest() - - file.set(content) - - response_dict = { - 'num_chars': num_chars, - 'content_size': content_size, - 'content_sha256': content_sha256 - } - - response_body = json.dumps(response_dict, indent=2) - - return func.HttpResponse( - body=response_body, - mimetype="application/json", - status_code=200 - ) - - -@app.function_name(name="put_blob_bytes") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="put_blob_bytes") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_output_binding( - arg_name="file", - type="blob", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-bytes.txt") -def put_blob_bytes(req: func.HttpRequest, file: func.Out[bytes]) -> str: - file.set(req.get_body()) - return 'OK' - - -@app.function_name(name="put_blob_filelike") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="put_blob_filelike") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_output_binding( - arg_name="file", - type="blob", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-filelike.txt") -def put_blob_filelike(req: func.HttpRequest, - file: func.Out[io.StringIO]) -> str: - file.set(io.StringIO('filelike')) - return 'OK' - - -@app.function_name(name="put_blob_return") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="put_blob_return") -@app.generic_output_binding( - arg_name="$return", - type="blob", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-return.txt") -def put_blob_return(req: func.HttpRequest) -> str: - return 'FROM RETURN' - - -@app.function_name(name="put_blob_str") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="put_blob_str") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_output_binding( - arg_name="file", - type="blob", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-str.txt") -def put_blob_str(req: func.HttpRequest, file: func.Out[str]) -> str: - file.set(req.get_body()) - return 'OK' - - -@app.function_name(name="put_blob_trigger") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="put_blob_trigger") -@app.generic_output_binding( - arg_name="file", - type="blob", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-blob-trigger.txt") -def put_blob_trigger(req: func.HttpRequest, file: func.Out[str]) -> str: - file.set(req.get_body()) - return 'OK' - - -def _generate_content_and_digest(content_size): - content = bytearray(random.getrandbits(8) for _ in range(content_size)) - content_sha256 = hashlib.sha256(content).hexdigest() - return content, content_sha256 - - -@app.function_name(name="put_get_multiple_blobs_as_bytes_return_http_response") -@app.generic_trigger( - arg_name="req", type="httpTrigger", - route="put_get_multiple_blobs_as_bytes_return_http_response") -@app.generic_input_binding( - arg_name="inputfile1", - connection="AzureWebJobsStorage", - type="blob", - data_type="BINARY", - path="python-worker-tests/shmem-test-bytes-1.txt") -@app.generic_input_binding( - arg_name="inputfile2", - connection="AzureWebJobsStorage", - type="blob", - data_type="BINARY", - path="python-worker-tests/shmem-test-bytes-2.txt") -@app.generic_output_binding( - arg_name="outputfile1", - connection="AzureWebJobsStorage", - type="blob", - data_type="BINARY", - path="python-worker-tests/shmem-test-bytes-out-1.txt") -@app.generic_output_binding( - arg_name="outputfile2", - connection="AzureWebJobsStorage", - type="blob", - data_type="BINARY", - path="python-worker-tests/shmem-test-bytes-out-2.txt") -def put_get_multiple_blobs_as_bytes_return_http_response( - req: func.HttpRequest, - inputfile1: bytes, - inputfile2: bytes, - outputfile1: func.Out[bytes], - outputfile2: func.Out[bytes]) -> func.HttpResponse: - """ - Read two blobs (bytes) and respond back (in HTTP response) with the number - of bytes read from each blob and the MD5 digest of the content of each. - Write two blobs (bytes) and respond back (in HTTP response) with the number - bytes written in each blob and the MD5 digest of the content of each. - The number of bytes to write are specified in the input HTTP request. - """ - input_content_size_1 = len(inputfile1) - input_content_size_2 = len(inputfile2) - - input_content_sha256_1 = hashlib.sha256(inputfile1).hexdigest() - input_content_sha256_2 = hashlib.sha256(inputfile2).hexdigest() - - output_content_size_1 = int(req.params['output_content_size_1']) - output_content_size_2 = int(req.params['output_content_size_2']) - - output_content_1, output_content_sha256_1 = \ - _generate_content_and_digest(output_content_size_1) - output_content_2, output_content_sha256_2 = \ - _generate_content_and_digest(output_content_size_2) - - outputfile1.set(output_content_1) - outputfile2.set(output_content_2) - - response_dict = { - 'input_content_size_1': input_content_size_1, - 'input_content_size_2': input_content_size_2, - 'input_content_sha256_1': input_content_sha256_1, - 'input_content_sha256_2': input_content_sha256_2, - 'output_content_size_1': output_content_size_1, - 'output_content_size_2': output_content_size_2, - 'output_content_sha256_1': output_content_sha256_1, - 'output_content_sha256_2': output_content_sha256_2 - } - - response_body = json.dumps(response_dict, indent=2) - - return func.HttpResponse( - body=response_body, - mimetype="application/json", - status_code=200 - ) diff --git a/tests/emulator_tests/blob_functions/blob_trigger/function.json b/tests/emulator_tests/blob_functions/blob_trigger/function.json deleted file mode 100644 index 85f59728d..000000000 --- a/tests/emulator_tests/blob_functions/blob_trigger/function.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "blobTrigger", - "direction": "in", - "name": "file", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-blob-trigger.txt" - }, - { - "type": "blob", - "direction": "out", - "name": "$return", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-blob-triggered.txt" - } - ] -} diff --git a/tests/emulator_tests/blob_functions/blob_trigger/main.py b/tests/emulator_tests/blob_functions/blob_trigger/main.py deleted file mode 100644 index 5f162baaf..000000000 --- a/tests/emulator_tests/blob_functions/blob_trigger/main.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json - -import azure.functions as azf - - -def main(file: azf.InputStream) -> str: - return json.dumps({ - 'name': file.name, - 'length': file.length, - 'content': file.read().decode('utf-8') - }) diff --git a/tests/emulator_tests/blob_functions/get_blob_as_bytes/function.json b/tests/emulator_tests/blob_functions/get_blob_as_bytes/function.json deleted file mode 100644 index 79caf12a9..000000000 --- a/tests/emulator_tests/blob_functions/get_blob_as_bytes/function.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "in", - "name": "file", - "dataType": "binary", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-bytes.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/emulator_tests/blob_functions/get_blob_as_bytes/main.py b/tests/emulator_tests/blob_functions/get_blob_as_bytes/main.py deleted file mode 100644 index 94a73d99e..000000000 --- a/tests/emulator_tests/blob_functions/get_blob_as_bytes/main.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest, file: bytes) -> str: - assert isinstance(file, bytes) - return file.decode('utf-8') diff --git a/tests/emulator_tests/blob_functions/get_blob_as_bytes_return_http_response/function.json b/tests/emulator_tests/blob_functions/get_blob_as_bytes_return_http_response/function.json deleted file mode 100644 index 59e8d01cf..000000000 --- a/tests/emulator_tests/blob_functions/get_blob_as_bytes_return_http_response/function.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "in", - "name": "file", - "dataType": "binary", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/shmem-test-bytes.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/emulator_tests/blob_functions/get_blob_as_bytes_return_http_response/main.py b/tests/emulator_tests/blob_functions/get_blob_as_bytes_return_http_response/main.py deleted file mode 100644 index f7069cab6..000000000 --- a/tests/emulator_tests/blob_functions/get_blob_as_bytes_return_http_response/main.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import hashlib -import json - -import azure.functions as azf - - -def main(req: azf.HttpRequest, file: bytes) -> azf.HttpResponse: - """ - Read a blob (bytes) and respond back (in HTTP response) with the number of - bytes read and the MD5 digest of the content. - """ - assert isinstance(file, bytes) - - content_size = len(file) - content_sha256 = hashlib.sha256(file).hexdigest() - - response_dict = { - 'content_size': content_size, - 'content_sha256': content_sha256 - } - - response_body = json.dumps(response_dict, indent=2) - - return azf.HttpResponse( - body=response_body, - mimetype="application/json", - status_code=200 - ) diff --git a/tests/emulator_tests/blob_functions/get_blob_as_bytes_stream_return_http_response/function.json b/tests/emulator_tests/blob_functions/get_blob_as_bytes_stream_return_http_response/function.json deleted file mode 100644 index 59e8d01cf..000000000 --- a/tests/emulator_tests/blob_functions/get_blob_as_bytes_stream_return_http_response/function.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "in", - "name": "file", - "dataType": "binary", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/shmem-test-bytes.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/emulator_tests/blob_functions/get_blob_as_bytes_stream_return_http_response/main.py b/tests/emulator_tests/blob_functions/get_blob_as_bytes_stream_return_http_response/main.py deleted file mode 100644 index bd65835b5..000000000 --- a/tests/emulator_tests/blob_functions/get_blob_as_bytes_stream_return_http_response/main.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import hashlib -import json - -import azure.functions as azf - - -def main(req: azf.HttpRequest, file: azf.InputStream) -> azf.HttpResponse: - """ - Read a blob (as azf.InputStream) and respond back (in HTTP response) with - the number of bytes read and the MD5 digest of the content. - """ - file_bytes = file.read() - - content_size = len(file_bytes) - content_sha256 = hashlib.sha256(file_bytes).hexdigest() - - response_dict = { - 'content_size': content_size, - 'content_sha256': content_sha256 - } - - response_body = json.dumps(response_dict, indent=2) - - return azf.HttpResponse( - body=response_body, - mimetype="application/json", - status_code=200 - ) diff --git a/tests/emulator_tests/blob_functions/get_blob_as_str/function.json b/tests/emulator_tests/blob_functions/get_blob_as_str/function.json deleted file mode 100644 index ef991b625..000000000 --- a/tests/emulator_tests/blob_functions/get_blob_as_str/function.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "in", - "name": "file", - "dataType": "string", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-str.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/emulator_tests/blob_functions/get_blob_as_str/main.py b/tests/emulator_tests/blob_functions/get_blob_as_str/main.py deleted file mode 100644 index 59034f9ba..000000000 --- a/tests/emulator_tests/blob_functions/get_blob_as_str/main.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest, file: str) -> str: - assert isinstance(file, str) - return file diff --git a/tests/emulator_tests/blob_functions/get_blob_as_str_return_http_response/function.json b/tests/emulator_tests/blob_functions/get_blob_as_str_return_http_response/function.json deleted file mode 100644 index 5da04fd22..000000000 --- a/tests/emulator_tests/blob_functions/get_blob_as_str_return_http_response/function.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "in", - "name": "file", - "dataType": "string", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/shmem-test-str.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/emulator_tests/blob_functions/get_blob_as_str_return_http_response/main.py b/tests/emulator_tests/blob_functions/get_blob_as_str_return_http_response/main.py deleted file mode 100644 index 16f98375f..000000000 --- a/tests/emulator_tests/blob_functions/get_blob_as_str_return_http_response/main.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import hashlib -import json - -import azure.functions as azf - - -def main(req: azf.HttpRequest, file: str) -> azf.HttpResponse: - """ - Read a blob (string) and respond back (in HTTP response) with the number of - characters read and the MD5 digest of the utf-8 encoded content. - """ - assert isinstance(file, str) - - num_chars = len(file) - content_bytes = file.encode('utf-8') - content_sha256 = hashlib.sha256(content_bytes).hexdigest() - - response_dict = { - 'num_chars': num_chars, - 'content_sha256': content_sha256 - } - - response_body = json.dumps(response_dict, indent=2) - - return azf.HttpResponse( - body=response_body, - mimetype="application/json", - status_code=200 - ) diff --git a/tests/emulator_tests/blob_functions/get_blob_bytes/function.json b/tests/emulator_tests/blob_functions/get_blob_bytes/function.json deleted file mode 100644 index 36e6472a2..000000000 --- a/tests/emulator_tests/blob_functions/get_blob_bytes/function.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "in", - "name": "file", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-bytes.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/emulator_tests/blob_functions/get_blob_bytes/main.py b/tests/emulator_tests/blob_functions/get_blob_bytes/main.py deleted file mode 100644 index 46dc607e2..000000000 --- a/tests/emulator_tests/blob_functions/get_blob_bytes/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest, file: azf.InputStream) -> str: - return file.read().decode('utf-8') diff --git a/tests/emulator_tests/blob_functions/get_blob_filelike/function.json b/tests/emulator_tests/blob_functions/get_blob_filelike/function.json deleted file mode 100644 index bea089b58..000000000 --- a/tests/emulator_tests/blob_functions/get_blob_filelike/function.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "in", - "name": "file", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-filelike.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/emulator_tests/blob_functions/get_blob_filelike/main.py b/tests/emulator_tests/blob_functions/get_blob_filelike/main.py deleted file mode 100644 index 46dc607e2..000000000 --- a/tests/emulator_tests/blob_functions/get_blob_filelike/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest, file: azf.InputStream) -> str: - return file.read().decode('utf-8') diff --git a/tests/emulator_tests/blob_functions/get_blob_return/function.json b/tests/emulator_tests/blob_functions/get_blob_return/function.json deleted file mode 100644 index 208e1dd53..000000000 --- a/tests/emulator_tests/blob_functions/get_blob_return/function.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "in", - "name": "file", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-return.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/emulator_tests/blob_functions/get_blob_return/main.py b/tests/emulator_tests/blob_functions/get_blob_return/main.py deleted file mode 100644 index 46dc607e2..000000000 --- a/tests/emulator_tests/blob_functions/get_blob_return/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest, file: azf.InputStream) -> str: - return file.read().decode('utf-8') diff --git a/tests/emulator_tests/blob_functions/get_blob_str/function.json b/tests/emulator_tests/blob_functions/get_blob_str/function.json deleted file mode 100644 index 7117f87d7..000000000 --- a/tests/emulator_tests/blob_functions/get_blob_str/function.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "in", - "name": "file", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-str.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/emulator_tests/blob_functions/get_blob_str/main.py b/tests/emulator_tests/blob_functions/get_blob_str/main.py deleted file mode 100644 index 46dc607e2..000000000 --- a/tests/emulator_tests/blob_functions/get_blob_str/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest, file: azf.InputStream) -> str: - return file.read().decode('utf-8') diff --git a/tests/emulator_tests/blob_functions/get_blob_triggered/function.json b/tests/emulator_tests/blob_functions/get_blob_triggered/function.json deleted file mode 100644 index 9ba913e86..000000000 --- a/tests/emulator_tests/blob_functions/get_blob_triggered/function.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "in", - "name": "file", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-blob-triggered.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/emulator_tests/blob_functions/get_blob_triggered/main.py b/tests/emulator_tests/blob_functions/get_blob_triggered/main.py deleted file mode 100644 index 46dc607e2..000000000 --- a/tests/emulator_tests/blob_functions/get_blob_triggered/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest, file: azf.InputStream) -> str: - return file.read().decode('utf-8') diff --git a/tests/emulator_tests/blob_functions/put_blob_as_bytes_return_http_response/function.json b/tests/emulator_tests/blob_functions/put_blob_as_bytes_return_http_response/function.json deleted file mode 100644 index 4f8821813..000000000 --- a/tests/emulator_tests/blob_functions/put_blob_as_bytes_return_http_response/function.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "out", - "name": "file", - "dataType": "binary", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/shmem-test-bytes-out.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/emulator_tests/blob_functions/put_blob_as_bytes_return_http_response/main.py b/tests/emulator_tests/blob_functions/put_blob_as_bytes_return_http_response/main.py deleted file mode 100644 index 583258820..000000000 --- a/tests/emulator_tests/blob_functions/put_blob_as_bytes_return_http_response/main.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import hashlib -import json -import random - -import azure.functions as azf - - -def main(req: azf.HttpRequest, file: azf.Out[bytes]) -> azf.HttpResponse: - """ - Write a blob (bytes) and respond back (in HTTP response) with the number of - bytes written and the MD5 digest of the content. - The number of bytes to write are specified in the input HTTP request. - """ - content_size = int(req.params['content_size']) - - # When this is set, then 0x01 byte is repeated content_size number of - # times to use as input. - # This is to avoid generating random input for large size which can be - # slow. - if 'no_random_input' in req.params: - content = b'\x01' * content_size - else: - content = bytearray(random.getrandbits(8) for _ in range(content_size)) - content_sha256 = hashlib.sha256(content).hexdigest() - - file.set(content) - - response_dict = { - 'content_size': content_size, - 'content_sha256': content_sha256 - } - - response_body = json.dumps(response_dict, indent=2) - - return azf.HttpResponse( - body=response_body, - mimetype="application/json", - status_code=200 - ) diff --git a/tests/emulator_tests/blob_functions/put_blob_as_str_return_http_response/function.json b/tests/emulator_tests/blob_functions/put_blob_as_str_return_http_response/function.json deleted file mode 100644 index 59a6ff68a..000000000 --- a/tests/emulator_tests/blob_functions/put_blob_as_str_return_http_response/function.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "out", - "name": "file", - "dataType": "string", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/shmem-test-str-out.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/emulator_tests/blob_functions/put_blob_as_str_return_http_response/main.py b/tests/emulator_tests/blob_functions/put_blob_as_str_return_http_response/main.py deleted file mode 100644 index 3174d3cf0..000000000 --- a/tests/emulator_tests/blob_functions/put_blob_as_str_return_http_response/main.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import hashlib -import json -import random -import string - -import azure.functions as azf - - -def main(req: azf.HttpRequest, file: azf.Out[str]) -> azf.HttpResponse: - """ - Write a blob (string) and respond back (in HTTP response) with the number of - characters written and the MD5 digest of the utf-8 encoded content. - The number of characters to write are specified in the input HTTP request. - """ - num_chars = int(req.params['num_chars']) - - content = ''.join(random.choices(string.ascii_uppercase + string.digits, - k=num_chars)) - content_bytes = content.encode('utf-8') - content_size = len(content_bytes) - content_sha256 = hashlib.sha256(content_bytes).hexdigest() - - file.set(content) - - response_dict = { - 'num_chars': num_chars, - 'content_size': content_size, - 'content_sha256': content_sha256 - } - - response_body = json.dumps(response_dict, indent=2) - - return azf.HttpResponse( - body=response_body, - mimetype="application/json", - status_code=200 - ) diff --git a/tests/emulator_tests/blob_functions/put_blob_bytes/function.json b/tests/emulator_tests/blob_functions/put_blob_bytes/function.json deleted file mode 100644 index 21b47df00..000000000 --- a/tests/emulator_tests/blob_functions/put_blob_bytes/function.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "out", - "name": "file", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-bytes.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/emulator_tests/blob_functions/put_blob_bytes/main.py b/tests/emulator_tests/blob_functions/put_blob_bytes/main.py deleted file mode 100644 index 605677ab2..000000000 --- a/tests/emulator_tests/blob_functions/put_blob_bytes/main.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest, file: azf.Out[str]) -> str: - file.set(req.get_body()) - return 'OK' diff --git a/tests/emulator_tests/blob_functions/put_blob_filelike/function.json b/tests/emulator_tests/blob_functions/put_blob_filelike/function.json deleted file mode 100644 index 09a1bb480..000000000 --- a/tests/emulator_tests/blob_functions/put_blob_filelike/function.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "out", - "name": "file", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-filelike.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/emulator_tests/blob_functions/put_blob_filelike/main.py b/tests/emulator_tests/blob_functions/put_blob_filelike/main.py deleted file mode 100644 index e5b4e51b5..000000000 --- a/tests/emulator_tests/blob_functions/put_blob_filelike/main.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import io - -import azure.functions as azf - - -def main(req: azf.HttpRequest, file: azf.Out[io.StringIO]) -> str: - file.set(io.StringIO('filelike')) - return 'OK' diff --git a/tests/emulator_tests/blob_functions/put_blob_return/function.json b/tests/emulator_tests/blob_functions/put_blob_return/function.json deleted file mode 100644 index 662d67396..000000000 --- a/tests/emulator_tests/blob_functions/put_blob_return/function.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "out", - "name": "$return", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-return.txt" - } - ] -} diff --git a/tests/emulator_tests/blob_functions/put_blob_return/main.py b/tests/emulator_tests/blob_functions/put_blob_return/main.py deleted file mode 100644 index 73491c93d..000000000 --- a/tests/emulator_tests/blob_functions/put_blob_return/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest) -> str: - return 'FROM RETURN' diff --git a/tests/emulator_tests/blob_functions/put_blob_str/function.json b/tests/emulator_tests/blob_functions/put_blob_str/function.json deleted file mode 100644 index 8b2543f90..000000000 --- a/tests/emulator_tests/blob_functions/put_blob_str/function.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "out", - "name": "file", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-str.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/emulator_tests/blob_functions/put_blob_str/main.py b/tests/emulator_tests/blob_functions/put_blob_str/main.py deleted file mode 100644 index 605677ab2..000000000 --- a/tests/emulator_tests/blob_functions/put_blob_str/main.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest, file: azf.Out[str]) -> str: - file.set(req.get_body()) - return 'OK' diff --git a/tests/emulator_tests/blob_functions/put_blob_trigger/function.json b/tests/emulator_tests/blob_functions/put_blob_trigger/function.json deleted file mode 100644 index b6bb70d32..000000000 --- a/tests/emulator_tests/blob_functions/put_blob_trigger/function.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "out", - "name": "file", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-blob-trigger.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/emulator_tests/blob_functions/put_blob_trigger/main.py b/tests/emulator_tests/blob_functions/put_blob_trigger/main.py deleted file mode 100644 index 605677ab2..000000000 --- a/tests/emulator_tests/blob_functions/put_blob_trigger/main.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest, file: azf.Out[str]) -> str: - file.set(req.get_body()) - return 'OK' diff --git a/tests/emulator_tests/blob_functions/put_get_multiple_blobs_as_bytes_return_http_response/function.json b/tests/emulator_tests/blob_functions/put_get_multiple_blobs_as_bytes_return_http_response/function.json deleted file mode 100644 index 8ec3b7737..000000000 --- a/tests/emulator_tests/blob_functions/put_get_multiple_blobs_as_bytes_return_http_response/function.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "in", - "name": "inputfile1", - "dataType": "binary", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/shmem-test-bytes-1.txt" - }, - { - "type": "blob", - "direction": "in", - "name": "inputfile2", - "dataType": "binary", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/shmem-test-bytes-2.txt" - }, - { - "type": "blob", - "direction": "out", - "name": "outputfile1", - "dataType": "binary", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/shmem-test-bytes-out-1.txt" - }, - { - "type": "blob", - "direction": "out", - "name": "outputfile2", - "dataType": "binary", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/shmem-test-bytes-out-2.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/emulator_tests/blob_functions/put_get_multiple_blobs_as_bytes_return_http_response/main.py b/tests/emulator_tests/blob_functions/put_get_multiple_blobs_as_bytes_return_http_response/main.py deleted file mode 100644 index 95710c9c5..000000000 --- a/tests/emulator_tests/blob_functions/put_get_multiple_blobs_as_bytes_return_http_response/main.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import hashlib -import json -import random - -import azure.functions as azf - - -def _generate_content_and_digest(content_size): - content = bytearray(random.getrandbits(8) for _ in range(content_size)) - content_sha256 = hashlib.sha256(content).hexdigest() - return content, content_sha256 - - -def main( - req: azf.HttpRequest, - inputfile1: bytes, - inputfile2: bytes, - outputfile1: azf.Out[bytes], - outputfile2: azf.Out[bytes]) -> azf.HttpResponse: - """ - Read two blobs (bytes) and respond back (in HTTP response) with the number - of bytes read from each blob and the MD5 digest of the content of each. - Write two blobs (bytes) and respond back (in HTTP response) with the number - bytes written in each blob and the MD5 digest of the content of each. - The number of bytes to write are specified in the input HTTP request. - """ - input_content_size_1 = len(inputfile1) - input_content_size_2 = len(inputfile2) - - input_content_sha256_1 = hashlib.sha256(inputfile1).hexdigest() - input_content_sha256_2 = hashlib.sha256(inputfile2).hexdigest() - - output_content_size_1 = int(req.params['output_content_size_1']) - output_content_size_2 = int(req.params['output_content_size_2']) - - output_content_1, output_content_sha256_1 = \ - _generate_content_and_digest(output_content_size_1) - output_content_2, output_content_sha256_2 = \ - _generate_content_and_digest(output_content_size_2) - - outputfile1.set(output_content_1) - outputfile2.set(output_content_2) - - response_dict = { - 'input_content_size_1': input_content_size_1, - 'input_content_size_2': input_content_size_2, - 'input_content_sha256_1': input_content_sha256_1, - 'input_content_sha256_2': input_content_sha256_2, - 'output_content_size_1': output_content_size_1, - 'output_content_size_2': output_content_size_2, - 'output_content_sha256_1': output_content_sha256_1, - 'output_content_sha256_2': output_content_sha256_2 - } - - response_body = json.dumps(response_dict, indent=2) - - return azf.HttpResponse( - body=response_body, - mimetype="application/json", - status_code=200 - ) diff --git a/tests/emulator_tests/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py b/tests/emulator_tests/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py deleted file mode 100644 index 0e4569132..000000000 --- a/tests/emulator_tests/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py +++ /dev/null @@ -1,126 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json -import os -import typing - -import azure.functions as func -from azure.eventhub import EventData, EventHubProducerClient - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -# This is an actual EventHub trigger which handles Eventhub events in batches. -# It serializes multiple event data into a json and store it into a blob. -@app.function_name(name="eventhub_multiple") -@app.event_hub_message_trigger( - arg_name="events", - event_hub_name="python-worker-ci-eventhub-batch", - 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: - table_entries = [] - for event in events: - json_entry = event.get_body() - table_entry = json.loads(json_entry) - table_entries.append(table_entry) - - table_json = json.dumps(table_entries) - - return table_json - - -# An HttpTrigger to generating EventHub event from EventHub Output Binding -@app.function_name(name="eventhub_output_batch") -@app.event_hub_output(arg_name="$return", - connection="AzureWebJobsEventHubConnectionString", - event_hub_name="python-worker-ci-eventhub-batch") -@app.route(route="eventhub_output_batch", binding_arg_name="out") -def eventhub_output_batch(req: func.HttpRequest, out: func.Out[str]) -> str: - events = req.get_body().decode('utf-8') - return events - - -# 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')) - - -# Retrieve the event data from storage blob and return it as Http response -@app.function_name(name="get_metadata_batch_triggered") -@app.route(route="get_metadata_batch_triggered") -@app.blob_input(arg_name="file", - path="python-worker-tests/test-metadata-batch-triggered.txt", - connection="AzureWebJobsStorage") -def get_metadata_batch_triggered(req: func.HttpRequest, - file: func.InputStream) -> str: - return func.HttpResponse(body=file.read().decode('utf-8'), - status_code=200, - mimetype='application/json') - - -# This is an actual EventHub trigger which handles Eventhub events in batches. -# It serializes multiple event data into a json and store it into a blob. -@app.function_name(name="metadata_multiple") -@app.event_hub_message_trigger( - arg_name="events", - event_hub_name="python-worker-ci-eventhub-batch-metadata", - connection="AzureWebJobsEventHubConnectionString", - data_type="binary", - cardinality="many") -@app.blob_output(arg_name="$return", - path="python-worker-tests/test-metadata-batch-triggered.txt", - connection="AzureWebJobsStorage") -def metadata_multiple(events: typing.List[func.EventHubEvent]) -> bytes: - event_list = [] - for event in events: - event_dict: typing.Mapping[str, typing.Any] = { - 'body': event.get_body().decode('utf-8'), - 'enqueued_time': event.enqueued_time.isoformat(), - 'partition_key': event.partition_key, - 'sequence_number': event.sequence_number, - 'offset': event.offset, - 'metadata': event.metadata - } - event_list.append(event_dict) - - return json.dumps(event_list) - - -# 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_batch") -@app.route(route="metadata_output_batch") -def main(req: func.HttpRequest): - # Get event count from http request query parameter - count = int(req.params.get('count', '1')) - - # 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-batch-metadata') - - # Generate new event based on http request with full metadata - event_data_batch = client.create_batch() - random_number = int(event_dict.get('body', '0')) - for i in range(count): - event_data_batch.add(EventData(str(random_number + i))) - - # Send out event into event hub - with client: - client.send_batch(event_data_batch) - - return 'OK' diff --git a/tests/emulator_tests/eventhub_batch_functions/eventhub_multiple/__init__.py b/tests/emulator_tests/eventhub_batch_functions/eventhub_multiple/__init__.py deleted file mode 100644 index ea0a96284..000000000 --- a/tests/emulator_tests/eventhub_batch_functions/eventhub_multiple/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json - - -# This is an actual EventHub trigger which handles Eventhub events in batches. -# It serializes multiple event data into a json and store it into a blob. -def main(events) -> str: - table_entries = [] - for event in events: - json_entry = event.get_body() - table_entry = json.loads(json_entry) - table_entries.append(table_entry) - - table_json = json.dumps(table_entries) - - return table_json diff --git a/tests/emulator_tests/eventhub_batch_functions/eventhub_multiple/function.json b/tests/emulator_tests/eventhub_batch_functions/eventhub_multiple/function.json deleted file mode 100644 index c4e9626da..000000000 --- a/tests/emulator_tests/eventhub_batch_functions/eventhub_multiple/function.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "scriptFile": "__init__.py", - - "bindings": [ - { - "type": "eventHubTrigger", - "name": "events", - "direction": "in", - "cardinality": "many", - "dataType": "string", - "eventHubName": "python-worker-ci-eventhub-batch", - "connection": "AzureWebJobsEventHubConnectionString" - }, - { - "direction": "out", - "type": "blob", - "name": "$return", - "path": "python-worker-tests/test-eventhub-batch-triggered.txt", - "connection": "AzureWebJobsStorage" - } - ] -} diff --git a/tests/emulator_tests/eventhub_batch_functions/eventhub_output_batch/__init__.py b/tests/emulator_tests/eventhub_batch_functions/eventhub_output_batch/__init__.py deleted file mode 100644 index 19be8848e..000000000 --- a/tests/emulator_tests/eventhub_batch_functions/eventhub_output_batch/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func - - -# An HttpTrigger to generating EventHub event from EventHub Output Binding -def main(req: func.HttpRequest) -> str: - events = req.get_body().decode('utf-8') - return events diff --git a/tests/emulator_tests/eventhub_batch_functions/eventhub_output_batch/function.json b/tests/emulator_tests/eventhub_batch_functions/eventhub_output_batch/function.json deleted file mode 100644 index 60de05e61..000000000 --- a/tests/emulator_tests/eventhub_batch_functions/eventhub_output_batch/function.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "scriptFile": "__init__.py", - - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "eventHub", - "name": "$return", - "direction": "out", - "eventHubName": "python-worker-ci-eventhub-batch", - "connection": "AzureWebJobsEventHubConnectionString" - } - ] -} diff --git a/tests/emulator_tests/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py b/tests/emulator_tests/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py deleted file mode 100644 index 153829b31..000000000 --- a/tests/emulator_tests/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func - - -# 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')) diff --git a/tests/emulator_tests/eventhub_batch_functions/get_eventhub_batch_triggered/function.json b/tests/emulator_tests/eventhub_batch_functions/get_eventhub_batch_triggered/function.json deleted file mode 100644 index 8ec2e9d65..000000000 --- a/tests/emulator_tests/eventhub_batch_functions/get_eventhub_batch_triggered/function.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "authLevel": "anonymous", - "methods": [ - "get" - ], - "name": "req" - }, - { - "direction": "in", - "type": "blob", - "name": "testEntities", - "path": "python-worker-tests/test-eventhub-batch-triggered.txt", - "connection": "AzureWebJobsStorage" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} \ No newline at end of file diff --git a/tests/emulator_tests/eventhub_batch_functions/get_metadata_batch_triggered/__init__.py b/tests/emulator_tests/eventhub_batch_functions/get_metadata_batch_triggered/__init__.py deleted file mode 100644 index a0cb5a619..000000000 --- a/tests/emulator_tests/eventhub_batch_functions/get_metadata_batch_triggered/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func - - -# Retrieve the event data from storage blob and return it as Http response -def main(req: func.HttpRequest, file: func.InputStream) -> str: - return func.HttpResponse(body=file.read().decode('utf-8'), - status_code=200, - mimetype='application/json') diff --git a/tests/emulator_tests/eventhub_batch_functions/get_metadata_batch_triggered/function.json b/tests/emulator_tests/eventhub_batch_functions/get_metadata_batch_triggered/function.json deleted file mode 100644 index 4de82ecd9..000000000 --- a/tests/emulator_tests/eventhub_batch_functions/get_metadata_batch_triggered/function.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "in", - "name": "file", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-metadata-batch-triggered.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/emulator_tests/eventhub_batch_functions/metadata_multiple/__init__.py b/tests/emulator_tests/eventhub_batch_functions/metadata_multiple/__init__.py deleted file mode 100644 index 3c9845d9f..000000000 --- a/tests/emulator_tests/eventhub_batch_functions/metadata_multiple/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import json -import typing - -import azure.functions as func - - -# This is an actual EventHub trigger which handles Eventhub events in batches. -# It serializes multiple event data into a json and store it into a blob. -def main(events: typing.List[func.EventHubEvent]) -> bytes: - event_list = [] - for event in events: - event_dict: typing.Mapping[str, typing.Any] = { - 'body': event.get_body().decode('utf-8'), - 'enqueued_time': event.enqueued_time.isoformat(), - 'partition_key': event.partition_key, - 'sequence_number': event.sequence_number, - 'offset': event.offset, - 'metadata': event.metadata - } - event_list.append(event_dict) - - return json.dumps(event_list) diff --git a/tests/emulator_tests/eventhub_batch_functions/metadata_multiple/function.json b/tests/emulator_tests/eventhub_batch_functions/metadata_multiple/function.json deleted file mode 100644 index a3bfaed41..000000000 --- a/tests/emulator_tests/eventhub_batch_functions/metadata_multiple/function.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "type": "eventHubTrigger", - "name": "events", - "direction": "in", - "cardinality": "many", - "dataType": "binary", - "eventHubName": "python-worker-ci-eventhub-batch-metadata", - "connection": "AzureWebJobsEventHubConnectionString" - }, - { - "type": "blob", - "direction": "out", - "name": "$return", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-metadata-batch-triggered.txt" - } - ] - } diff --git a/tests/emulator_tests/eventhub_batch_functions/metadata_output_batch/__init__.py b/tests/emulator_tests/eventhub_batch_functions/metadata_output_batch/__init__.py deleted file mode 100644 index 54322c2af..000000000 --- a/tests/emulator_tests/eventhub_batch_functions/metadata_output_batch/__init__.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import json -import os - -import azure.functions as func -from azure.eventhub import EventData, EventHubProducerClient - - -# An HttpTrigger to generating EventHub event from azure-eventhub SDK. -# Events generated from azure-eventhub contain the full metadata. -def main(req: func.HttpRequest): - # Get event count from http request query parameter - count = int(req.params.get('count', '1')) - - # 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-batch-metadata') - - # Generate new event based on http request with full metadata - event_data_batch = client.create_batch() - random_number = int(event_dict.get('body', '0')) - for i in range(count): - event_data_batch.add(EventData(str(random_number + i))) - - # Send out event into event hub - with client: - client.send_batch(event_data_batch) - - return 'OK' diff --git a/tests/emulator_tests/eventhub_batch_functions/metadata_output_batch/function.json b/tests/emulator_tests/eventhub_batch_functions/metadata_output_batch/function.json deleted file mode 100644 index 89747eb27..000000000 --- a/tests/emulator_tests/eventhub_batch_functions/metadata_output_batch/function.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "direction": "out", - "name": "$return", - "type": "http" - } - ] -} diff --git a/tests/emulator_tests/eventhub_functions/eventhub_functions_stein/function_app.py b/tests/emulator_tests/eventhub_functions/eventhub_functions_stein/function_app.py deleted file mode 100644 index 1481f7b55..000000000 --- a/tests/emulator_tests/eventhub_functions/eventhub_functions_stein/function_app.py +++ /dev/null @@ -1,107 +0,0 @@ -import json -import os -import typing - -import azure.functions as func -from azure.eventhub import EventData -from azure.eventhub.aio import EventHubProducerClient - -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/emulator_tests/eventhub_functions/eventhub_functions_stein/generic/function_app.py b/tests/emulator_tests/eventhub_functions/eventhub_functions_stein/generic/function_app.py deleted file mode 100644 index 1b2940488..000000000 --- a/tests/emulator_tests/eventhub_functions/eventhub_functions_stein/generic/function_app.py +++ /dev/null @@ -1,126 +0,0 @@ -import json -import os -import typing - -import azure.functions as func -from azure.eventhub import EventData -from azure.eventhub.aio import EventHubProducerClient - -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.generic_trigger(arg_name="req", type="httpTrigger", - route="eventhub_output") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_output_binding(arg_name="event", type="eventHub", - 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.generic_trigger(arg_name="event", - type="eventHubTrigger", - event_hub_name="python-worker-ci-eventhub-one", - connection="AzureWebJobsEventHubConnectionString") -@app.generic_output_binding( - arg_name="$return", - type="blob", - 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.generic_trigger(arg_name="req", - type="httpTrigger", - route="get_eventhub_triggered") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding( - arg_name="file", - type="blob", - 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.generic_trigger(arg_name="req", type="httpTrigger", - route="get_metadata_triggered") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding(arg_name="file", - type="blob", - 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.generic_trigger(arg_name="req", - type="httpTrigger", - route="metadata_output") -@app.generic_output_binding(arg_name="$return", type="http") -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.generic_trigger( - arg_name="event", - type="eventHubTrigger", - event_hub_name="python-worker-ci-eventhub-one-metadata", - connection="AzureWebJobsEventHubConnectionString") -@app.generic_output_binding( - arg_name="$return", - type="blob", - 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/emulator_tests/eventhub_functions/eventhub_output/__init__.py b/tests/emulator_tests/eventhub_functions/eventhub_output/__init__.py deleted file mode 100644 index 9a41012b7..000000000 --- a/tests/emulator_tests/eventhub_functions/eventhub_output/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func - - -# An HttpTrigger to generating EventHub event from EventHub Output Binding -def main(req: func.HttpRequest, event: func.Out[str]): - event.set(req.get_body().decode('utf-8')) - - return 'OK' diff --git a/tests/emulator_tests/eventhub_functions/eventhub_output/function.json b/tests/emulator_tests/eventhub_functions/eventhub_output/function.json deleted file mode 100644 index ec96c1617..000000000 --- a/tests/emulator_tests/eventhub_functions/eventhub_output/function.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "scriptFile": "__init__.py", - - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "eventHub", - "name": "event", - "direction": "out", - "eventHubName": "python-worker-ci-eventhub-one", - "connection": "AzureWebJobsEventHubConnectionString" - }, - { - "direction": "out", - "name": "$return", - "type": "http" - } - ] -} diff --git a/tests/emulator_tests/eventhub_functions/eventhub_trigger/__init__.py b/tests/emulator_tests/eventhub_functions/eventhub_trigger/__init__.py deleted file mode 100644 index bc177d499..000000000 --- a/tests/emulator_tests/eventhub_functions/eventhub_trigger/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func - - -# This is an actual EventHub trigger which will convert the event data -# into a storage blob. -def main(event: func.EventHubEvent) -> bytes: - return event.get_body() diff --git a/tests/emulator_tests/eventhub_functions/eventhub_trigger/function.json b/tests/emulator_tests/eventhub_functions/eventhub_trigger/function.json deleted file mode 100644 index f8d15f4e6..000000000 --- a/tests/emulator_tests/eventhub_functions/eventhub_trigger/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "__init__.py", - - "bindings": [ - { - "type": "eventHubTrigger", - "name": "event", - "direction": "in", - "eventHubName": "python-worker-ci-eventhub-one", - "connection": "AzureWebJobsEventHubConnectionString" - }, - { - "type": "blob", - "direction": "out", - "name": "$return", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-eventhub-triggered.txt" - } - ] -} diff --git a/tests/emulator_tests/eventhub_functions/get_eventhub_triggered/function.json b/tests/emulator_tests/eventhub_functions/get_eventhub_triggered/function.json deleted file mode 100644 index 4328b71a5..000000000 --- a/tests/emulator_tests/eventhub_functions/get_eventhub_triggered/function.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "in", - "name": "file", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-eventhub-triggered.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/emulator_tests/eventhub_functions/get_eventhub_triggered/main.py b/tests/emulator_tests/eventhub_functions/get_eventhub_triggered/main.py deleted file mode 100644 index 78ba500ab..000000000 --- a/tests/emulator_tests/eventhub_functions/get_eventhub_triggered/main.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func - - -# Retrieve the event data from storage blob and return it as Http response -def main(req: func.HttpRequest, file: func.InputStream) -> str: - return file.read().decode('utf-8') diff --git a/tests/emulator_tests/eventhub_functions/get_metadata_triggered/__init__.py b/tests/emulator_tests/eventhub_functions/get_metadata_triggered/__init__.py deleted file mode 100644 index 597270397..000000000 --- a/tests/emulator_tests/eventhub_functions/get_metadata_triggered/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func - - -# Retrieve the event data from storage blob and return it as Http response -async def main(req: func.HttpRequest, file: func.InputStream) -> str: - return func.HttpResponse(body=file.read().decode('utf-8'), - status_code=200, - mimetype='application/json') diff --git a/tests/emulator_tests/eventhub_functions/get_metadata_triggered/function.json b/tests/emulator_tests/eventhub_functions/get_metadata_triggered/function.json deleted file mode 100644 index 4244ca821..000000000 --- a/tests/emulator_tests/eventhub_functions/get_metadata_triggered/function.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "in", - "name": "file", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-metadata-triggered.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/emulator_tests/eventhub_functions/metadata_output/__init__.py b/tests/emulator_tests/eventhub_functions/metadata_output/__init__.py deleted file mode 100644 index e02c79f13..000000000 --- a/tests/emulator_tests/eventhub_functions/metadata_output/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import json -import os - -import azure.functions as func -from azure.eventhub import EventData -from azure.eventhub.aio import EventHubProducerClient - - -# An HttpTrigger to generating EventHub event from azure-eventhub SDK. -# Events generated from azure-eventhub contain the full metadata. -async def main(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' diff --git a/tests/emulator_tests/eventhub_functions/metadata_output/function.json b/tests/emulator_tests/eventhub_functions/metadata_output/function.json deleted file mode 100644 index 9b4018660..000000000 --- a/tests/emulator_tests/eventhub_functions/metadata_output/function.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "scriptFile": "__init__.py", - - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "direction": "out", - "name": "$return", - "type": "http" - } - ] -} diff --git a/tests/emulator_tests/eventhub_functions/metadata_trigger/__init__.py b/tests/emulator_tests/eventhub_functions/metadata_trigger/__init__.py deleted file mode 100644 index 5088e6637..000000000 --- a/tests/emulator_tests/eventhub_functions/metadata_trigger/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import json -import typing - -import azure.functions as func - - -# This is an actual EventHub trigger. It picks a few of EventHub properties -# and converts them into a storage blob -async def main(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/emulator_tests/eventhub_functions/metadata_trigger/function.json b/tests/emulator_tests/eventhub_functions/metadata_trigger/function.json deleted file mode 100644 index 9a3b2e13c..000000000 --- a/tests/emulator_tests/eventhub_functions/metadata_trigger/function.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "type": "eventHubTrigger", - "name": "event", - "direction": "in", - "eventHubName": "python-worker-ci-eventhub-one-metadata", - "connection": "AzureWebJobsEventHubConnectionString" - }, - { - "type": "blob", - "direction": "out", - "name": "$return", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-metadata-triggered.txt" - } - ] -} diff --git a/tests/emulator_tests/generic_functions/generic_functions_stein/function_app.py b/tests/emulator_tests/generic_functions/generic_functions_stein/function_app.py deleted file mode 100644 index 2da6d44ca..000000000 --- a/tests/emulator_tests/generic_functions/generic_functions_stein/function_app.py +++ /dev/null @@ -1,159 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json -import logging -import uuid - -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.function_name(name="return_processed_last") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="return_processed_last") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding( - arg_name="testEntity", - type="table", - connection="AzureWebJobsStorage", - table_name="BindingTestTable") -def return_processed_last(req: func.HttpRequest, testEntity): - return func.HttpResponse(status_code=200) - - -@app.function_name(name="return_not_processed_last") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="return_not_processed_last") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding( - arg_name="testEntities", - type="table", - connection="AzureWebJobsStorage", - table_name="BindingTestTable") -def return_not_processed_last(req: func.HttpRequest, testEntities): - return func.HttpResponse(status_code=200) - - -@app.function_name(name="mytimer") -@app.schedule(schedule="*/1 * * * * *", arg_name="mytimer", - run_on_startup=False, - use_monitor=False) -@app.generic_input_binding( - arg_name="testEntity", - type="table", - connection="AzureWebJobsStorage", - table_name="BindingTestTable") -def mytimer(mytimer: func.TimerRequest, testEntity) -> None: - logging.info("This timer trigger function executed successfully") - - -@app.function_name(name="return_string") -@app.schedule(schedule="*/1 * * * * *", arg_name="mytimer", - run_on_startup=False, - use_monitor=False) -@app.generic_input_binding( - arg_name="testEntity", - type="table", - connection="AzureWebJobsStorage", - table_name="BindingTestTable") -def return_string(mytimer: func.TimerRequest, testEntity): - logging.info("Return string") - return "hi!" - - -@app.function_name(name="return_bytes") -@app.schedule(schedule="*/1 * * * * *", arg_name="mytimer", - run_on_startup=False, - use_monitor=False) -@app.generic_input_binding( - arg_name="testEntity", - type="table", - connection="AzureWebJobsStorage", - table_name="BindingTestTable") -def return_bytes(mytimer: func.TimerRequest, testEntity): - logging.info("Return bytes") - return "test-dată" - - -@app.function_name(name="return_dict") -@app.schedule(schedule="*/1 * * * * *", arg_name="mytimer", - run_on_startup=False, - use_monitor=False) -@app.generic_input_binding( - arg_name="testEntity", - type="table", - connection="AzureWebJobsStorage", - table_name="BindingTestTable") -def return_dict(mytimer: func.TimerRequest, testEntity): - logging.info("Return dict") - return {"hello": "world"} - - -@app.function_name(name="return_list") -@app.schedule(schedule="*/1 * * * * *", arg_name="mytimer", - run_on_startup=False, - use_monitor=False) -@app.generic_input_binding( - arg_name="testEntity", - type="table", - connection="AzureWebJobsStorage", - table_name="BindingTestTable") -def return_list(mytimer: func.TimerRequest, testEntity): - logging.info("Return list") - return [1, 2, 3] - - -@app.function_name(name="return_int") -@app.schedule(schedule="*/1 * * * * *", arg_name="mytimer", - run_on_startup=False, - use_monitor=False) -@app.generic_input_binding( - arg_name="testEntity", - type="table", - connection="AzureWebJobsStorage", - table_name="BindingTestTable") -def return_int(mytimer: func.TimerRequest, testEntity): - logging.info("Return int") - return 12 - - -@app.function_name(name="return_double") -@app.schedule(schedule="*/1 * * * * *", arg_name="mytimer", - run_on_startup=False, - use_monitor=False) -@app.generic_input_binding( - arg_name="testEntity", - type="table", - connection="AzureWebJobsStorage", - table_name="BindingTestTable") -def return_double(mytimer: func.TimerRequest, testEntity): - logging.info("Return double") - return 12.34 - - -@app.function_name(name="return_bool") -@app.schedule(schedule="*/1 * * * * *", arg_name="mytimer", - run_on_startup=False, - use_monitor=False) -@app.generic_input_binding( - arg_name="testEntity", - type="table", - connection="AzureWebJobsStorage", - table_name="BindingTestTable") -def return_bool(mytimer: func.TimerRequest, testEntity): - logging.info("Return bool") - return True - - -@app.function_name(name="table_out_binding") -@app.route(route="table_out_binding", binding_arg_name="resp") -@app.table_output(arg_name="$return", - connection="AzureWebJobsStorage", - table_name="BindingTestTable") -def table_out_binding(req: func.HttpRequest, resp: func.Out[func.HttpResponse]): - row_key_uuid = str(uuid.uuid4()) - table_dict = {'PartitionKey': 'test', 'RowKey': row_key_uuid} - table_json = json.dumps(table_dict) - resp.set(table_json) - return table_json diff --git a/tests/emulator_tests/generic_functions/return_bool/function.json b/tests/emulator_tests/generic_functions/return_bool/function.json deleted file mode 100644 index 4dc852e37..000000000 --- a/tests/emulator_tests/generic_functions/return_bool/function.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "name": "mytimer", - "type": "timerTrigger", - "direction": "in", - "schedule": "*/1 * * * * *", - "runOnStartup": false - }, - { - "direction": "in", - "type": "table", - "name": "testEntity", - "partitionKey": "test", - "rowKey": "WillBePopulatedWithGuid", - "tableName": "BindingTestTable", - "connection": "AzureWebJobsStorage" - } - ] -} \ No newline at end of file diff --git a/tests/emulator_tests/generic_functions/return_bool/main.py b/tests/emulator_tests/generic_functions/return_bool/main.py deleted file mode 100644 index 08d693dff..000000000 --- a/tests/emulator_tests/generic_functions/return_bool/main.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging - -import azure.functions as func - - -def main(mytimer: func.TimerRequest, testEntity): - logging.info("Return bool") - return True diff --git a/tests/emulator_tests/generic_functions/return_bytes/function.json b/tests/emulator_tests/generic_functions/return_bytes/function.json deleted file mode 100644 index 4dc852e37..000000000 --- a/tests/emulator_tests/generic_functions/return_bytes/function.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "name": "mytimer", - "type": "timerTrigger", - "direction": "in", - "schedule": "*/1 * * * * *", - "runOnStartup": false - }, - { - "direction": "in", - "type": "table", - "name": "testEntity", - "partitionKey": "test", - "rowKey": "WillBePopulatedWithGuid", - "tableName": "BindingTestTable", - "connection": "AzureWebJobsStorage" - } - ] -} \ No newline at end of file diff --git a/tests/emulator_tests/generic_functions/return_bytes/main.py b/tests/emulator_tests/generic_functions/return_bytes/main.py deleted file mode 100644 index c02b678c0..000000000 --- a/tests/emulator_tests/generic_functions/return_bytes/main.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging - -import azure.functions as func - - -def main(mytimer: func.TimerRequest, testEntity): - logging.info("Return bytes") - return "test-dată" diff --git a/tests/emulator_tests/generic_functions/return_dict/function.json b/tests/emulator_tests/generic_functions/return_dict/function.json deleted file mode 100644 index 4dc852e37..000000000 --- a/tests/emulator_tests/generic_functions/return_dict/function.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "name": "mytimer", - "type": "timerTrigger", - "direction": "in", - "schedule": "*/1 * * * * *", - "runOnStartup": false - }, - { - "direction": "in", - "type": "table", - "name": "testEntity", - "partitionKey": "test", - "rowKey": "WillBePopulatedWithGuid", - "tableName": "BindingTestTable", - "connection": "AzureWebJobsStorage" - } - ] -} \ No newline at end of file diff --git a/tests/emulator_tests/generic_functions/return_dict/main.py b/tests/emulator_tests/generic_functions/return_dict/main.py deleted file mode 100644 index 27f343fcb..000000000 --- a/tests/emulator_tests/generic_functions/return_dict/main.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging - -import azure.functions as func - - -def main(mytimer: func.TimerRequest, testEntity): - logging.info("Return dict") - return {"hello": "world"} diff --git a/tests/emulator_tests/generic_functions/return_double/function.json b/tests/emulator_tests/generic_functions/return_double/function.json deleted file mode 100644 index 4dc852e37..000000000 --- a/tests/emulator_tests/generic_functions/return_double/function.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "name": "mytimer", - "type": "timerTrigger", - "direction": "in", - "schedule": "*/1 * * * * *", - "runOnStartup": false - }, - { - "direction": "in", - "type": "table", - "name": "testEntity", - "partitionKey": "test", - "rowKey": "WillBePopulatedWithGuid", - "tableName": "BindingTestTable", - "connection": "AzureWebJobsStorage" - } - ] -} \ No newline at end of file diff --git a/tests/emulator_tests/generic_functions/return_double/main.py b/tests/emulator_tests/generic_functions/return_double/main.py deleted file mode 100644 index 6bfc4b9d7..000000000 --- a/tests/emulator_tests/generic_functions/return_double/main.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging - -import azure.functions as func - - -def main(mytimer: func.TimerRequest, testEntity): - logging.info("Return double") - return 12.34 diff --git a/tests/emulator_tests/generic_functions/return_int/function.json b/tests/emulator_tests/generic_functions/return_int/function.json deleted file mode 100644 index 54c81f8f3..000000000 --- a/tests/emulator_tests/generic_functions/return_int/function.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "name": "mytimer", - "type": "timerTrigger", - "direction": "in", - "schedule": "*/1 * * * * *", - "runOnStartup": false - }, - { - "direction": "in", - "type": "table", - "name": "testEntity", - "partitionKey": "test", - "rowKey": "WillBePopulatedWithGuid", - "tableName": "BindingTestTable", - "connection": "AzureWebJobsStorage" - } - ] -} diff --git a/tests/emulator_tests/generic_functions/return_int/main.py b/tests/emulator_tests/generic_functions/return_int/main.py deleted file mode 100644 index 3a5e7175d..000000000 --- a/tests/emulator_tests/generic_functions/return_int/main.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging - -import azure.functions as func - - -def main(mytimer: func.TimerRequest, testEntity): - logging.info("Return int") - return 12 diff --git a/tests/emulator_tests/generic_functions/return_list/function.json b/tests/emulator_tests/generic_functions/return_list/function.json deleted file mode 100644 index 4dc852e37..000000000 --- a/tests/emulator_tests/generic_functions/return_list/function.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "name": "mytimer", - "type": "timerTrigger", - "direction": "in", - "schedule": "*/1 * * * * *", - "runOnStartup": false - }, - { - "direction": "in", - "type": "table", - "name": "testEntity", - "partitionKey": "test", - "rowKey": "WillBePopulatedWithGuid", - "tableName": "BindingTestTable", - "connection": "AzureWebJobsStorage" - } - ] -} \ No newline at end of file diff --git a/tests/emulator_tests/generic_functions/return_list/main.py b/tests/emulator_tests/generic_functions/return_list/main.py deleted file mode 100644 index feccec7e2..000000000 --- a/tests/emulator_tests/generic_functions/return_list/main.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging - -import azure.functions as func - - -def main(mytimer: func.TimerRequest, testEntity): - logging.info("Return list") - return [1, 2, 3] diff --git a/tests/emulator_tests/generic_functions/return_none/function.json b/tests/emulator_tests/generic_functions/return_none/function.json deleted file mode 100644 index 4dc852e37..000000000 --- a/tests/emulator_tests/generic_functions/return_none/function.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "name": "mytimer", - "type": "timerTrigger", - "direction": "in", - "schedule": "*/1 * * * * *", - "runOnStartup": false - }, - { - "direction": "in", - "type": "table", - "name": "testEntity", - "partitionKey": "test", - "rowKey": "WillBePopulatedWithGuid", - "tableName": "BindingTestTable", - "connection": "AzureWebJobsStorage" - } - ] -} \ No newline at end of file diff --git a/tests/emulator_tests/generic_functions/return_none/main.py b/tests/emulator_tests/generic_functions/return_none/main.py deleted file mode 100644 index 8f52c716b..000000000 --- a/tests/emulator_tests/generic_functions/return_none/main.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging - -import azure.functions as func - - -def main(mytimer: func.TimerRequest, testEntity) -> None: - logging.info("This timer trigger function executed successfully") diff --git a/tests/emulator_tests/generic_functions/return_none_no_type_hint/function.json b/tests/emulator_tests/generic_functions/return_none_no_type_hint/function.json deleted file mode 100644 index 4dc852e37..000000000 --- a/tests/emulator_tests/generic_functions/return_none_no_type_hint/function.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "name": "mytimer", - "type": "timerTrigger", - "direction": "in", - "schedule": "*/1 * * * * *", - "runOnStartup": false - }, - { - "direction": "in", - "type": "table", - "name": "testEntity", - "partitionKey": "test", - "rowKey": "WillBePopulatedWithGuid", - "tableName": "BindingTestTable", - "connection": "AzureWebJobsStorage" - } - ] -} \ No newline at end of file diff --git a/tests/emulator_tests/generic_functions/return_none_no_type_hint/main.py b/tests/emulator_tests/generic_functions/return_none_no_type_hint/main.py deleted file mode 100644 index 69877f6d8..000000000 --- a/tests/emulator_tests/generic_functions/return_none_no_type_hint/main.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging - -import azure.functions as func - - -def main(mytimer: func.TimerRequest, testEntity): - logging.info("Timer trigger with none return and no type hint") diff --git a/tests/emulator_tests/generic_functions/return_not_processed_last/__init__.py b/tests/emulator_tests/generic_functions/return_not_processed_last/__init__.py deleted file mode 100644 index 300fae398..000000000 --- a/tests/emulator_tests/generic_functions/return_not_processed_last/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func - - -# There are 3 bindings defined in function.json: -# 1. req: HTTP trigger -# 2. testEntities: table input (generic) -# 3. $return: HTTP response -# The bindings will be processed by the worker in this order: -# req -> $return -> testEntities -def main(req: func.HttpRequest, testEntities): - return func.HttpResponse(status_code=200) diff --git a/tests/emulator_tests/generic_functions/return_not_processed_last/function.json b/tests/emulator_tests/generic_functions/return_not_processed_last/function.json deleted file mode 100644 index e02ae4d15..000000000 --- a/tests/emulator_tests/generic_functions/return_not_processed_last/function.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "authLevel": "anonymous", - "methods": [ - "get" - ], - "name": "req" - }, - { - "direction": "in", - "type": "table", - "name": "testEntities", - "tableName": "BindingTestTable", - "connection": "AzureWebJobsStorage" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} \ No newline at end of file diff --git a/tests/emulator_tests/generic_functions/return_processed_last/__init__.py b/tests/emulator_tests/generic_functions/return_processed_last/__init__.py deleted file mode 100644 index 3d8f56122..000000000 --- a/tests/emulator_tests/generic_functions/return_processed_last/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func - - -# There are 3 bindings defined in function.json: -# 1. req: HTTP trigger -# 2. testEntity: table input (generic) -# 3. $return: HTTP response -# The bindings will be processed by the worker in this order: -# req -> testEntity -> $return -def main(req: func.HttpRequest, testEntity): - return func.HttpResponse(status_code=200) diff --git a/tests/emulator_tests/generic_functions/return_processed_last/function.json b/tests/emulator_tests/generic_functions/return_processed_last/function.json deleted file mode 100644 index d23f01a86..000000000 --- a/tests/emulator_tests/generic_functions/return_processed_last/function.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "authLevel": "anonymous", - "methods": [ - "get" - ], - "name": "req" - }, - { - "direction": "in", - "type": "table", - "name": "testEntity", - "tableName": "BindingTestTable", - "connection": "AzureWebJobsStorage" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} \ No newline at end of file diff --git a/tests/emulator_tests/generic_functions/return_string/function.json b/tests/emulator_tests/generic_functions/return_string/function.json deleted file mode 100644 index 4dc852e37..000000000 --- a/tests/emulator_tests/generic_functions/return_string/function.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "name": "mytimer", - "type": "timerTrigger", - "direction": "in", - "schedule": "*/1 * * * * *", - "runOnStartup": false - }, - { - "direction": "in", - "type": "table", - "name": "testEntity", - "partitionKey": "test", - "rowKey": "WillBePopulatedWithGuid", - "tableName": "BindingTestTable", - "connection": "AzureWebJobsStorage" - } - ] -} \ No newline at end of file diff --git a/tests/emulator_tests/generic_functions/return_string/main.py b/tests/emulator_tests/generic_functions/return_string/main.py deleted file mode 100644 index 02f7aa432..000000000 --- a/tests/emulator_tests/generic_functions/return_string/main.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging - -import azure.functions as func - - -def main(mytimer: func.TimerRequest, testEntity): - logging.info("Return string") - return "hi!" diff --git a/tests/emulator_tests/generic_functions/table_out_binding/__init__.py b/tests/emulator_tests/generic_functions/table_out_binding/__init__.py deleted file mode 100644 index 09c7058e9..000000000 --- a/tests/emulator_tests/generic_functions/table_out_binding/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json -import uuid -import azure.functions as func - - -def main(req: func.HttpRequest, resp: func.Out[func.HttpResponse]): - row_key_uuid = str(uuid.uuid4()) - table_dict = {'PartitionKey': 'test', 'RowKey': row_key_uuid} - table_json = json.dumps(table_dict) - resp.set(table_json) - return table_json diff --git a/tests/emulator_tests/generic_functions/table_out_binding/function.json b/tests/emulator_tests/generic_functions/table_out_binding/function.json deleted file mode 100644 index 25537873a..000000000 --- a/tests/emulator_tests/generic_functions/table_out_binding/function.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "authLevel": "anonymous", - "methods": ["post"], - "name": "req" - }, - { - "direction": "out", - "type": "table", - "name": "$return", - "tableName": "BindingTestTable", - "connection": "AzureWebJobsStorage" - }, - { - "name": "resp", - "type": "http", - "direction": "out" - } - ] - } \ No newline at end of file diff --git a/tests/emulator_tests/queue_functions/get_queue_blob/function.json b/tests/emulator_tests/queue_functions/get_queue_blob/function.json deleted file mode 100644 index 4abc22167..000000000 --- a/tests/emulator_tests/queue_functions/get_queue_blob/function.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "in", - "name": "file", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-queue-blob.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return", - } - ] -} diff --git a/tests/emulator_tests/queue_functions/get_queue_blob/main.py b/tests/emulator_tests/queue_functions/get_queue_blob/main.py deleted file mode 100644 index b82ec6efd..000000000 --- a/tests/emulator_tests/queue_functions/get_queue_blob/main.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json - -import azure.functions as azf - - -def main(req: azf.HttpRequest, file: azf.InputStream) -> str: - return json.dumps({ - 'queue': json.loads(file.read().decode('utf-8')) - }) diff --git a/tests/emulator_tests/queue_functions/get_queue_blob_message_return/function.json b/tests/emulator_tests/queue_functions/get_queue_blob_message_return/function.json deleted file mode 100644 index 7f040a6be..000000000 --- a/tests/emulator_tests/queue_functions/get_queue_blob_message_return/function.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "in", - "name": "file", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-queue-blob-message-return.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return", - } - ] -} diff --git a/tests/emulator_tests/queue_functions/get_queue_blob_message_return/main.py b/tests/emulator_tests/queue_functions/get_queue_blob_message_return/main.py deleted file mode 100644 index 46dc607e2..000000000 --- a/tests/emulator_tests/queue_functions/get_queue_blob_message_return/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest, file: azf.InputStream) -> str: - return file.read().decode('utf-8') diff --git a/tests/emulator_tests/queue_functions/get_queue_blob_return/function.json b/tests/emulator_tests/queue_functions/get_queue_blob_return/function.json deleted file mode 100644 index ce3b8c8c2..000000000 --- a/tests/emulator_tests/queue_functions/get_queue_blob_return/function.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "in", - "name": "file", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-queue-blob-return.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return", - } - ] -} diff --git a/tests/emulator_tests/queue_functions/get_queue_blob_return/main.py b/tests/emulator_tests/queue_functions/get_queue_blob_return/main.py deleted file mode 100644 index 46dc607e2..000000000 --- a/tests/emulator_tests/queue_functions/get_queue_blob_return/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest, file: azf.InputStream) -> str: - return file.read().decode('utf-8') diff --git a/tests/emulator_tests/queue_functions/get_queue_untyped_blob_return/function.json b/tests/emulator_tests/queue_functions/get_queue_untyped_blob_return/function.json deleted file mode 100644 index f0f0b8c6c..000000000 --- a/tests/emulator_tests/queue_functions/get_queue_untyped_blob_return/function.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "scriptFile": "main.py", - "disabled": false, - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "in", - "name": "file", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-queue-untyped-blob-return.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/emulator_tests/queue_functions/get_queue_untyped_blob_return/main.py b/tests/emulator_tests/queue_functions/get_queue_untyped_blob_return/main.py deleted file mode 100644 index 46dc607e2..000000000 --- a/tests/emulator_tests/queue_functions/get_queue_untyped_blob_return/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest, file: azf.InputStream) -> str: - return file.read().decode('utf-8') diff --git a/tests/emulator_tests/queue_functions/put_queue/function.json b/tests/emulator_tests/queue_functions/put_queue/function.json deleted file mode 100644 index b8e03f2be..000000000 --- a/tests/emulator_tests/queue_functions/put_queue/function.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "scriptFile": "main.py", - - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "direction": "out", - "name": "msg", - "queueName": "testqueue", - "connection": "AzureWebJobsStorage", - "type": "queue" - }, - { - "direction": "out", - "name": "$return", - "type": "http" - } - ] -} diff --git a/tests/emulator_tests/queue_functions/put_queue/main.py b/tests/emulator_tests/queue_functions/put_queue/main.py deleted file mode 100644 index fde178e41..000000000 --- a/tests/emulator_tests/queue_functions/put_queue/main.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest, msg: azf.Out[str]): - msg.set(req.get_body()) - - return 'OK' diff --git a/tests/emulator_tests/queue_functions/put_queue_message_return/function.json b/tests/emulator_tests/queue_functions/put_queue_message_return/function.json deleted file mode 100644 index ce1de0819..000000000 --- a/tests/emulator_tests/queue_functions/put_queue_message_return/function.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "scriptFile": "main.py", - - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "direction": "out", - "name": "$return", - "queueName": "testqueue-message-return", - "connection": "AzureWebJobsStorage", - "type": "queue" - } - ] -} diff --git a/tests/emulator_tests/queue_functions/put_queue_message_return/main.py b/tests/emulator_tests/queue_functions/put_queue_message_return/main.py deleted file mode 100644 index 3550166f3..000000000 --- a/tests/emulator_tests/queue_functions/put_queue_message_return/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest) -> bytes: - return azf.QueueMessage(body=req.get_body()) diff --git a/tests/emulator_tests/queue_functions/put_queue_multiple_out/function.json b/tests/emulator_tests/queue_functions/put_queue_multiple_out/function.json deleted file mode 100644 index 7fb98a3c2..000000000 --- a/tests/emulator_tests/queue_functions/put_queue_multiple_out/function.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "scriptFile": "main.py", - - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "name": "resp", - "type": "http", - "direction": "out" - }, - { - "direction": "out", - "name": "msg", - "queueName": "testqueue-return-multiple-outparam", - "connection": "AzureWebJobsStorage", - "type": "queue" - } - ] -} diff --git a/tests/emulator_tests/queue_functions/put_queue_multiple_out/main.py b/tests/emulator_tests/queue_functions/put_queue_multiple_out/main.py deleted file mode 100644 index afb174337..000000000 --- a/tests/emulator_tests/queue_functions/put_queue_multiple_out/main.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func - - -def main(req: func.HttpRequest, resp: func.Out[func.HttpResponse], - msg: func.Out[func.QueueMessage]) -> None: - data = req.get_body().decode() - msg.set(func.QueueMessage(body=data)) - resp.set(func.HttpResponse(body='HTTP response: {}'.format(data))) diff --git a/tests/emulator_tests/queue_functions/put_queue_return/function.json b/tests/emulator_tests/queue_functions/put_queue_return/function.json deleted file mode 100644 index 129b7cb20..000000000 --- a/tests/emulator_tests/queue_functions/put_queue_return/function.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "scriptFile": "main.py", - - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "direction": "out", - "name": "$return", - "queueName": "testqueue-return", - "connection": "AzureWebJobsStorage", - "type": "queue" - } - ] -} diff --git a/tests/emulator_tests/queue_functions/put_queue_return/main.py b/tests/emulator_tests/queue_functions/put_queue_return/main.py deleted file mode 100644 index 21f3b275b..000000000 --- a/tests/emulator_tests/queue_functions/put_queue_return/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest) -> bytes: - return req.get_body() diff --git a/tests/emulator_tests/queue_functions/put_queue_return_multiple/function.json b/tests/emulator_tests/queue_functions/put_queue_return_multiple/function.json deleted file mode 100644 index cc1f2fc14..000000000 --- a/tests/emulator_tests/queue_functions/put_queue_return_multiple/function.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "scriptFile": "main.py", - - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "direction": "out", - "name": "msgs", - "queueName": "testqueue-return-multiple", - "connection": "AzureWebJobsStorage", - "type": "queue" - } - ] -} diff --git a/tests/emulator_tests/queue_functions/put_queue_return_multiple/main.py b/tests/emulator_tests/queue_functions/put_queue_return_multiple/main.py deleted file mode 100644 index 93152d7bb..000000000 --- a/tests/emulator_tests/queue_functions/put_queue_return_multiple/main.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import typing - -import azure.functions as azf - - -def main(req: azf.HttpRequest, msgs: azf.Out[typing.List[str]]): - msgs.set(['one', 'two']) diff --git a/tests/emulator_tests/queue_functions/put_queue_untyped_return/function.json b/tests/emulator_tests/queue_functions/put_queue_untyped_return/function.json deleted file mode 100644 index 8dee2e9c5..000000000 --- a/tests/emulator_tests/queue_functions/put_queue_untyped_return/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "main.py", - "disabled": false, - - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "direction": "out", - "name": "$return", - "queueName": "testqueue-untyped-return", - "connection": "AzureWebJobsStorage", - "type": "queue" - } - ] -} diff --git a/tests/emulator_tests/queue_functions/put_queue_untyped_return/main.py b/tests/emulator_tests/queue_functions/put_queue_untyped_return/main.py deleted file mode 100644 index 3550166f3..000000000 --- a/tests/emulator_tests/queue_functions/put_queue_untyped_return/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest) -> bytes: - return azf.QueueMessage(body=req.get_body()) diff --git a/tests/emulator_tests/queue_functions/queue_functions_stein/function_app.py b/tests/emulator_tests/queue_functions/queue_functions_stein/function_app.py deleted file mode 100644 index 087cf4592..000000000 --- a/tests/emulator_tests/queue_functions/queue_functions_stein/function_app.py +++ /dev/null @@ -1,185 +0,0 @@ -import json -import logging -import typing - -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.function_name(name="get_queue_blob") -@app.route(route="get_queue_blob") -@app.blob_input(arg_name="file", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-queue-blob.txt") -def get_queue_blob(req: func.HttpRequest, file: func.InputStream) -> str: - return json.dumps({ - 'queue': json.loads(file.read().decode('utf-8')) - }) - - -@app.function_name(name="get_queue_blob_message_return") -@app.route(route="get_queue_blob_message_return") -@app.blob_input(arg_name="file", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-queue-blob-message-return.txt") -def get_queue_blob_message_return(req: func.HttpRequest, - file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -@app.function_name(name="get_queue_blob_return") -@app.route(route="get_queue_blob_return") -@app.blob_input(arg_name="file", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-queue-blob-return.txt") -def get_queue_blob_return(req: func.HttpRequest, file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -@app.function_name(name="get_queue_untyped_blob_return") -@app.route(route="get_queue_untyped_blob_return") -@app.blob_input(arg_name="file", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-queue-untyped-blob-return.txt") -def get_queue_untyped_blob_return(req: func.HttpRequest, - file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -@app.function_name(name="put_queue") -@app.route(route="put_queue") -@app.queue_output(arg_name="msg", - connection="AzureWebJobsStorage", - queue_name="testqueue") -def put_queue(req: func.HttpRequest, msg: func.Out[str]): - msg.set(req.get_body()) - - return 'OK' - - -@app.function_name(name="put_queue_message_return") -@app.route(route="put_queue_message_return", binding_arg_name="resp") -@app.queue_output(arg_name="$return", - connection="AzureWebJobsStorage", - queue_name="testqueue-message-return") -def main(req: func.HttpRequest, resp: func.Out[str]) -> bytes: - return func.QueueMessage(body=req.get_body()) - - -@app.function_name("put_queue_multiple_out") -@app.route(route="put_queue_multiple_out", binding_arg_name="resp") -@app.queue_output(arg_name="msg", - connection="AzureWebJobsStorage", - queue_name="testqueue-return-multiple-outparam") -def put_queue_multiple_out(req: func.HttpRequest, - resp: func.Out[func.HttpResponse], - msg: func.Out[func.QueueMessage]) -> None: - data = req.get_body().decode() - msg.set(func.QueueMessage(body=data)) - resp.set(func.HttpResponse(body='HTTP response: {}'.format(data))) - - -@app.function_name("put_queue_return") -@app.route(route="put_queue_return", binding_arg_name="resp") -@app.queue_output(arg_name="$return", - connection="AzureWebJobsStorage", - queue_name="testqueue-return") -def put_queue_return(req: func.HttpRequest, resp: func.Out[str]) -> bytes: - return req.get_body() - - -@app.function_name(name="put_queue_multiple_return") -@app.route(route="put_queue_multiple_return") -@app.queue_output(arg_name="msgs", - connection="AzureWebJobsStorage", - queue_name="testqueue-return-multiple") -def put_queue_multiple_return(req: func.HttpRequest, - msgs: func.Out[typing.List[str]]): - msgs.set(['one', 'two']) - - -@app.function_name(name="put_queue_untyped_return") -@app.route(route="put_queue_untyped_return", binding_arg_name="resp") -@app.queue_output(arg_name="$return", - connection="AzureWebJobsStorage", - queue_name="testqueue-untyped-return") -def put_queue_untyped_return(req: func.HttpRequest, - resp: func.Out[str]) -> bytes: - return func.QueueMessage(body=req.get_body()) - - -@app.function_name(name="queue_trigger") -@app.queue_trigger(arg_name="msg", - queue_name="testqueue", - connection="AzureWebJobsStorage") -@app.blob_output(arg_name="$return", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-queue-blob.txt") -def queue_trigger(msg: func.QueueMessage) -> str: - result = json.dumps({ - 'id': msg.id, - 'body': msg.get_body().decode('utf-8'), - 'expiration_time': (msg.expiration_time.isoformat() - if msg.expiration_time else None), - 'insertion_time': (msg.insertion_time.isoformat() - if msg.insertion_time else None), - 'time_next_visible': (msg.time_next_visible.isoformat() - if msg.time_next_visible else None), - 'pop_receipt': msg.pop_receipt, - 'dequeue_count': msg.dequeue_count - }) - - return result - - -@app.function_name(name="queue_trigger_message_return") -@app.queue_trigger(arg_name="msg", - queue_name="testqueue-message-return", - connection="AzureWebJobsStorage") -@app.blob_output(arg_name="$return", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-queue-blob-message-return.txt") -def queue_trigger_message_return(msg: func.QueueMessage) -> bytes: - return msg.get_body() - - -@app.function_name(name="queue_trigger_return") -@app.queue_trigger(arg_name="msg", - queue_name="testqueue-return", - connection="AzureWebJobsStorage") -@app.blob_output(arg_name="$return", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-queue-blob-return.txt") -def queue_trigger_return(msg: func.QueueMessage) -> bytes: - return msg.get_body() - - -@app.function_name(name="queue_trigger_return_multiple") -@app.queue_trigger(arg_name="msg", - queue_name="testqueue-return-multiple", - connection="AzureWebJobsStorage") -def queue_trigger_return_multiple(msg: func.QueueMessage) -> None: - logging.info('trigger on message: %s', msg.get_body().decode('utf-8')) - - -@app.function_name(name="queue_trigger_untyped") -@app.queue_trigger(arg_name="msg", - queue_name="testqueue-untyped-return", - connection="AzureWebJobsStorage") -@app.blob_output(arg_name="$return", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-queue-untyped-blob-return.txt") -def queue_trigger_untyped(msg: str) -> str: - return msg - - -@app.function_name(name="put_queue_return_multiple") -@app.route(route="put_queue_return_multiple", binding_arg_name="resp") -@app.queue_output(arg_name="msgs", - connection="AzureWebJobsStorage", - queue_name="testqueue-return-multiple") -def put_queue_return_multiple(req: func.HttpRequest, - resp: func.Out[str], - msgs: func.Out[typing.List[str]]): - msgs.set(['one', 'two']) diff --git a/tests/emulator_tests/queue_functions/queue_functions_stein/generic/function_app.py b/tests/emulator_tests/queue_functions/queue_functions_stein/generic/function_app.py deleted file mode 100644 index a2ad14b58..000000000 --- a/tests/emulator_tests/queue_functions/queue_functions_stein/generic/function_app.py +++ /dev/null @@ -1,253 +0,0 @@ -import json -import logging -import typing - -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.function_name(name="get_queue_blob") -@app.generic_trigger(arg_name="req", - type="httpTrigger", - route="get_queue_blob") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding( - arg_name="file", - type="blob", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-queue-blob.txt") -def get_queue_blob(req: func.HttpRequest, file: func.InputStream) -> str: - return json.dumps({ - 'queue': json.loads(file.read().decode('utf-8')) - }) - - -@app.function_name(name="get_queue_blob_message_return") -@app.generic_trigger(arg_name="req", - type="httpTrigger", - route="get_queue_blob_message_return") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding( - arg_name="file", - type="blob", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-queue-blob-message-return.txt") -def get_queue_blob_message_return(req: func.HttpRequest, - file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -@app.function_name(name="get_queue_blob_return") -@app.generic_trigger(arg_name="req", - type="httpTrigger", - route="get_queue_blob_return") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding(arg_name="file", - type="blob", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-queue-blob-return" - ".txt") -def get_queue_blob_return(req: func.HttpRequest, - file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -@app.function_name(name="get_queue_untyped_blob_return") -@app.generic_trigger(arg_name="req", - type="httpTrigger", - route="get_queue_untyped_blob_return") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding( - arg_name="file", - type="blob", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-queue-untyped-blob-return.txt") -def get_queue_untyped_blob_return(req: func.HttpRequest, - file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -@app.function_name(name="put_queue") -@app.generic_trigger(arg_name="req", - type="httpTrigger", - route="put_queue") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_output_binding( - arg_name="msg", - type="queue", - connection="AzureWebJobsStorage", - queue_name="testqueue") -def put_queue(req: func.HttpRequest, msg: func.Out[str]): - msg.set(req.get_body()) - - return 'OK' - - -@app.function_name(name="put_queue_message_return") -@app.generic_trigger(arg_name="req", - type="httpTrigger", - route="put_queue_message_return") -@app.generic_output_binding(arg_name="resp", type="http") -@app.generic_output_binding( - arg_name="$return", - type="queue", - connection="AzureWebJobsStorage", - queue_name="testqueue-message-return") -def main(req: func.HttpRequest, resp: func.Out[str]) -> bytes: - return func.QueueMessage(body=req.get_body()) - - -@app.function_name(name="put_queue_multiple_out") -@app.generic_trigger(arg_name="req", - type="httpTrigger", - route="put_queue_multiple_out") -@app.generic_output_binding(arg_name="resp", type="http") -@app.generic_output_binding( - arg_name="msg", - type="queue", - connection="AzureWebJobsStorage", - queue_name="testqueue-return-multiple-outparam") -def put_queue_multiple_out(req: func.HttpRequest, - resp: func.Out[func.HttpResponse], - msg: func.Out[func.QueueMessage]) -> None: - data = req.get_body().decode() - msg.set(func.QueueMessage(body=data)) - resp.set(func.HttpResponse(body='HTTP response: {}'.format(data))) - - -@app.function_name("put_queue_return") -@app.generic_trigger(arg_name="req", - type="httpTrigger", - route="put_queue_return") -@app.generic_output_binding(arg_name="resp", type="http") -@app.generic_output_binding( - arg_name="$return", - type="queue", - connection="AzureWebJobsStorage", - queue_name="testqueue-return") -def put_queue_return(req: func.HttpRequest, resp: func.Out[str]) -> bytes: - return req.get_body() - - -@app.function_name(name="put_queue_multiple_return") -@app.generic_trigger(arg_name="req", - type="httpTrigger", - route="put_queue_multiple_return") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_output_binding( - arg_name="msgs", - type="queue", - connection="AzureWebJobsStorage", - queue_name="testqueue-return-multiple") -def put_queue_multiple_return(req: func.HttpRequest, - msgs: func.Out[typing.List[str]]): - msgs.set(['one', 'two']) - - -@app.function_name(name="put_queue_untyped_return") -@app.generic_trigger(arg_name="req", - type="httpTrigger", - route="put_queue_untyped_return") -@app.generic_output_binding(arg_name="resp", type="http") -@app.generic_output_binding( - arg_name="$return", - type="queue", - connection="AzureWebJobsStorage", - queue_name="testqueue-untyped-return") -def put_queue_untyped_return(req: func.HttpRequest, - resp: func.Out[str]) -> bytes: - return func.QueueMessage(body=req.get_body()) - - -@app.function_name(name="queue_trigger") -@app.generic_trigger(arg_name="msg", - type="queueTrigger", - queue_name="testqueue", - connection="AzureWebJobsStorage") -@app.generic_output_binding(arg_name="$return", - type="blob", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-queue-blob.txt") -def queue_trigger(msg: func.QueueMessage) -> str: - result = json.dumps({ - 'id': msg.id, - 'body': msg.get_body().decode('utf-8'), - 'expiration_time': (msg.expiration_time.isoformat() - if msg.expiration_time else None), - 'insertion_time': (msg.insertion_time.isoformat() - if msg.insertion_time else None), - 'time_next_visible': (msg.time_next_visible.isoformat() - if msg.time_next_visible else None), - 'pop_receipt': msg.pop_receipt, - 'dequeue_count': msg.dequeue_count - }) - - return result - - -@app.function_name(name="queue_trigger_message_return") -@app.generic_trigger(arg_name="msg", - type="queueTrigger", - queue_name="testqueue-message-return", - connection="AzureWebJobsStorage") -@app.generic_output_binding( - arg_name="$return", - type="blob", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-queue-blob-message-return.txt") -def queue_trigger_message_return(msg: func.QueueMessage) -> bytes: - return msg.get_body() - - -@app.function_name(name="queue_trigger_return") -@app.generic_trigger(arg_name="msg", - type="queueTrigger", - queue_name="testqueue-return", - connection="AzureWebJobsStorage") -@app.generic_output_binding( - arg_name="$return", - type="blob", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-queue-blob-return.txt") -def queue_trigger_return(msg: func.QueueMessage) -> bytes: - return msg.get_body() - - -@app.function_name(name="queue_trigger_return_multiple") -@app.generic_trigger(arg_name="msg", - type="queueTrigger", - queue_name="testqueue-return-multiple", - connection="AzureWebJobsStorage") -def queue_trigger_return_multiple(msg: func.QueueMessage) -> None: - logging.info('trigger on message: %s', msg.get_body().decode('utf-8')) - - -@app.function_name(name="queue_trigger_untyped") -@app.generic_trigger(arg_name="msg", - type="queueTrigger", - queue_name="testqueue-untyped-return", - connection="AzureWebJobsStorage") -@app.generic_output_binding(arg_name="$return", - type="blob", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-queue-untyped" - "-blob-return.txt") -def queue_trigger_untyped(msg: str) -> str: - return msg - - -@app.function_name(name="put_queue_return_multiple") -@app.generic_trigger(arg_name="req", - type="httpTrigger", - route="put_queue_return_multiple") -@app.generic_output_binding(arg_name="resp", type="http") -@app.generic_output_binding( - arg_name="msgs", - type="queue", - connection="AzureWebJobsStorage", - queue_name="testqueue-return-multiple") -def put_queue_return_multiple(req: func.HttpRequest, - resp: func.Out[str], - msgs: func.Out[typing.List[str]]): - msgs.set(['one', 'two']) diff --git a/tests/emulator_tests/queue_functions/queue_trigger/function.json b/tests/emulator_tests/queue_functions/queue_trigger/function.json deleted file mode 100644 index 9c7f2b322..000000000 --- a/tests/emulator_tests/queue_functions/queue_trigger/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "main.py", - - "bindings": [ - { - "type": "queueTrigger", - "direction": "in", - "name": "msg", - "queueName": "testqueue", - "connection": "AzureWebJobsStorage", - }, - { - "type": "blob", - "direction": "out", - "name": "$return", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-queue-blob.txt" - } - ] -} diff --git a/tests/emulator_tests/queue_functions/queue_trigger/main.py b/tests/emulator_tests/queue_functions/queue_trigger/main.py deleted file mode 100644 index 08a5d4e9d..000000000 --- a/tests/emulator_tests/queue_functions/queue_trigger/main.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json - -import azure.functions as azf - - -def main(msg: azf.QueueMessage) -> str: - result = json.dumps({ - 'id': msg.id, - 'body': msg.get_body().decode('utf-8'), - 'expiration_time': (msg.expiration_time.isoformat() - if msg.expiration_time else None), - 'insertion_time': (msg.insertion_time.isoformat() - if msg.insertion_time else None), - 'time_next_visible': (msg.time_next_visible.isoformat() - if msg.time_next_visible else None), - 'pop_receipt': msg.pop_receipt, - 'dequeue_count': msg.dequeue_count - }) - - return result diff --git a/tests/emulator_tests/queue_functions/queue_trigger_message_return/function.json b/tests/emulator_tests/queue_functions/queue_trigger_message_return/function.json deleted file mode 100644 index 9c9e5a03a..000000000 --- a/tests/emulator_tests/queue_functions/queue_trigger_message_return/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "main.py", - - "bindings": [ - { - "type": "queueTrigger", - "direction": "in", - "name": "msg", - "queueName": "testqueue-message-return", - "connection": "AzureWebJobsStorage" - }, - { - "type": "blob", - "direction": "out", - "name": "$return", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-queue-blob-message-return.txt" - } - ] -} diff --git a/tests/emulator_tests/queue_functions/queue_trigger_message_return/main.py b/tests/emulator_tests/queue_functions/queue_trigger_message_return/main.py deleted file mode 100644 index be573b50b..000000000 --- a/tests/emulator_tests/queue_functions/queue_trigger_message_return/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(msg: azf.QueueMessage) -> bytes: - return msg.get_body() diff --git a/tests/emulator_tests/queue_functions/queue_trigger_return/function.json b/tests/emulator_tests/queue_functions/queue_trigger_return/function.json deleted file mode 100644 index 3604e0eb8..000000000 --- a/tests/emulator_tests/queue_functions/queue_trigger_return/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "main.py", - - "bindings": [ - { - "type": "queueTrigger", - "direction": "in", - "name": "msg", - "queueName": "testqueue-return", - "connection": "AzureWebJobsStorage" - }, - { - "type": "blob", - "direction": "out", - "name": "$return", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-queue-blob-return.txt" - } - ] -} diff --git a/tests/emulator_tests/queue_functions/queue_trigger_return/main.py b/tests/emulator_tests/queue_functions/queue_trigger_return/main.py deleted file mode 100644 index be573b50b..000000000 --- a/tests/emulator_tests/queue_functions/queue_trigger_return/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(msg: azf.QueueMessage) -> bytes: - return msg.get_body() diff --git a/tests/emulator_tests/queue_functions/queue_trigger_return_multiple/function.json b/tests/emulator_tests/queue_functions/queue_trigger_return_multiple/function.json deleted file mode 100644 index 0eb42ab89..000000000 --- a/tests/emulator_tests/queue_functions/queue_trigger_return_multiple/function.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "scriptFile": "main.py", - - "bindings": [ - { - "type": "queueTrigger", - "direction": "in", - "name": "msg", - "queueName": "testqueue-return-multiple", - "connection": "AzureWebJobsStorage", - } - ] -} diff --git a/tests/emulator_tests/queue_functions/queue_trigger_return_multiple/main.py b/tests/emulator_tests/queue_functions/queue_trigger_return_multiple/main.py deleted file mode 100644 index 6abb82b0a..000000000 --- a/tests/emulator_tests/queue_functions/queue_trigger_return_multiple/main.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import logging - -import azure.functions as azf - -logger = logging.getLogger(__name__) - - -def main(msg: azf.QueueMessage) -> None: - logging.info('trigger on message: %s', msg.get_body().decode('utf-8')) diff --git a/tests/emulator_tests/queue_functions/queue_trigger_untyped/function.json b/tests/emulator_tests/queue_functions/queue_trigger_untyped/function.json deleted file mode 100644 index a4d434ad3..000000000 --- a/tests/emulator_tests/queue_functions/queue_trigger_untyped/function.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "scriptFile": "main.py", - "disabled": false, - - "bindings": [ - { - "type": "queueTrigger", - "direction": "in", - "name": "msg", - "queueName": "testqueue-untyped-return", - "connection": "AzureWebJobsStorage" - }, - { - "type": "blob", - "direction": "out", - "name": "$return", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-queue-untyped-blob-return.txt" - } - ] -} diff --git a/tests/emulator_tests/queue_functions/queue_trigger_untyped/main.py b/tests/emulator_tests/queue_functions/queue_trigger_untyped/main.py deleted file mode 100644 index 64fa31587..000000000 --- a/tests/emulator_tests/queue_functions/queue_trigger_untyped/main.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import logging - -logger = logging.getLogger(__name__) - - -def main(msg: str) -> str: - return msg diff --git a/tests/emulator_tests/servicebus_functions/get_servicebus_triggered/__init__.py b/tests/emulator_tests/servicebus_functions/get_servicebus_triggered/__init__.py deleted file mode 100644 index 1e5ac3d90..000000000 --- a/tests/emulator_tests/servicebus_functions/get_servicebus_triggered/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func - - -def main(req: func.HttpRequest, file: func.InputStream) -> str: - return func.HttpResponse( - file.read().decode('utf-8'), mimetype='application/json') diff --git a/tests/emulator_tests/servicebus_functions/get_servicebus_triggered/function.json b/tests/emulator_tests/servicebus_functions/get_servicebus_triggered/function.json deleted file mode 100644 index 944dd45a9..000000000 --- a/tests/emulator_tests/servicebus_functions/get_servicebus_triggered/function.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "in", - "name": "file", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-servicebus-triggered.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return", - } - ] -} diff --git a/tests/emulator_tests/servicebus_functions/put_message/__init__.py b/tests/emulator_tests/servicebus_functions/put_message/__init__.py deleted file mode 100644 index 85ad99bf7..000000000 --- a/tests/emulator_tests/servicebus_functions/put_message/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest, msg: azf.Out[str]): - msg.set(req.get_body().decode('utf-8')) - - return 'OK' diff --git a/tests/emulator_tests/servicebus_functions/put_message/function.json b/tests/emulator_tests/servicebus_functions/put_message/function.json deleted file mode 100644 index 722f19541..000000000 --- a/tests/emulator_tests/servicebus_functions/put_message/function.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "scriptFile": "__init__.py", - - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "direction": "out", - "name": "msg", - "queueName": "testqueue", - "connection": "AzureWebJobsServiceBusConnectionString", - "type": "serviceBus" - }, - { - "direction": "out", - "name": "$return", - "type": "http" - } - ] -} diff --git a/tests/emulator_tests/servicebus_functions/servicebus_functions_stein/function_app.py b/tests/emulator_tests/servicebus_functions/servicebus_functions_stein/function_app.py deleted file mode 100644 index 9e9d12246..000000000 --- a/tests/emulator_tests/servicebus_functions/servicebus_functions_stein/function_app.py +++ /dev/null @@ -1,73 +0,0 @@ -import json - -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.route(route="put_message") -@app.service_bus_queue_output( - arg_name="msg", - connection="AzureWebJobsServiceBusConnectionString", - queue_name="testqueue") -def put_message(req: func.HttpRequest, msg: func.Out[str]): - msg.set(req.get_body().decode('utf-8')) - return 'OK' - - -@app.route(route="get_servicebus_triggered") -@app.blob_input(arg_name="file", - path="python-worker-tests/test-servicebus-triggered.txt", - connection="AzureWebJobsStorage") -def get_servicebus_triggered(req: func.HttpRequest, - file: func.InputStream) -> str: - return func.HttpResponse( - file.read().decode('utf-8'), mimetype='application/json') - - -@app.service_bus_queue_trigger( - arg_name="msg", - connection="AzureWebJobsServiceBusConnectionString", - queue_name="testqueue") -@app.blob_output(arg_name="$return", - path="python-worker-tests/test-servicebus-triggered.txt", - connection="AzureWebJobsStorage") -def servicebus_trigger(msg: func.ServiceBusMessage) -> str: - result = json.dumps({ - 'message_id': msg.message_id, - 'body': msg.get_body().decode('utf-8'), - 'content_type': msg.content_type, - 'delivery_count': msg.delivery_count, - 'expiration_time': (msg.expiration_time.isoformat() if - msg.expiration_time else None), - 'label': msg.label, - 'partition_key': msg.partition_key, - 'reply_to': msg.reply_to, - 'reply_to_session_id': msg.reply_to_session_id, - 'scheduled_enqueue_time': (msg.scheduled_enqueue_time.isoformat() if - msg.scheduled_enqueue_time else None), - 'session_id': msg.session_id, - 'time_to_live': msg.time_to_live, - 'to': msg.to, - 'user_properties': msg.user_properties, - - 'application_properties': msg.application_properties, - 'correlation_id': msg.correlation_id, - 'dead_letter_error_description': msg.dead_letter_error_description, - 'dead_letter_reason': msg.dead_letter_reason, - 'dead_letter_source': msg.dead_letter_source, - 'enqueued_sequence_number': msg.enqueued_sequence_number, - 'enqueued_time_utc': (msg.enqueued_time_utc.isoformat() if - msg.enqueued_time_utc else None), - 'expires_at_utc': (msg.expires_at_utc.isoformat() if - msg.expires_at_utc else None), - 'locked_until': (msg.locked_until.isoformat() if - msg.locked_until else None), - 'lock_token': msg.lock_token, - 'sequence_number': msg.sequence_number, - 'state': msg.state, - 'subject': msg.subject, - 'transaction_partition_key': msg.transaction_partition_key - }) - - return result diff --git a/tests/emulator_tests/servicebus_functions/servicebus_functions_stein/generic/function_app.py b/tests/emulator_tests/servicebus_functions/servicebus_functions_stein/generic/function_app.py deleted file mode 100644 index 4fd48785a..000000000 --- a/tests/emulator_tests/servicebus_functions/servicebus_functions_stein/generic/function_app.py +++ /dev/null @@ -1,81 +0,0 @@ -import json - -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.function_name(name="put_message") -@app.generic_trigger(arg_name="req", type="httpTrigger", route="put_message") -@app.generic_output_binding(arg_name="msg", - type="serviceBus", - connection="AzureWebJobsServiceBusConnectionString", - queue_name="testqueue") -@app.generic_output_binding(arg_name="$return", type="http") -def put_message(req: func.HttpRequest, msg: func.Out[str]): - msg.set(req.get_body().decode('utf-8')) - return 'OK' - - -@app.function_name(name="get_servicebus_triggered") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="get_servicebus_triggered") -@app.generic_input_binding(arg_name="file", - type="blob", - path="python-worker-tests/test-servicebus-triggered.txt", # NoQA - connection="AzureWebJobsStorage") -@app.generic_output_binding(arg_name="$return", type="http") -def get_servicebus_triggered(req: func.HttpRequest, - file: func.InputStream) -> str: - return func.HttpResponse( - file.read().decode('utf-8'), mimetype='application/json') - - -@app.generic_trigger( - arg_name="msg", - type="serviceBusTrigger", - connection="AzureWebJobsServiceBusConnectionString", - queue_name="testqueue") -@app.generic_output_binding(arg_name="$return", - path="python-worker-tests/test-servicebus-triggered.txt", # NoQA - type="blob", - connection="AzureWebJobsStorage") -def servicebus_trigger(msg: func.ServiceBusMessage) -> str: - result = json.dumps({ - 'message_id': msg.message_id, - 'body': msg.get_body().decode('utf-8'), - 'content_type': msg.content_type, - 'delivery_count': msg.delivery_count, - 'expiration_time': (msg.expiration_time.isoformat() if - msg.expiration_time else None), - 'label': msg.label, - 'partition_key': msg.partition_key, - 'reply_to': msg.reply_to, - 'reply_to_session_id': msg.reply_to_session_id, - 'scheduled_enqueue_time': (msg.scheduled_enqueue_time.isoformat() if - msg.scheduled_enqueue_time else None), - 'session_id': msg.session_id, - 'time_to_live': msg.time_to_live, - 'to': msg.to, - 'user_properties': msg.user_properties, - - 'application_properties': msg.application_properties, - 'correlation_id': msg.correlation_id, - 'dead_letter_error_description': msg.dead_letter_error_description, - 'dead_letter_reason': msg.dead_letter_reason, - 'dead_letter_source': msg.dead_letter_source, - 'enqueued_sequence_number': msg.enqueued_sequence_number, - 'enqueued_time_utc': (msg.enqueued_time_utc.isoformat() if - msg.enqueued_time_utc else None), - 'expires_at_utc': (msg.expires_at_utc.isoformat() if - msg.expires_at_utc else None), - 'locked_until': (msg.locked_until.isoformat() if - msg.locked_until else None), - 'lock_token': msg.lock_token, - 'sequence_number': msg.sequence_number, - 'state': msg.state, - 'subject': msg.subject, - 'transaction_partition_key': msg.transaction_partition_key - }) - - return result diff --git a/tests/emulator_tests/servicebus_functions/servicebus_trigger/__init__.py b/tests/emulator_tests/servicebus_functions/servicebus_trigger/__init__.py deleted file mode 100644 index 341779a4d..000000000 --- a/tests/emulator_tests/servicebus_functions/servicebus_trigger/__init__.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json - -import azure.functions as azf - - -def main(msg: azf.ServiceBusMessage) -> str: - result = json.dumps({ - 'message_id': msg.message_id, - 'body': msg.get_body().decode('utf-8'), - 'content_type': msg.content_type, - 'delivery_count': msg.delivery_count, - 'expiration_time': (msg.expiration_time.isoformat() if - msg.expiration_time else None), - 'label': msg.label, - 'partition_key': msg.partition_key, - 'reply_to': msg.reply_to, - 'reply_to_session_id': msg.reply_to_session_id, - 'scheduled_enqueue_time': (msg.scheduled_enqueue_time.isoformat() if - msg.scheduled_enqueue_time else None), - 'session_id': msg.session_id, - 'time_to_live': msg.time_to_live, - 'to': msg.to, - 'user_properties': msg.user_properties, - - 'application_properties': msg.application_properties, - 'correlation_id': msg.correlation_id, - 'dead_letter_error_description': msg.dead_letter_error_description, - 'dead_letter_reason': msg.dead_letter_reason, - 'dead_letter_source': msg.dead_letter_source, - 'enqueued_sequence_number': msg.enqueued_sequence_number, - 'enqueued_time_utc': (msg.enqueued_time_utc.isoformat() if - msg.enqueued_time_utc else None), - 'expires_at_utc': (msg.expires_at_utc.isoformat() if - msg.expires_at_utc else None), - 'locked_until': (msg.locked_until.isoformat() if - msg.locked_until else None), - 'lock_token': msg.lock_token, - 'sequence_number': msg.sequence_number, - 'state': msg.state, - 'subject': msg.subject, - 'transaction_partition_key': msg.transaction_partition_key - }) - - return result diff --git a/tests/emulator_tests/servicebus_functions/servicebus_trigger/function.json b/tests/emulator_tests/servicebus_functions/servicebus_trigger/function.json deleted file mode 100644 index b6fe4355e..000000000 --- a/tests/emulator_tests/servicebus_functions/servicebus_trigger/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "__init__.py", - - "bindings": [ - { - "type": "serviceBusTrigger", - "direction": "in", - "name": "msg", - "queueName": "testqueue", - "connection": "AzureWebJobsServiceBusConnectionString", - }, - { - "type": "blob", - "direction": "out", - "name": "$return", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-servicebus-triggered.txt" - } - ] -} diff --git a/tests/emulator_tests/table_functions/table_functions_stein/function_app.py b/tests/emulator_tests/table_functions/table_functions_stein/function_app.py deleted file mode 100644 index 5ebd10e07..000000000 --- a/tests/emulator_tests/table_functions/table_functions_stein/function_app.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json -import uuid - -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.function_name(name="table_in_binding") -@app.route(route="table_in_binding/{id}") -@app.table_input(arg_name="testEntity", - connection="AzureWebJobsStorage", - table_name="BindingTestTable", - row_key='{id}', - partition_key="test") -def table_in_binding(req: func.HttpRequest, testEntity): - return func.HttpResponse(status_code=200, body=testEntity) - - -@app.function_name(name="table_out_binding") -@app.route(route="table_out_binding", binding_arg_name="resp") -@app.table_output(arg_name="$return", - connection="AzureWebJobsStorage", - table_name="BindingTestTable") -def table_out_binding(req: func.HttpRequest, resp: func.Out[func.HttpResponse]): - row_key_uuid = str(uuid.uuid4()) - table_dict = {'PartitionKey': 'test', 'RowKey': row_key_uuid} - table_json = json.dumps(table_dict) - resp.set(table_json) - return table_json diff --git a/tests/emulator_tests/table_functions/table_functions_stein/generic/function_app.py b/tests/emulator_tests/table_functions/table_functions_stein/generic/function_app.py deleted file mode 100644 index 0a03a2366..000000000 --- a/tests/emulator_tests/table_functions/table_functions_stein/generic/function_app.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json -import uuid - -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.function_name(name="table_in_binding") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="table_in_binding/{id}") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding( - arg_name="testEntity", - type="table", - connection="AzureWebJobsStorage", - table_name="BindingTestTable", - row_key="{id}", - partition_key="test") -def table_in_binding(req: func.HttpRequest, testEntity): - return func.HttpResponse(status_code=200, body=testEntity) - - -@app.function_name(name="table_out_binding") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="table_out_binding") -@app.generic_output_binding(arg_name="resp", type="http") -@app.generic_output_binding( - arg_name="$return", - type="table", - connection="AzureWebJobsStorage", - table_name="BindingTestTable") -def table_out_binding(req: func.HttpRequest, resp: func.Out[func.HttpResponse]): - row_key_uuid = str(uuid.uuid4()) - table_dict = {'PartitionKey': 'test', 'RowKey': row_key_uuid} - table_json = json.dumps(table_dict) - resp.set(table_json) - return table_json diff --git a/tests/emulator_tests/table_functions/table_in_binding/__init__.py b/tests/emulator_tests/table_functions/table_in_binding/__init__.py deleted file mode 100644 index a125e2bdb..000000000 --- a/tests/emulator_tests/table_functions/table_in_binding/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func - - -def main(req: func.HttpRequest, testEntity): - return func.HttpResponse(status_code=200, body=testEntity) diff --git a/tests/emulator_tests/table_functions/table_in_binding/function.json b/tests/emulator_tests/table_functions/table_in_binding/function.json deleted file mode 100644 index d62461d0b..000000000 --- a/tests/emulator_tests/table_functions/table_in_binding/function.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "authLevel": "anonymous", - "methods": [ - "get" - ], - "name": "req" - }, - { - "direction": "in", - "type": "table", - "name": "testEntity", - "partitionKey": "test", - "rowKey": "WillBePopulatedWithGuid", - "tableName": "BindingTestTable", - "connection": "AzureWebJobsStorage" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} \ No newline at end of file diff --git a/tests/emulator_tests/table_functions/table_out_binding/__init__.py b/tests/emulator_tests/table_functions/table_out_binding/__init__.py deleted file mode 100644 index 5e869a2ae..000000000 --- a/tests/emulator_tests/table_functions/table_out_binding/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json -import uuid - -import azure.functions as func - - -def main(req: func.HttpRequest, resp: func.Out[func.HttpResponse]): - row_key_uuid = str(uuid.uuid4()) - table_dict = {'PartitionKey': 'test', 'RowKey': row_key_uuid} - table_json = json.dumps(table_dict) - resp.set(table_json) - return table_json diff --git a/tests/emulator_tests/table_functions/table_out_binding/function.json b/tests/emulator_tests/table_functions/table_out_binding/function.json deleted file mode 100644 index 416920ca4..000000000 --- a/tests/emulator_tests/table_functions/table_out_binding/function.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "authLevel": "anonymous", - "methods": ["post"], - "name": "req" - }, - { - "direction": "out", - "type": "table", - "name": "$return", - "tableName": "BindingTestTable", - "connection": "AzureWebJobsStorage" - }, - { - "name": "resp", - "type": "http", - "direction": "out" - } - ] -} \ No newline at end of file diff --git a/tests/emulator_tests/test_blob_functions.py b/tests/emulator_tests/test_blob_functions.py deleted file mode 100644 index d6a840a38..000000000 --- a/tests/emulator_tests/test_blob_functions.py +++ /dev/null @@ -1,166 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import time - -from requests import JSONDecodeError -from tests.utils import testutils - - -class TestBlobFunctions(testutils.WebHostTestCase): - - @classmethod - def get_script_dir(cls): - return testutils.EMULATOR_TESTS_FOLDER / 'blob_functions' - - @testutils.retryable_test(3, 5) - def test_blob_io_str(self): - r = self.webhost.request('POST', 'put_blob_str', data='test-data') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - r = self.webhost.request('GET', 'get_blob_str') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'test-data') - - r = self.webhost.request('GET', 'get_blob_as_str') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'test-data') - - def test_blob_io_large_str(self): - large_string = 'DummyDataDummyDataDummyData' * 1024 * 1024 # 27 MB - - r = self.webhost.request('POST', 'put_blob_str', data=large_string) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - r = self.webhost.request('GET', 'get_blob_str') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, large_string) - - r = self.webhost.request('GET', 'get_blob_as_str') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, large_string) - - def test_blob_io_bytes(self): - r = self.webhost.request('POST', 'put_blob_bytes', - data='test-dată'.encode('utf-8')) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - r = self.webhost.request('POST', 'get_blob_bytes') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'test-dată') - - r = self.webhost.request('POST', 'get_blob_as_bytes') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'test-dată') - - def test_blob_io_large_bytes(self): - large_string = 'DummyDataDummyDataDummyData' * 1024 * 1024 # 27 MB - - r = self.webhost.request('POST', 'put_blob_bytes', - data=large_string.encode('utf-8')) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - r = self.webhost.request('POST', 'get_blob_bytes') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, large_string) - - r = self.webhost.request('POST', 'get_blob_as_bytes') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, large_string) - - def test_blob_io_filelike(self): - r = self.webhost.request('POST', 'put_blob_filelike') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - r = self.webhost.request('POST', 'get_blob_filelike') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'filelike') - - def test_blob_io_return(self): - r = self.webhost.request('POST', 'put_blob_return') - self.assertEqual(r.status_code, 200) - - r = self.webhost.request('POST', 'get_blob_return') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'FROM RETURN') - - def test_blob_trigger(self): - data = "DummyData" - - r = self.webhost.request('POST', 'put_blob_trigger', - data=data.encode('utf-8')) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - # Blob trigger may be processed after some delay - # We check it every 2 seconds to allow the trigger to be fired - max_retries = 10 - for try_no in range(max_retries): - time.sleep(2) - - try: - # Check that the trigger has fired - r = self.webhost.request('GET', 'get_blob_triggered') - self.assertEqual(r.status_code, 200) - response = r.json() - - self.assertEqual(response['name'], - 'python-worker-tests/test-blob-trigger.txt') - self.assertEqual(response['content'], data) - - break - # JSONDecodeError will be thrown if the response is empty. - except (AssertionError, JSONDecodeError): - if try_no == max_retries - 1: - raise - - def test_blob_trigger_with_large_content(self): - data = 'DummyDataDummyDataDummyData' * 1024 * 1024 # 27 MB - - r = self.webhost.request('POST', 'put_blob_trigger', - data=data.encode('utf-8')) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - # Blob trigger may be processed after some delay - # We check it every 2 seconds to allow the trigger to be fired - max_retries = 10 - for try_no in range(max_retries): - try: - # Check that the trigger has fired - r = self.webhost.request('GET', 'get_blob_triggered') - - # Waiting for blob to get updated - time.sleep(2) - - self.assertEqual(r.status_code, 200) - response = r.json() - - self.assertEqual(response['name'], - 'python-worker-tests/test-blob-trigger.txt') - self.assertEqual(response['content'], data) - break - # JSONDecodeError will be thrown if the response is empty. - except (AssertionError, JSONDecodeError): - if try_no == max_retries - 1: - raise - - -class TestBlobFunctionsStein(TestBlobFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.EMULATOR_TESTS_FOLDER / 'blob_functions' / \ - 'blob_functions_stein' - - -class TestBlobFunctionsSteinGeneric(TestBlobFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.EMULATOR_TESTS_FOLDER / 'blob_functions' / \ - 'blob_functions_stein' / 'generic' diff --git a/tests/emulator_tests/test_eventhub_batch_functions.py b/tests/emulator_tests/test_eventhub_batch_functions.py deleted file mode 100644 index 1a8ae2a9e..000000000 --- a/tests/emulator_tests/test_eventhub_batch_functions.py +++ /dev/null @@ -1,242 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json -import sys -import time -from datetime import datetime -from unittest.case import skipIf - -from dateutil import parser -from tests.utils import testutils - - -class TestEventHubFunctions(testutils.WebHostTestCase): - """Test EventHub Trigger and Output Bindings (cardinality: many). - - Each testcase consists of 3 part: - 1. An eventhub_output_batch HTTP trigger for generating EventHub event - 2. An eventhub_multiple EventHub trigger for converting event into blob - 3. A get_eventhub_batch_triggered HTTP trigger for the event body - """ - - @classmethod - def get_script_dir(cls): - return testutils.EMULATOR_TESTS_FOLDER / 'eventhub_batch_functions' - - @classmethod - def get_libraries_to_install(cls): - return ['azure-eventhub'] - - @testutils.retryable_test(3, 5) - def test_eventhub_multiple(self): - NUM_EVENTS = 3 - all_row_keys_seen = dict([(i, True) for i in range(NUM_EVENTS)]) - partition_key = str(round(time.time())) - - # wait for host to restart after change - time.sleep(5) - - docs = [] - for i in range(NUM_EVENTS): - doc = {'PartitionKey': partition_key, 'RowKey': i} - docs.append(doc) - - r = self.webhost.request('POST', 'eventhub_output_batch', - data=json.dumps(docs)) - self.assertEqual(r.status_code, 200) - - row_keys = [i for i in range(NUM_EVENTS)] - seen = [False] * NUM_EVENTS - row_keys_seen = dict(zip(row_keys, seen)) - - # Allow trigger to fire. - time.sleep(5) - - r = self.webhost.request('GET', 'get_eventhub_batch_triggered') - - # Waiting for the blob get updated with the latest data from the - # eventhub output binding - time.sleep(2) - self.assertEqual(r.status_code, 200) - entries = r.json() - for entry in entries: - self.assertEqual(entry['PartitionKey'], partition_key) - row_key = entry['RowKey'] - row_keys_seen[row_key] = True - - self.assertDictEqual(all_row_keys_seen, row_keys_seen) - - @skipIf(sys.version_info.minor == 7, - "Using azure-eventhub SDK with the EventHub Emulator" - "requires Python 3.8+") - @testutils.retryable_test(3, 5) - def test_eventhub_multiple_with_metadata(self): - # Generate a unique event body for EventHub event - # Record the start_time and end_time for checking event enqueue time - start_time = datetime.utcnow() - count = 10 - random_number = str(round(time.time()) % 1000) - req_body = { - 'body': random_number - } - - # Invoke metadata_output HttpTrigger to generate an EventHub event - # from azure-eventhub SDK - r = self.webhost.request('POST', - f'metadata_output_batch?count={count}', - data=json.dumps(req_body)) - self.assertEqual(r.status_code, 200) - self.assertIn('OK', r.text) - end_time = datetime.utcnow() - - # Once the event get generated, allow function host to pool from - # EventHub and wait for metadata_multiple to execute, - # converting the event metadata into a blob. - time.sleep(5) - - # Call get_metadata_batch_triggered to retrieve event metadata - r = self.webhost.request('GET', 'get_metadata_batch_triggered') - self.assertEqual(r.status_code, 200) - - # Check metadata and events length, events should be batched processed - events = r.json() - self.assertIsInstance(events, list) - self.assertGreater(len(events), 1) - - # EventhubEvent property check - for event_index in range(len(events)): - event = events[event_index] - - # Check if the event is enqueued between start_time and end_time - enqueued_time = parser.isoparse(event['enqueued_time']) - self.assertTrue(start_time < enqueued_time < end_time) - - # Check if event properties are properly set - self.assertIsNone(event['partition_key']) # only 1 partition - self.assertGreaterEqual(event['sequence_number'], 0) - self.assertIsNotNone(event['offset']) - - # Check if event.metadata field is properly set - self.assertIsNotNone(event['metadata']) - metadata = event['metadata'] - sys_props_array = metadata['SystemPropertiesArray'] - sys_props = sys_props_array[event_index] - enqueued_time = parser.isoparse(sys_props['EnqueuedTimeUtc']) - - # Check event trigger time and other system properties - self.assertTrue( - start_time.timestamp() < enqueued_time.timestamp() < end_time.timestamp()) # NoQA - self.assertIsNone(sys_props['PartitionKey']) - self.assertGreaterEqual(sys_props['SequenceNumber'], 0) - self.assertIsNotNone(sys_props['Offset']) - - -class TestEventHubBatchFunctionsStein(testutils.WebHostTestCase): - - @classmethod - def get_script_dir(cls): - return testutils.EMULATOR_TESTS_FOLDER / 'eventhub_batch_functions' / \ - 'eventhub_batch_functions_stein' - - @classmethod - def get_libraries_to_install(cls): - return ['azure-eventhub'] - - @testutils.retryable_test(3, 5) - def test_eventhub_multiple(self): - NUM_EVENTS = 3 - all_row_keys_seen = dict([(i, True) for i in range(NUM_EVENTS)]) - partition_key = str(round(time.time())) - - docs = [] - for i in range(NUM_EVENTS): - doc = {'PartitionKey': partition_key, 'RowKey': i} - docs.append(doc) - - r = self.webhost.request('POST', 'eventhub_output_batch', - data=json.dumps(docs)) - self.assertEqual(r.status_code, 200) - - row_keys = [i for i in range(NUM_EVENTS)] - seen = [False] * NUM_EVENTS - row_keys_seen = dict(zip(row_keys, seen)) - - # Allow trigger to fire. - time.sleep(5) - - r = self.webhost.request( - 'GET', - 'get_eventhub_batch_triggered') - self.assertEqual(r.status_code, 200) - entries = r.json() - for entry in entries: - self.assertEqual(entry['PartitionKey'], partition_key) - row_key = entry['RowKey'] - row_keys_seen[row_key] = True - - self.assertDictEqual(all_row_keys_seen, row_keys_seen) - - @skipIf(sys.version_info.minor == 7, - "Using azure-eventhub SDK with the EventHub Emulator" - "requires Python 3.8+") - @testutils.retryable_test(3, 5) - def test_eventhub_multiple_with_metadata(self): - # Generate a unique event body for EventHub event - # Record the start_time and end_time for checking event enqueue time - start_time = datetime.utcnow() - count = 10 - random_number = str(round(time.time()) % 1000) - req_body = { - 'body': random_number - } - - # Invoke metadata_output HttpTrigger to generate an EventHub event - # from azure-eventhub SDK - r = self.webhost.request('POST', - f'metadata_output_batch?count={count}', - data=json.dumps(req_body)) - self.assertEqual(r.status_code, 200) - self.assertIn('OK', r.text) - end_time = datetime.utcnow() - - # Once the event get generated, allow function host to pool from - # EventHub and wait for metadata_multiple to execute, - # converting the event metadata into a blob. - time.sleep(5) - - # Call get_metadata_batch_triggered to retrieve event metadata - r = self.webhost.request('GET', 'get_metadata_batch_triggered') - self.assertEqual(r.status_code, 200) - - # Check metadata and events length, events should be batched processed - events = r.json() - self.assertIsInstance(events, list) - self.assertGreater(len(events), 1) - - # EventhubEvent property check - for event_index in range(len(events)): - event = events[event_index] - - # Check if the event is enqueued between start_time and end_time - enqueued_time = parser.isoparse(event['enqueued_time']) - self.assertTrue(start_time < enqueued_time < end_time) - - # Check if event properties are properly set - self.assertIsNone(event['partition_key']) # only 1 partition - self.assertGreaterEqual(event['sequence_number'], 0) - self.assertIsNotNone(event['offset']) - - # Check if event.metadata field is properly set - self.assertIsNotNone(event['metadata']) - metadata = event['metadata'] - sys_props_array = metadata['SystemPropertiesArray'] - sys_props = sys_props_array[event_index] - enqueued_time = parser.isoparse(sys_props['EnqueuedTimeUtc']) - - # Check event trigger time and other system properties - self.assertTrue( - start_time.timestamp() < enqueued_time.timestamp() - < end_time.timestamp()) # NoQA - self.assertIsNone(sys_props['PartitionKey']) - self.assertGreaterEqual(sys_props['SequenceNumber'], 0) - self.assertIsNotNone(sys_props['Offset']) diff --git a/tests/emulator_tests/test_eventhub_functions.py b/tests/emulator_tests/test_eventhub_functions.py deleted file mode 100644 index 03088c731..000000000 --- a/tests/emulator_tests/test_eventhub_functions.py +++ /dev/null @@ -1,124 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json -import sys -import time - -from unittest import skipIf - -from tests.utils import testutils - - -class TestEventHubFunctions(testutils.WebHostTestCase): - """Test EventHub Trigger and Output Bindings (cardinality: one). - - Each testcase consists of 3 part: - 1. An eventhub_output HTTP trigger for generating EventHub event - 2. An actual eventhub_trigger EventHub trigger for storing event into blob - 3. A get_eventhub_triggered HTTP trigger for retrieving event info blob - """ - - @classmethod - def get_script_dir(cls): - return testutils.EMULATOR_TESTS_FOLDER / 'eventhub_functions' - - @classmethod - def get_libraries_to_install(cls): - return ['azure-eventhub'] - - @testutils.retryable_test(3, 5) - def test_eventhub_trigger(self): - # Generate a unique event body for the EventHub event - data = str(round(time.time())) - doc = {'id': data} - - # Invoke eventhub_output HttpTrigger to generate an Eventhub Event. - r = self.webhost.request('POST', 'eventhub_output', - data=json.dumps(doc)) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - # Once the event get generated, allow function host to poll from - # EventHub and wait for eventhub_trigger to execute, - # converting the event metadata into a blob. - time.sleep(5) - - # Call get_eventhub_triggered to retrieve event metadata from blob. - r = self.webhost.request('GET', 'get_eventhub_triggered') - - # Waiting for the blob get updated with the latest data from the - # eventhub output binding - time.sleep(5) - self.assertEqual(r.status_code, 200) - response = r.json() - - # Check if the event body matches the initial data - self.assertEqual(response, doc) - - @skipIf(sys.version_info.minor == 7, - "Using azure-eventhub SDK with the EventHub Emulator" - "requires Python 3.8+") - @testutils.retryable_test(3, 5) - def test_eventhub_trigger_with_metadata(self): - # Generate a unique event body for EventHub event - # Record the start_time and end_time for checking event enqueue time - random_number = str(round(time.time()) % 1000) - req_body = { - 'body': random_number - } - - # Invoke metadata_output HttpTrigger to generate an EventHub event - # from azure-eventhub SDK - r = self.webhost.request('POST', 'metadata_output', - data=json.dumps(req_body)) - self.assertEqual(r.status_code, 200) - self.assertIn('OK', r.text) - - # Once the event get generated, allow function host to pool from - # EventHub and wait for eventhub_trigger to execute, - # converting the event metadata into a blob. - time.sleep(5) - - # Call get_metadata_triggered to retrieve event metadata from blob - r = self.webhost.request('GET', 'get_metadata_triggered') - - # Waiting for the blob get updated with the latest data from the - # eventhub output binding - time.sleep(5) - self.assertEqual(r.status_code, 200) - - # Check if the event body matches the unique random_number - event = r.json() - self.assertEqual(event['body'], random_number) - - # EventhubEvent property check - # Reenable these lines after enqueued_time property is fixed - # enqueued_time = parser.isoparse(event['enqueued_time']) - # self.assertIsNotNone(enqueued_time) - self.assertIsNone(event['partition_key']) # There's only 1 partition - self.assertGreaterEqual(event['sequence_number'], 0) - self.assertIsNotNone(event['offset']) - - # Check if the event contains proper metadata fields - self.assertIsNotNone(event['metadata']) - metadata = event['metadata'] - sys_props = metadata['SystemProperties'] - self.assertIsNone(sys_props['PartitionKey']) - self.assertGreaterEqual(sys_props['SequenceNumber'], 0) - self.assertIsNotNone(sys_props['Offset']) - - -class TestEventHubFunctionsStein(TestEventHubFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.EMULATOR_TESTS_FOLDER / 'eventhub_functions' / \ - 'eventhub_functions_stein' - - -class TestEventHubFunctionsSteinGeneric(TestEventHubFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.EMULATOR_TESTS_FOLDER / 'eventhub_functions' / \ - 'eventhub_functions_stein' / 'generic' diff --git a/tests/emulator_tests/test_generic_functions.py b/tests/emulator_tests/test_generic_functions.py deleted file mode 100644 index 8dc44c835..000000000 --- a/tests/emulator_tests/test_generic_functions.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import time -import typing - -from tests.utils import testutils - - -class TestGenericFunctions(testutils.WebHostTestCase): - """Test Generic Functions with implicit output enabled - - With implicit output enabled for generic types, these tests cover - scenarios where a function has both explicit and implicit output - set to true. We prioritize explicit output. These tests check - that no matter the ordering, the return type is still correctly set. - """ - - @classmethod - def get_script_dir(cls): - return testutils.EMULATOR_TESTS_FOLDER / 'generic_functions' - - def test_return_processed_last(self): - # Tests the case where implicit and explicit return are true - # in the same function and $return is processed before - # the generic binding is - out_resp = self.webhost.request('POST', 'table_out_binding') - self.assertEqual(out_resp.status_code, 200) - - r = self.webhost.request('GET', 'return_processed_last') - self.assertEqual(r.status_code, 200) - - def test_return_not_processed_last(self): - # Tests the case where implicit and explicit return are true - # in the same function and the generic binding is processed - # before $return - out_resp = self.webhost.request('POST', 'table_out_binding') - self.assertEqual(out_resp.status_code, 200) - - r = self.webhost.request('GET', 'return_not_processed_last') - self.assertEqual(r.status_code, 200) - - def test_return_types(self): - out_resp = self.webhost.request('POST', 'table_out_binding') - self.assertEqual(out_resp.status_code, 200) - # Checking that the function app is okay - time.sleep(10) - # Checking webhost status. - r = self.webhost.request('GET', '', no_prefix=True, - timeout=5) - self.assertTrue(r.ok) - - def check_log_return_types(self, host_out: typing.List[str]): - # Checks that functions executed correctly - self.assertIn("This timer trigger function executed " - "successfully", host_out) - self.assertIn("Return string", host_out) - self.assertIn("Return bytes", host_out) - self.assertIn("Return dict", host_out) - self.assertIn("Return list", host_out) - self.assertIn("Return int", host_out) - self.assertIn("Return double", host_out) - self.assertIn("Return bool", host_out) - - # Checks for failed executions (TypeErrors, etc.) - errors_found = False - for log in host_out: - if "Exception" in log: - errors_found = True - break - self.assertFalse(errors_found) - - -class TestGenericFunctionsStein(TestGenericFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.EMULATOR_TESTS_FOLDER / 'generic_functions' / \ - 'generic_functions_stein' diff --git a/tests/emulator_tests/test_queue_functions.py b/tests/emulator_tests/test_queue_functions.py deleted file mode 100644 index 793628169..000000000 --- a/tests/emulator_tests/test_queue_functions.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import time - -from tests.utils import testutils - - -class TestQueueFunctions(testutils.WebHostTestCase): - - @classmethod - def get_script_dir(cls): - return testutils.EMULATOR_TESTS_FOLDER / 'queue_functions' - - def test_queue_basic(self): - r = self.webhost.request('POST', 'put_queue', - data='test-message') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - # wait for queue_trigger to process the queue item - time.sleep(1) - - r = self.webhost.request('GET', 'get_queue_blob') - self.assertEqual(r.status_code, 200) - msg_info = r.json() - - self.assertIn('queue', msg_info) - msg = msg_info['queue'] - - self.assertEqual(msg['body'], 'test-message') - for attr in {'id', 'expiration_time', 'insertion_time', - 'time_next_visible', 'pop_receipt', 'dequeue_count'}: - self.assertIsNotNone(msg.get(attr)) - - def test_queue_return(self): - r = self.webhost.request('POST', 'put_queue_return', - data='test-message-return') - self.assertEqual(r.status_code, 200) - - # wait for queue_trigger to process the queue item - time.sleep(1) - - r = self.webhost.request('GET', 'get_queue_blob_return') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'test-message-return') - - def test_queue_message_object_return(self): - r = self.webhost.request('POST', 'put_queue_message_return', - data='test-message-object-return') - self.assertEqual(r.status_code, 200) - - # wait for queue_trigger to process the queue item - time.sleep(1) - - r = self.webhost.request('GET', 'get_queue_blob_message_return') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'test-message-object-return') - - def test_queue_untyped_return(self): - r = self.webhost.request('POST', 'put_queue_untyped_return', - data='test-untyped-return') - self.assertEqual(r.status_code, 200) - - # wait for queue_trigger to process the queue item - time.sleep(1) - - r = self.webhost.request('GET', 'get_queue_untyped_blob_return') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'test-untyped-return') - - def test_queue_return_multiple(self): - r = self.webhost.request('POST', 'put_queue_return_multiple', - data='foo') - self.assertTrue(200 <= r.status_code < 300, - f"Returned status code {r.status_code}, " - "not in the 200-300 range.") - - # wait for queue_trigger to process the queue item - time.sleep(1) - - def test_queue_return_multiple_outparam(self): - r = self.webhost.request('POST', 'put_queue_multiple_out', - data='foo') - self.assertTrue(200 <= r.status_code < 300, - f"Returned status code {r.status_code}, " - "not in the 200-300 range.") - self.assertEqual(r.text, 'HTTP response: foo') - - -class TestQueueFunctionsStein(TestQueueFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.EMULATOR_TESTS_FOLDER / 'queue_functions' / \ - 'queue_functions_stein' - - -class TestQueueFunctionsSteinGeneric(TestQueueFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.EMULATOR_TESTS_FOLDER / 'queue_functions' / \ - 'queue_functions_stein' / 'generic' diff --git a/tests/emulator_tests/test_servicebus_functions.py b/tests/emulator_tests/test_servicebus_functions.py deleted file mode 100644 index 2e6bd7310..000000000 --- a/tests/emulator_tests/test_servicebus_functions.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json -import time - -from tests.utils import testutils - - -class TestServiceBusFunctions(testutils.WebHostTestCase): - - @classmethod - def get_script_dir(cls): - return testutils.EMULATOR_TESTS_FOLDER / 'servicebus_functions' - - @testutils.retryable_test(3, 5) - def test_servicebus_basic(self): - data = str(round(time.time())) - r = self.webhost.request('POST', 'put_message', - data=data) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - max_retries = 10 - - for try_no in range(max_retries): - # wait for trigger to process the queue item - time.sleep(1) - - try: - r = self.webhost.request('GET', 'get_servicebus_triggered') - 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'}: - self.assertIn(attr, msg) - except (AssertionError, json.JSONDecodeError): - if try_no == max_retries - 1: - raise - else: - break - - -class TestServiceBusFunctionsStein(TestServiceBusFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.EMULATOR_TESTS_FOLDER / 'servicebus_functions' / \ - 'servicebus_functions_stein' - - -class TestServiceBusFunctionsSteinGeneric(TestServiceBusFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.EMULATOR_TESTS_FOLDER / 'servicebus_functions' / \ - 'servicebus_functions_stein' / 'generic' diff --git a/tests/emulator_tests/test_table_functions.py b/tests/emulator_tests/test_table_functions.py deleted file mode 100644 index b5282bd92..000000000 --- a/tests/emulator_tests/test_table_functions.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json -import pathlib -import time - -from tests.utils import testutils - - -class TestTableFunctions(testutils.WebHostTestCase): - - @classmethod - def get_script_dir(cls): - return testutils.EMULATOR_TESTS_FOLDER / 'table_functions' - - def test_table_bindings(self): - out_resp = self.webhost.request('POST', 'table_out_binding') - self.assertEqual(out_resp.status_code, 200) - row_key = json.loads(out_resp.text)['RowKey'] - - script_dir = pathlib.Path(self.get_script_dir()) - json_path = pathlib.Path('table_in_binding/function.json') - full_json_path = testutils.TESTS_ROOT / script_dir / json_path - # Dynamically rewrite function.json to point to new row key - with open(full_json_path, 'r') as f: - func_dict = json.load(f) - func_dict['bindings'][1]['rowKey'] = row_key - - with open(full_json_path, 'w') as f: - json.dump(func_dict, f, indent=2) - - # wait for host to restart after change - time.sleep(1) - - in_resp = self.webhost.request('GET', 'table_in_binding') - self.assertEqual(in_resp.status_code, 200) - row_key_present = False - for row in json.loads(in_resp.text): - if row["RowKey"] == row_key: - row_key_present = True - break - self.assertTrue(row_key_present) - - -class TestTableFunctionsStein(testutils.WebHostTestCase): - - @classmethod - def get_script_dir(cls): - return testutils.EMULATOR_TESTS_FOLDER / 'table_functions' / \ - 'table_functions_stein' - - def test_table_bindings(self): - out_resp = self.webhost.request('POST', 'table_out_binding') - self.assertEqual(out_resp.status_code, 200) - row_key = json.loads(out_resp.text)['RowKey'] - - in_resp = self.webhost.request('GET', f'table_in_binding/{row_key}') - self.assertEqual(in_resp.status_code, 200) - row_key_present = False - for row in json.loads(in_resp.text): - if row["RowKey"] == row_key: - row_key_present = True - break - self.assertTrue(row_key_present) - - -class TestTableFunctionsGeneric(TestTableFunctionsStein): - - @classmethod - def get_script_dir(cls): - return testutils.EMULATOR_TESTS_FOLDER / 'table_functions' / \ - 'table_functions_stein' / \ - 'generic' diff --git a/tests/emulator_tests/utils/eventhub/config.json b/tests/emulator_tests/utils/eventhub/config.json deleted file mode 100644 index 710935c14..000000000 --- a/tests/emulator_tests/utils/eventhub/config.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "UserConfig": { - "NamespaceConfig": [ - { - "Type": "EventHub", - "Name": "emulatorNs1", - "Entities": [ - { - "Name": "python-worker-ci-eventhub-batch", - "PartitionCount": 2, - "ConsumerGroups": [ - { - "Name": "cg1" - } - ] - }, - { - "Name": "python-worker-ci-eventhub-batch-metadata", - "PartitionCount": 2, - "ConsumerGroups": [ - { - "Name": "cg1" - } - ] - }, - { - "Name": "python-worker-ci-eventhub-one", - "PartitionCount": 2, - "ConsumerGroups": [ - { - "Name": "cg1" - } - ] - }, - { - "Name": "python-worker-ci-eventhub-one-metadata", - "PartitionCount": 2, - "ConsumerGroups": [ - { - "Name": "cg1" - } - ] - } - ] - } - ], - "LoggingConfig": { - "Type": "File" - } - } -} \ No newline at end of file diff --git a/tests/emulator_tests/utils/eventhub/docker-compose.yml b/tests/emulator_tests/utils/eventhub/docker-compose.yml deleted file mode 100644 index 2c40aa042..000000000 --- a/tests/emulator_tests/utils/eventhub/docker-compose.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: microsoft-azure-eventhubs -services: - # Service for the Event Hubs Emulator - emulator: - container_name: "eventhubs-emulator" - image: "mcr.microsoft.com/azure-messaging/eventhubs-emulator:latest" - volumes: - - "./config.json:/Eventhubs_Emulator/ConfigFiles/Config.json" - ports: - - "5672:5672" - environment: - BLOB_SERVER: azurite - METADATA_SERVER: azurite - ACCEPT_EULA: Y - depends_on: - - azurite - networks: - eh-emulator: - aliases: - - "eventhubs-emulator" - # Service for the Azurite Storage Emulator - azurite: - container_name: "azurite" - image: "mcr.microsoft.com/azure-storage/azurite:latest" - ports: - - "10000:10000" - - "10001:10001" - - "10002:10002" - networks: - eh-emulator: - aliases: - - "azurite" -networks: - eh-emulator: \ No newline at end of file diff --git a/tests/emulator_tests/utils/servicebus/config.json b/tests/emulator_tests/utils/servicebus/config.json deleted file mode 100644 index 20cf83447..000000000 --- a/tests/emulator_tests/utils/servicebus/config.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "UserConfig": { - "Namespaces": [ - { - "Name": "sbemulatorns", - "Queues": [ - { - "Name": "testqueue", - "Properties": { - "DeadLetteringOnMessageExpiration": false, - "DefaultMessageTimeToLive": "PT1H", - "DuplicateDetectionHistoryTimeWindow": "PT20S", - "ForwardDeadLetteredMessagesTo": "", - "ForwardTo": "", - "LockDuration": "PT1M", - "MaxDeliveryCount": 10, - "RequiresDuplicateDetection": false, - "RequiresSession": false - } - } - ] - } - ], - "Logging": { - "Type": "File" - } - } -} \ No newline at end of file diff --git a/tests/emulator_tests/utils/servicebus/docker-compose.yml b/tests/emulator_tests/utils/servicebus/docker-compose.yml deleted file mode 100644 index c1781a858..000000000 --- a/tests/emulator_tests/utils/servicebus/docker-compose.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: microsoft-azure-servicebus -services: - # Service for the Service Bus Emulator - sbemulator: - container_name: "servicebus-emulator" - image: mcr.microsoft.com/azure-messaging/servicebus-emulator:latest - volumes: - - "./config.json:/ServiceBus_Emulator/ConfigFiles/Config.json" - ports: - - "5672:5672" - environment: - SQL_SERVER: sqledge - MSSQL_SA_PASSWORD: ${AzureWebJobsSQLPassword} - ACCEPT_EULA: Y - depends_on: - - sqledge - networks: - sb-emulator: - aliases: - - "sb-emulator" - sqledge: - container_name: "sqledge" - image: "mcr.microsoft.com/azure-sql-edge:latest" - networks: - sb-emulator: - aliases: - - "sqledge" - environment: - ACCEPT_EULA: Y - MSSQL_SA_PASSWORD: ${AzureWebJobsSQLPassword} - # Service for the Azurite Storage Emulator - azurite: - container_name: "azurite-sb" - image: "mcr.microsoft.com/azure-storage/azurite:latest" - ports: - - "10003:10003" - - "10004:10004" - - "10005:10005" -networks: - sb-emulator: \ No newline at end of file diff --git a/tests/endtoend/blueprint_functions/blueprint_different_dir/blueprint_directory/blueprint.py b/tests/endtoend/blueprint_functions/blueprint_different_dir/blueprint_directory/blueprint.py deleted file mode 100644 index d232dcead..000000000 --- a/tests/endtoend/blueprint_functions/blueprint_different_dir/blueprint_directory/blueprint.py +++ /dev/null @@ -1,41 +0,0 @@ -import logging -import time -from datetime import datetime - -import azure.functions as func - -bp = func.Blueprint() - - -@bp.route(route="default_template") -def default_template(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - name = req.params.get('name') - if not name: - try: - req_body = req.get_json() - except ValueError: - pass - else: - name = req_body.get('name') - - if name: - return func.HttpResponse( - f"Hello, {name}. This HTTP triggered function " - f"executed successfully.") - else: - return func.HttpResponse( - "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 - ) - - -@bp.route(route="http_func") -def http_func(req: func.HttpRequest) -> func.HttpResponse: - time.sleep(1) - - current_time = datetime.now().strftime("%H:%M:%S") - return func.HttpResponse(f"{current_time}") diff --git a/tests/endtoend/blueprint_functions/blueprint_different_dir/function_app.py b/tests/endtoend/blueprint_functions/blueprint_different_dir/function_app.py deleted file mode 100644 index 3f1ba52d6..000000000 --- a/tests/endtoend/blueprint_functions/blueprint_different_dir/function_app.py +++ /dev/null @@ -1,6 +0,0 @@ -import azure.functions as func -from blueprint_directory.blueprint import bp - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - -app.register_functions(bp) diff --git a/tests/endtoend/blueprint_functions/functions_in_blueprint_only/blueprint.py b/tests/endtoend/blueprint_functions/functions_in_blueprint_only/blueprint.py deleted file mode 100644 index d232dcead..000000000 --- a/tests/endtoend/blueprint_functions/functions_in_blueprint_only/blueprint.py +++ /dev/null @@ -1,41 +0,0 @@ -import logging -import time -from datetime import datetime - -import azure.functions as func - -bp = func.Blueprint() - - -@bp.route(route="default_template") -def default_template(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - name = req.params.get('name') - if not name: - try: - req_body = req.get_json() - except ValueError: - pass - else: - name = req_body.get('name') - - if name: - return func.HttpResponse( - f"Hello, {name}. This HTTP triggered function " - f"executed successfully.") - else: - return func.HttpResponse( - "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 - ) - - -@bp.route(route="http_func") -def http_func(req: func.HttpRequest) -> func.HttpResponse: - time.sleep(1) - - current_time = datetime.now().strftime("%H:%M:%S") - return func.HttpResponse(f"{current_time}") diff --git a/tests/endtoend/blueprint_functions/functions_in_blueprint_only/function_app.py b/tests/endtoend/blueprint_functions/functions_in_blueprint_only/function_app.py deleted file mode 100644 index 44712bfee..000000000 --- a/tests/endtoend/blueprint_functions/functions_in_blueprint_only/function_app.py +++ /dev/null @@ -1,6 +0,0 @@ -import azure.functions as func -from blueprint import bp - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - -app.register_functions(bp) diff --git a/tests/endtoend/blueprint_functions/functions_in_both_blueprint_functionapp/blueprint.py b/tests/endtoend/blueprint_functions/functions_in_both_blueprint_functionapp/blueprint.py deleted file mode 100644 index 785049396..000000000 --- a/tests/endtoend/blueprint_functions/functions_in_both_blueprint_functionapp/blueprint.py +++ /dev/null @@ -1,31 +0,0 @@ -import logging - -import azure.functions as func - -bp = func.Blueprint() - - -@bp.route(route="default_template") -def default_template(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - name = req.params.get('name') - if not name: - try: - req_body = req.get_json() - except ValueError: - pass - else: - name = req_body.get('name') - - if name: - return func.HttpResponse( - f"Hello, {name}. This HTTP triggered function " - f"executed successfully.") - else: - return func.HttpResponse( - "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 - ) diff --git a/tests/endtoend/blueprint_functions/functions_in_both_blueprint_functionapp/function_app.py b/tests/endtoend/blueprint_functions/functions_in_both_blueprint_functionapp/function_app.py deleted file mode 100644 index 1813e0578..000000000 --- a/tests/endtoend/blueprint_functions/functions_in_both_blueprint_functionapp/function_app.py +++ /dev/null @@ -1,12 +0,0 @@ -import azure.functions as func -from blueprint import bp - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - -app.register_blueprint(bp) - - -@app.route(route="return_http") -def return_http(req: func.HttpRequest): - return func.HttpResponse('

    Hello Worldâ„¢

    ', - mimetype='text/html') diff --git a/tests/endtoend/blueprint_functions/multiple_function_registers/function_app.py b/tests/endtoend/blueprint_functions/multiple_function_registers/function_app.py deleted file mode 100644 index 2b212266d..000000000 --- a/tests/endtoend/blueprint_functions/multiple_function_registers/function_app.py +++ /dev/null @@ -1,12 +0,0 @@ -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.route(route="return_http") -def return_http(req: func.HttpRequest): - return func.HttpResponse('

    Hello Worldâ„¢

    ', - mimetype='text/html') - - -asgi_app = func.AsgiFunctionApp() diff --git a/tests/endtoend/blueprint_functions/only_blueprint/function_app.py b/tests/endtoend/blueprint_functions/only_blueprint/function_app.py deleted file mode 100644 index 785049396..000000000 --- a/tests/endtoend/blueprint_functions/only_blueprint/function_app.py +++ /dev/null @@ -1,31 +0,0 @@ -import logging - -import azure.functions as func - -bp = func.Blueprint() - - -@bp.route(route="default_template") -def default_template(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - name = req.params.get('name') - if not name: - try: - req_body = req.get_json() - except ValueError: - pass - else: - name = req_body.get('name') - - if name: - return func.HttpResponse( - f"Hello, {name}. This HTTP triggered function " - f"executed successfully.") - else: - return func.HttpResponse( - "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 - ) diff --git a/tests/endtoend/cosmosdb_functions/cosmosdb_functions_stein/function_app.py b/tests/endtoend/cosmosdb_functions/cosmosdb_functions_stein/function_app.py deleted file mode 100644 index c0ddcaad1..000000000 --- a/tests/endtoend/cosmosdb_functions/cosmosdb_functions_stein/function_app.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.route() -@app.cosmos_db_input( - arg_name="docs", database_name="test", - container_name="items", - id="cosmosdb-input-test", - connection="AzureWebJobsCosmosDBConnectionString") -def cosmosdb_input(req: func.HttpRequest, docs: func.DocumentList) -> str: - return func.HttpResponse(docs[0].to_json(), mimetype='application/json') - - -@app.cosmos_db_trigger( - arg_name="docs", database_name="test", - container_name="items", - lease_container_name="leases", - connection="AzureWebJobsCosmosDBConnectionString", - create_lease_container_if_not_exists=True) -@app.blob_output(arg_name="$return", connection="AzureWebJobsStorage", - path="python-worker-tests/test-cosmosdb-triggered.txt") -def cosmosdb_trigger(docs: func.DocumentList) -> str: - return docs[0].to_json() - - -@app.route() -@app.blob_input(arg_name="file", connection="AzureWebJobsStorage", - path="python-worker-tests/test-cosmosdb-triggered.txt") -def get_cosmosdb_triggered(req: func.HttpRequest, - file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -@app.route() -@app.cosmos_db_output( - arg_name="doc", database_name="test", - container_name="items", - create_if_not_exists=True, - connection="AzureWebJobsCosmosDBConnectionString") -def put_document(req: func.HttpRequest, doc: func.Out[func.Document]): - doc.set(func.Document.from_json(req.get_body())) - return 'OK' diff --git a/tests/endtoend/cosmosdb_functions/cosmosdb_functions_stein/generic/function_app.py b/tests/endtoend/cosmosdb_functions/cosmosdb_functions_stein/generic/function_app.py deleted file mode 100644 index baf665be6..000000000 --- a/tests/endtoend/cosmosdb_functions/cosmosdb_functions_stein/generic/function_app.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.generic_trigger(arg_name="req", type="httpTrigger") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding( - arg_name="docs", - type="cosmosDB", - database_name="test", - container_name="items", - id="cosmosdb-input-test", - connection="AzureWebJobsCosmosDBConnectionString") -def cosmosdb_input(req: func.HttpRequest, docs: func.DocumentList) -> str: - return func.HttpResponse(docs[0].to_json(), mimetype='application/json') - - -@app.generic_trigger( - arg_name="docs", - type="cosmosDBTrigger", - database_name="test", - container_name="items", - lease_container_name="leases", - connection="AzureWebJobsCosmosDBConnectionString", - create_lease_container_if_not_exists=True) -@app.generic_output_binding( - arg_name="$return", - type="blob", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-cosmosdb-triggered.txt") -def cosmosdb_trigger(docs: func.DocumentList) -> str: - return docs[0].to_json() - - -@app.generic_trigger(arg_name="req", type="httpTrigger") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding( - arg_name="file", - connection="AzureWebJobsStorage", - type="blob", - path="python-worker-tests/test-cosmosdb-triggered.txt") -def get_cosmosdb_triggered(req: func.HttpRequest, - file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -@app.generic_trigger(arg_name="req", type="httpTrigger") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_output_binding( - arg_name="doc", - database_name="test", - type="cosmosDB", - container_name="items", - create_if_not_exists=True, - connection="AzureWebJobsCosmosDBConnectionString") -def put_document(req: func.HttpRequest, doc: func.Out[func.Document]): - doc.set(func.Document.from_json(req.get_body())) - return 'OK' diff --git a/tests/endtoend/cosmosdb_functions/cosmosdb_input/__init__.py b/tests/endtoend/cosmosdb_functions/cosmosdb_input/__init__.py deleted file mode 100644 index 313d63137..000000000 --- a/tests/endtoend/cosmosdb_functions/cosmosdb_input/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func - - -def main(req: func.HttpRequest, docs: func.DocumentList) -> str: - return func.HttpResponse(docs[0].to_json(), mimetype='application/json') diff --git a/tests/endtoend/cosmosdb_functions/cosmosdb_input/function.json b/tests/endtoend/cosmosdb_functions/cosmosdb_input/function.json deleted file mode 100644 index 23608f043..000000000 --- a/tests/endtoend/cosmosdb_functions/cosmosdb_input/function.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "scriptFile": "__init__.py", - - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "direction": "in", - "type": "cosmosDB", - "name": "docs", - "databaseName": "test", - "containerName": "items", - "id": "cosmosdb-input-test", - "leaseContainerName": "leases", - "connection": "AzureWebJobsCosmosDBConnectionString", - "createLeaseContainerIfNotExists": true - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/endtoend/cosmosdb_functions/cosmosdb_trigger/__init__.py b/tests/endtoend/cosmosdb_functions/cosmosdb_trigger/__init__.py deleted file mode 100644 index a8868aa79..000000000 --- a/tests/endtoend/cosmosdb_functions/cosmosdb_trigger/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(docs: azf.DocumentList) -> str: - return docs[0].to_json() diff --git a/tests/endtoend/cosmosdb_functions/cosmosdb_trigger/function.json b/tests/endtoend/cosmosdb_functions/cosmosdb_trigger/function.json deleted file mode 100644 index 76a24c07d..000000000 --- a/tests/endtoend/cosmosdb_functions/cosmosdb_trigger/function.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "scriptFile": "__init__.py", - - "bindings": [ - { - "direction": "in", - "type": "cosmosDBTrigger", - "name": "docs", - "databaseName": "test", - "containerName": "items", - "id": "cosmosdb-trigger-test", - "leaseContainerName": "leases", - "connection": "AzureWebJobsCosmosDBConnectionString", - "createLeaseContainerIfNotExists": true - }, - { - "type": "blob", - "direction": "out", - "name": "$return", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-cosmosdb-triggered.txt" - } - ] -} diff --git a/tests/endtoend/cosmosdb_functions/cosmosdb_v3_functions_stein/function_app.py b/tests/endtoend/cosmosdb_functions/cosmosdb_v3_functions_stein/function_app.py deleted file mode 100644 index 27f51e38a..000000000 --- a/tests/endtoend/cosmosdb_functions/cosmosdb_v3_functions_stein/function_app.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func - -app = func.FunctionApp() - - -@app.route() -@app.cosmos_db_input_v3( - arg_name="docs", database_name="test", - collection_name="items", - id="cosmosdb-input-test", - connection_string_setting="AzureWebJobsCosmosDBConnectionString") -def cosmosdb_input(req: func.HttpRequest, docs: func.DocumentList) -> str: - return func.HttpResponse(docs[0].to_json(), mimetype='application/json') - - -@app.cosmos_db_trigger_v3( - arg_name="docs", database_name="test", - collection_name="items", - lease_collection_name="leases", - connection_string_setting="AzureWebJobsCosmosDBConnectionString", - create_lease_collection_if_not_exists=True) -@app.blob_output(arg_name="$return", connection="AzureWebJobsStorage", - path="python-worker-tests/test-cosmosdb-triggered.txt") -def cosmosdb_trigger(docs: func.DocumentList) -> str: - return docs[0].to_json() - - -@app.route() -@app.blob_input(arg_name="file", connection="AzureWebJobsStorage", - path="python-worker-tests/test-cosmosdb-triggered.txt") -def get_cosmosdb_triggered(req: func.HttpRequest, - file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -@app.route() -@app.cosmos_db_output_v3( - arg_name="doc", database_name="test", - collection_name="items", - create_if_not_exists=True, - connection_string_setting="AzureWebJobsCosmosDBConnectionString") -def put_document(req: func.HttpRequest, doc: func.Out[func.Document]): - doc.set(func.Document.from_json(req.get_body())) - - return 'OK' diff --git a/tests/endtoend/cosmosdb_functions/cosmosdb_v3_functions_stein/generic/function_app.py b/tests/endtoend/cosmosdb_functions/cosmosdb_v3_functions_stein/generic/function_app.py deleted file mode 100644 index dee78952a..000000000 --- a/tests/endtoend/cosmosdb_functions/cosmosdb_v3_functions_stein/generic/function_app.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func - -app = func.FunctionApp() - - -@app.generic_trigger(arg_name="req", type="httpTrigger") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding( - arg_name="docs", - type="cosmosDB", - database_name="test", - collection_name="items", - id="cosmosdb-input-test", - connection_string_setting="AzureWebJobsCosmosDBConnectionString") -def cosmosdb_input(req: func.HttpRequest, docs: func.DocumentList) -> str: - return func.HttpResponse(docs[0].to_json(), mimetype='application/json') - - -@app.generic_trigger( - arg_name="docs", - type="cosmosDBTrigger", - database_name="test", - collection_name="items", - lease_collection_name="leases", - connection_string_setting="AzureWebJobsCosmosDBConnectionString", - create_lease_collection_if_not_exists=True) -@app.generic_output_binding( - arg_name="$return", - type="blob", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-cosmosdb-triggered.txt") -def cosmosdb_trigger(docs: func.DocumentList) -> str: - return docs[0].to_json() - - -@app.generic_trigger(arg_name="req", type="httpTrigger") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding( - arg_name="file", - connection="AzureWebJobsStorage", - type="blob", - path="python-worker-tests/test-cosmosdb-triggered.txt") -def get_cosmosdb_triggered(req: func.HttpRequest, - file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -@app.generic_trigger(arg_name="req", type="httpTrigger") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_output_binding( - arg_name="doc", - database_name="test", - type="cosmosDB", - collection_name="items", - create_if_not_exists=True, - connection_string_setting="AzureWebJobsCosmosDBConnectionString") -def put_document(req: func.HttpRequest, doc: func.Out[func.Document]): - doc.set(func.Document.from_json(req.get_body())) - - return 'OK' diff --git a/tests/endtoend/cosmosdb_functions/get_cosmosdb_triggered/function.json b/tests/endtoend/cosmosdb_functions/get_cosmosdb_triggered/function.json deleted file mode 100644 index e3778812e..000000000 --- a/tests/endtoend/cosmosdb_functions/get_cosmosdb_triggered/function.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "in", - "name": "file", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-cosmosdb-triggered.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return", - } - ] -} diff --git a/tests/endtoend/cosmosdb_functions/get_cosmosdb_triggered/main.py b/tests/endtoend/cosmosdb_functions/get_cosmosdb_triggered/main.py deleted file mode 100644 index 167c7a574..000000000 --- a/tests/endtoend/cosmosdb_functions/get_cosmosdb_triggered/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func - - -def main(req: func.HttpRequest, file: func.InputStream) -> str: - return file.read().decode('utf-8') diff --git a/tests/endtoend/cosmosdb_functions/put_document/__init__.py b/tests/endtoend/cosmosdb_functions/put_document/__init__.py deleted file mode 100644 index 5e481332e..000000000 --- a/tests/endtoend/cosmosdb_functions/put_document/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func - - -def main(req: func.HttpRequest, doc: func.Out[func.Document]): - doc.set(func.Document.from_json(req.get_body())) - - return 'OK' diff --git a/tests/endtoend/cosmosdb_functions/put_document/function.json b/tests/endtoend/cosmosdb_functions/put_document/function.json deleted file mode 100644 index b385fbfd5..000000000 --- a/tests/endtoend/cosmosdb_functions/put_document/function.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "scriptFile": "__init__.py", - - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "direction": "out", - "type": "cosmosDB", - "name": "doc", - "databaseName": "test", - "containerName": "items", - "leaseContainerName": "leases", - "createLeaseContainerIfNotExists": true, - "connection": "AzureWebJobsCosmosDBConnectionString", - "createIfNotExists": true - }, - { - "direction": "out", - "name": "$return", - "type": "http" - } - ] -} diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/__init__.py b/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/__init__.py deleted file mode 100644 index 88cb6b2dc..000000000 --- a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -# Import binding implementations to register them -from . import http # NoQA -from ._abc import Context, Out -from ._http import HttpRequest, HttpResponse -from .meta import get_binding_registry - -__all__ = ( - # Functions - 'get_binding_registry', - - # Generics. - 'Context', - 'Out', - - # Binding rich types, sorted alphabetically. - 'HttpRequest', - 'HttpResponse', -) - -__version__ = '9.9.9' diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_abc.py b/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_abc.py deleted file mode 100644 index 8add53c99..000000000 --- a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_abc.py +++ /dev/null @@ -1,422 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import abc -import datetime -import io -import typing - -T = typing.TypeVar('T') - - -class Out(abc.ABC, typing.Generic[T]): - """An interface to set function output parameters.""" - - @abc.abstractmethod - def set(self, val: T) -> None: - """Set the value of the output parameter.""" - pass - - @abc.abstractmethod - def get(self) -> T: - """Get the value of the output parameter.""" - pass - - -class RpcException: - """Rpc Exception object.""" - - @property - @abc.abstractmethod - def source(self) -> str: - """Source of the exception.""" - pass - - @property - @abc.abstractmethod - def stack_trace(self) -> str: - """Stack trace for the exception.""" - pass - - @property - @abc.abstractmethod - def message(self) -> str: - """Textual message describing the exception.""" - pass - - -class TraceContext(abc.ABC): - """Trace context object.""" - - @property - @abc.abstractmethod - def trace_state(self) -> str: - """Gets trace state from trace-context.""" - pass - - @property - @abc.abstractmethod - def trace_parent(self) -> str: - """Gets trace parent from trace-context.""" - pass - - @property - @abc.abstractmethod - def attributes(self) -> typing.Dict[str, str]: - """Gets trace-context attributes.""" - pass - - -class RetryContext(abc.ABC): - """Retry Context object. - For more information refer: https://aka.ms/azfunc-retries-policies - """ - - @property - @abc.abstractmethod - def retry_count(self) -> int: - """Gets the current retry count from retry-context.""" - pass - - @property - @abc.abstractmethod - def max_retry_count(self) -> int: - """Gets the max retry count from retry-context.""" - pass - - @property - @abc.abstractmethod - def exception(self) -> RpcException: - """Gets the RpcException""" - pass - - -class Context(abc.ABC): - """Function invocation context.""" - - @property - @abc.abstractmethod - def invocation_id(self) -> str: - """Function invocation ID.""" - pass - - @property - @abc.abstractmethod - def function_name(self) -> str: - """Function name.""" - pass - - @property - @abc.abstractmethod - def function_directory(self) -> str: - """Function directory.""" - pass - - @property - @abc.abstractmethod - def trace_context(self) -> TraceContext: - """Context for distributed tracing.""" - pass - - @property - @abc.abstractmethod - def retry_context(self) -> RetryContext: - """Context for retries to the function.""" - pass - - -class HttpRequest(abc.ABC): - """HTTP request object.""" - - @property - @abc.abstractmethod - def method(self) -> str: - """Request method.""" - pass - - @property - @abc.abstractmethod - def url(self) -> str: - """Request URL.""" - pass - - @property - @abc.abstractmethod - def headers(self) -> typing.Mapping[str, str]: - """A dictionary containing request headers.""" - pass - - @property - @abc.abstractmethod - def params(self) -> typing.Mapping[str, str]: - """A dictionary containing request GET parameters.""" - pass - - @property - @abc.abstractmethod - def route_params(self) -> typing.Mapping[str, str]: - """A dictionary containing request route parameters.""" - pass - - @abc.abstractmethod - def get_body(self) -> bytes: - """Return request body as bytes.""" - pass - - @abc.abstractmethod - def get_json(self) -> typing.Any: - """Decode and return request body as JSON. - - :raises ValueError: - when the request does not contain valid JSON data. - """ - pass - - -class HttpResponse(abc.ABC): - - @property - @abc.abstractmethod - def status_code(self) -> int: - pass - - @property - @abc.abstractmethod - def mimetype(self): - pass - - @property - @abc.abstractmethod - def charset(self): - pass - - @property - @abc.abstractmethod - def headers(self) -> typing.MutableMapping[str, str]: - pass - - @abc.abstractmethod - def get_body(self) -> bytes: - pass - - -class TimerRequest(abc.ABC): - """Timer request object.""" - - @property - @abc.abstractmethod - def past_due(self) -> bool: - """Whether the timer is past due.""" - pass - - -class InputStream(io.BufferedIOBase, abc.ABC): - """File-like object representing an input blob.""" - - @abc.abstractmethod - def read(self, size=-1) -> bytes: - """Return and read up to *size* bytes. - - :param int size: - The number of bytes to read. If the argument is omitted, - ``None``, or negative, data is read and returned until - EOF is reached. - - :return: - Bytes read from the input stream. - """ - pass - - @property - @abc.abstractmethod - def name(self) -> typing.Optional[str]: - """The name of the blob.""" - pass - - @property - @abc.abstractmethod - def length(self) -> typing.Optional[int]: - """The size of the blob in bytes.""" - pass - - @property - @abc.abstractmethod - def uri(self) -> typing.Optional[str]: - """The blob's primary location URI.""" - pass - - -class QueueMessage(abc.ABC): - - @property - @abc.abstractmethod - def id(self) -> typing.Optional[str]: - pass - - @abc.abstractmethod - def get_body(self) -> typing.Union[str, bytes]: - pass - - @abc.abstractmethod - def get_json(self) -> typing.Any: - pass - - @property - @abc.abstractmethod - def dequeue_count(self) -> typing.Optional[int]: - pass - - @property - @abc.abstractmethod - def expiration_time(self) -> typing.Optional[datetime.datetime]: - pass - - @property - @abc.abstractmethod - def insertion_time(self) -> typing.Optional[datetime.datetime]: - pass - - @property - @abc.abstractmethod - def time_next_visible(self) -> typing.Optional[datetime.datetime]: - pass - - @property - @abc.abstractmethod - def pop_receipt(self) -> typing.Optional[str]: - pass - - -class EventGridEvent(abc.ABC): - @property - @abc.abstractmethod - def id(self) -> str: - pass - - @abc.abstractmethod - def get_json(self) -> typing.Any: - pass - - @property - @abc.abstractmethod - def topic(self) -> str: - pass - - @property - @abc.abstractmethod - def subject(self) -> str: - pass - - @property - @abc.abstractmethod - def event_type(self) -> str: - pass - - @property - @abc.abstractmethod - def event_time(self) -> typing.Optional[datetime.datetime]: - pass - - @property - @abc.abstractmethod - def data_version(self) -> str: - pass - - -class EventGridOutputEvent(abc.ABC): - @property - @abc.abstractmethod - def id(self) -> str: - pass - - @abc.abstractmethod - def get_json(self) -> typing.Any: - pass - - @property - @abc.abstractmethod - def subject(self) -> str: - pass - - @property - @abc.abstractmethod - def event_type(self) -> str: - pass - - @property - @abc.abstractmethod - def event_time(self) -> typing.Optional[datetime.datetime]: - pass - - @property - @abc.abstractmethod - def data_version(self) -> str: - pass - - -class Document(abc.ABC): - - @classmethod - @abc.abstractmethod - def from_json(cls, json_data: str) -> 'Document': - pass - - @classmethod - @abc.abstractmethod - def from_dict(cls, dct: dict) -> 'Document': - pass - - @abc.abstractmethod - def __getitem__(self, key): - pass - - @abc.abstractmethod - def __setitem__(self, key, value): - pass - - @abc.abstractmethod - def to_json(self) -> str: - pass - - -class DocumentList(abc.ABC): - pass - - -class EventHubEvent(abc.ABC): - - @abc.abstractmethod - def get_body(self) -> bytes: - pass - - @property - @abc.abstractmethod - def partition_key(self) -> typing.Optional[str]: - pass - - @property - @abc.abstractmethod - def sequence_number(self) -> typing.Optional[int]: - pass - - @property - @abc.abstractmethod - def iothub_metadata(self) -> typing.Optional[typing.Mapping[str, str]]: - pass - - @property - @abc.abstractmethod - def enqueued_time(self) -> typing.Optional[datetime.datetime]: - pass - - @property - @abc.abstractmethod - def offset(self) -> typing.Optional[str]: - pass - - -class OrchestrationContext(abc.ABC): - @property - @abc.abstractmethod - def body(self) -> str: - pass diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_http.py b/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_http.py deleted file mode 100644 index 89ee2678c..000000000 --- a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_http.py +++ /dev/null @@ -1,230 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import collections.abc -import io -import json -import types -import typing - -from . import _abc -from ._thirdparty.werkzeug import datastructures as _wk_datastructures -from ._thirdparty.werkzeug import formparser as _wk_parser -from ._thirdparty.werkzeug import http as _wk_http - - -class BaseHeaders(collections.abc.Mapping): - - def __init__(self, source: typing.Optional[typing.Mapping] = None) -> None: - self.__http_headers__: typing.Dict[str, str] = {} - - if source is not None: - self.__http_headers__.update( - {k.lower(): v for k, v in source.items()}) - - def __getitem__(self, key: str) -> str: - return self.__http_headers__[key.lower()] - - def __len__(self): - return len(self.__http_headers__) - - def __contains__(self, key: typing.Any): - return key.lower() in self.__http_headers__ - - def __iter__(self): - return iter(self.__http_headers__) - - -class HttpRequestHeaders(BaseHeaders): - pass - - -class HttpResponseHeaders(BaseHeaders, collections.abc.MutableMapping): - - def __setitem__(self, key: str, value: str): - self.__http_headers__[key.lower()] = value - - def __delitem__(self, key: str): - del self.__http_headers__[key.lower()] - - -class HttpResponse(_abc.HttpResponse): - """An HTTP response object. - - :param str/bytes body: - Optional response body. - - :param int status_code: - Response status code. If not specified, defaults to 200. - - :param dict headers: - An optional mapping containing response HTTP headers. - - :param str mimetype: - An optional response MIME type. If not specified, defaults to - ``'text/plain'``. - - :param str charset: - Response content text encoding. If not specified, defaults to - ``'utf-8'``. - """ - - def __init__(self, body=None, *, - status_code=None, headers=None, mimetype=None, charset=None): - if status_code is None: - status_code = 200 - self.__status_code = status_code - - if mimetype is None: - mimetype = 'text/plain' - self.__mimetype = mimetype - - if charset is None: - charset = 'utf-8' - self.__charset = charset - - if headers is None: - headers = {} - self.__headers = HttpResponseHeaders(headers) - - if body is not None: - self.__set_body(body) - else: - self.__body = b'' - - @property - def mimetype(self): - """Response MIME type.""" - return self.__mimetype - - @property - def charset(self): - """Response text encoding.""" - return self.__charset - - @property - def headers(self): - """A dictionary of response HTTP headers.""" - return self.__headers - - @property - def status_code(self): - """Response status code.""" - return self.__status_code - - def __set_body(self, body): - if isinstance(body, str): - body = body.encode(self.__charset) - - if not isinstance(body, (bytes, bytearray)): - raise TypeError( - f'response is expected to be either of ' - f'str, bytes, or bytearray, got {type(body).__name__}') - - self.__body = bytes(body) - - def get_body(self) -> bytes: - """Response body as a bytes object.""" - return self.__body - - -class HttpRequest(_abc.HttpRequest): - """An HTTP request object. - - :param str method: - HTTP request method name. - - :param str url: - HTTP URL. - - :param dict headers: - An optional mapping containing HTTP request headers. - - :param dict params: - An optional mapping containing HTTP request params. - - :param dict route_params: - An optional mapping containing HTTP request route params. - - :param bytes body: - HTTP request body. - """ - - def __init__(self, - method: str, - url: str, *, - headers: typing.Optional[typing.Mapping[str, str]] = None, - params: typing.Optional[typing.Mapping[str, str]] = None, - route_params: typing.Optional[ - typing.Mapping[str, str]] = None, - body: bytes) -> None: - self.__method = method - self.__url = url - self.__headers = HttpRequestHeaders(headers or {}) - self.__params = types.MappingProxyType(params or {}) - self.__route_params = types.MappingProxyType(route_params or {}) - self.__body_bytes = body - self.__form_parsed = False - self.__form = None - self.__files = None - - @property - def url(self): - return self.__url - - @property - def method(self): - return self.__method.upper() - - @property - def headers(self): - return self.__headers - - @property - def params(self): - return self.__params - - @property - def route_params(self): - return self.__route_params - - @property - def form(self): - self._parse_form_data() - return self.__form - - @property - def files(self): - self._parse_form_data() - return self.__files - - def get_body(self) -> bytes: - return self.__body_bytes - - def get_json(self) -> typing.Any: - return json.loads(self.__body_bytes.decode('utf-8')) - - def _parse_form_data(self): - if self.__form_parsed: - return - - body = self.get_body() - content_type = self.headers.get('Content-Type', '') - content_length = len(body) - mimetype, options = _wk_http.parse_options_header(content_type) - parser = _wk_parser.FormDataParser( - _wk_parser.default_stream_factory, - options.get('charset') or 'utf-8', - 'replace', - None, - None, - _wk_datastructures.ImmutableMultiDict, - ) - - body_stream = io.BytesIO(body) - - _, self.__form, self.__files = parser.parse( - body_stream, mimetype, content_length, options - ) - - self.__form_parsed = True diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_thirdparty/__init__.py b/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_thirdparty/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_thirdparty/typing_inspect.py b/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_thirdparty/typing_inspect.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_thirdparty/werkzeug/datastructures.py b/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_thirdparty/werkzeug/datastructures.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_thirdparty/werkzeug/formparser.py b/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_thirdparty/werkzeug/formparser.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_thirdparty/werkzeug/http.py b/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_thirdparty/werkzeug/http.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_utils.py b/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_utils.py deleted file mode 100644 index a1bd9f3b8..000000000 --- a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/_utils.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from datetime import datetime, timedelta -from typing import List, Optional, Tuple - - -def try_parse_datetime_with_formats( - datetime_str: str, - datetime_formats: List[str] -) -> Tuple[Optional[datetime], Optional[str], Optional[Exception]]: - """Try parsing the datetime string with a list of formats - Parameters - ---------- - datetime_str: str - The datetime string needs to be parsed (e.g. 2018-12-12T03:16:34.2191Z) - datetime_formats: List[str] - A list of datetime formats that the parser would try to match - - Returns - ------- - dict_obj: A serializable dictionary with enough metadata to reconstruct - `obj` - - Exceptions - ---------- - Tuple[Optional[datetime], Optional[str], Optional[Exception]]: - If the datetime can be successfully parsed, the first element is the - paresd datetime object and the second is the matched format. - If the datetime cannot be parsed, the first and second element will be - None, and the third is the exception from the datetime.strptime() - method. - """ - for fmt in datetime_formats: - try: - dt = datetime.strptime(datetime_str, fmt) - return (dt, fmt, None) - except ValueError as ve: - last_exception = ve - - return (None, None, last_exception) - - -def try_parse_timedelta_with_formats( - timedelta_str: str, - timedelta_formats: List[str] -) -> Tuple[Optional[timedelta], Optional[str], Optional[Exception]]: - """Try parsing the datetime delta string with a list of formats - Parameters - ---------- - timedelta_str: str - The timedelta string needs to be parsed (e.g. 12:34:56) - timedelta_formats: List[str] - A list of datetime formats that the parser would try to match - - Returns - ------- - dict_obj: A serializable dictionary with enough metadata to reconstruct - `obj` - - Exceptions - ---------- - Tuple[Optional[timedelta], Optional[str], Optional[Exception]]: - If the timedelta can be successfully parsed, the first element is the - paresd timedelta object and the second is the matched format. - If the timedelta cannot be parsed, the first and second element will be - None, and the third is the exception from the datetime.strptime() - method. - """ - - for fmt in timedelta_formats: - try: - # If singular form %S, %M, %H, will just return the timedelta - if fmt == '%S': - td = timedelta(seconds=int(timedelta_str)) - elif fmt == '%M': - td = timedelta(minutes=int(timedelta_str)) - elif fmt == '%H': - td = timedelta(hours=int(timedelta_str)) - else: - dt = datetime.strptime(timedelta_str, fmt) - td = timedelta(hours=dt.hour, - minutes=dt.minute, - seconds=dt.second) - return (td, fmt, None) - except ValueError as ve: - last_exception = ve - - return (None, None, last_exception) diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/http.py b/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/http.py deleted file mode 100644 index 211711d67..000000000 --- a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/http.py +++ /dev/null @@ -1,134 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import json -import typing - -from azure.functions import _abc as azf_abc -from azure.functions import _http as azf_http - -from . import meta - - -class HttpRequest(azf_http.HttpRequest): - """An HTTP request object.""" - - __body_bytes: typing.Optional[bytes] - __body_str: typing.Optional[str] - - def __init__(self, - method: str, - url: str, *, - headers: typing.Mapping[str, str], - params: typing.Mapping[str, str], - route_params: typing.Mapping[str, str], - body_type: str, - body: typing.Union[str, bytes]) -> None: - - body_str: typing.Optional[str] = None - body_bytes: typing.Optional[bytes] = None - if isinstance(body, str): - body_str = body - body_bytes = body_str.encode('utf-8') - elif isinstance(body, bytes): - body_bytes = body - else: - raise TypeError( - f'unexpected HTTP request body type: {type(body).__name__}') - - super().__init__(method=method, url=url, headers=headers, - params=params, route_params=route_params, - body=body_bytes) - - self.__body_type = body_type - self.__body_str = body_str - self.__body_bytes = body_bytes - - def get_body(self) -> bytes: - if self.__body_bytes is None: - assert self.__body_str is not None - self.__body_bytes = self.__body_str.encode('utf-8') - return self.__body_bytes - - def get_json(self) -> typing.Any: - if self.__body_type in ('json', 'string'): - assert self.__body_str is not None - return json.loads(self.__body_str) - elif self.__body_bytes is not None: - try: - return json.loads(self.__body_bytes.decode('utf-8')) - except ValueError as e: - raise ValueError( - 'HTTP request does not contain valid JSON data') from e - else: - raise ValueError( - 'Request body cannot be empty in JSON deserialization') - - -class HttpResponseConverter(meta.OutConverter, binding='http'): - - @classmethod - def check_output_type_annotation(cls, pytype: type) -> bool: - return issubclass(pytype, (azf_abc.HttpResponse, str)) - - @classmethod - def encode(cls, obj: typing.Any, *, - expected_type: typing.Optional[type]) -> meta.Datum: - if isinstance(obj, str): - return meta.Datum(type='string', value=obj) - - if isinstance(obj, azf_abc.HttpResponse): - status = obj.status_code - headers = dict(obj.headers) - if 'content-type' not in headers: - if obj.mimetype.startswith('text/'): - ct = f'{obj.mimetype}; charset={obj.charset}' - else: - ct = f'{obj.mimetype}' - headers['content-type'] = ct - - body = obj.get_body() - if body is not None: - datum_body = meta.Datum(type='bytes', value=body) - else: - datum_body = meta.Datum(type='bytes', value=b'') - - return meta.Datum( - type='http', - value=dict( - status_code=meta.Datum(type='string', value=str(status)), - headers={ - n: meta.Datum(type='string', value=h) - for n, h in headers.items() - }, - body=datum_body, - ) - ) - - raise NotImplementedError - - -class HttpRequestConverter(meta.InConverter, - binding='httpTrigger', trigger=True): - - @classmethod - def check_input_type_annotation(cls, pytype: type) -> bool: - return issubclass(pytype, azf_abc.HttpRequest) - - @classmethod - def decode(cls, data: meta.Datum, *, - trigger_metadata) -> typing.Any: - if data.type != 'http': - raise NotImplementedError - - val = data.value - - return HttpRequest( - method=val['method'].value, - url=val['url'].value, - headers={n: v.value for n, v in val['headers'].items()}, - params={n: v.value for n, v in val['query'].items()}, - route_params={n: v.value for n, v in val['params'].items()}, - body_type=val['body'].type, - body=val['body'].value, - ) diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/meta.py b/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/meta.py deleted file mode 100644 index 3dcff6da8..000000000 --- a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_newer_version/lib/site-packages/azure/functions/meta.py +++ /dev/null @@ -1,401 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import abc -import collections.abc -import datetime -import json -import re -from typing import Any, Dict, Mapping, Optional, Tuple, Union - -from ._thirdparty import typing_inspect -from ._utils import try_parse_datetime_with_formats, try_parse_timedelta_with_formats - - -def is_iterable_type_annotation(annotation: object, pytype: object) -> bool: - is_iterable_anno = ( - typing_inspect.is_generic_type(annotation) - and issubclass(typing_inspect.get_origin(annotation), - collections.abc.Iterable) - ) - - if not is_iterable_anno: - return False - - args = typing_inspect.get_args(annotation) - if not args: - return False - - if isinstance(pytype, tuple): - return any(isinstance(t, type) and issubclass(t, arg) - for t in pytype for arg in args) - else: - return any(isinstance(pytype, type) and issubclass(pytype, arg) - for arg in args) - - -class Datum: - def __init__(self, value: Any, type: Optional[str]): - self.value: Any = value - self.type: Optional[str] = type - - @property - def python_value(self) -> Any: - if self.value is None or self.type is None: - return None - elif self.type in ('bytes', 'string', 'int', 'double'): - return self.value - elif self.type == 'json': - return json.loads(self.value) - elif self.type == 'collection_string': - return [v for v in self.value.string] - elif self.type == 'collection_bytes': - return [v for v in self.value.bytes] - elif self.type == 'collection_double': - return [v for v in self.value.double] - elif self.type == 'collection_sint64': - return [v for v in self.value.sint64] - else: - return self.value - - @property - def python_type(self) -> type: - return type(self.python_value) - - def __eq__(self, other): - if not isinstance(other, type(self)): - return False - - return self.value == other.value and self.type == other.type - - def __hash__(self): - return hash((type(self), (self.value, self.type))) - - def __repr__(self): - val_repr = repr(self.value) - if len(val_repr) > 10: - val_repr = val_repr[:10] + '...' - return ''.format(self.type, val_repr) - - -class _ConverterMeta(abc.ABCMeta): - - _bindings: Dict[str, type] = {} - - def __new__(mcls, name, bases, dct, *, - binding: Optional[str], - trigger: Optional[str] = None): - cls = super().__new__(mcls, name, bases, dct) - cls._trigger = trigger # type: ignore - if binding is None: - return cls - - if binding in mcls._bindings: - raise RuntimeError( - f'cannot register a converter for {binding!r} binding: ' - f'another converter for this binding has already been ' - f'registered') - - mcls._bindings[binding] = cls - if trigger is not None: - mcls._bindings[trigger] = cls - - return cls - - @classmethod - def get(cls, binding_name): - return cls._bindings.get(binding_name) - - def has_trigger_support(cls) -> bool: - return cls._trigger is not None # type: ignore - - -class _BaseConverter(metaclass=_ConverterMeta, binding=None): - - @classmethod - def _decode_typed_data( - cls, data: Datum, *, - python_type: Union[type, Tuple[type, ...]], - context: str = 'data') -> Any: - if data is None: - return None - - data_type = data.type - if data_type == 'json': - result = json.loads(data.value) - - elif data_type == 'string': - result = data.value - - elif data_type == 'int': - result = data.value - - elif data_type == 'double': - result = data.value - - elif data_type == 'collection_bytes': - result = data.value - - elif data_type == 'collection_string': - result = data.value - - elif data_type == 'collection_sint64': - result = data.value - - elif data_type is None: - return None - - else: - raise ValueError( - f'unsupported type of {context}: {data_type}') - - if not isinstance(result, python_type): - if isinstance(python_type, (tuple, list, dict)): - raise ValueError( - f'unexpected value type in {context}: ' - f'{type(result).__name__}, expected one of: ' - f'{", ".join(t.__name__ for t in python_type)}') - else: - try: - # Try coercing into the requested type - result = python_type(result) - except (TypeError, ValueError) as e: - raise ValueError( - f'cannot convert value of {context} into ' - f'{python_type.__name__}: {e}') from None - - return result - - @classmethod - def _decode_trigger_metadata_field( - cls, trigger_metadata: Mapping[str, Datum], - field: str, *, - python_type: Union[type, Tuple[type, ...]]) \ - -> Any: - data = trigger_metadata.get(field) - if data is None: - return None - else: - return cls._decode_typed_data( - data, python_type=python_type, - context=f'field {field!r} in trigger metadata') - - @classmethod - def _parse_datetime_metadata( - cls, trigger_metadata: Mapping[str, Datum], - field: str) -> Optional[datetime.datetime]: - - datetime_str = cls._decode_trigger_metadata_field( - trigger_metadata, field, python_type=str) - - if datetime_str is None: - return None - else: - return cls._parse_datetime(datetime_str) - - @classmethod - def _parse_timedelta_metadata( - cls, trigger_metadata: Mapping[str, Datum], - field: str) -> Optional[datetime.timedelta]: - - timedelta_str = cls._decode_trigger_metadata_field( - trigger_metadata, field, python_type=str) - - if timedelta_str is None: - return None - else: - return cls._parse_timedelta(timedelta_str) - - @classmethod - def _parse_datetime( - cls, datetime_str: Optional[str]) -> Optional[datetime.datetime]: - - if not datetime_str: - return None - - too_fractional = re.match( - r'(.*\.\d{6})(\d+)(Z|[\+|-]\d{1,2}:\d{1,2}){0,1}', datetime_str) - - if too_fractional: - # The supplied value contains seven digits in the - # fractional second part, whereas Python expects - # a maxium of six, so strip it. - # https://github.com/Azure/azure-functions-python-worker/issues/269 - datetime_str = too_fractional.group(1) + ( - too_fractional.group(3) or '') - - # Try parse time - utc_time, utc_time_error = cls._parse_datetime_utc(datetime_str) - if not utc_time_error and utc_time: - return utc_time.replace(tzinfo=datetime.timezone.utc) - - local_time, local_time_error = cls._parse_datetime_local(datetime_str) - if not local_time_error and local_time: - return local_time.replace(tzinfo=None) - - # Report error - if utc_time_error: - raise utc_time_error - elif local_time_error: - raise local_time_error - else: - return None - - @classmethod - def _parse_timedelta( - cls, - timedelta_str: Optional[str] - ) -> Optional[datetime.timedelta]: - - if not timedelta_str: - return None - - # Try parse timedelta - timedelta, td_error = cls._parse_timedelta_internal(timedelta_str) - if timedelta is not None: - return timedelta - - # Report error - if td_error: - raise td_error - else: - return None - - @classmethod - def _parse_datetime_utc( - cls, - datetime_str: str - ) -> Tuple[Optional[datetime.datetime], Optional[Exception]]: - - # UTC ISO 8601 assumed - # 2018-08-07T23:17:57.461050Z - utc_formats = [ - '%Y-%m-%dT%H:%M:%S+00:00', - '%Y-%m-%dT%H:%M:%S-00:00', - '%Y-%m-%dT%H:%M:%S.%f+00:00', - '%Y-%m-%dT%H:%M:%S.%f-00:00', - '%Y-%m-%dT%H:%M:%SZ', - '%Y-%m-%dT%H:%M:%S.%fZ', - - '%m/%d/%Y %H:%M:%SZ', - '%m/%d/%Y %H:%M:%S.%fZ', - '%m/%d/%Y %H:%M:%S+00:00', - '%m/%d/%Y %H:%M:%S-00:00', - '%m/%d/%Y %H:%M:%S.%f+00:00', - '%m/%d/%Y %H:%M:%S.%f-00:00', - ] - - dt, _, excpt = try_parse_datetime_with_formats( - datetime_str, utc_formats) - - if excpt is not None: - return None, excpt - return dt, None - - @classmethod - def _parse_datetime_local( - cls, datetime_str: str - ) -> Tuple[Optional[datetime.datetime], Optional[Exception]]: - """Parse a string into a datetime object, accepts following formats - 1. Without fractional seconds (e.g. 2018-08-07T23:17:57) - 2. With fractional seconds (e.g. 2018-08-07T23:17:57.461050) - - Parameters - ---------- - datetime_str: str - The string represents a datetime - - Returns - ------- - Tuple[Optional[datetime.datetime], Optional[Exception]] - If the datetime_str is None, will return None immediately. - If the datetime_str can be parsed correctly, it will return as the - first element in the tuple. - If the datetime_str cannot be parsed with all attempts, it will - return None in the first element, the exception in the second - element. - """ - - local_formats = [ - '%Y-%m-%dT%H:%M:%S.%f', - '%Y-%m-%dT%H:%M:%S', - - '%m/%d/%YT%H:%M:%S.%f', - '%m/%d/%YT%H:%M:%S' - ] - - dt, _, excpt = try_parse_datetime_with_formats( - datetime_str, local_formats) - - if excpt is not None: - return None, excpt - return dt, None - - @classmethod - def _parse_timedelta_internal( - cls, timedelta_str: str - ) -> Tuple[Optional[datetime.timedelta], Optional[Exception]]: - """Parse a string into a timedelta object, accepts following formats - 1. HH:MM:SS (e.g. 12:34:56) - 2. MM:SS (e.g. 34:56) - 3. Pure integer as seconds (e.g. 5819) - - Parameters - ---------- - timedelta_str: str - The string represents a datetime - - Returns - ------- - Tuple[Optional[datetime.timedelta], Optional[Exception]] - If the timedelta_str is None, will return None immediately. - If the timedelta_str can be parsed correctly, it will return as the - first element in the tuple. - If the timedelta_str cannot be parsed with all attempts, it will - return None in the first element, the exception in the second - element. - """ - - timedelta_formats = [ - '%H:%M:%S', - '%M:%S', - '%S' - ] - - td, _, excpt = try_parse_timedelta_with_formats( - timedelta_str, timedelta_formats) - - if td is not None: - return td, None - return None, excpt - - -class InConverter(_BaseConverter, binding=None): - - @abc.abstractclassmethod - def check_input_type_annotation(cls, pytype: type) -> bool: - pass - - @abc.abstractclassmethod - def decode(cls, data: Datum, *, trigger_metadata) -> Any: - raise NotImplementedError - - @abc.abstractclassmethod - def has_implicit_output(cls) -> bool: - return False - - -class OutConverter(_BaseConverter, binding=None): - - @abc.abstractclassmethod - def check_output_type_annotation(cls, pytype: type) -> bool: - pass - - @abc.abstractclassmethod - def encode(cls, obj: Any, *, - expected_type: Optional[type]) -> Optional[Datum]: - raise NotImplementedError - - -def get_binding_registry(): - return _ConverterMeta diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/__init__.py b/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/__init__.py deleted file mode 100644 index 36953c55b..000000000 --- a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -# Import binding implementations to register them -from . import http # NoQA -from ._abc import Context, Out -from ._http import HttpRequest, HttpResponse -from .meta import get_binding_registry - -__all__ = ( - # Functions - 'get_binding_registry', - - # Generics. - 'Context', - 'Out', - - # Binding rich types, sorted alphabetically. - 'HttpRequest', - 'HttpResponse', -) - -__version__ = '1.5.0' diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_abc.py b/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_abc.py deleted file mode 100644 index 8add53c99..000000000 --- a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_abc.py +++ /dev/null @@ -1,422 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import abc -import datetime -import io -import typing - -T = typing.TypeVar('T') - - -class Out(abc.ABC, typing.Generic[T]): - """An interface to set function output parameters.""" - - @abc.abstractmethod - def set(self, val: T) -> None: - """Set the value of the output parameter.""" - pass - - @abc.abstractmethod - def get(self) -> T: - """Get the value of the output parameter.""" - pass - - -class RpcException: - """Rpc Exception object.""" - - @property - @abc.abstractmethod - def source(self) -> str: - """Source of the exception.""" - pass - - @property - @abc.abstractmethod - def stack_trace(self) -> str: - """Stack trace for the exception.""" - pass - - @property - @abc.abstractmethod - def message(self) -> str: - """Textual message describing the exception.""" - pass - - -class TraceContext(abc.ABC): - """Trace context object.""" - - @property - @abc.abstractmethod - def trace_state(self) -> str: - """Gets trace state from trace-context.""" - pass - - @property - @abc.abstractmethod - def trace_parent(self) -> str: - """Gets trace parent from trace-context.""" - pass - - @property - @abc.abstractmethod - def attributes(self) -> typing.Dict[str, str]: - """Gets trace-context attributes.""" - pass - - -class RetryContext(abc.ABC): - """Retry Context object. - For more information refer: https://aka.ms/azfunc-retries-policies - """ - - @property - @abc.abstractmethod - def retry_count(self) -> int: - """Gets the current retry count from retry-context.""" - pass - - @property - @abc.abstractmethod - def max_retry_count(self) -> int: - """Gets the max retry count from retry-context.""" - pass - - @property - @abc.abstractmethod - def exception(self) -> RpcException: - """Gets the RpcException""" - pass - - -class Context(abc.ABC): - """Function invocation context.""" - - @property - @abc.abstractmethod - def invocation_id(self) -> str: - """Function invocation ID.""" - pass - - @property - @abc.abstractmethod - def function_name(self) -> str: - """Function name.""" - pass - - @property - @abc.abstractmethod - def function_directory(self) -> str: - """Function directory.""" - pass - - @property - @abc.abstractmethod - def trace_context(self) -> TraceContext: - """Context for distributed tracing.""" - pass - - @property - @abc.abstractmethod - def retry_context(self) -> RetryContext: - """Context for retries to the function.""" - pass - - -class HttpRequest(abc.ABC): - """HTTP request object.""" - - @property - @abc.abstractmethod - def method(self) -> str: - """Request method.""" - pass - - @property - @abc.abstractmethod - def url(self) -> str: - """Request URL.""" - pass - - @property - @abc.abstractmethod - def headers(self) -> typing.Mapping[str, str]: - """A dictionary containing request headers.""" - pass - - @property - @abc.abstractmethod - def params(self) -> typing.Mapping[str, str]: - """A dictionary containing request GET parameters.""" - pass - - @property - @abc.abstractmethod - def route_params(self) -> typing.Mapping[str, str]: - """A dictionary containing request route parameters.""" - pass - - @abc.abstractmethod - def get_body(self) -> bytes: - """Return request body as bytes.""" - pass - - @abc.abstractmethod - def get_json(self) -> typing.Any: - """Decode and return request body as JSON. - - :raises ValueError: - when the request does not contain valid JSON data. - """ - pass - - -class HttpResponse(abc.ABC): - - @property - @abc.abstractmethod - def status_code(self) -> int: - pass - - @property - @abc.abstractmethod - def mimetype(self): - pass - - @property - @abc.abstractmethod - def charset(self): - pass - - @property - @abc.abstractmethod - def headers(self) -> typing.MutableMapping[str, str]: - pass - - @abc.abstractmethod - def get_body(self) -> bytes: - pass - - -class TimerRequest(abc.ABC): - """Timer request object.""" - - @property - @abc.abstractmethod - def past_due(self) -> bool: - """Whether the timer is past due.""" - pass - - -class InputStream(io.BufferedIOBase, abc.ABC): - """File-like object representing an input blob.""" - - @abc.abstractmethod - def read(self, size=-1) -> bytes: - """Return and read up to *size* bytes. - - :param int size: - The number of bytes to read. If the argument is omitted, - ``None``, or negative, data is read and returned until - EOF is reached. - - :return: - Bytes read from the input stream. - """ - pass - - @property - @abc.abstractmethod - def name(self) -> typing.Optional[str]: - """The name of the blob.""" - pass - - @property - @abc.abstractmethod - def length(self) -> typing.Optional[int]: - """The size of the blob in bytes.""" - pass - - @property - @abc.abstractmethod - def uri(self) -> typing.Optional[str]: - """The blob's primary location URI.""" - pass - - -class QueueMessage(abc.ABC): - - @property - @abc.abstractmethod - def id(self) -> typing.Optional[str]: - pass - - @abc.abstractmethod - def get_body(self) -> typing.Union[str, bytes]: - pass - - @abc.abstractmethod - def get_json(self) -> typing.Any: - pass - - @property - @abc.abstractmethod - def dequeue_count(self) -> typing.Optional[int]: - pass - - @property - @abc.abstractmethod - def expiration_time(self) -> typing.Optional[datetime.datetime]: - pass - - @property - @abc.abstractmethod - def insertion_time(self) -> typing.Optional[datetime.datetime]: - pass - - @property - @abc.abstractmethod - def time_next_visible(self) -> typing.Optional[datetime.datetime]: - pass - - @property - @abc.abstractmethod - def pop_receipt(self) -> typing.Optional[str]: - pass - - -class EventGridEvent(abc.ABC): - @property - @abc.abstractmethod - def id(self) -> str: - pass - - @abc.abstractmethod - def get_json(self) -> typing.Any: - pass - - @property - @abc.abstractmethod - def topic(self) -> str: - pass - - @property - @abc.abstractmethod - def subject(self) -> str: - pass - - @property - @abc.abstractmethod - def event_type(self) -> str: - pass - - @property - @abc.abstractmethod - def event_time(self) -> typing.Optional[datetime.datetime]: - pass - - @property - @abc.abstractmethod - def data_version(self) -> str: - pass - - -class EventGridOutputEvent(abc.ABC): - @property - @abc.abstractmethod - def id(self) -> str: - pass - - @abc.abstractmethod - def get_json(self) -> typing.Any: - pass - - @property - @abc.abstractmethod - def subject(self) -> str: - pass - - @property - @abc.abstractmethod - def event_type(self) -> str: - pass - - @property - @abc.abstractmethod - def event_time(self) -> typing.Optional[datetime.datetime]: - pass - - @property - @abc.abstractmethod - def data_version(self) -> str: - pass - - -class Document(abc.ABC): - - @classmethod - @abc.abstractmethod - def from_json(cls, json_data: str) -> 'Document': - pass - - @classmethod - @abc.abstractmethod - def from_dict(cls, dct: dict) -> 'Document': - pass - - @abc.abstractmethod - def __getitem__(self, key): - pass - - @abc.abstractmethod - def __setitem__(self, key, value): - pass - - @abc.abstractmethod - def to_json(self) -> str: - pass - - -class DocumentList(abc.ABC): - pass - - -class EventHubEvent(abc.ABC): - - @abc.abstractmethod - def get_body(self) -> bytes: - pass - - @property - @abc.abstractmethod - def partition_key(self) -> typing.Optional[str]: - pass - - @property - @abc.abstractmethod - def sequence_number(self) -> typing.Optional[int]: - pass - - @property - @abc.abstractmethod - def iothub_metadata(self) -> typing.Optional[typing.Mapping[str, str]]: - pass - - @property - @abc.abstractmethod - def enqueued_time(self) -> typing.Optional[datetime.datetime]: - pass - - @property - @abc.abstractmethod - def offset(self) -> typing.Optional[str]: - pass - - -class OrchestrationContext(abc.ABC): - @property - @abc.abstractmethod - def body(self) -> str: - pass diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_http.py b/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_http.py deleted file mode 100644 index 89ee2678c..000000000 --- a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_http.py +++ /dev/null @@ -1,230 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import collections.abc -import io -import json -import types -import typing - -from . import _abc -from ._thirdparty.werkzeug import datastructures as _wk_datastructures -from ._thirdparty.werkzeug import formparser as _wk_parser -from ._thirdparty.werkzeug import http as _wk_http - - -class BaseHeaders(collections.abc.Mapping): - - def __init__(self, source: typing.Optional[typing.Mapping] = None) -> None: - self.__http_headers__: typing.Dict[str, str] = {} - - if source is not None: - self.__http_headers__.update( - {k.lower(): v for k, v in source.items()}) - - def __getitem__(self, key: str) -> str: - return self.__http_headers__[key.lower()] - - def __len__(self): - return len(self.__http_headers__) - - def __contains__(self, key: typing.Any): - return key.lower() in self.__http_headers__ - - def __iter__(self): - return iter(self.__http_headers__) - - -class HttpRequestHeaders(BaseHeaders): - pass - - -class HttpResponseHeaders(BaseHeaders, collections.abc.MutableMapping): - - def __setitem__(self, key: str, value: str): - self.__http_headers__[key.lower()] = value - - def __delitem__(self, key: str): - del self.__http_headers__[key.lower()] - - -class HttpResponse(_abc.HttpResponse): - """An HTTP response object. - - :param str/bytes body: - Optional response body. - - :param int status_code: - Response status code. If not specified, defaults to 200. - - :param dict headers: - An optional mapping containing response HTTP headers. - - :param str mimetype: - An optional response MIME type. If not specified, defaults to - ``'text/plain'``. - - :param str charset: - Response content text encoding. If not specified, defaults to - ``'utf-8'``. - """ - - def __init__(self, body=None, *, - status_code=None, headers=None, mimetype=None, charset=None): - if status_code is None: - status_code = 200 - self.__status_code = status_code - - if mimetype is None: - mimetype = 'text/plain' - self.__mimetype = mimetype - - if charset is None: - charset = 'utf-8' - self.__charset = charset - - if headers is None: - headers = {} - self.__headers = HttpResponseHeaders(headers) - - if body is not None: - self.__set_body(body) - else: - self.__body = b'' - - @property - def mimetype(self): - """Response MIME type.""" - return self.__mimetype - - @property - def charset(self): - """Response text encoding.""" - return self.__charset - - @property - def headers(self): - """A dictionary of response HTTP headers.""" - return self.__headers - - @property - def status_code(self): - """Response status code.""" - return self.__status_code - - def __set_body(self, body): - if isinstance(body, str): - body = body.encode(self.__charset) - - if not isinstance(body, (bytes, bytearray)): - raise TypeError( - f'response is expected to be either of ' - f'str, bytes, or bytearray, got {type(body).__name__}') - - self.__body = bytes(body) - - def get_body(self) -> bytes: - """Response body as a bytes object.""" - return self.__body - - -class HttpRequest(_abc.HttpRequest): - """An HTTP request object. - - :param str method: - HTTP request method name. - - :param str url: - HTTP URL. - - :param dict headers: - An optional mapping containing HTTP request headers. - - :param dict params: - An optional mapping containing HTTP request params. - - :param dict route_params: - An optional mapping containing HTTP request route params. - - :param bytes body: - HTTP request body. - """ - - def __init__(self, - method: str, - url: str, *, - headers: typing.Optional[typing.Mapping[str, str]] = None, - params: typing.Optional[typing.Mapping[str, str]] = None, - route_params: typing.Optional[ - typing.Mapping[str, str]] = None, - body: bytes) -> None: - self.__method = method - self.__url = url - self.__headers = HttpRequestHeaders(headers or {}) - self.__params = types.MappingProxyType(params or {}) - self.__route_params = types.MappingProxyType(route_params or {}) - self.__body_bytes = body - self.__form_parsed = False - self.__form = None - self.__files = None - - @property - def url(self): - return self.__url - - @property - def method(self): - return self.__method.upper() - - @property - def headers(self): - return self.__headers - - @property - def params(self): - return self.__params - - @property - def route_params(self): - return self.__route_params - - @property - def form(self): - self._parse_form_data() - return self.__form - - @property - def files(self): - self._parse_form_data() - return self.__files - - def get_body(self) -> bytes: - return self.__body_bytes - - def get_json(self) -> typing.Any: - return json.loads(self.__body_bytes.decode('utf-8')) - - def _parse_form_data(self): - if self.__form_parsed: - return - - body = self.get_body() - content_type = self.headers.get('Content-Type', '') - content_length = len(body) - mimetype, options = _wk_http.parse_options_header(content_type) - parser = _wk_parser.FormDataParser( - _wk_parser.default_stream_factory, - options.get('charset') or 'utf-8', - 'replace', - None, - None, - _wk_datastructures.ImmutableMultiDict, - ) - - body_stream = io.BytesIO(body) - - _, self.__form, self.__files = parser.parse( - body_stream, mimetype, content_length, options - ) - - self.__form_parsed = True diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_thirdparty/__init__.py b/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_thirdparty/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_thirdparty/typing_inspect.py b/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_thirdparty/typing_inspect.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_thirdparty/werkzeug/datastructures.py b/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_thirdparty/werkzeug/datastructures.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_thirdparty/werkzeug/formparser.py b/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_thirdparty/werkzeug/formparser.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_thirdparty/werkzeug/http.py b/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_thirdparty/werkzeug/http.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_utils.py b/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_utils.py deleted file mode 100644 index a1bd9f3b8..000000000 --- a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/_utils.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from datetime import datetime, timedelta -from typing import List, Optional, Tuple - - -def try_parse_datetime_with_formats( - datetime_str: str, - datetime_formats: List[str] -) -> Tuple[Optional[datetime], Optional[str], Optional[Exception]]: - """Try parsing the datetime string with a list of formats - Parameters - ---------- - datetime_str: str - The datetime string needs to be parsed (e.g. 2018-12-12T03:16:34.2191Z) - datetime_formats: List[str] - A list of datetime formats that the parser would try to match - - Returns - ------- - dict_obj: A serializable dictionary with enough metadata to reconstruct - `obj` - - Exceptions - ---------- - Tuple[Optional[datetime], Optional[str], Optional[Exception]]: - If the datetime can be successfully parsed, the first element is the - paresd datetime object and the second is the matched format. - If the datetime cannot be parsed, the first and second element will be - None, and the third is the exception from the datetime.strptime() - method. - """ - for fmt in datetime_formats: - try: - dt = datetime.strptime(datetime_str, fmt) - return (dt, fmt, None) - except ValueError as ve: - last_exception = ve - - return (None, None, last_exception) - - -def try_parse_timedelta_with_formats( - timedelta_str: str, - timedelta_formats: List[str] -) -> Tuple[Optional[timedelta], Optional[str], Optional[Exception]]: - """Try parsing the datetime delta string with a list of formats - Parameters - ---------- - timedelta_str: str - The timedelta string needs to be parsed (e.g. 12:34:56) - timedelta_formats: List[str] - A list of datetime formats that the parser would try to match - - Returns - ------- - dict_obj: A serializable dictionary with enough metadata to reconstruct - `obj` - - Exceptions - ---------- - Tuple[Optional[timedelta], Optional[str], Optional[Exception]]: - If the timedelta can be successfully parsed, the first element is the - paresd timedelta object and the second is the matched format. - If the timedelta cannot be parsed, the first and second element will be - None, and the third is the exception from the datetime.strptime() - method. - """ - - for fmt in timedelta_formats: - try: - # If singular form %S, %M, %H, will just return the timedelta - if fmt == '%S': - td = timedelta(seconds=int(timedelta_str)) - elif fmt == '%M': - td = timedelta(minutes=int(timedelta_str)) - elif fmt == '%H': - td = timedelta(hours=int(timedelta_str)) - else: - dt = datetime.strptime(timedelta_str, fmt) - td = timedelta(hours=dt.hour, - minutes=dt.minute, - seconds=dt.second) - return (td, fmt, None) - except ValueError as ve: - last_exception = ve - - return (None, None, last_exception) diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/http.py b/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/http.py deleted file mode 100644 index 211711d67..000000000 --- a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/http.py +++ /dev/null @@ -1,134 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import json -import typing - -from azure.functions import _abc as azf_abc -from azure.functions import _http as azf_http - -from . import meta - - -class HttpRequest(azf_http.HttpRequest): - """An HTTP request object.""" - - __body_bytes: typing.Optional[bytes] - __body_str: typing.Optional[str] - - def __init__(self, - method: str, - url: str, *, - headers: typing.Mapping[str, str], - params: typing.Mapping[str, str], - route_params: typing.Mapping[str, str], - body_type: str, - body: typing.Union[str, bytes]) -> None: - - body_str: typing.Optional[str] = None - body_bytes: typing.Optional[bytes] = None - if isinstance(body, str): - body_str = body - body_bytes = body_str.encode('utf-8') - elif isinstance(body, bytes): - body_bytes = body - else: - raise TypeError( - f'unexpected HTTP request body type: {type(body).__name__}') - - super().__init__(method=method, url=url, headers=headers, - params=params, route_params=route_params, - body=body_bytes) - - self.__body_type = body_type - self.__body_str = body_str - self.__body_bytes = body_bytes - - def get_body(self) -> bytes: - if self.__body_bytes is None: - assert self.__body_str is not None - self.__body_bytes = self.__body_str.encode('utf-8') - return self.__body_bytes - - def get_json(self) -> typing.Any: - if self.__body_type in ('json', 'string'): - assert self.__body_str is not None - return json.loads(self.__body_str) - elif self.__body_bytes is not None: - try: - return json.loads(self.__body_bytes.decode('utf-8')) - except ValueError as e: - raise ValueError( - 'HTTP request does not contain valid JSON data') from e - else: - raise ValueError( - 'Request body cannot be empty in JSON deserialization') - - -class HttpResponseConverter(meta.OutConverter, binding='http'): - - @classmethod - def check_output_type_annotation(cls, pytype: type) -> bool: - return issubclass(pytype, (azf_abc.HttpResponse, str)) - - @classmethod - def encode(cls, obj: typing.Any, *, - expected_type: typing.Optional[type]) -> meta.Datum: - if isinstance(obj, str): - return meta.Datum(type='string', value=obj) - - if isinstance(obj, azf_abc.HttpResponse): - status = obj.status_code - headers = dict(obj.headers) - if 'content-type' not in headers: - if obj.mimetype.startswith('text/'): - ct = f'{obj.mimetype}; charset={obj.charset}' - else: - ct = f'{obj.mimetype}' - headers['content-type'] = ct - - body = obj.get_body() - if body is not None: - datum_body = meta.Datum(type='bytes', value=body) - else: - datum_body = meta.Datum(type='bytes', value=b'') - - return meta.Datum( - type='http', - value=dict( - status_code=meta.Datum(type='string', value=str(status)), - headers={ - n: meta.Datum(type='string', value=h) - for n, h in headers.items() - }, - body=datum_body, - ) - ) - - raise NotImplementedError - - -class HttpRequestConverter(meta.InConverter, - binding='httpTrigger', trigger=True): - - @classmethod - def check_input_type_annotation(cls, pytype: type) -> bool: - return issubclass(pytype, azf_abc.HttpRequest) - - @classmethod - def decode(cls, data: meta.Datum, *, - trigger_metadata) -> typing.Any: - if data.type != 'http': - raise NotImplementedError - - val = data.value - - return HttpRequest( - method=val['method'].value, - url=val['url'].value, - headers={n: v.value for n, v in val['headers'].items()}, - params={n: v.value for n, v in val['query'].items()}, - route_params={n: v.value for n, v in val['params'].items()}, - body_type=val['body'].type, - body=val['body'].value, - ) diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/meta.py b/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/meta.py deleted file mode 100644 index 3dcff6da8..000000000 --- a/tests/endtoend/dependency_isolation_functions/.python_packages_azf_older_version/lib/site-packages/azure/functions/meta.py +++ /dev/null @@ -1,401 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import abc -import collections.abc -import datetime -import json -import re -from typing import Any, Dict, Mapping, Optional, Tuple, Union - -from ._thirdparty import typing_inspect -from ._utils import try_parse_datetime_with_formats, try_parse_timedelta_with_formats - - -def is_iterable_type_annotation(annotation: object, pytype: object) -> bool: - is_iterable_anno = ( - typing_inspect.is_generic_type(annotation) - and issubclass(typing_inspect.get_origin(annotation), - collections.abc.Iterable) - ) - - if not is_iterable_anno: - return False - - args = typing_inspect.get_args(annotation) - if not args: - return False - - if isinstance(pytype, tuple): - return any(isinstance(t, type) and issubclass(t, arg) - for t in pytype for arg in args) - else: - return any(isinstance(pytype, type) and issubclass(pytype, arg) - for arg in args) - - -class Datum: - def __init__(self, value: Any, type: Optional[str]): - self.value: Any = value - self.type: Optional[str] = type - - @property - def python_value(self) -> Any: - if self.value is None or self.type is None: - return None - elif self.type in ('bytes', 'string', 'int', 'double'): - return self.value - elif self.type == 'json': - return json.loads(self.value) - elif self.type == 'collection_string': - return [v for v in self.value.string] - elif self.type == 'collection_bytes': - return [v for v in self.value.bytes] - elif self.type == 'collection_double': - return [v for v in self.value.double] - elif self.type == 'collection_sint64': - return [v for v in self.value.sint64] - else: - return self.value - - @property - def python_type(self) -> type: - return type(self.python_value) - - def __eq__(self, other): - if not isinstance(other, type(self)): - return False - - return self.value == other.value and self.type == other.type - - def __hash__(self): - return hash((type(self), (self.value, self.type))) - - def __repr__(self): - val_repr = repr(self.value) - if len(val_repr) > 10: - val_repr = val_repr[:10] + '...' - return ''.format(self.type, val_repr) - - -class _ConverterMeta(abc.ABCMeta): - - _bindings: Dict[str, type] = {} - - def __new__(mcls, name, bases, dct, *, - binding: Optional[str], - trigger: Optional[str] = None): - cls = super().__new__(mcls, name, bases, dct) - cls._trigger = trigger # type: ignore - if binding is None: - return cls - - if binding in mcls._bindings: - raise RuntimeError( - f'cannot register a converter for {binding!r} binding: ' - f'another converter for this binding has already been ' - f'registered') - - mcls._bindings[binding] = cls - if trigger is not None: - mcls._bindings[trigger] = cls - - return cls - - @classmethod - def get(cls, binding_name): - return cls._bindings.get(binding_name) - - def has_trigger_support(cls) -> bool: - return cls._trigger is not None # type: ignore - - -class _BaseConverter(metaclass=_ConverterMeta, binding=None): - - @classmethod - def _decode_typed_data( - cls, data: Datum, *, - python_type: Union[type, Tuple[type, ...]], - context: str = 'data') -> Any: - if data is None: - return None - - data_type = data.type - if data_type == 'json': - result = json.loads(data.value) - - elif data_type == 'string': - result = data.value - - elif data_type == 'int': - result = data.value - - elif data_type == 'double': - result = data.value - - elif data_type == 'collection_bytes': - result = data.value - - elif data_type == 'collection_string': - result = data.value - - elif data_type == 'collection_sint64': - result = data.value - - elif data_type is None: - return None - - else: - raise ValueError( - f'unsupported type of {context}: {data_type}') - - if not isinstance(result, python_type): - if isinstance(python_type, (tuple, list, dict)): - raise ValueError( - f'unexpected value type in {context}: ' - f'{type(result).__name__}, expected one of: ' - f'{", ".join(t.__name__ for t in python_type)}') - else: - try: - # Try coercing into the requested type - result = python_type(result) - except (TypeError, ValueError) as e: - raise ValueError( - f'cannot convert value of {context} into ' - f'{python_type.__name__}: {e}') from None - - return result - - @classmethod - def _decode_trigger_metadata_field( - cls, trigger_metadata: Mapping[str, Datum], - field: str, *, - python_type: Union[type, Tuple[type, ...]]) \ - -> Any: - data = trigger_metadata.get(field) - if data is None: - return None - else: - return cls._decode_typed_data( - data, python_type=python_type, - context=f'field {field!r} in trigger metadata') - - @classmethod - def _parse_datetime_metadata( - cls, trigger_metadata: Mapping[str, Datum], - field: str) -> Optional[datetime.datetime]: - - datetime_str = cls._decode_trigger_metadata_field( - trigger_metadata, field, python_type=str) - - if datetime_str is None: - return None - else: - return cls._parse_datetime(datetime_str) - - @classmethod - def _parse_timedelta_metadata( - cls, trigger_metadata: Mapping[str, Datum], - field: str) -> Optional[datetime.timedelta]: - - timedelta_str = cls._decode_trigger_metadata_field( - trigger_metadata, field, python_type=str) - - if timedelta_str is None: - return None - else: - return cls._parse_timedelta(timedelta_str) - - @classmethod - def _parse_datetime( - cls, datetime_str: Optional[str]) -> Optional[datetime.datetime]: - - if not datetime_str: - return None - - too_fractional = re.match( - r'(.*\.\d{6})(\d+)(Z|[\+|-]\d{1,2}:\d{1,2}){0,1}', datetime_str) - - if too_fractional: - # The supplied value contains seven digits in the - # fractional second part, whereas Python expects - # a maxium of six, so strip it. - # https://github.com/Azure/azure-functions-python-worker/issues/269 - datetime_str = too_fractional.group(1) + ( - too_fractional.group(3) or '') - - # Try parse time - utc_time, utc_time_error = cls._parse_datetime_utc(datetime_str) - if not utc_time_error and utc_time: - return utc_time.replace(tzinfo=datetime.timezone.utc) - - local_time, local_time_error = cls._parse_datetime_local(datetime_str) - if not local_time_error and local_time: - return local_time.replace(tzinfo=None) - - # Report error - if utc_time_error: - raise utc_time_error - elif local_time_error: - raise local_time_error - else: - return None - - @classmethod - def _parse_timedelta( - cls, - timedelta_str: Optional[str] - ) -> Optional[datetime.timedelta]: - - if not timedelta_str: - return None - - # Try parse timedelta - timedelta, td_error = cls._parse_timedelta_internal(timedelta_str) - if timedelta is not None: - return timedelta - - # Report error - if td_error: - raise td_error - else: - return None - - @classmethod - def _parse_datetime_utc( - cls, - datetime_str: str - ) -> Tuple[Optional[datetime.datetime], Optional[Exception]]: - - # UTC ISO 8601 assumed - # 2018-08-07T23:17:57.461050Z - utc_formats = [ - '%Y-%m-%dT%H:%M:%S+00:00', - '%Y-%m-%dT%H:%M:%S-00:00', - '%Y-%m-%dT%H:%M:%S.%f+00:00', - '%Y-%m-%dT%H:%M:%S.%f-00:00', - '%Y-%m-%dT%H:%M:%SZ', - '%Y-%m-%dT%H:%M:%S.%fZ', - - '%m/%d/%Y %H:%M:%SZ', - '%m/%d/%Y %H:%M:%S.%fZ', - '%m/%d/%Y %H:%M:%S+00:00', - '%m/%d/%Y %H:%M:%S-00:00', - '%m/%d/%Y %H:%M:%S.%f+00:00', - '%m/%d/%Y %H:%M:%S.%f-00:00', - ] - - dt, _, excpt = try_parse_datetime_with_formats( - datetime_str, utc_formats) - - if excpt is not None: - return None, excpt - return dt, None - - @classmethod - def _parse_datetime_local( - cls, datetime_str: str - ) -> Tuple[Optional[datetime.datetime], Optional[Exception]]: - """Parse a string into a datetime object, accepts following formats - 1. Without fractional seconds (e.g. 2018-08-07T23:17:57) - 2. With fractional seconds (e.g. 2018-08-07T23:17:57.461050) - - Parameters - ---------- - datetime_str: str - The string represents a datetime - - Returns - ------- - Tuple[Optional[datetime.datetime], Optional[Exception]] - If the datetime_str is None, will return None immediately. - If the datetime_str can be parsed correctly, it will return as the - first element in the tuple. - If the datetime_str cannot be parsed with all attempts, it will - return None in the first element, the exception in the second - element. - """ - - local_formats = [ - '%Y-%m-%dT%H:%M:%S.%f', - '%Y-%m-%dT%H:%M:%S', - - '%m/%d/%YT%H:%M:%S.%f', - '%m/%d/%YT%H:%M:%S' - ] - - dt, _, excpt = try_parse_datetime_with_formats( - datetime_str, local_formats) - - if excpt is not None: - return None, excpt - return dt, None - - @classmethod - def _parse_timedelta_internal( - cls, timedelta_str: str - ) -> Tuple[Optional[datetime.timedelta], Optional[Exception]]: - """Parse a string into a timedelta object, accepts following formats - 1. HH:MM:SS (e.g. 12:34:56) - 2. MM:SS (e.g. 34:56) - 3. Pure integer as seconds (e.g. 5819) - - Parameters - ---------- - timedelta_str: str - The string represents a datetime - - Returns - ------- - Tuple[Optional[datetime.timedelta], Optional[Exception]] - If the timedelta_str is None, will return None immediately. - If the timedelta_str can be parsed correctly, it will return as the - first element in the tuple. - If the timedelta_str cannot be parsed with all attempts, it will - return None in the first element, the exception in the second - element. - """ - - timedelta_formats = [ - '%H:%M:%S', - '%M:%S', - '%S' - ] - - td, _, excpt = try_parse_timedelta_with_formats( - timedelta_str, timedelta_formats) - - if td is not None: - return td, None - return None, excpt - - -class InConverter(_BaseConverter, binding=None): - - @abc.abstractclassmethod - def check_input_type_annotation(cls, pytype: type) -> bool: - pass - - @abc.abstractclassmethod - def decode(cls, data: Datum, *, trigger_metadata) -> Any: - raise NotImplementedError - - @abc.abstractclassmethod - def has_implicit_output(cls) -> bool: - return False - - -class OutConverter(_BaseConverter, binding=None): - - @abc.abstractclassmethod - def check_output_type_annotation(cls, pytype: type) -> bool: - pass - - @abc.abstractclassmethod - def encode(cls, obj: Any, *, - expected_type: Optional[type]) -> Optional[Datum]: - raise NotImplementedError - - -def get_binding_registry(): - return _ConverterMeta diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_grpc_protobuf/lib/site-packages/google/protobuf/__init__.py b/tests/endtoend/dependency_isolation_functions/.python_packages_grpc_protobuf/lib/site-packages/google/protobuf/__init__.py deleted file mode 100644 index 814beb791..000000000 --- a/tests/endtoend/dependency_isolation_functions/.python_packages_grpc_protobuf/lib/site-packages/google/protobuf/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -# This is a dummy protobuf==3.9.0 package used for E2E -# testing in Azure Functions Python Worker - -__version__ = '3.9.0' diff --git a/tests/endtoend/dependency_isolation_functions/.python_packages_grpc_protobuf/lib/site-packages/grpc/__init__.py b/tests/endtoend/dependency_isolation_functions/.python_packages_grpc_protobuf/lib/site-packages/grpc/__init__.py deleted file mode 100644 index fc42cae68..000000000 --- a/tests/endtoend/dependency_isolation_functions/.python_packages_grpc_protobuf/lib/site-packages/grpc/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -# This is a dummy grpcio==1.35.0 package used for E2E -# testing in Azure Functions Python Worker. - -__version__ = '1.35.0' diff --git a/tests/endtoend/dependency_isolation_functions/report_dependencies/__init__.py b/tests/endtoend/dependency_isolation_functions/report_dependencies/__init__.py deleted file mode 100644 index 709fd6ca1..000000000 --- a/tests/endtoend/dependency_isolation_functions/report_dependencies/__init__.py +++ /dev/null @@ -1,50 +0,0 @@ -import json -import os -import sys - -import azure.functions as func -import google.protobuf as proto -import grpc - -# Load dependency manager from customer' context -from azure_functions_worker.utils.dependency import DependencyManager as dm - - -def main(req: func.HttpRequest) -> func.HttpResponse: - """This function is an HttpTrigger to check if the modules are loaded from - customer's dependencies. We have mock a .python_packages/ folder in - this e2e test function app which contains the following stub package: - - protobuf==3.9.0 - grpc==1.35.0 - - If the version we check is the same as the one in local .python_packages/, - that means the isolate worker dependencies are working as expected. - """ - result = { - "sys.path": list(sys.path), - "dependency_manager": { - "cx_deps_path": dm._get_cx_deps_path(), - "cx_working_dir": dm._get_cx_working_dir(), - "worker_deps_path": dm._get_worker_deps_path(), - }, - "libraries": { - "func.version": func.__version__, - "func.file": func.__file__, - "proto.expected.version": "3.9.0", - "proto.version": proto.__version__, - "proto.file": proto.__file__, - "grpc.expected.version": "1.35.0", - "grpc.version": grpc.__version__, - "grpc.file": grpc.__file__, - }, - "environments": { - "PYTHON_ISOLATE_WORKER_DEPENDENCIES": ( - os.getenv('PYTHON_ISOLATE_WORKER_DEPENDENCIES') - ), - "AzureWebJobsScriptRoot": os.getenv('AzureWebJobsScriptRoot'), - "PYTHONPATH": os.getenv('PYTHONPATH'), - "HOST_VERSION": os.getenv('HOST_VERSION') - } - } - return func.HttpResponse(json.dumps(result)) diff --git a/tests/endtoend/dependency_isolation_functions/report_dependencies/function.json b/tests/endtoend/dependency_isolation_functions/report_dependencies/function.json deleted file mode 100644 index c76954425..000000000 --- a/tests/endtoend/dependency_isolation_functions/report_dependencies/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req", - "methods": [ - "get", - "post" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] - } diff --git a/tests/endtoend/durable_functions/DurableFunctionsHttpStart/__init__.py b/tests/endtoend/durable_functions/DurableFunctionsHttpStart/__init__.py deleted file mode 100644 index 18709fccd..000000000 --- a/tests/endtoend/durable_functions/DurableFunctionsHttpStart/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# This function an HTTP starter function for Durable Functions. -# Before running this sample, please: -# - create a Durable orchestration function -# - create a Durable activity function (default name is "Hello") -# - add azure-functions-durable to requirements.txt -# - run pip install -r requirements.txt -import logging - -import azure.durable_functions as df -import azure.functions as func - - -async def main(req: func.HttpRequest, starter: str) -> func.HttpResponse: - client = df.DurableOrchestrationClient(starter) - instance_id = await client.start_new(req.route_params["functionName"], None, - None) - - logging.info(f"Started orchestration with ID = '{instance_id}'.") - - return client.create_check_status_response(req, instance_id) diff --git a/tests/endtoend/durable_functions/DurableFunctionsHttpStart/function.json b/tests/endtoend/durable_functions/DurableFunctionsHttpStart/function.json deleted file mode 100644 index 51a0411ca..000000000 --- a/tests/endtoend/durable_functions/DurableFunctionsHttpStart/function.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "authLevel": "anonymous", - "name": "req", - "type": "httpTrigger", - "direction": "in", - "route": "orchestrators/{functionName}", - "methods": [ - "post", - "get" - ] - }, - { - "name": "$return", - "type": "http", - "direction": "out" - }, - { - "name": "starter", - "type": "durableClient", - "direction": "in" - } - ] -} diff --git a/tests/endtoend/durable_functions/DurableFunctionsOrchestrator/__init__.py b/tests/endtoend/durable_functions/DurableFunctionsOrchestrator/__init__.py deleted file mode 100644 index 9a7c43ce8..000000000 --- a/tests/endtoend/durable_functions/DurableFunctionsOrchestrator/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# This function is not intended to be invoked directly. Instead it will be -# triggered by an HTTP starter function. -# Before running this sample, please: -# - create a Durable activity function (default name is "Hello") -# - create a Durable HTTP starter function -# - add azure-functions-durable to requirements.txt -# - run pip install -r requirements.txt -import azure.durable_functions as df - - -def orchestrator_function(context: df.DurableOrchestrationContext): - result1 = yield context.call_activity('Hello', "Tokyo") - result2 = yield context.call_activity('Hello', "Seattle") - result3 = yield context.call_activity('Hello', "London") - return [result1, result2, result3] - - -main = df.Orchestrator.create(orchestrator_function) diff --git a/tests/endtoend/durable_functions/DurableFunctionsOrchestrator/function.json b/tests/endtoend/durable_functions/DurableFunctionsOrchestrator/function.json deleted file mode 100644 index 83baac61e..000000000 --- a/tests/endtoend/durable_functions/DurableFunctionsOrchestrator/function.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "name": "context", - "type": "orchestrationTrigger", - "direction": "in" - } - ] -} diff --git a/tests/endtoend/durable_functions/Hello/__init__.py b/tests/endtoend/durable_functions/Hello/__init__.py deleted file mode 100644 index 15cee6a5b..000000000 --- a/tests/endtoend/durable_functions/Hello/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# This function is not intended to be invoked directly. Instead it will be -# triggered by an orchestrator function. -# Before running this sample, please: -# - create a Durable orchestration function -# - create a Durable HTTP starter function -# - add azure-functions-durable to requirements.txt -# - run pip install -r requirements.txt - -def main(name: str) -> str: - return f"Hello {name}!" diff --git a/tests/endtoend/durable_functions/Hello/function.json b/tests/endtoend/durable_functions/Hello/function.json deleted file mode 100644 index 1b03f1100..000000000 --- a/tests/endtoend/durable_functions/Hello/function.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "name": "name", - "type": "activityTrigger", - "direction": "in" - } - ] -} diff --git a/tests/endtoend/durable_functions/durable_functions_stein/function_app.py b/tests/endtoend/durable_functions/durable_functions_stein/function_app.py deleted file mode 100644 index f0e95135b..000000000 --- a/tests/endtoend/durable_functions/durable_functions_stein/function_app.py +++ /dev/null @@ -1,29 +0,0 @@ -import logging - -import azure.durable_functions as df -import azure.functions as func - -app = df.DFApp() - - -@app.orchestration_trigger(context_name="context") -def durablefunctionsorchestrator(context): - result1 = yield context.call_activity('Hello', "Tokyo") - result2 = yield context.call_activity('Hello', "Seattle") - result3 = yield context.call_activity('Hello', "London") - return [result1, result2, result3] - - -@app.route(route="orchestrators/{functionName}", - auth_level=func.AuthLevel.ANONYMOUS) -@app.durable_client_input(client_name="client") -async def durable_client(req: func.HttpRequest, client) -> func.HttpResponse: - instance_id = await client.start_new(req.route_params["functionName"], None, - None) - logging.info(f"Started orchestration with ID = '{instance_id}'.") - return client.create_check_status_response(req, instance_id) - - -@app.activity_trigger(input_name="name") -def hello(name: str) -> str: - return f"Hello {name}!" diff --git a/tests/endtoend/eventgrid_functions/eventgrid_functions_stein/function_app.py b/tests/endtoend/eventgrid_functions/eventgrid_functions_stein/function_app.py deleted file mode 100644 index 94f05bf22..000000000 --- a/tests/endtoend/eventgrid_functions/eventgrid_functions_stein/function_app.py +++ /dev/null @@ -1,83 +0,0 @@ -import json -from datetime import datetime - -import azure.functions as func - -from azure_functions_worker import logging - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.function_name(name="eventGridTrigger") -@app.event_grid_trigger(arg_name="event") -@app.blob_output(arg_name="$return", - path="python-worker-tests/test-eventgrid-triggered.txt", - connection="AzureWebJobsStorage") -def event_grid_trigger(event: func.EventGridEvent) -> str: - logging.info("Event grid function is triggered!") - return json.dumps({ - 'id': event.id, - 'data': event.get_json(), - 'topic': event.topic, - 'subject': event.subject, - 'event_type': event.event_type, - }) - - -@app.function_name(name="eventgrid_output_binding") -@app.route(route="eventgrid_output_binding") -@app.event_grid_output( - arg_name="outputEvent", - topic_endpoint_uri="AzureWebJobsEventGridTopicUri", - topic_key_setting="AzureWebJobsEventGridConnectionKey") -def eventgrid_output_binding( - req: func.HttpRequest, - outputEvent: func.Out[func.EventGridOutputEvent]) -> func.HttpResponse: - test_uuid = req.params.get('test_uuid') - data_to_event_grid = func.EventGridOutputEvent(id="test-id", - data={ - "test_uuid": test_uuid - }, - subject="test-subject", - event_type="test-event-1", - event_time=datetime.utcnow(), - data_version="1.0") - - outputEvent.set(data_to_event_grid) - r_value = "Sent event with subject: {}, id: {}, data: {}, event_type: {} " \ - "to EventGrid!".format(data_to_event_grid.subject, - data_to_event_grid.id, - data_to_event_grid.get_json(), - data_to_event_grid.event_type) - return func.HttpResponse(r_value) - - -@app.function_name(name="eventgrid_output_binding_message_to_blobstore") -@app.queue_trigger(arg_name="msg", queue_name="test-event-grid-storage-queue", - connection="AzureWebJobsStorage") -@app.blob_output(arg_name="$return", - path="python-worker-tests/test-eventgrid-output-binding.txt", - connection="AzureWebJobsStorage") -def eventgrid_output_binding_message_to_blobstore( - msg: func.QueueMessage) -> bytes: - return msg.get_body() - - -@app.function_name(name="eventgrid_output_binding_success") -@app.route(route="eventgrid_output_binding_success") -@app.blob_input(arg_name="file", - path="python-worker-tests/test-eventgrid-output-binding.txt", - connection="AzureWebJobsStorage") -def eventgrid_output_binding_success( - req: func.HttpRequest, file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -@app.function_name(name="get_eventgrid_triggered") -@app.route(route="get_eventgrid_triggered") -@app.blob_input(arg_name="file", - path="python-worker-tests/test-eventgrid-triggered.txt", - connection="AzureWebJobsStorage") -def get_eventgrid_triggered( - req: func.HttpRequest, file: func.InputStream) -> str: - return file.read().decode('utf-8') diff --git a/tests/endtoend/eventgrid_functions/eventgrid_functions_stein/generic/function_app.py b/tests/endtoend/eventgrid_functions/eventgrid_functions_stein/generic/function_app.py deleted file mode 100644 index 5dff24d80..000000000 --- a/tests/endtoend/eventgrid_functions/eventgrid_functions_stein/generic/function_app.py +++ /dev/null @@ -1,100 +0,0 @@ -import json -from datetime import datetime - -import azure.functions as func - -from azure_functions_worker import logging - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.function_name(name="eventGridTrigger") -@app.event_grid_trigger(arg_name="event", type="eventGridTrigger") -@app.generic_output_binding( - arg_name="$return", - type="blob", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-eventgrid-triggered.txt") -def event_grid_trigger(event: func.EventGridEvent) -> str: - logging.info("Event grid function is triggered!") - return json.dumps({ - 'id': event.id, - 'data': event.get_json(), - 'topic': event.topic, - 'subject': event.subject, - 'event_type': event.event_type, - }) - - -@app.function_name(name="eventgrid_output_binding") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="eventgrid_output_binding") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_output_binding( - arg_name="outputEvent", - type="eventGrid", - topic_endpoint_uri="AzureWebJobsEventGridTopicUri", - topic_key_setting="AzureWebJobsEventGridConnectionKey") -def eventgrid_output_binding( - req: func.HttpRequest, - outputEvent: func.Out[func.EventGridOutputEvent]) -> func.HttpResponse: - test_uuid = req.params.get('test_uuid') - data_to_event_grid = func.EventGridOutputEvent(id="test-id", - data={ - "test_uuid": test_uuid - }, - subject="test-subject", - event_type="test-event-1", - event_time=datetime.utcnow(), - data_version="1.0") - - outputEvent.set(data_to_event_grid) - r_value = "Sent event with subject: {}, id: {}, data: {}, event_type: {} " \ - "to EventGrid!".format(data_to_event_grid.subject, - data_to_event_grid.id, - data_to_event_grid.get_json(), - data_to_event_grid.event_type) - return func.HttpResponse(r_value) - - -@app.function_name(name="eventgrid_output_binding_message_to_blobstore") -@app.generic_trigger(arg_name="msg", - type="queueTrigger", - queue_name="test-event-grid-storage-queue", - connection="AzureWebJobsStorage") -@app.generic_output_binding( - arg_name="$return", - type="blob", - connection="AzureWebJobsStorage", - path="python-worker-tests/test-eventgrid-output-binding.txt") -def eventgrid_output_binding_message_to_blobstore( - msg: func.QueueMessage) -> bytes: - return msg.get_body() - - -@app.function_name(name="eventgrid_output_binding_success") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="eventgrid_output_binding_success") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding( - arg_name="file", - type="blob", - path="python-worker-tests/test-eventgrid-output-binding.txt", - connection="AzureWebJobsStorage") -def eventgrid_output_binding_success( - req: func.HttpRequest, file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -@app.function_name(name="get_eventgrid_triggered") -@app.generic_trigger(arg_name="req", type="httpTrigger", - route="get_eventgrid_triggered") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding( - arg_name="file", - type="blob", - path="python-worker-tests/test-eventgrid-triggered.txt", - connection="AzureWebJobsStorage") -def get_eventgrid_triggered( - req: func.HttpRequest, file: func.InputStream) -> str: - return file.read().decode('utf-8') diff --git a/tests/endtoend/eventgrid_functions/eventgrid_output_binding/__init__.py b/tests/endtoend/eventgrid_functions/eventgrid_output_binding/__init__.py deleted file mode 100644 index 817e31396..000000000 --- a/tests/endtoend/eventgrid_functions/eventgrid_output_binding/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from datetime import datetime - -import azure.functions as func - - -def main(req: func.HttpRequest, - outputEvent: func.Out[func.EventGridOutputEvent]) -> func.HttpResponse: - test_uuid = req.params.get('test_uuid') - data_to_event_grid = func.EventGridOutputEvent(id="test-id", - data={ - "test_uuid": test_uuid - }, - subject="test-subject", - event_type="test-event-1", - event_time=datetime.utcnow(), - data_version="1.0") - - outputEvent.set(data_to_event_grid) - r_value = "Sent event with subject: {}, id: {}, data: {}, event_type: {} " \ - "to EventGrid!".format(data_to_event_grid.subject, - data_to_event_grid.id, - data_to_event_grid.get_json(), - data_to_event_grid.event_type) - return func.HttpResponse(r_value) diff --git a/tests/endtoend/eventgrid_functions/eventgrid_output_binding/function.json b/tests/endtoend/eventgrid_functions/eventgrid_output_binding/function.json deleted file mode 100644 index 1c0343465..000000000 --- a/tests/endtoend/eventgrid_functions/eventgrid_output_binding/function.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "scriptFile": "__init__.py", - - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "eventGrid", - "name": "outputEvent", - "topicEndpointUri": "AzureWebJobsEventGridTopicUri", - "topicKeySetting": "AzureWebJobsEventGridConnectionKey", - "direction": "out" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/endtoend/eventgrid_functions/eventgrid_output_binding_message_to_blobstore/__init__.py b/tests/endtoend/eventgrid_functions/eventgrid_output_binding_message_to_blobstore/__init__.py deleted file mode 100644 index b0d9c3096..000000000 --- a/tests/endtoend/eventgrid_functions/eventgrid_output_binding_message_to_blobstore/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import azure.functions as func - - -def main(msg: func.QueueMessage) -> bytes: - return msg.get_body() diff --git a/tests/endtoend/eventgrid_functions/eventgrid_output_binding_message_to_blobstore/function.json b/tests/endtoend/eventgrid_functions/eventgrid_output_binding_message_to_blobstore/function.json deleted file mode 100644 index f25661fdb..000000000 --- a/tests/endtoend/eventgrid_functions/eventgrid_output_binding_message_to_blobstore/function.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "name": "msg", - "type": "queueTrigger", - "direction": "in", - "queueName": "test-event-grid-storage-queue", - "connection": "AzureWebJobsStorage" - }, - { - "type": "blob", - "direction": "out", - "name": "$return", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-eventgrid-output-binding.txt" - } - ] -} diff --git a/tests/endtoend/eventgrid_functions/eventgrid_output_binding_success/__init__.py b/tests/endtoend/eventgrid_functions/eventgrid_output_binding_success/__init__.py deleted file mode 100644 index 7e9725f0b..000000000 --- a/tests/endtoend/eventgrid_functions/eventgrid_output_binding_success/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import azure.functions as func - - -def main(req: func.HttpRequest, file: func.InputStream) -> str: - return file.read().decode('utf-8') diff --git a/tests/endtoend/eventgrid_functions/eventgrid_output_binding_success/function.json b/tests/endtoend/eventgrid_functions/eventgrid_output_binding_success/function.json deleted file mode 100644 index e63945d3a..000000000 --- a/tests/endtoend/eventgrid_functions/eventgrid_output_binding_success/function.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "in", - "name": "file", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-eventgrid-output-binding.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/endtoend/eventgrid_functions/eventgrid_trigger/__init__.py b/tests/endtoend/eventgrid_functions/eventgrid_trigger/__init__.py deleted file mode 100644 index b2b414623..000000000 --- a/tests/endtoend/eventgrid_functions/eventgrid_trigger/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json - -import azure.functions as func - - -def main(event: func.EventGridEvent) -> str: - return json.dumps({ - 'id': event.id, - 'data': event.get_json(), - 'topic': event.topic, - 'subject': event.subject, - 'event_type': event.event_type, - }) diff --git a/tests/endtoend/eventgrid_functions/eventgrid_trigger/function.json b/tests/endtoend/eventgrid_functions/eventgrid_trigger/function.json deleted file mode 100644 index bf33c7072..000000000 --- a/tests/endtoend/eventgrid_functions/eventgrid_trigger/function.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "scriptFile": "__init__.py", - - "bindings": [ - { - "type": "eventGridTrigger", - "direction": "in", - "name": "event" - }, - { - "type": "blob", - "direction": "out", - "name": "$return", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-eventgrid-triggered.txt" - } - ] -} diff --git a/tests/endtoend/eventgrid_functions/get_eventgrid_triggered/function.json b/tests/endtoend/eventgrid_functions/get_eventgrid_triggered/function.json deleted file mode 100644 index 2c2727754..000000000 --- a/tests/endtoend/eventgrid_functions/get_eventgrid_triggered/function.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "authLevel": "anonymous" - }, - { - "type": "blob", - "direction": "in", - "name": "file", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-eventgrid-triggered.txt" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/endtoend/eventgrid_functions/get_eventgrid_triggered/main.py b/tests/endtoend/eventgrid_functions/get_eventgrid_triggered/main.py deleted file mode 100644 index 167c7a574..000000000 --- a/tests/endtoend/eventgrid_functions/get_eventgrid_triggered/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func - - -def main(req: func.HttpRequest, file: func.InputStream) -> str: - return file.read().decode('utf-8') diff --git a/tests/endtoend/http_functions/common_libs_functions/common_libs_functions_stein/function_app.py b/tests/endtoend/http_functions/common_libs_functions/common_libs_functions_stein/function_app.py deleted file mode 100644 index e6174cae9..000000000 --- a/tests/endtoend/http_functions/common_libs_functions/common_libs_functions_stein/function_app.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging - -import azure.functions as func -import cv2 -import dotenv -import numpy as np -import plotly -import requests -from pandas import DataFrame -from sklearn.datasets import load_iris - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.route(route="dotenv_func") -def dotenv_func(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - res = "found" if "load_dotenv" in dotenv.__all__ else "not found" - - return func.HttpResponse(res) - - -@app.route(route="numpy_func") -def numpy_func(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - res = "numpy version: {}".format(np.__version__) - - return func.HttpResponse(res) - - -@app.route(route="opencv_func") -def opencv_func(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - res = "opencv version: {}".format(cv2.__version__) - - return func.HttpResponse(res) - - -@app.route(route="pandas_func") -def pandas_func(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - array = np.arange(6).reshape(3, 2) - df = DataFrame(array, columns=['x', 'y'], index=['T1', 'T2', 'T3']) - - res = "two-dimensional DataFrame: \n {}".format(df) - - return func.HttpResponse(res) - - -@app.route(route="plotly_func") -def plotly_func(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - res = "plotly version: {}".format(plotly.__version__) - - return func.HttpResponse(res) - - -@app.route(route="requests_func") -def requests_func(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - req = requests.get('https://github.com') - res = "req status code: {}".format(req.status_code) - - return func.HttpResponse(res) - - -@app.route(route="sklearn_func") -def sklearn_func(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - iris = load_iris() - - res = "First 5 records of array: \n {}".format(iris.data[:5]) - - return func.HttpResponse(res) diff --git a/tests/endtoend/http_functions/common_libs_functions/dotenv_func/__init__.py b/tests/endtoend/http_functions/common_libs_functions/dotenv_func/__init__.py deleted file mode 100644 index 2046b551a..000000000 --- a/tests/endtoend/http_functions/common_libs_functions/dotenv_func/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging - -import azure.functions as func -import dotenv - - -def main(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - res = "found" if "load_dotenv" in dotenv.__all__ else "not found" - - return func.HttpResponse(res) diff --git a/tests/endtoend/http_functions/common_libs_functions/dotenv_func/function.json b/tests/endtoend/http_functions/common_libs_functions/dotenv_func/function.json deleted file mode 100644 index 8c4cbe307..000000000 --- a/tests/endtoend/http_functions/common_libs_functions/dotenv_func/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req", - "methods": [ - "get", - "post" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/endtoend/http_functions/common_libs_functions/numpy_func/__init__.py b/tests/endtoend/http_functions/common_libs_functions/numpy_func/__init__.py deleted file mode 100644 index 588ebb061..000000000 --- a/tests/endtoend/http_functions/common_libs_functions/numpy_func/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging - -import azure.functions as func -import numpy as np - - -def main(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - res = "numpy version: {}".format(np.__version__) - - return func.HttpResponse(res) diff --git a/tests/endtoend/http_functions/common_libs_functions/numpy_func/function.json b/tests/endtoend/http_functions/common_libs_functions/numpy_func/function.json deleted file mode 100644 index 8c4cbe307..000000000 --- a/tests/endtoend/http_functions/common_libs_functions/numpy_func/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req", - "methods": [ - "get", - "post" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/endtoend/http_functions/common_libs_functions/opencv_func/__init__.py b/tests/endtoend/http_functions/common_libs_functions/opencv_func/__init__.py deleted file mode 100644 index e102878b9..000000000 --- a/tests/endtoend/http_functions/common_libs_functions/opencv_func/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging - -import azure.functions as func -import cv2 - - -def main(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - res = "opencv version: {}".format(cv2.__version__) - - return func.HttpResponse(res) diff --git a/tests/endtoend/http_functions/common_libs_functions/opencv_func/function.json b/tests/endtoend/http_functions/common_libs_functions/opencv_func/function.json deleted file mode 100644 index 8c4cbe307..000000000 --- a/tests/endtoend/http_functions/common_libs_functions/opencv_func/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req", - "methods": [ - "get", - "post" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/endtoend/http_functions/common_libs_functions/pandas_func/__init__.py b/tests/endtoend/http_functions/common_libs_functions/pandas_func/__init__.py deleted file mode 100644 index 4d6692015..000000000 --- a/tests/endtoend/http_functions/common_libs_functions/pandas_func/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging - -import azure.functions as func -import numpy as np -from pandas import DataFrame - - -def main(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - array = np.arange(6).reshape(3, 2) - df = DataFrame(array, columns=['x', 'y'], index=['T1', 'T2', 'T3']) - - res = "two-dimensional DataFrame: \n {}".format(df) - - return func.HttpResponse(res) diff --git a/tests/endtoend/http_functions/common_libs_functions/pandas_func/function.json b/tests/endtoend/http_functions/common_libs_functions/pandas_func/function.json deleted file mode 100644 index 8c4cbe307..000000000 --- a/tests/endtoend/http_functions/common_libs_functions/pandas_func/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req", - "methods": [ - "get", - "post" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/endtoend/http_functions/common_libs_functions/plotly_func/__init__.py b/tests/endtoend/http_functions/common_libs_functions/plotly_func/__init__.py deleted file mode 100644 index 4d93da913..000000000 --- a/tests/endtoend/http_functions/common_libs_functions/plotly_func/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging - -import azure.functions as func -import plotly - - -def main(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - res = "plotly version: {}".format(plotly.__version__) - - return func.HttpResponse(res) diff --git a/tests/endtoend/http_functions/common_libs_functions/plotly_func/function.json b/tests/endtoend/http_functions/common_libs_functions/plotly_func/function.json deleted file mode 100644 index 8c4cbe307..000000000 --- a/tests/endtoend/http_functions/common_libs_functions/plotly_func/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req", - "methods": [ - "get", - "post" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/endtoend/http_functions/common_libs_functions/requests_func/__init__.py b/tests/endtoend/http_functions/common_libs_functions/requests_func/__init__.py deleted file mode 100644 index ffee90749..000000000 --- a/tests/endtoend/http_functions/common_libs_functions/requests_func/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging - -import azure.functions as func -import requests - - -def main(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - req = requests.get('https://github.com') - res = "req status code: {}".format(req.status_code) - - return func.HttpResponse(res) diff --git a/tests/endtoend/http_functions/common_libs_functions/requests_func/function.json b/tests/endtoend/http_functions/common_libs_functions/requests_func/function.json deleted file mode 100644 index 8c4cbe307..000000000 --- a/tests/endtoend/http_functions/common_libs_functions/requests_func/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req", - "methods": [ - "get", - "post" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/endtoend/http_functions/common_libs_functions/sklearn_func/__init__.py b/tests/endtoend/http_functions/common_libs_functions/sklearn_func/__init__.py deleted file mode 100644 index 578e925e8..000000000 --- a/tests/endtoend/http_functions/common_libs_functions/sklearn_func/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging - -import azure.functions as func -from sklearn.datasets import load_iris - - -def main(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - iris = load_iris() - - res = "First 5 records of array: \n {}".format(iris.data[:5]) - - return func.HttpResponse(res) diff --git a/tests/endtoend/http_functions/common_libs_functions/sklearn_func/function.json b/tests/endtoend/http_functions/common_libs_functions/sklearn_func/function.json deleted file mode 100644 index 8c4cbe307..000000000 --- a/tests/endtoend/http_functions/common_libs_functions/sklearn_func/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req", - "methods": [ - "get", - "post" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/endtoend/http_functions/default_template/__init__.py b/tests/endtoend/http_functions/default_template/__init__.py deleted file mode 100644 index 464477bb9..000000000 --- a/tests/endtoend/http_functions/default_template/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -# flake8: noqa -import logging - -import azure.functions as func - - -def main(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - name = req.params.get('name') - if not name: - try: - req_body = req.get_json() - except ValueError: - pass - else: - name = req_body.get('name') - - if name: - return func.HttpResponse(f"Hello, {name}. This HTTP triggered function executed successfully.") - else: - return func.HttpResponse( - "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 - ) diff --git a/tests/endtoend/http_functions/default_template/function.json b/tests/endtoend/http_functions/default_template/function.json deleted file mode 100644 index 8c4cbe307..000000000 --- a/tests/endtoend/http_functions/default_template/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req", - "methods": [ - "get", - "post" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/endtoend/http_functions/http_func/__init__.py b/tests/endtoend/http_functions/http_func/__init__.py deleted file mode 100644 index 0743efe9a..000000000 --- a/tests/endtoend/http_functions/http_func/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import time -from datetime import datetime - -# flake8: noqa -import azure.functions as func - - -def main(req: func.HttpRequest) -> func.HttpResponse: - time.sleep(1) - - current_time = datetime.now().strftime("%H:%M:%S") - return func.HttpResponse(f"{current_time}") diff --git a/tests/endtoend/http_functions/http_func/function.json b/tests/endtoend/http_functions/http_func/function.json deleted file mode 100644 index 8c4cbe307..000000000 --- a/tests/endtoend/http_functions/http_func/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req", - "methods": [ - "get", - "post" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/endtoend/http_functions/http_functions_stein/file_name/main.py b/tests/endtoend/http_functions/http_functions_stein/file_name/main.py deleted file mode 100644 index 512cf83c2..000000000 --- a/tests/endtoend/http_functions/http_functions_stein/file_name/main.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging -import time -from datetime import datetime - -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.route(route="default_template") -def default_template(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - name = req.params.get('name') - if not name: - try: - req_body = req.get_json() - except ValueError: - pass - else: - name = req_body.get('name') - - if name: - return func.HttpResponse( - f"Hello, {name}. This HTTP triggered function " - f"executed successfully.") - else: - return func.HttpResponse( - "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: func.HttpRequest) -> func.HttpResponse: - time.sleep(1) - - current_time = datetime.now().strftime("%H:%M:%S") - return func.HttpResponse(f"{current_time}") diff --git a/tests/endtoend/http_functions/http_functions_stein/function_app.py b/tests/endtoend/http_functions/http_functions_stein/function_app.py deleted file mode 100644 index f60697475..000000000 --- a/tests/endtoend/http_functions/http_functions_stein/function_app.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging -import time -from datetime import datetime - -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.route(route="default_template") -def default_template(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - name = req.params.get('name') - if not name: - try: - req_body = req.get_json() - except ValueError: - pass - else: - name = req_body.get('name') - - if name: - return func.HttpResponse( - f"Hello, {name}. This HTTP triggered function " - f"executed successfully.") - else: - return func.HttpResponse( - "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: func.HttpRequest) -> func.HttpResponse: - time.sleep(1) - - current_time = datetime.now().strftime("%H:%M:%S") - return func.HttpResponse(f"{current_time}") diff --git a/tests/endtoend/http_functions/http_functions_stein/generic/function_app.py b/tests/endtoend/http_functions/http_functions_stein/generic/function_app.py deleted file mode 100644 index 17e715a89..000000000 --- a/tests/endtoend/http_functions/http_functions_stein/generic/function_app.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging - -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.function_name(name="default_template") -@app.generic_trigger(arg_name="req", - type="httpTrigger", - route="default_template") -@app.generic_output_binding(arg_name="$return", type="http") -def default_template(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - name = req.params.get('name') - if not name: - try: - req_body = req.get_json() - except ValueError: - pass - else: - name = req_body.get('name') - - if name: - return func.HttpResponse( - f"Hello, {name}. This HTTP triggered function " - f"executed successfully.") - else: - return func.HttpResponse( - "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 - ) diff --git a/tests/endtoend/http_functions/user_thread_logging/async_thread/__init__.py b/tests/endtoend/http_functions/user_thread_logging/async_thread/__init__.py deleted file mode 100644 index 64da1d3ae..000000000 --- a/tests/endtoend/http_functions/user_thread_logging/async_thread/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -# flake8: noqa -import logging -import threading - -import azure.functions as func - - -async def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse: - logging.info('Before threads.') - - t1 = threading.Thread(target=thread_function, args=(context, 'Thread1 used.')) - t2 = threading.Thread(target=thread_function, args=(context, 'Thread2 used.')) - t3 = threading.Thread(target=thread_function, args=(context, 'Thread3 used.')) - - t1.start() - t2.start() - t3.start() - - t1.join() - t2.join() - t3.join() - - logging.info('After threads.') - - return func.HttpResponse('This HTTP triggered function executed successfully.', status_code=200) - - -def thread_function(context: func.Context, message: str): - context.thread_local_storage.invocation_id = context.invocation_id - logging.info(message) diff --git a/tests/endtoend/http_functions/user_thread_logging/async_thread/function.json b/tests/endtoend/http_functions/user_thread_logging/async_thread/function.json deleted file mode 100644 index 8c4cbe307..000000000 --- a/tests/endtoend/http_functions/user_thread_logging/async_thread/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req", - "methods": [ - "get", - "post" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/endtoend/http_functions/user_thread_logging/async_thread_pool_executor/__init__.py b/tests/endtoend/http_functions/user_thread_logging/async_thread_pool_executor/__init__.py deleted file mode 100644 index 3c124f4da..000000000 --- a/tests/endtoend/http_functions/user_thread_logging/async_thread_pool_executor/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import concurrent.futures - -# flake8: noqa -import logging - -import azure.functions as func - - -async def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse: - logging.info('Before TPE.') - - with concurrent.futures.ThreadPoolExecutor() as tpe: - tpe.submit(thread_function, context, 'Using TPE.') - - logging.info('After TPE.') - - return func.HttpResponse('This HTTP triggered function executed successfully.', status_code=200) - - -def thread_function(context: func.Context, message: str): - context.thread_local_storage.invocation_id = context.invocation_id - logging.info(message) diff --git a/tests/endtoend/http_functions/user_thread_logging/async_thread_pool_executor/function.json b/tests/endtoend/http_functions/user_thread_logging/async_thread_pool_executor/function.json deleted file mode 100644 index 8c4cbe307..000000000 --- a/tests/endtoend/http_functions/user_thread_logging/async_thread_pool_executor/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req", - "methods": [ - "get", - "post" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/endtoend/http_functions/user_thread_logging/thread/__init__.py b/tests/endtoend/http_functions/user_thread_logging/thread/__init__.py deleted file mode 100644 index 327b91f83..000000000 --- a/tests/endtoend/http_functions/user_thread_logging/thread/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -# flake8: noqa -import logging -import threading - -import azure.functions as func - - -def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse: - logging.info('Before threads.') - - t1 = threading.Thread(target=thread_function, args=(context, 'Thread1 used.')) - t2 = threading.Thread(target=thread_function, args=(context, 'Thread2 used.')) - t3 = threading.Thread(target=thread_function, args=(context, 'Thread3 used.')) - - t1.start() - t2.start() - t3.start() - - t1.join() - t2.join() - t3.join() - - logging.info('After threads.') - - return func.HttpResponse('This HTTP triggered function executed successfully.', status_code=200) - - -def thread_function(context: func.Context, message: str): - context.thread_local_storage.invocation_id = context.invocation_id - logging.info(message) diff --git a/tests/endtoend/http_functions/user_thread_logging/thread/function.json b/tests/endtoend/http_functions/user_thread_logging/thread/function.json deleted file mode 100644 index 8c4cbe307..000000000 --- a/tests/endtoend/http_functions/user_thread_logging/thread/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req", - "methods": [ - "get", - "post" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/endtoend/http_functions/user_thread_logging/thread_pool_executor/__init__.py b/tests/endtoend/http_functions/user_thread_logging/thread_pool_executor/__init__.py deleted file mode 100644 index 4ed94266e..000000000 --- a/tests/endtoend/http_functions/user_thread_logging/thread_pool_executor/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import concurrent.futures - -# flake8: noqa -import logging - -import azure.functions as func - - -def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse: - logging.info('Before TPE.') - - with concurrent.futures.ThreadPoolExecutor() as tpe: - tpe.submit(thread_function, context, 'Using TPE.') - - logging.info('After TPE.') - - return func.HttpResponse('This HTTP triggered function executed successfully.', status_code=200) - - -def thread_function(context: func.Context, message: str): - context.thread_local_storage.invocation_id = context.invocation_id - logging.info(message) diff --git a/tests/endtoend/http_functions/user_thread_logging/thread_pool_executor/function.json b/tests/endtoend/http_functions/user_thread_logging/thread_pool_executor/function.json deleted file mode 100644 index 8c4cbe307..000000000 --- a/tests/endtoend/http_functions/user_thread_logging/thread_pool_executor/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req", - "methods": [ - "get", - "post" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/endtoend/retry_policy_functions/exponential_strategy/function_app.py b/tests/endtoend/retry_policy_functions/exponential_strategy/function_app.py deleted file mode 100644 index 5180119ee..000000000 --- a/tests/endtoend/retry_policy_functions/exponential_strategy/function_app.py +++ /dev/null @@ -1,23 +0,0 @@ -import logging - -from azure.functions import AuthLevel, Context, FunctionApp, TimerRequest - -app = FunctionApp(http_auth_level=AuthLevel.ANONYMOUS) - - -@app.timer_trigger(schedule="*/1 * * * * *", arg_name="mytimer", - run_on_startup=False, - use_monitor=False) -@app.retry(strategy="exponential_backoff", max_retry_count="3", - minimum_interval="00:00:01", - maximum_interval="00:00:02") -def mytimer(mytimer: TimerRequest, context: Context) -> None: - logging.info(f'Current retry count: {context.retry_context.retry_count}') - - if context.retry_context.retry_count == \ - context.retry_context.max_retry_count: - logging.info( - f"Max retries of {context.retry_context.max_retry_count} for " - f"function {context.function_name} has been reached") - else: - raise Exception("This is a retryable exception") diff --git a/tests/endtoend/retry_policy_functions/fixed_strategy/function_app.py b/tests/endtoend/retry_policy_functions/fixed_strategy/function_app.py deleted file mode 100644 index 1f5863f47..000000000 --- a/tests/endtoend/retry_policy_functions/fixed_strategy/function_app.py +++ /dev/null @@ -1,22 +0,0 @@ -import logging - -from azure.functions import AuthLevel, Context, FunctionApp, TimerRequest - -app = FunctionApp(http_auth_level=AuthLevel.ANONYMOUS) - - -@app.timer_trigger(schedule="*/1 * * * * *", arg_name="mytimer", - run_on_startup=False, - use_monitor=False) -@app.retry(strategy="fixed_delay", max_retry_count="3", - delay_interval="00:00:01") -def mytimer(mytimer: TimerRequest, context: Context) -> None: - logging.info(f'Current retry count: {context.retry_context.retry_count}') - - if context.retry_context.retry_count == \ - context.retry_context.max_retry_count: - logging.info( - f"Max retries of {context.retry_context.max_retry_count} for " - f"function {context.function_name} has been reached") - else: - raise Exception("This is a retryable exception") diff --git a/tests/endtoend/sql_functions/sql_functions_stein/function_app.py b/tests/endtoend/sql_functions/sql_functions_stein/function_app.py deleted file mode 100644 index 07c78c385..000000000 --- a/tests/endtoend/sql_functions/sql_functions_stein/function_app.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import json - -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.route(route="sql_input/{productid}") -@app.sql_input(arg_name="products", - command_text="SELECT * FROM Products WHERE ProductId = @ProductId", - command_type="Text", - parameters="@ProductId={productid}", - connection_string_setting="AzureWebJobsSqlConnectionString") -def sql_input(req: func.HttpRequest, products: func.SqlRowList) \ - -> func.HttpResponse: - rows = list(map(lambda r: json.loads(r.to_json()), products)) - - return func.HttpResponse( - json.dumps(rows), - status_code=200, - mimetype="application/json" - ) - - -@app.route(route="sql_input2/{productid}") -@app.sql_input(arg_name="products", - command_text="SELECT * FROM Products2 WHERE ProductId = @ProductId", - command_type="Text", - parameters="@ProductId={productid}", - connection_string_setting="AzureWebJobsSqlConnectionString") -def sql_input2(req: func.HttpRequest, products: func.SqlRowList) -> func.HttpResponse: - rows = list(map(lambda r: json.loads(r.to_json()), products)) - - return func.HttpResponse( - json.dumps(rows), - status_code=200, - mimetype="application/json" - ) - - -@app.route(route="sql_output") -@app.sql_output(arg_name="r", - command_text="[dbo].[Products]", - connection_string_setting="AzureWebJobsSqlConnectionString") -def sql_output(req: func.HttpRequest, r: func.Out[func.SqlRow]) -> func.HttpResponse: - body = json.loads(req.get_body()) - row = func.SqlRow.from_dict(body) - r.set(row) - - return func.HttpResponse( - body=req.get_body(), - status_code=201, - mimetype="application/json" - ) - - -@app.sql_trigger(arg_name="changes", - table_name="Products", - connection_string_setting="AzureWebJobsSqlConnectionString") -@app.sql_output(arg_name="r", - command_text="[dbo].[Products2]", - connection_string_setting="AzureWebJobsSqlConnectionString") -def sql_trigger(changes, r: func.Out[func.SqlRow]) -> str: - row = func.SqlRow.from_dict(json.loads(changes)[0]["Item"]) - r.set(row) - return "OK" diff --git a/tests/endtoend/sql_functions/sql_functions_stein/generic/function_app.py b/tests/endtoend/sql_functions/sql_functions_stein/generic/function_app.py deleted file mode 100644 index 2e796f8bb..000000000 --- a/tests/endtoend/sql_functions/sql_functions_stein/generic/function_app.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import json - -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.generic_trigger(arg_name="req", type="httpTrigger", route="sql_input/{productid}") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding(arg_name="products", type="sql", - command_text="SELECT * FROM Products " - "WHERE ProductId = @ProductId", - command_type="Text", - parameters="@ProductId={productid}", - connection_string_setting="AzureWebJobsSqlConnectionString") -def sql_input(req: func.HttpRequest, products: func.SqlRowList) -> func.HttpResponse: - rows = list(map(lambda r: json.loads(r.to_json()), products)) - - return func.HttpResponse( - json.dumps(rows), - status_code=200, - mimetype="application/json" - ) - - -@app.generic_trigger(arg_name="req", type="httpTrigger", route="sql_input2/{productid}") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_input_binding(arg_name="products", type="sql", - command_text="SELECT * FROM Products2 " - "WHERE ProductId = @ProductId", - command_type="Text", - parameters="@ProductId={productid}", - connection_string_setting="AzureWebJobsSqlConnectionString") -def sql_input2(req: func.HttpRequest, products: func.SqlRowList) -> func.HttpResponse: - rows = list(map(lambda r: json.loads(r.to_json()), products)) - - return func.HttpResponse( - json.dumps(rows), - status_code=200, - mimetype="application/json" - ) - - -@app.generic_trigger(arg_name="req", type="httpTrigger", route="sql_output") -@app.generic_output_binding(arg_name="$return", type="http") -@app.generic_output_binding(arg_name="r", type="sql", - command_text="[dbo].[Products]", - connection_string_setting="AzureWebJobs" - "SqlConnectionString") -def sql_output(req: func.HttpRequest, r: func.Out[func.SqlRow]) \ - -> func.HttpResponse: - body = json.loads(req.get_body()) - row = func.SqlRow.from_dict(body) - r.set(row) - - return func.HttpResponse( - body=req.get_body(), - status_code=201, - mimetype="application/json" - ) - - -@app.generic_trigger(arg_name="changes", type="sqlTrigger", - table_name="Products", - connection_string_setting="AzureWebJobsSqlConnectionString") -@app.generic_output_binding(arg_name="r", type="sql", - command_text="[dbo].[Products2]", - connection_string_setting="AzureWebJobsSqlConnectionString") -def sql_trigger(changes, r: func.Out[func.SqlRow]) -> str: - row = func.SqlRow.from_dict(json.loads(changes)[0]["Item"]) - r.set(row) - return "OK" diff --git a/tests/endtoend/sql_functions/sql_input/__init__.py b/tests/endtoend/sql_functions/sql_input/__init__.py deleted file mode 100644 index 03c622492..000000000 --- a/tests/endtoend/sql_functions/sql_input/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import json - -import azure.functions as func - - -def main(req: func.HttpRequest, products: func.SqlRowList) -> func.HttpResponse: - rows = list(map(lambda r: json.loads(r.to_json()), products)) - - return func.HttpResponse( - json.dumps(rows), - status_code=200, - mimetype="application/json" - ) diff --git a/tests/endtoend/sql_functions/sql_input/function.json b/tests/endtoend/sql_functions/sql_input/function.json deleted file mode 100644 index 38ec00f6f..000000000 --- a/tests/endtoend/sql_functions/sql_input/function.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "authLevel": "anonymous", - "name": "req", - "type": "httpTrigger", - "direction": "in", - "methods": [ - "get" - ], - "route": "sql_input/{productid}" - }, - { - "name": "$return", - "type": "http", - "direction": "out" - }, - { - "name": "products", - "type": "sql", - "direction": "in", - "commandText": "SELECT * FROM Products WHERE ProductId = @ProductId", - "commandType": "Text", - "parameters": "@ProductId={productid}", - "connectionStringSetting": "AzureWebJobsSqlConnectionString" - } - ] -} \ No newline at end of file diff --git a/tests/endtoend/sql_functions/sql_input2/__init__.py b/tests/endtoend/sql_functions/sql_input2/__init__.py deleted file mode 100644 index 03c622492..000000000 --- a/tests/endtoend/sql_functions/sql_input2/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import json - -import azure.functions as func - - -def main(req: func.HttpRequest, products: func.SqlRowList) -> func.HttpResponse: - rows = list(map(lambda r: json.loads(r.to_json()), products)) - - return func.HttpResponse( - json.dumps(rows), - status_code=200, - mimetype="application/json" - ) diff --git a/tests/endtoend/sql_functions/sql_input2/function.json b/tests/endtoend/sql_functions/sql_input2/function.json deleted file mode 100644 index 07f8d7722..000000000 --- a/tests/endtoend/sql_functions/sql_input2/function.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "authLevel": "anonymous", - "name": "req", - "type": "httpTrigger", - "direction": "in", - "methods": [ - "get" - ], - "route": "sql_input2/{productid}" - }, - { - "name": "$return", - "type": "http", - "direction": "out" - }, - { - "name": "products", - "type": "sql", - "direction": "in", - "commandText": "SELECT * FROM Products2 WHERE ProductId = @ProductId", - "commandType": "Text", - "parameters": "@ProductId={productid}", - "connectionStringSetting": "AzureWebJobsSqlConnectionString" - } - ] -} \ No newline at end of file diff --git a/tests/endtoend/sql_functions/sql_output/__init__.py b/tests/endtoend/sql_functions/sql_output/__init__.py deleted file mode 100644 index 42a21ff24..000000000 --- a/tests/endtoend/sql_functions/sql_output/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import json - -import azure.functions as func - - -def main(req: func.HttpRequest, r: func.Out[func.SqlRow]) -> func.HttpResponse: - body = json.loads(req.get_body()) - row = func.SqlRow.from_dict(body) - r.set(row) - - return func.HttpResponse( - body=req.get_body(), - status_code=201, - mimetype="application/json" - ) diff --git a/tests/endtoend/sql_functions/sql_output/function.json b/tests/endtoend/sql_functions/sql_output/function.json deleted file mode 100644 index 44ede8421..000000000 --- a/tests/endtoend/sql_functions/sql_output/function.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "authLevel": "anonymous", - "name": "req", - "type": "httpTrigger", - "direction": "in", - "methods": [ - "post" - ] - }, - { - "name": "$return", - "type": "http", - "direction": "out" - }, - { - "name": "r", - "type": "sql", - "direction": "out", - "commandText": "[dbo].[Products]", - "connectionStringSetting": "AzureWebJobsSqlConnectionString" - } - ] -} diff --git a/tests/endtoend/sql_functions/sql_trigger/__init__.py b/tests/endtoend/sql_functions/sql_trigger/__init__.py deleted file mode 100644 index 56115c75d..000000000 --- a/tests/endtoend/sql_functions/sql_trigger/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json - -import azure.functions as func - - -def main(changes, r: func.Out[func.SqlRow]) -> str: - row = func.SqlRow.from_dict(json.loads(changes)[0]["Item"]) - r.set(row) - return "OK" diff --git a/tests/endtoend/sql_functions/sql_trigger/function.json b/tests/endtoend/sql_functions/sql_trigger/function.json deleted file mode 100644 index db68da83d..000000000 --- a/tests/endtoend/sql_functions/sql_trigger/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "__init__.py", - "disabled": false, - "bindings": [ - { - "name": "changes", - "type": "sqlTrigger", - "direction": "in", - "tableName": "dbo.Products", - "connectionStringSetting": "AzureWebJobsSqlConnectionString" - }, - { - "name": "r", - "type": "sql", - "direction": "out", - "commandText": "[dbo].[Products2]", - "connectionStringSetting": "AzureWebJobsSqlConnectionString" - } - ] -} \ No newline at end of file diff --git a/tests/endtoend/test_blueprint_functions.py b/tests/endtoend/test_blueprint_functions.py deleted file mode 100644 index c421f583b..000000000 --- a/tests/endtoend/test_blueprint_functions.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from tests.utils import testutils - - -class TestFunctionInBluePrintOnly(testutils.WebHostTestCase): - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'blueprint_functions' / \ - 'functions_in_blueprint_only' - - def test_function_in_blueprint_only(self): - r = self.webhost.request('GET', 'default_template') - self.assertTrue(r.ok) - - -class TestFunctionsInBothBlueprintAndFuncApp(testutils.WebHostTestCase): - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'blueprint_functions' / \ - 'functions_in_both_blueprint_functionapp' - - def test_functions_in_both_blueprint_functionapp(self): - r = self.webhost.request('GET', 'default_template') - self.assertTrue(r.ok) - - r = self.webhost.request('GET', 'return_http') - self.assertTrue(r.ok) - - -class TestMultipleFunctionRegisters(testutils.WebHostTestCase): - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'blueprint_functions' / \ - 'multiple_function_registers' - - def test_function_in_blueprint_only(self): - r = self.webhost.request('GET', 'return_http') - self.assertEqual(r.status_code, 404) - - -class TestOnlyBlueprint(testutils.WebHostTestCase): - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'blueprint_functions' / \ - 'only_blueprint' - - def test_only_blueprint(self): - """Test if the default template of Http trigger in Python - Function app - will return OK - """ - r = self.webhost.request('GET', 'default_template') - self.assertEqual(r.status_code, 404) - - -class TestBlueprintDifferentDirectory(testutils.WebHostTestCase): - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'blueprint_functions' / \ - 'blueprint_different_dir' - - def test_blueprint_in_different_dir(self): - r = self.webhost.request('GET', 'default_template') - self.assertTrue(r.ok) - - r = self.webhost.request('GET', 'http_func') - self.assertTrue(r.ok) diff --git a/tests/endtoend/test_cosmosdb_functions.py b/tests/endtoend/test_cosmosdb_functions.py deleted file mode 100644 index 13b32c9cf..000000000 --- a/tests/endtoend/test_cosmosdb_functions.py +++ /dev/null @@ -1,102 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json -import time - -from tests.utils import testutils - - -class TestCosmosDBFunctions(testutils.WebHostTestCase): - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'cosmosdb_functions' - - @testutils.retryable_test(3, 5) - def test_cosmosdb_trigger(self): - time.sleep(5) - data = str(round(time.time())) - doc = {'id': 'cosmosdb-trigger-test', - 'data': data} - r = self.webhost.request('POST', 'put_document', - data=json.dumps(doc)) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - max_retries = 10 - - for try_no in range(max_retries): - # Allow trigger to fire - time.sleep(2) - - try: - # Check that the trigger has fired - r = self.webhost.request('GET', 'get_cosmosdb_triggered') - self.assertEqual(r.status_code, 200) - response = r.json() - response.pop('_metadata', None) - - self.assertEqual(response['id'], doc['id']) - self.assertEqual(response['data'], doc['data']) - self.assertTrue('_etag' in response) - self.assertTrue('_lsn' in response) - self.assertTrue('_rid' in response) - self.assertTrue('_self' in response) - self.assertTrue('_ts' in response) - except AssertionError: - if try_no == max_retries - 1: - raise - else: - break - - def test_cosmosdb_input(self): - time.sleep(5) - data = str(round(time.time())) - doc = {'id': 'cosmosdb-input-test', - 'data': data} - r = self.webhost.request('POST', 'put_document', - data=json.dumps(doc)) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - max_retries = 10 - - for try_no in range(max_retries): - # Allow trigger to fire - time.sleep(2) - - try: - # Check that the trigger has fired - r = self.webhost.request('GET', 'cosmosdb_input') - self.assertEqual(r.status_code, 200) - response = r.json() - - # _lsn is present for cosmosdb change feed only, - # ref https://aka.ms/cosmos-change-feed - self.assertEqual(response['id'], doc['id']) - self.assertEqual(response['data'], doc['data']) - self.assertTrue('_etag' in response) - self.assertTrue('_rid' in response) - self.assertTrue('_self' in response) - self.assertTrue('_ts' in response) - except AssertionError: - if try_no == max_retries - 1: - raise - else: - break - - -class TestCosmosDBFunctionsStein(TestCosmosDBFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'cosmosdb_functions' / \ - 'cosmosdb_functions_stein' - - -class TestCosmosDBFunctionsSteinGeneric(TestCosmosDBFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'cosmosdb_functions' / \ - 'cosmosdb_functions_stein' / 'generic' diff --git a/tests/endtoend/test_dependency_isolation_functions.py b/tests/endtoend/test_dependency_isolation_functions.py deleted file mode 100644 index 7ada0ae69..000000000 --- a/tests/endtoend/test_dependency_isolation_functions.py +++ /dev/null @@ -1,235 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import importlib.util -import os -from unittest import skip -from unittest.case import skipIf -from unittest.mock import patch - -from requests import Response -from tests.utils import testutils -from tests.utils.constants import ( - CONSUMPTION_DOCKER_TEST, - DEDICATED_DOCKER_TEST, - PYAZURE_INTEGRATION_TEST, -) - -from azure_functions_worker.utils.common import is_envvar_true - -REQUEST_TIMEOUT_SEC = 5 - - -@skipIf(is_envvar_true(DEDICATED_DOCKER_TEST) - or is_envvar_true(CONSUMPTION_DOCKER_TEST), - 'Docker tests do not work with dependency isolation ') -class TestGRPCandProtobufDependencyIsolationOnDedicated( - testutils.WebHostTestCase): - """Test the dependency manager E2E scenario via Http Trigger. - - The following E2E tests ensures the dependency manager is behaving as - expected. They are tested against the dependency_isolation_functions/ - folder which contain a dummy .python_packages_grpc_protobuf folder. - This testcase checks if the customers library version of grpc and protobuf - are being loaded in the functionapp - """ - function_name = 'dependency_isolation_functions' - package_name = '.python_packages_grpc_protobuf' - project_root = testutils.E2E_TESTS_ROOT / function_name - customer_deps = project_root / package_name / 'lib' / 'site-packages' - - @classmethod - def setUpClass(cls): - # Turn on feature flag - cls.env_variables['PYTHON_ISOLATE_WORKER_DEPENDENCIES'] = '1' - - # Emulate Python worker in Azure environment. - # For how the PYTHONPATH is set in Azure, check prodV4/worker.py. - cls.env_variables['PYTHONPATH'] = str(cls.customer_deps) - - os_environ = os.environ.copy() - os_environ.update(cls.env_variables) - - cls._patch_environ = patch.dict('os.environ', os_environ) - cls._patch_environ.start() - super().setUpClass() - - @classmethod - def tearDownClass(self): - super().tearDownClass() - self._patch_environ.stop() - - @classmethod - def get_script_dir(cls): - return cls.project_root - - @classmethod - def get_environment_variables(cls): - return cls.env_variables - - def test_dependency_function_should_return_ok(self): - """The common scenario of general import should return OK in any - circumstances - """ - r: Response = self.webhost.request('GET', 'report_dependencies') - self.assertTrue(r.ok) - - def test_feature_flag_is_turned_on(self): - """Since passing the feature flag PYTHON_ISOLATE_WORKER_DEPENDENCIES to - the host, the customer's function should also be able to receive it - """ - r: Response = self.webhost.request('GET', 'report_dependencies') - environments = r.json()['environments'] - flag_value = environments['PYTHON_ISOLATE_WORKER_DEPENDENCIES'] - self.assertEqual(flag_value, '1') - - def test_working_directory_resolution(self): - """Check from the dependency manager and see if the current working - directory is resolved correctly - """ - r: Response = self.webhost.request('GET', 'report_dependencies') - environments = r.json()['environments'] - - dir = os.path.dirname(__file__) - self.assertEqual( - environments['AzureWebJobsScriptRoot'].lower(), - os.path.join(dir, 'dependency_isolation_functions').lower() - ) - - @skipIf(is_envvar_true(PYAZURE_INTEGRATION_TEST), - 'Integration test expects dependencies derived from core ' - 'tools folder') - def test_paths_resolution(self): - """Dependency manager requires paths to be resolved correctly before - switching to customer's modules. This test is to ensure when the app - is in ready state, check if the paths are in good state. - """ - r: Response = self.webhost.request('GET', 'report_dependencies') - dm = r.json()['dependency_manager'] - self.assertEqual( - dm['cx_working_dir'].lower(), str(self.project_root).lower() - ) - self.assertEqual( - dm['cx_deps_path'].lower(), str(self.customer_deps).lower() - ) - - # Should derive the package location from the built-in azure.functions - azf_spec = importlib.util.find_spec('azure.functions') - self.assertEqual( - dm['worker_deps_path'].lower(), - os.path.abspath( - os.path.join(os.path.dirname(azf_spec.origin), '..', '..') - ).lower() - ) - - @skipIf(is_envvar_true('skipTest'), - 'Running tests using an editable azure-functions package.') - def test_loading_libraries_from_customers_package(self): - """Since the Python now loaded the customer's dependencies, the - libraries version should match the ones in - .python_packages_grpc_protobuf/ folder - """ - r: Response = self.webhost.request('GET', 'report_dependencies') - libraries = r.json()['libraries'] - self.assertEqual( - libraries['proto.expected.version'], libraries['proto.version'] - ) - - self.assertEqual( - libraries['grpc.expected.version'], libraries['grpc.version'] - ) - - -@skip("Skipping dependency isolation test for dedicated. Needs investigation") -class TestOlderVersionOfAzFuncDependencyIsolationOnDedicated( - testutils.WebHostTestCase): - """Test the dependency manager E2E scenario via Http Trigger. - - The following E2E tests ensures the dependency manager is behaving as - expected. They are tested against the dependency_isolation_functions/ - folder which contain a dummy .python_packages_azf_older_version folder. - This testcase checks if the customers older library version of azure - functions is being loaded in the functionapp - """ - - function_name = 'dependency_isolation_functions' - package_name = '.python_packages_azf_older_version' - project_root = testutils.E2E_TESTS_ROOT / function_name - customer_deps = project_root / package_name / 'lib' / 'site-packages' - expected_azfunc_version = '1.5.0' - - @classmethod - def setUpClass(cls): - os_environ = os.environ.copy() - # Turn on feature flag - os_environ['PYTHON_ISOLATE_WORKER_DEPENDENCIES'] = '1' - # Emulate Python worker in Azure environment. - # For how the PYTHONPATH is set in Azure, check prodV3/worker.py. - os_environ['PYTHONPATH'] = str(cls.customer_deps) - - cls._patch_environ = patch.dict('os.environ', os_environ) - cls._patch_environ.start() - super().setUpClass() - - @classmethod - def tearDownClass(self): - super().tearDownClass() - self._patch_environ.stop() - - @classmethod - def get_script_dir(cls): - return cls.project_root - - def test_loading_libraries_from_customers_package(self): - r: Response = self.webhost.request('GET', 'report_dependencies') - libraries = r.json()['libraries'] - - self.assertEqual( - self.expected_azfunc_version, libraries['func.version']) - - -@skip("Skipping dependency isolation test for dedicated. Needs investigation") -class TestNewerVersionOfAzFuncDependencyIsolationOnDedicated( - testutils.WebHostTestCase): - """Test the dependency manager E2E scenario via Http Trigger. - - The following E2E tests ensures the dependency manager is behaving as - expected. They are tested against the dependency_isolation_functions/ - folder which contain a dummy .python_packages_azf_newer_version folder. - This testcase checks if the customers newer library version of azure - functions is being loaded in the functionapp - """ - - function_name = 'dependency_isolation_functions' - package_name = '.python_packages_azf_newer_version' - project_root = testutils.E2E_TESTS_ROOT / function_name - customer_deps = project_root / package_name / 'lib' / 'site-packages' - expected_azfunc_version = '9.9.9' - - @classmethod - def setUpClass(cls): - os_environ = os.environ.copy() - # Turn on feature flag - os_environ['PYTHON_ISOLATE_WORKER_DEPENDENCIES'] = '1' - # Emulate Python worker in Azure environment. - # For how the PYTHONPATH is set in Azure, check prodV3/worker.py. - os_environ['PYTHONPATH'] = str(cls.customer_deps) - - cls._patch_environ = patch.dict('os.environ', os_environ) - cls._patch_environ.start() - super().setUpClass() - - @classmethod - def tearDownClass(self): - super().tearDownClass() - self._patch_environ.stop() - - @classmethod - def get_script_dir(cls): - return cls.project_root - - def test_loading_libraries_from_customers_package(self): - r: Response = self.webhost.request('GET', 'report_dependencies') - libraries = r.json()['libraries'] - - self.assertEqual( - self.expected_azfunc_version, libraries['func.version']) diff --git a/tests/endtoend/test_durable_functions.py b/tests/endtoend/test_durable_functions.py deleted file mode 100644 index 8cab19b6f..000000000 --- a/tests/endtoend/test_durable_functions.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json -import os -import time -from unittest import skipIf - -import requests -from tests.utils import testutils -from tests.utils.constants import CONSUMPTION_DOCKER_TEST, DEDICATED_DOCKER_TEST - -from azure_functions_worker.utils.common import is_envvar_true - - -@skipIf(is_envvar_true(DEDICATED_DOCKER_TEST) - or is_envvar_true(CONSUMPTION_DOCKER_TEST), - "Docker tests cannot retrieve port needed for a webhook") -class TestDurableFunctions(testutils.WebHostTestCase): - - @classmethod - def setUpClass(cls): - os.environ["WEBSITE_HOSTNAME"] = "http:" - super().setUpClass() - - @classmethod - def tearDownClass(cls): - # Remove the WEBSITE_HOSTNAME environment variable - os.environ.pop('WEBSITE_HOSTNAME') - super().tearDownClass() - - @classmethod - def get_environment_variables(cls): - return cls.env_variables - - @classmethod - def get_libraries_to_install(cls): - return ['azure-functions-durable'] - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'durable_functions' - - @testutils.retryable_test(3, 5) - def test_durable(self): - r = self.webhost.request('GET', - 'orchestrators/DurableFunctionsOrchestrator') - time.sleep(4) # wait for the activity to complete - self.assertEqual(r.status_code, 202) - content = json.loads(r.content) - - status = requests.get(content['statusQueryGetUri']) - self.assertEqual(status.status_code, 200) - - status_content = json.loads(status.content) - self.assertEqual(status_content['runtimeStatus'], 'Completed') - self.assertEqual(status_content['output'], - ['Hello Tokyo!', 'Hello Seattle!', 'Hello London!']) - - -class TestDurableFunctionsStein(TestDurableFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'durable_functions' / \ - 'durable_functions_stein' diff --git a/tests/endtoend/test_eventgrid_functions.py b/tests/endtoend/test_eventgrid_functions.py deleted file mode 100644 index 7a878ca32..000000000 --- a/tests/endtoend/test_eventgrid_functions.py +++ /dev/null @@ -1,171 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import time -import unittest -import uuid - -import requests -from tests.utils import testutils - - -class TestEventGridFunctions(testutils.WebHostTestCase): - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'eventgrid_functions' - - def eventgrid_webhook_request(self, meth, funcname, *args, **kwargs): - request_method = getattr(requests, meth.lower()) - url = f'{self.webhost._addr}/runtime/webhooks/eventgrid' - params = dict(kwargs.pop('params', {})) - params['functionName'] = funcname - if 'code' not in params: - params['code'] = 'testSystemKey' - headers = dict(kwargs.pop('headers', {})) - headers['aeg-event-type'] = 'Notification' - return request_method(url, *args, params=params, headers=headers, - **kwargs) - - @unittest.skip("Run locally. Running on Azure fails with 401/403 as the" - "host does not pick up the SecretKey from the" - "azure_functions_worker.testutils.py.SECRETS_TEMPLATE and" - "because of which we cannot test eventGrid webhook" - "invocation correctly.") - def test_eventgrid_trigger(self): - """test event_grid trigger - - This test calls the eventgrid_trigger function, sends in `data` as body - to the webhook for eventgrid. Once the event is received, the function - writes the data to the blob store. - - Then get_eventgrid_triggered gets called (httpTrigger) and takes blob - input binding, reading the previously written text in blob store - `python-worker-tests/test-eventgrid-triggered.txt`, and then we validate - that the written text matches the one passed to the eventgrid trigger. - """ - data = [{ - "topic": "test-topic", - "subject": "test-subject", - "eventType": "Microsoft.Storage.BlobCreated", - "eventTime": "2018-01-01T00:00:00.000000123Z", - "id": str(uuid.uuid4()), - "data": { - "api": "PutBlockList", - "clientRequestId": "2c169f2f-7b3b-4d99-839b-c92a2d25801b", - "requestId": "44d4f022-001e-003c-466b-940cba000000", - "eTag": "0x8D562831044DDD0", - "contentType": "application/octet-stream", - "contentLength": 2248, - "blobType": "BlockBlob", - "ur1": "foo", - "sequencer": "000000000000272D000000000003D60F", - "storageDiagnostics": { - "batchId": "b4229b3a-4d50-4ff4-a9f2-039ccf26efe9" - } - }, - "dataVersion": "", - "metadataVersion": "1" - }] - - r = self.eventgrid_webhook_request('POST', 'eventgrid_trigger', - json=data) - self.assertEqual(r.status_code, 202) - - max_retries = 10 - - for try_no in range(max_retries): - # Allow trigger to fire. - time.sleep(2) - - try: - # Check that the trigger has fired. - r = self.webhost.request('GET', 'get_eventgrid_triggered') - self.assertEqual(r.status_code, 200) - - response = r.json() - self.assertLessEqual(response.items(), data[0].items()) - except AssertionError: - if try_no == max_retries - 1: - raise - else: - break - - def test_eventgrid_output_binding(self): - """test event_grid output binding - - This test needs three functions to work. - 1. `eventgrid_output_binding` - 2. `eventgrid_output_binding_message_to_blobstore` - 3. `eventgrid_output_binding_success` - - This test calls the eventgrid_output_binding function, sends in a unique - uuid as `data` in the body to the httpTrigger which sends in that value - in the eventGrid output data. The eventGrid topic is configured to - send the event to a storage queue. - - The second function (`eventgrid_output_binding_message_to_blobstore`) - reads from that storage queue and puts into a blob store. - - The third function (`eventgrid_output_binding_success`) reads the - text from the blob store and compares with the expected result. The - unique uuid should confirm if the message went through correctly to - EventGrid and came back as a blob. - """ - - test_uuid = uuid.uuid4().__str__() - expected_response = "Sent event with subject: {}, id: {}, data: {}, " \ - "event_type: {} to EventGrid!".format( - "test-subject", "test-id", - f"{{'test_uuid': '{test_uuid}'}}", - "test-event-1") - expected_final_data = { - 'id': 'test-id', 'subject': 'test-subject', 'dataVersion': '1.0', - 'eventType': 'test-event-1', - 'data': {'test_uuid': test_uuid} - } - - r = self.webhost.request('GET', 'eventgrid_output_binding', - params={'test_uuid': test_uuid}) - self.assertEqual(r.status_code, 200) - response = r.text - - self.assertEqual(expected_response, response) - - max_retries = 10 - for try_no in range(max_retries): - # Allow trigger to fire. - time.sleep(2) - - try: - # Check that the trigger has fired. - r = self.webhost.request('GET', - 'eventgrid_output_binding_success') - self.assertEqual(r.status_code, 200) - response = r.json() - - # list of fields to check are limited as other fields contain - # datetime or other uncertain values - for f in ['data', 'id', 'eventType', 'subject', 'dataVersion']: - self.assertEqual(response[f], expected_final_data[f]) - - except AssertionError: - if try_no == max_retries - 1: - raise - else: - break - - -class TestEventGridFunctionsStein(TestEventGridFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'eventgrid_functions' / \ - 'eventgrid_functions_stein' - - -class TestEventGridFunctionsGeneric(TestEventGridFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'eventgrid_functions' / \ - 'eventgrid_functions_stein' / 'generic' diff --git a/tests/endtoend/test_file_name_functions.py b/tests/endtoend/test_file_name_functions.py deleted file mode 100644 index faf3b722f..000000000 --- a/tests/endtoend/test_file_name_functions.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import os - -import requests -from tests.utils import testutils - -from azure_functions_worker.constants import PYTHON_SCRIPT_FILE_NAME - -REQUEST_TIMEOUT_SEC = 10 - - -class TestHttpFunctionsFileName(testutils.WebHostTestCase): - """Test the native Http Trigger in the local webhost. - - This test class will spawn a webhost from your /build/webhost - folder and replace the built-in Python with azure_functions_worker from - your code base. Since the Http Trigger is a native suport from host, we - don't need to setup any external resources. - - Compared to the unittests/test_http_functions.py, this file is more focus - on testing the E2E flow scenarios. - """ - - @classmethod - def setUpClass(cls): - os.environ["PYTHON_SCRIPT_FILE_NAME"] = "main.py" - cls.env_variables['PYTHON_SCRIPT_FILE_NAME'] = 'main.py' - super().setUpClass() - - @classmethod - def tearDownClass(cls): - # Remove the PYTHON_SCRIPT_FILE_NAME environment variable - os.environ.pop('PYTHON_SCRIPT_FILE_NAME') - super().tearDownClass() - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'http_functions' / \ - 'http_functions_stein' / \ - 'file_name' - - @classmethod - def get_environment_variables(cls): - return cls.env_variables - - @testutils.retryable_test(3, 5) - def test_index_page_should_return_ok(self): - """The index page of Azure Functions should return OK in any - circumstances - """ - r = self.webhost.request('GET', '', no_prefix=True, - timeout=REQUEST_TIMEOUT_SEC) - self.assertTrue(r.ok) - - @testutils.retryable_test(3, 5) - def test_default_http_template_should_return_ok(self): - """Test if the default template of Http trigger in Python Function app - will return OK - """ - r = self.webhost.request('GET', 'default_template', - timeout=REQUEST_TIMEOUT_SEC) - self.assertTrue(r.ok) - - @testutils.retryable_test(3, 5) - def test_default_http_template_should_accept_query_param(self): - """Test if the azure.functions SDK is able to deserialize query - parameter from the default template - """ - r = self.webhost.request('GET', 'default_template', - params={'name': 'query'}, - timeout=REQUEST_TIMEOUT_SEC) - self.assertTrue(r.ok) - self.assertEqual( - r.content, - b'Hello, query. This HTTP triggered function executed successfully.' - ) - - @testutils.retryable_test(3, 5) - def test_default_http_template_should_accept_body(self): - """Test if the azure.functions SDK is able to deserialize http body - and pass it to default template - """ - r = self.webhost.request('POST', 'default_template', - data='{ "name": "body" }'.encode('utf-8'), - timeout=REQUEST_TIMEOUT_SEC) - self.assertTrue(r.ok) - self.assertEqual( - r.content, - b'Hello, body. This HTTP triggered function executed successfully.' - ) - - @testutils.retryable_test(3, 5) - def test_worker_status_endpoint_should_return_ok(self): - """Test if the worker status endpoint will trigger - _handle__worker_status_request and sends a worker status response back - to host - """ - root_url = self.webhost._addr - health_check_url = f'{root_url}/admin/host/ping' - r = requests.post(health_check_url, - params={'checkHealth': '1'}, - timeout=REQUEST_TIMEOUT_SEC) - self.assertTrue(r.ok) - - @testutils.retryable_test(3, 5) - def test_worker_status_endpoint_should_return_ok_when_disabled(self): - """Test if the worker status endpoint will trigger - _handle__worker_status_request and sends a worker status response back - to host - """ - os.environ['WEBSITE_PING_METRICS_SCALE_ENABLED'] = '0' - root_url = self.webhost._addr - health_check_url = f'{root_url}/admin/host/ping' - r = requests.post(health_check_url, - params={'checkHealth': '1'}, - timeout=REQUEST_TIMEOUT_SEC) - self.assertTrue(r.ok) - - def test_correct_file_name(self): - self.assertIsNotNone(os.environ.get(PYTHON_SCRIPT_FILE_NAME)) - self.assertEqual(os.environ.get(PYTHON_SCRIPT_FILE_NAME), - 'main.py') diff --git a/tests/endtoend/test_http_functions.py b/tests/endtoend/test_http_functions.py deleted file mode 100644 index 3128dfd38..000000000 --- a/tests/endtoend/test_http_functions.py +++ /dev/null @@ -1,289 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import os -import typing -from unittest.mock import patch - -import requests -from tests.utils import testutils - -from azure_functions_worker.constants import PYTHON_ENABLE_INIT_INDEXING - -REQUEST_TIMEOUT_SEC = 5 - - -class TestHttpFunctions(testutils.WebHostTestCase): - """Test the native Http Trigger in the local webhost. - - This test class will spawn a webhost from your /build/webhost - folder and replace the built-in Python with azure_functions_worker from - your code base. Since the Http Trigger is a native suport from host, we - don't need to setup any external resources. - - Compared to the unittests/test_http_functions.py, this file is more focus - on testing the E2E flow scenarios. - """ - - def setUp(self): - self._patch_environ = patch.dict('os.environ', os.environ.copy()) - self._patch_environ.start() - super().setUp() - - def tearDown(self): - super().tearDown() - self._patch_environ.stop() - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'http_functions' - - @testutils.retryable_test(3, 5) - def test_function_index_page_should_return_ok(self): - """The index page of Azure Functions should return OK in any - circumstances - """ - r = self.webhost.request('GET', '', no_prefix=True, - timeout=REQUEST_TIMEOUT_SEC) - self.assertTrue(r.ok) - - @testutils.retryable_test(3, 5) - def test_default_http_template_should_return_ok(self): - """Test if the default template of Http trigger in Python Function app - will return OK - """ - r = self.webhost.request('GET', 'default_template', - timeout=REQUEST_TIMEOUT_SEC) - self.assertTrue(r.ok) - - @testutils.retryable_test(3, 5) - def test_default_http_template_should_accept_query_param(self): - """Test if the azure.functions SDK is able to deserialize query - parameter from the default template - """ - r = self.webhost.request('GET', 'default_template', - params={'name': 'query'}, - timeout=REQUEST_TIMEOUT_SEC) - self.assertTrue(r.ok) - self.assertEqual( - r.content, - b'Hello, query. This HTTP triggered function executed successfully.' - ) - - @testutils.retryable_test(3, 5) - def test_default_http_template_should_accept_body(self): - """Test if the azure.functions SDK is able to deserialize http body - and pass it to default template - """ - r = self.webhost.request('POST', 'default_template', - data='{ "name": "body" }'.encode('utf-8'), - timeout=REQUEST_TIMEOUT_SEC) - self.assertTrue(r.ok) - self.assertEqual( - r.content, - b'Hello, body. This HTTP triggered function executed successfully.' - ) - - @testutils.retryable_test(3, 5) - def test_worker_status_endpoint_should_return_ok(self): - """Test if the worker status endpoint will trigger - _handle__worker_status_request and sends a worker status response back - to host - """ - root_url = self.webhost._addr - health_check_url = f'{root_url}/admin/host/ping' - r = requests.post(health_check_url, - params={'checkHealth': '1'}, - timeout=REQUEST_TIMEOUT_SEC) - self.assertTrue(r.ok) - - @testutils.retryable_test(3, 5) - def test_worker_status_endpoint_should_return_ok_when_disabled(self): - """Test if the worker status endpoint will trigger - _handle__worker_status_request and sends a worker status response back - to host - """ - os.environ['WEBSITE_PING_METRICS_SCALE_ENABLED'] = '0' - root_url = self.webhost._addr - health_check_url = f'{root_url}/admin/host/ping' - r = requests.post(health_check_url, - params={'checkHealth': '1'}, - timeout=REQUEST_TIMEOUT_SEC) - self.assertTrue(r.ok) - - -class TestHttpFunctionsStein(TestHttpFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'http_functions' / \ - 'http_functions_stein' - - -class TestHttpFunctionsSteinGeneric(TestHttpFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'http_functions' / \ - 'http_functions_stein' / \ - 'generic' - - -class TestCommonLibsHttpFunctions(testutils.WebHostTestCase): - """Test the common libs scenarios in the local webhost. - - This test class will spawn a webhost from your /build/webhost - folder and replace the built-in Python with azure_functions_worker from - your code base. this file is more focus on testing the E2E flow scenarios. - """ - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'http_functions' / \ - 'common_libs_functions' - - @classmethod - def get_libraries_to_install(cls): - return ['requests', 'python-dotenv', "plotly", "scikit-learn", - "opencv-python", "pandas", "numpy"] - - @testutils.retryable_test(3, 5) - def test_numpy(self): - r = self.webhost.request('GET', 'numpy_func', - timeout=REQUEST_TIMEOUT_SEC) - - self.assertIn("numpy version", r.content.decode("UTF-8")) - - def test_requests(self): - r = self.webhost.request('GET', 'requests_func', - timeout=10) - - self.assertTrue(r.ok) - self.assertEqual(r.content.decode("UTF-8"), 'req status code: 200') - - def test_pandas(self): - r = self.webhost.request('GET', 'pandas_func', - timeout=REQUEST_TIMEOUT_SEC) - - self.assertIn("two-dimensional", - r.content.decode("UTF-8")) - - def test_sklearn(self): - r = self.webhost.request('GET', 'sklearn_func', - timeout=REQUEST_TIMEOUT_SEC) - - self.assertIn("First 5 records of array:", - r.content.decode("UTF-8")) - - def test_opencv(self): - r = self.webhost.request('GET', 'opencv_func', - timeout=REQUEST_TIMEOUT_SEC) - - self.assertIn("opencv version:", - r.content.decode("UTF-8")) - - def test_dotenv(self): - r = self.webhost.request('GET', 'dotenv_func', - timeout=REQUEST_TIMEOUT_SEC) - - self.assertEqual(r.content.decode("UTF-8"), "found") - - def test_plotly(self): - r = self.webhost.request('GET', 'plotly_func', - timeout=REQUEST_TIMEOUT_SEC) - - self.assertIn("plotly version:", - r.content.decode("UTF-8")) - - -class TestCommonLibsHttpFunctionsStein(TestCommonLibsHttpFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'http_functions' / \ - 'common_libs_functions' / \ - 'common_libs_functions_stein' - - -class TestHttpFunctionsWithInitIndexing(TestHttpFunctions): - - @classmethod - def setUpClass(cls): - cls.env_variables[PYTHON_ENABLE_INIT_INDEXING] = '1' - os.environ[PYTHON_ENABLE_INIT_INDEXING] = "1" - super().setUpClass() - - @classmethod - def tearDownClass(cls): - os.environ.pop(PYTHON_ENABLE_INIT_INDEXING) - super().tearDownClass() - - @classmethod - def get_environment_variables(cls): - return cls.env_variables - - -class TestUserThreadLoggingHttpFunctions(testutils.WebHostTestCase): - """Test the Http trigger that contains logging with user threads. - - This test class will spawn a webhost from your /build/webhost - folder and replace the built-in Python with azure_functions_worker from - your code base. this file is more focus on testing the E2E flow scenarios. - """ - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'http_functions' / \ - 'user_thread_logging' - - @testutils.retryable_test(3, 5) - def test_http_thread(self): - r = self.webhost.request('GET', 'thread', - timeout=REQUEST_TIMEOUT_SEC) - - self.assertTrue(r.ok) - - def check_log_http_thread(self, host_out: typing.List[str]): - self.assertEqual(host_out.count('Before threads.'), 1) - self.assertEqual(host_out.count('Thread1 used.'), 1) - self.assertEqual(host_out.count('Thread2 used.'), 1) - self.assertEqual(host_out.count('Thread3 used.'), 1) - self.assertEqual(host_out.count('After threads.'), 1) - - @testutils.retryable_test(3, 5) - def test_http_async_thread(self): - r = self.webhost.request('GET', 'async_thread', - timeout=REQUEST_TIMEOUT_SEC) - - self.assertTrue(r.ok) - - def check_log_http_async_thread(self, host_out: typing.List[str]): - self.assertEqual(host_out.count('Before threads.'), 1) - self.assertEqual(host_out.count('Thread1 used.'), 1) - self.assertEqual(host_out.count('Thread2 used.'), 1) - self.assertEqual(host_out.count('Thread3 used.'), 1) - self.assertEqual(host_out.count('After threads.'), 1) - - @testutils.retryable_test(3, 5) - def test_http_thread_pool_executor(self): - r = self.webhost.request('GET', 'thread_pool_executor', - timeout=REQUEST_TIMEOUT_SEC) - - self.assertTrue(r.ok) - - def check_log_http_thread_pool_executor(self, host_out: typing.List[str]): - self.assertEqual(host_out.count('Before TPE.'), 1) - self.assertEqual(host_out.count('Using TPE.'), 1) - self.assertEqual(host_out.count('After TPE.'), 1) - - @testutils.retryable_test(3, 5) - def test_http_async_thread_pool_executor(self): - r = self.webhost.request('GET', 'async_thread_pool_executor', - timeout=REQUEST_TIMEOUT_SEC) - - self.assertTrue(r.ok) - - def check_log_http_async_thread_pool_executor(self, - host_out: typing.List[str]): - self.assertEqual(host_out.count('Before TPE.'), 1) - self.assertEqual(host_out.count('Using TPE.'), 1) - self.assertEqual(host_out.count('After TPE.'), 1) diff --git a/tests/endtoend/test_retry_policy_functions.py b/tests/endtoend/test_retry_policy_functions.py deleted file mode 100644 index 58851f353..000000000 --- a/tests/endtoend/test_retry_policy_functions.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import time -import typing - -from tests.utils import testutils - - -class TestFixedRetryPolicyFunctions(testutils.WebHostTestCase): - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'retry_policy_functions' / \ - 'fixed_strategy' - - def test_fixed_retry_policy(self): - # Checking webhost status. - time.sleep(5) - r = self.webhost.request('GET', '', no_prefix=True) - self.assertTrue(r.ok) - - def check_log_fixed_retry_policy(self, host_out: typing.List[str]): - self.assertIn('Current retry count: 0', host_out) - self.assertIn('Current retry count: 1', host_out) - self.assertIn("Max retries of 3 for function mytimer" - " has been reached", host_out) - - -class TestExponentialRetryPolicyFunctions(testutils.WebHostTestCase): - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'retry_policy_functions' / \ - 'exponential_strategy' - - def test_retry_policy(self): - # Checking webhost status. - r = self.webhost.request('GET', '', no_prefix=True, - timeout=5) - time.sleep(5) - self.assertTrue(r.ok) - - def check_log_retry_policy(self, host_out: typing.List[str]): - self.assertIn('Current retry count: 1', host_out) - self.assertIn('Current retry count: 2', host_out) - self.assertIn('Current retry count: 3', host_out) - self.assertIn("Max retries of 3 for function mytimer" - " has been reached", host_out) diff --git a/tests/endtoend/test_sql_functions.py b/tests/endtoend/test_sql_functions.py deleted file mode 100644 index 842f0a27a..000000000 --- a/tests/endtoend/test_sql_functions.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json -import time - -from tests.utils import testutils - - -class TestSqlFunctions(testutils.WebHostTestCase): - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'sql_functions' - - @testutils.retryable_test(3, 5) - def test_sql_binding_trigger(self): - id = str(round(time.time())) - row = {"ProductId": id, "Name": "test", "Cost": 100} - # Insert a row into Products table using sql_output function - r = self.webhost.request('POST', 'sql_output', - data=json.dumps(row)) - self.assertEqual(r.status_code, 201) - - # Check that the row was successfully inserted using sql_input function - r = self.webhost.request('GET', 'sql_input/' + id) - self.assertEqual(r.status_code, 200) - expectedText = "[{\"ProductId\": " + id + \ - ", \"Name\": \"test\", \"Cost\": 100}]" - self.assertEqual(r.text, expectedText) - - # Check that the sql_trigger function has been triggered and - # the row has been inserted into Products2 table using sql_input2 - # function - max_retries = 10 - - for try_no in range(max_retries): - # Allow trigger to fire - time.sleep(2) - - try: - # Check that the trigger has fired - r = self.webhost.request('GET', 'sql_input2/' + id) - self.assertEqual(r.status_code, 200) - expectedText = "[{\"ProductId\": " + id + \ - ", \"Name\": \"test\", \"Cost\": 100}]" - self.assertEqual(r.text, expectedText) - - except AssertionError: - if try_no == max_retries - 1: - raise - else: - break - - -class TestSqlFunctionsStein(TestSqlFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'sql_functions' / \ - 'sql_functions_stein' - - -class TestSqlFunctionsSteinGeneric(TestSqlFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'sql_functions' / \ - 'sql_functions_stein' / 'generic' diff --git a/tests/endtoend/test_third_party_http_functions.py b/tests/endtoend/test_third_party_http_functions.py deleted file mode 100644 index 5fa01a2bd..000000000 --- a/tests/endtoend/test_third_party_http_functions.py +++ /dev/null @@ -1,162 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import os - -import requests -from tests.utils import testutils as utils -from tests.utils.testutils import E2E_TESTS_ROOT - -HOST_JSON_TEMPLATE = """\ -{ - "version": "2.0", - "logging": { - "logLevel": { - "default": "Trace" - } - }, - "extensions": { - "http": { - "routePrefix": "" - } - }, - "functionTimeout": "00:05:00" -} -""" - - -class ThirdPartyHttpFunctionsTestBase: - """Base test class containing common asgi/wsgi testcases, only testcases - in classes extending TestThirdPartyHttpFunctions will by run""" - - class TestThirdPartyHttpFunctions(utils.WebHostTestCase): - @classmethod - def setUpClass(cls): - host_json = cls.get_script_dir() / 'host.json' - with open(host_json, 'w+') as f: - f.write(HOST_JSON_TEMPLATE) - super().setUpClass() - - @classmethod - def tearDownClass(cls): - super().tearDownClass() - - @classmethod - def get_script_dir(cls): - pass - - @classmethod - def get_libraries_to_install(cls): - libraries_required = ["flask", "fastapi"] - return libraries_required - - @utils.retryable_test(3, 5) - def test_function_index_page_should_return_undefined(self): - root_url = self.webhost._addr - r = requests.get(root_url) - self.assertEqual(r.status_code, 404) - - @utils.retryable_test(3, 5) - def test_get_endpoint_should_return_ok(self): - """Test if the default template of Http trigger in Python - Function app - will return OK - """ - r = self.webhost.request('GET', 'get_query_param', no_prefix=True) - self.assertTrue(r.ok) - self.assertEqual(r.text, "hello world") - - @utils.retryable_test(3, 5) - def test_get_endpoint_should_accept_query_param(self): - """Test if the azure.functions SDK is able to deserialize query - parameter from the default template - """ - r = self.webhost.request('GET', 'get_query_param', - params={'name': 'dummy'}, no_prefix=True) - self.assertTrue(r.ok) - self.assertEqual( - r.text, - "hello dummy" - ) - - @utils.retryable_test(3, 5) - def test_post_endpoint_should_accept_body(self): - """Test if the azure.functions SDK is able to deserialize http body - and pass it to default template - """ - r = self.webhost.request('POST', 'post_str', - data="dummy", - headers={'content-type': 'text/plain'}, - no_prefix=True) - self.assertTrue(r.ok) - self.assertEqual( - r.text, - "hello dummy" - ) - - @utils.retryable_test(3, 5) - def test_worker_status_endpoint_should_return_ok(self): - """Test if the worker status endpoint will trigger - _handle__worker_status_request and sends a worker status - response back - to host - """ - root_url = self.webhost._addr - health_check_url = f'{root_url}/admin/host/ping' - r = requests.post(health_check_url, - params={'checkHealth': '1'}) - self.assertTrue(r.ok) - - @utils.retryable_test(3, 5) - def test_worker_status_endpoint_should_return_ok_when_disabled(self): - """Test if the worker status endpoint will trigger - _handle__worker_status_request and sends a worker status - response back - to host - """ - os.environ['WEBSITE_PING_METRICS_SCALE_ENABLED'] = '0' - root_url = self.webhost._addr - health_check_url = f'{root_url}/admin/host/ping' - r = requests.post(health_check_url, - params={'checkHealth': '1'}) - self.assertTrue(r.ok) - - @utils.retryable_test(3, 5) - def test_get_endpoint_should_accept_path_param(self): - r = self.webhost.request('GET', 'get_path_param/1', no_prefix=True) - self.assertTrue(r.ok) - self.assertEqual(r.text, "hello 1") - - @utils.retryable_test(3, 5) - def test_post_json_body_and_return_json_response(self): - test_data = { - "name": "apple", - "description": "yummy" - } - r = self.webhost.request('POST', 'post_json_return_json_response', - json=test_data, - no_prefix=True) - self.assertTrue(r.ok) - self.assertEqual(r.json(), test_data) - - @utils.retryable_test(3, 5) - def test_raise_exception_should_return_not_found(self): - r = self.webhost.request('GET', 'raise_http_exception', - no_prefix=True) - self.assertEqual(r.status_code, 404) - self.assertEqual(r.json(), {"detail": "Item not found"}) - - -class TestAsgiHttpFunctions( - ThirdPartyHttpFunctionsTestBase.TestThirdPartyHttpFunctions): - @classmethod - def get_script_dir(cls): - return E2E_TESTS_ROOT / 'third_party_http_functions' / 'stein' / \ - 'asgi_function' - - -class TestWsgiHttpFunctions( - ThirdPartyHttpFunctionsTestBase.TestThirdPartyHttpFunctions): - @classmethod - def get_script_dir(cls): - return E2E_TESTS_ROOT / 'third_party_http_functions' / 'stein' / \ - 'wsgi_function' diff --git a/tests/endtoend/test_threadpool_thread_count_functions.py b/tests/endtoend/test_threadpool_thread_count_functions.py deleted file mode 100644 index 2388aae9c..000000000 --- a/tests/endtoend/test_threadpool_thread_count_functions.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import os -from datetime import datetime -from threading import Thread - -from tests.utils import testutils - - -class TestPythonThreadpoolThreadCount(testutils.WebHostTestCase): - """ Test the Http Trigger with setting up the python threadpool thread - count to 2. this test will check if both requests should be processed - at the same time. this file is more focus on testing the E2E flow - scenarios. - """ - - @classmethod - def setUpClass(cls): - os.environ["PYTHON_THREADPOOL_THREAD_COUNT"] = "2" - - super().setUpClass() - - def tearDown(self): - os.environ.pop('PYTHON_THREADPOOL_THREAD_COUNT') - - super().tearDown() - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'http_functions' - - @testutils.retryable_test(4, 5) - def test_http_func_with_thread_count(self): - response = [None, None] - - def http_req(res_num): - r = self.webhost.request('GET', 'http_func') - self.assertTrue(r.ok) - response[res_num] = datetime.strptime( - r.content.decode("utf-8"), "%H:%M:%S") - - # creating 2 different threads to send HTTP request - thread1 = Thread(target=http_req, args=(0,)) - thread2 = Thread(target=http_req, args=(1,)) - thread1.start() - thread2.start() - thread1.join() - thread2.join() - """function execution time difference between both HTTP request - should be less than 1 since both the request should be processed at - the same time because PYTHON_THREADPOOL_THREAD_COUNT is 2. - """ - time_diff_in_seconds = abs((response[0] - response[1]).total_seconds()) - self.assertTrue(time_diff_in_seconds < 1) - - -class TestPythonThreadpoolThreadCountStein(TestPythonThreadpoolThreadCount): - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'http_functions' / \ - 'http_functions_stein' diff --git a/tests/endtoend/test_timer_functions.py b/tests/endtoend/test_timer_functions.py deleted file mode 100644 index 7923637bd..000000000 --- a/tests/endtoend/test_timer_functions.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import time -import typing - -from tests.utils import testutils - -REQUEST_TIMEOUT_SEC = 5 - - -class TestTimerFunctions(testutils.WebHostTestCase): - """Test the Timer in the local webhost. - - This test class will spawn a webhost from your /build/webhost - folder and replace the built-in Python with azure_functions_worker from - your code base. Since the Timer Trigger is a native support from host, we - don't need to setup any external resources. - - Compared to the unittests/test_timer_functions.py, this file is more focus - on testing the E2E flow scenarios. - """ - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'timer_functions' - - def test_timer(self): - time.sleep(1) - # Checking webhost status. - r = self.webhost.request('GET', '', no_prefix=True, - timeout=REQUEST_TIMEOUT_SEC) - self.assertTrue(r.ok) - - def check_log_timer(self, host_out: typing.List[str]): - self.assertEqual(host_out.count("This timer trigger function executed " - "successfully"), 1) - - -class TestTimerFunctionsStein(TestTimerFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'timer_functions' / \ - 'timer_functions_stein' diff --git a/tests/endtoend/test_warmup_functions.py b/tests/endtoend/test_warmup_functions.py deleted file mode 100644 index b33eee26f..000000000 --- a/tests/endtoend/test_warmup_functions.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import typing -from unittest import skipIf - -from tests.utils import testutils -from tests.utils.constants import CONSUMPTION_DOCKER_TEST, DEDICATED_DOCKER_TEST - -from azure_functions_worker.utils.common import is_envvar_true - - -@skipIf(is_envvar_true(DEDICATED_DOCKER_TEST) - or is_envvar_true(CONSUMPTION_DOCKER_TEST), - "Docker tests cannot call admin functions") -class TestWarmupFunctions(testutils.WebHostTestCase): - """Test the Warmup Trigger in the local webhost. - - This test class will spawn a webhost from your /build/webhost - folder and replace the built-in Python with azure_functions_worker from - your code base. This test is more focused on testing e2e scenario for - warmup trigger function. - - """ - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'warmup_functions' - - def test_warmup(self): - r = self.webhost.request('GET', 'admin/warmup', no_prefix=True) - - self.assertTrue(r.ok) - - def check_log_warmup(self, host_out: typing.List[str]): - self.assertEqual(host_out.count("Function App instance is warm"), 1) - - -@skipIf(is_envvar_true(DEDICATED_DOCKER_TEST) - or is_envvar_true(CONSUMPTION_DOCKER_TEST), - "Docker tests cannot call admin functions") -class TestWarmupFunctionsStein(TestWarmupFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'warmup_functions' / \ - 'warmup_functions_stein' diff --git a/tests/endtoend/test_worker_process_count_functions.py b/tests/endtoend/test_worker_process_count_functions.py deleted file mode 100644 index 44abcd2e2..000000000 --- a/tests/endtoend/test_worker_process_count_functions.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import os - -from datetime import datetime -from threading import Thread -from unittest import skipIf - -from tests.utils import testutils -from azure_functions_worker.utils.common import is_envvar_true -from tests.utils.constants import CONSUMPTION_DOCKER_TEST, DEDICATED_DOCKER_TEST - - -@skipIf(is_envvar_true(DEDICATED_DOCKER_TEST) - or is_envvar_true(CONSUMPTION_DOCKER_TEST), - "Tests are flaky when running on Docker") -class TestWorkerProcessCount(testutils.WebHostTestCase): - """Test the Http Trigger with setting up the python worker process count - to 2. this test will check if both requests should be processed at the - same time. this file is more focused on testing the E2E flow scenario for - FUNCTIONS_WORKER_PROCESS_COUNT feature. - """ - @classmethod - def setUpClass(cls): - cls.env_variables['PYTHON_THREADPOOL_THREAD_COUNT'] = '1' - cls.env_variables['FUNCTIONS_WORKER_PROCESS_COUNT'] = '2' - - os.environ["PYTHON_THREADPOOL_THREAD_COUNT"] = "1" - os.environ["FUNCTIONS_WORKER_PROCESS_COUNT"] = "2" - - super().setUpClass() - - @classmethod - def tearDownClass(cls): - os.environ.pop('PYTHON_THREADPOOL_THREAD_COUNT') - os.environ.pop('FUNCTIONS_WORKER_PROCESS_COUNT') - - super().tearDownClass() - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'http_functions' - - @classmethod - def get_environment_variables(cls): - return cls.env_variables - - @testutils.retryable_test(4, 5) - def test_http_func_with_worker_process_count_2(self): - response = [None, None] - - def http_req(res_num): - r = self.webhost.request('GET', 'http_func') - self.assertTrue(r.ok) - response[res_num] = datetime.strptime( - r.content.decode("utf-8"), "%H:%M:%S") - - # creating 2 different threads to send HTTP request - thread1 = Thread(target=http_req, args=(0,)) - thread2 = Thread(target=http_req, args=(1,)) - thread1.start() - thread2.start() - thread1.join() - thread2.join() - '''function execution time difference between both HTTP request - should be less than 1 since both request should be processed at the - same time because FUNCTIONS_WORKER_PROCESS_COUNT is 2. - ''' - time_diff_in_seconds = abs((response[0] - response[1]).total_seconds()) - self.assertTrue(time_diff_in_seconds < 1) - - -@skipIf(is_envvar_true(DEDICATED_DOCKER_TEST) - or is_envvar_true(CONSUMPTION_DOCKER_TEST), - "Tests are flaky when running on Docker") -class TestWorkerProcessCountStein(TestWorkerProcessCount): - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'http_functions' /\ - 'http_functions_stein' - - -@skipIf(is_envvar_true(DEDICATED_DOCKER_TEST) - or is_envvar_true(CONSUMPTION_DOCKER_TEST), - "Tests are flaky when running on Docker") -class TestWorkerProcessCountWithBlueprintStein(TestWorkerProcessCount): - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'blueprint_functions' /\ - 'functions_in_blueprint_only' - - -@skipIf(is_envvar_true(DEDICATED_DOCKER_TEST) - or is_envvar_true(CONSUMPTION_DOCKER_TEST), - "Tests are flaky when running on Docker") -class TestWorkerProcessCountWithBlueprintDiffDirStein(TestWorkerProcessCount): - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'blueprint_functions' /\ - 'blueprint_different_dir' diff --git a/tests/endtoend/third_party_http_functions/stein/asgi_function/function_app.py b/tests/endtoend/third_party_http_functions/stein/asgi_function/function_app.py deleted file mode 100644 index 391d036c0..000000000 --- a/tests/endtoend/third_party_http_functions/stein/asgi_function/function_app.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import Optional - -import azure.functions as func -from fastapi import Body, FastAPI, HTTPException, Response -from pydantic import BaseModel - -fast_app = FastAPI() - - -class Fruit(BaseModel): - name: str - description: Optional[str] = None - - -@fast_app.get("/get_query_param") -async def get_query_param(name: str = "world"): - return Response(content=f"hello {name}", media_type="text/plain") - - -@fast_app.post("/post_str") -async def post_str(person: str = Body(...)): - return Response(content=f"hello {person}", media_type="text/plain") - - -@fast_app.post("/post_json_return_json_response") -async def post_json_return_json_response(fruit: Fruit): - return fruit - - -@fast_app.get("/get_path_param/{id}") -async def get_path_param(id): - return Response(content=f"hello {id}", media_type="text/plain") - - -@fast_app.get("/raise_http_exception") -async def raise_http_exception(): - raise HTTPException(status_code=404, detail="Item not found") - - -app = func.AsgiFunctionApp(app=fast_app, - http_auth_level=func.AuthLevel.ANONYMOUS) diff --git a/tests/endtoend/third_party_http_functions/stein/wsgi_function/function_app.py b/tests/endtoend/third_party_http_functions/stein/wsgi_function/function_app.py deleted file mode 100644 index 264a67a03..000000000 --- a/tests/endtoend/third_party_http_functions/stein/wsgi_function/function_app.py +++ /dev/null @@ -1,36 +0,0 @@ -import azure.functions as func -from flask import Flask, request - -flask_app = Flask(__name__) - - -@flask_app.get("/get_query_param") -def get_query_param(): - name = request.args.get("name") - if name is None: - name = "world" - return f"hello {name}" - - -@flask_app.post("/post_str") -def post_str(): - return f"hello {request.data.decode()}" - - -@flask_app.post("/post_json_return_json_response") -def post_json_return_json_response(): - return request.get_json() - - -@flask_app.get("/get_path_param/") -def get_path_param(id): - return f"hello {id}" - - -@flask_app.get("/raise_http_exception") -def raise_http_exception(): - return {"detail": "Item not found"}, 404 - - -app = func.WsgiFunctionApp(app=flask_app.wsgi_app, - http_auth_level=func.AuthLevel.ANONYMOUS) diff --git a/tests/endtoend/timer_functions/timer_func/__init__.py b/tests/endtoend/timer_functions/timer_func/__init__.py deleted file mode 100644 index 5cdd1a102..000000000 --- a/tests/endtoend/timer_functions/timer_func/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging - -import azure.functions as func - - -def main(mytimer: func.TimerRequest) -> None: - logging.info("This timer trigger function executed successfully") diff --git a/tests/endtoend/timer_functions/timer_func/function.json b/tests/endtoend/timer_functions/timer_func/function.json deleted file mode 100644 index dba900a90..000000000 --- a/tests/endtoend/timer_functions/timer_func/function.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "name": "mytimer", - "type": "timerTrigger", - "direction": "in", - "schedule": "*/1 * * * * *", - "runOnStartup": false - } - ] -} \ No newline at end of file diff --git a/tests/endtoend/timer_functions/timer_functions_stein/function_app.py b/tests/endtoend/timer_functions/timer_functions_stein/function_app.py deleted file mode 100644 index 25937d316..000000000 --- a/tests/endtoend/timer_functions/timer_functions_stein/function_app.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging - -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.function_name(name="mytimer") -@app.schedule(schedule="*/1 * * * * *", arg_name="mytimer", - run_on_startup=False, - use_monitor=False) -def mytimer(mytimer: func.TimerRequest) -> None: - logging.info("This timer trigger function executed successfully") diff --git a/tests/endtoend/warmup_functions/warmup/__init__.py b/tests/endtoend/warmup_functions/warmup/__init__.py deleted file mode 100644 index 0d186eab6..000000000 --- a/tests/endtoend/warmup_functions/warmup/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging - - -def main(warmupContext) -> None: - logging.info('Function App instance is warm') diff --git a/tests/endtoend/warmup_functions/warmup/function.json b/tests/endtoend/warmup_functions/warmup/function.json deleted file mode 100644 index 04c3f9d07..000000000 --- a/tests/endtoend/warmup_functions/warmup/function.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "bindings": [ - { - "type": "warmupTrigger", - "direction": "in", - "name": "warmupContext" - } - ] -} \ No newline at end of file diff --git a/tests/endtoend/warmup_functions/warmup_functions_stein/function_app.py b/tests/endtoend/warmup_functions/warmup_functions_stein/function_app.py deleted file mode 100644 index 83968cc4d..000000000 --- a/tests/endtoend/warmup_functions/warmup_functions_stein/function_app.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging - -import azure.functions as func - -app = func.FunctionApp() - - -@app.warm_up_trigger('warmup') -def warmup(warmup) -> None: - logging.info('Function App instance is warm') diff --git a/tests/extension_tests/deferred_bindings_tests/deferred_bindings_blob_functions/function_app.py b/tests/extension_tests/deferred_bindings_tests/deferred_bindings_blob_functions/function_app.py deleted file mode 100644 index 075d8a78a..000000000 --- a/tests/extension_tests/deferred_bindings_tests/deferred_bindings_blob_functions/function_app.py +++ /dev/null @@ -1,294 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json - -import azure.functions as func -import azurefunctions.extensions.bindings.blob as blob - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.function_name(name="put_bc_trigger") -@app.blob_output(arg_name="file", - path="python-worker-tests/test-blobclient-trigger.txt", - connection="AzureWebJobsStorage") -@app.route(route="put_bc_trigger") -def put_bc_trigger(req: func.HttpRequest, file: func.Out[str]) -> str: - file.set(req.get_body()) - return 'OK' - - -@app.function_name(name="bc_blob_trigger") -@app.blob_trigger(arg_name="client", - path="python-worker-tests/test-blobclient-trigger.txt", - connection="AzureWebJobsStorage") -@app.blob_output(arg_name="$return", - path="python-worker-tests/test-blobclient-triggered.txt", - connection="AzureWebJobsStorage") -def bc_blob_trigger(client: blob.BlobClient) -> str: - blob_properties = client.get_blob_properties() - file = client.download_blob(encoding='utf-8').readall() - return json.dumps({ - 'name': blob_properties.name, - 'length': blob_properties.size, - 'content': file - }) - - -@app.function_name(name="get_bc_blob_triggered") -@app.blob_input(arg_name="client", - path="python-worker-tests/test-blobclient-triggered.txt", - connection="AzureWebJobsStorage") -@app.route(route="get_bc_blob_triggered") -def get_bc_blob_triggered(req: func.HttpRequest, - client: blob.BlobClient) -> str: - return client.download_blob(encoding='utf-8').readall() - - -@app.function_name(name="put_cc_trigger") -@app.blob_output(arg_name="file", - path="python-worker-tests/test-containerclient-trigger.txt", - connection="AzureWebJobsStorage") -@app.route(route="put_cc_trigger") -def put_cc_trigger(req: func.HttpRequest, file: func.Out[str]) -> str: - file.set(req.get_body()) - return 'OK' - - -@app.function_name(name="cc_blob_trigger") -@app.blob_trigger(arg_name="client", - path="python-worker-tests/test-containerclient-trigger.txt", - connection="AzureWebJobsStorage") -@app.blob_output(arg_name="$return", - path="python-worker-tests/test-containerclient-triggered.txt", - connection="AzureWebJobsStorage") -def cc_blob_trigger(client: blob.ContainerClient) -> str: - container_properties = client.get_container_properties() - file = client.download_blob("test-containerclient-trigger.txt", - encoding='utf-8').readall() - return json.dumps({ - 'name': container_properties.name, - 'content': file - }) - - -@app.function_name(name="get_cc_blob_triggered") -@app.blob_input(arg_name="client", - path="python-worker-tests/test-containerclient-triggered.txt", - connection="AzureWebJobsStorage") -@app.route(route="get_cc_blob_triggered") -def get_cc_blob_triggered(req: func.HttpRequest, - client: blob.ContainerClient) -> str: - return client.download_blob("test-containerclient-triggered.txt", - encoding='utf-8').readall() - - -@app.function_name(name="put_ssd_trigger") -@app.blob_output(arg_name="file", - path="python-worker-tests/test-ssd-trigger.txt", - connection="AzureWebJobsStorage") -@app.route(route="put_ssd_trigger") -def put_ssd_trigger(req: func.HttpRequest, file: func.Out[str]) -> str: - file.set(req.get_body()) - return 'OK' - - -@app.function_name(name="ssd_blob_trigger") -@app.blob_trigger(arg_name="stream", - path="python-worker-tests/test-ssd-trigger.txt", - connection="AzureWebJobsStorage") -@app.blob_output(arg_name="$return", - path="python-worker-tests/test-ssd-triggered.txt", - connection="AzureWebJobsStorage") -def ssd_blob_trigger(stream: blob.StorageStreamDownloader) -> str: - # testing chunking - file = "" - for chunk in stream.chunks(): - file += chunk.decode("utf-8") - return json.dumps({ - 'content': file - }) - - -@app.function_name(name="get_ssd_blob_triggered") -@app.blob_input(arg_name="stream", - path="python-worker-tests/test-ssd-triggered.txt", - connection="AzureWebJobsStorage") -@app.route(route="get_ssd_blob_triggered") -def get_ssd_blob_triggered(req: func.HttpRequest, - stream: blob.StorageStreamDownloader) -> str: - return stream.readall().decode('utf-8') - - -@app.function_name(name="get_bc_bytes") -@app.route(route="get_bc_bytes") -@app.blob_input(arg_name="client", - path="python-worker-tests/test-blob-extension-bytes.txt", - connection="AzureWebJobsStorage") -def get_bc_bytes(req: func.HttpRequest, client: blob.BlobClient) -> str: - return client.download_blob(encoding='utf-8').readall() - - -@app.function_name(name="get_cc_bytes") -@app.route(route="get_cc_bytes") -@app.blob_input(arg_name="client", - path="python-worker-tests/test-blob-extension-bytes.txt", - connection="AzureWebJobsStorage") -def get_cc_bytes(req: func.HttpRequest, - client: blob.ContainerClient) -> str: - return client.download_blob("test-blob-extension-bytes.txt", - encoding='utf-8').readall() - - -@app.function_name(name="get_ssd_bytes") -@app.route(route="get_ssd_bytes") -@app.blob_input(arg_name="stream", - path="python-worker-tests/test-blob-extension-bytes.txt", - connection="AzureWebJobsStorage") -def get_ssd_bytes(req: func.HttpRequest, - stream: blob.StorageStreamDownloader) -> str: - return stream.readall().decode('utf-8') - - -@app.function_name(name="get_bc_str") -@app.route(route="get_bc_str") -@app.blob_input(arg_name="client", - path="python-worker-tests/test-blob-extension-str.txt", - connection="AzureWebJobsStorage") -def get_bc_str(req: func.HttpRequest, client: blob.BlobClient) -> str: - return client.download_blob(encoding='utf-8').readall() - - -@app.function_name(name="get_cc_str") -@app.route(route="get_cc_str") -@app.blob_input(arg_name="client", - path="python-worker-tests", - connection="AzureWebJobsStorage") -def get_cc_str(req: func.HttpRequest, client: blob.ContainerClient) -> str: - return client.download_blob("test-blob-extension-str.txt", - encoding='utf-8').readall() - - -@app.function_name(name="get_ssd_str") -@app.route(route="get_ssd_str") -@app.blob_input(arg_name="stream", - path="python-worker-tests/test-blob-extension-str.txt", - connection="AzureWebJobsStorage") -def get_ssd_str(req: func.HttpRequest, stream: blob.StorageStreamDownloader) -> str: - return stream.readall().decode('utf-8') - - -@app.function_name(name="bc_and_inputstream_input") -@app.route(route="bc_and_inputstream_input") -@app.blob_input(arg_name="client", - path="python-worker-tests/test-blob-extension-str.txt", - data_type="STRING", - connection="AzureWebJobsStorage") -@app.blob_input(arg_name="blob", - path="python-worker-tests/test-blob-extension-str.txt", - data_type="STRING", - connection="AzureWebJobsStorage") -def bc_and_inputstream_input(req: func.HttpRequest, client: blob.BlobClient, - blob: func.InputStream) -> str: - output_msg = "" - file = blob.read().decode('utf-8') - client_file = client.download_blob(encoding='utf-8').readall() - output_msg = file + " - input stream " + client_file + " - blob client" - return output_msg - - -@app.function_name(name="inputstream_and_bc_input") -@app.route(route="inputstream_and_bc_input") -@app.blob_input(arg_name="blob", - path="python-worker-tests/test-blob-extension-str.txt", - data_type="STRING", - connection="AzureWebJobsStorage") -@app.blob_input(arg_name="client", - path="python-worker-tests/test-blob-extension-str.txt", - data_type="STRING", - connection="AzureWebJobsStorage") -def inputstream_and_bc_input(req: func.HttpRequest, blob: func.InputStream, - client: blob.BlobClient) -> str: - output_msg = "" - file = blob.read().decode('utf-8') - client_file = client.download_blob(encoding='utf-8').readall() - output_msg = file + " - input stream " + client_file + " - blob client" - return output_msg - - -@app.function_name(name="type_undefined") -@app.route(route="type_undefined") -@app.blob_input(arg_name="file", - path="python-worker-tests/test-blob-extension-str.txt", - data_type="STRING", - connection="AzureWebJobsStorage") -def type_undefined(req: func.HttpRequest, file) -> str: - assert not isinstance(file, blob.BlobClient) - assert not isinstance(file, blob.ContainerClient) - assert not isinstance(file, blob.StorageStreamDownloader) - return file.read().decode('utf-8') - - -@app.function_name(name="put_blob_str") -@app.blob_output(arg_name="file", - path="python-worker-tests/test-blob-extension-str.txt", - connection="AzureWebJobsStorage") -@app.route(route="put_blob_str") -def put_blob_str(req: func.HttpRequest, file: func.Out[str]) -> str: - file.set(req.get_body()) - return 'OK' - - -@app.function_name(name="put_blob_bytes") -@app.blob_output(arg_name="file", - path="python-worker-tests/test-blob-extension-bytes.txt", - connection="AzureWebJobsStorage") -@app.route(route="put_blob_bytes") -def put_blob_bytes(req: func.HttpRequest, file: func.Out[bytes]) -> str: - file.set(req.get_body()) - return 'OK' - - -@app.function_name(name="blob_cache") -@app.blob_input(arg_name="cachedClient", - path="python-worker-tests/test-blobclient-triggered.txt", - connection="AzureWebJobsStorage") -@app.route(route="blob_cache") -def blob_cache(req: func.HttpRequest, - cachedClient: blob.BlobClient) -> str: - return func.HttpResponse(repr(cachedClient)) - - -@app.function_name(name="blob_cache2") -@app.blob_input(arg_name="cachedClient", - path="python-worker-tests/test-blobclient-triggered.txt", - connection="AzureWebJobsStorage") -@app.route(route="blob_cache2") -def blob_cache2(req: func.HttpRequest, - cachedClient: blob.BlobClient) -> func.HttpResponse: - return func.HttpResponse(repr(cachedClient)) - - -@app.function_name(name="blob_cache3") -@app.blob_input(arg_name="cachedClient", - path="python-worker-tests/test-blobclient-triggered.txt", - connection="AzureWebJobsStorage") -@app.blob_input(arg_name="cachedClient2", - path="python-worker-tests/test-blobclient-triggered.txt", - connection="AzureWebJobsStorage") -@app.route(route="blob_cache3") -def blob_cache3(req: func.HttpRequest, - cachedClient: blob.BlobClient, - cachedClient2: blob.BlobClient) -> func.HttpResponse: - return func.HttpResponse("Client 1: " + repr(cachedClient) - + " | Client 2: " + repr(cachedClient2)) - - -@app.function_name(name="invalid_connection_info") -@app.blob_input(arg_name="client", - path="python-worker-tests/test-blobclient-triggered.txt", - connection="NotARealConnectionString") -@app.route(route="invalid_connection_info") -def invalid_connection_info(req: func.HttpRequest, - client: blob.BlobClient) -> func.HttpResponse: - return func.HttpResponse(repr(client)) diff --git a/tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_disabled/function_app.py b/tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_disabled/function_app.py deleted file mode 100644 index 2af0d9c20..000000000 --- a/tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_disabled/function_app.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json - -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.function_name(name="blob_trigger_only") -@app.blob_trigger(arg_name="file", - path="python-worker-tests/test-blob-trigger.txt", - connection="AzureWebJobsStorage") -@app.blob_output(arg_name="$return", - path="python-worker-tests/test-blob-triggered.txt", - connection="AzureWebJobsStorage") -def blob_trigger_only(file: func.InputStream) -> str: - return json.dumps({ - 'name': file.name, - 'length': file.length, - 'content': file.read().decode('utf-8') - }) diff --git a/tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_enabled/function_app.py b/tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_enabled/function_app.py deleted file mode 100644 index 8613ea467..000000000 --- a/tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_enabled/function_app.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func -import azurefunctions.extensions.bindings.blob as blob - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.function_name(name="blob_input_only") -@app.blob_input(arg_name="client", - path="python-worker-tests/test-blobclient-triggered.txt", - connection="AzureWebJobsStorage") -@app.route(route="blob_input_only") -def blob_input_only(req: func.HttpRequest, - client: blob.BlobClient) -> str: - return client.download_blob(encoding='utf-8').readall() diff --git a/tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_enabled_dual/function_app.py b/tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_enabled_dual/function_app.py deleted file mode 100644 index a22d8b7b7..000000000 --- a/tests/extension_tests/deferred_bindings_tests/deferred_bindings_functions/deferred_bindings_enabled_dual/function_app.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json - -import azure.functions as func -import azurefunctions.extensions.bindings.blob as blob - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.function_name(name="get_bc_blob_triggered_dual") -@app.blob_input(arg_name="client", - path="python-worker-tests/test-blobclient-triggered.txt", - connection="AzureWebJobsStorage") -@app.route(route="get_bc_blob_triggered_dual") -def get_bc_blob_triggered_dual(req: func.HttpRequest, - client: blob.BlobClient) -> str: - return client.download_blob(encoding='utf-8').readall() - - -@app.function_name(name="blob_trigger_dual") -@app.blob_trigger(arg_name="file", - path="python-worker-tests/test-blob-trigger.txt", - connection="AzureWebJobsStorage") -@app.blob_output(arg_name="$return", - path="python-worker-tests/test-blob-triggered.txt", - connection="AzureWebJobsStorage") -def blob_trigger_dual(file: func.InputStream) -> str: - return json.dumps({ - 'name': file.name, - 'length': file.length, - 'content': file.read().decode('utf-8') - }) diff --git a/tests/extension_tests/deferred_bindings_tests/test_deferred_bindings.py b/tests/extension_tests/deferred_bindings_tests/test_deferred_bindings.py deleted file mode 100644 index 1899f9e75..000000000 --- a/tests/extension_tests/deferred_bindings_tests/test_deferred_bindings.py +++ /dev/null @@ -1,198 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import sys -import unittest - -import azure.functions as func -from tests.utils import testutils - -from azure_functions_worker import protos -from azure_functions_worker.bindings import datumdef, meta - -# Even if the tests are skipped for <=3.8, the library is still imported as -# it is used for these tests. -if sys.version_info.minor >= 9: - from azurefunctions.extensions.bindings.blob import (BlobClient, - BlobClientConverter, - ContainerClient, - StorageStreamDownloader) - -DEFERRED_BINDINGS_ENABLED_DIR = testutils.EXTENSION_TESTS_FOLDER / \ - 'deferred_bindings_tests' / \ - 'deferred_bindings_functions' / \ - 'deferred_bindings_enabled' -DEFERRED_BINDINGS_DISABLED_DIR = testutils.EXTENSION_TESTS_FOLDER / \ - 'deferred_bindings_tests' / \ - 'deferred_bindings_functions' / \ - 'deferred_bindings_disabled' -DEFERRED_BINDINGS_ENABLED_DUAL_DIR = testutils.EXTENSION_TESTS_FOLDER / \ - 'deferred_bindings_tests' / \ - 'deferred_bindings_functions' / \ - 'deferred_bindings_enabled_dual' - - -class MockMBD: - def __init__(self, version: str, source: str, - content_type: str, content: str): - self.version = version - self.source = source - self.content_type = content_type - self.content = content - - -@unittest.skipIf(sys.version_info.minor <= 8, "The base extension" - "is only supported for 3.9+.") -class TestDeferredBindingsEnabled(testutils.AsyncTestCase): - - @testutils.retryable_test(3, 5) - async def test_deferred_bindings_enabled_metadata(self): - async with testutils.start_mockhost( - script_root=DEFERRED_BINDINGS_ENABLED_DIR) as host: - await host.init_worker() - r = await host.get_functions_metadata() - self.assertIsInstance(r.response, protos.FunctionMetadataResponse) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - del sys.modules['function_app'] - - @testutils.retryable_test(3, 5) - async def test_deferred_bindings_enabled_log(self): - async with testutils.start_mockhost( - script_root=DEFERRED_BINDINGS_ENABLED_DIR) as host: - await host.init_worker() - r = await host.get_functions_metadata() - enabled_log_present = False - for log in r.logs: - message = log.message - if "Deferred bindings enabled: True" in message: - enabled_log_present = True - break - self.assertTrue(enabled_log_present) - del sys.modules['function_app'] - - -@unittest.skipIf(sys.version_info.minor <= 8, "The base extension" - "is only supported for 3.9+.") -class TestDeferredBindingsDisabled(testutils.AsyncTestCase): - - @testutils.retryable_test(3, 5) - async def test_deferred_bindings_disabled_metadata(self): - async with testutils.start_mockhost( - script_root=DEFERRED_BINDINGS_DISABLED_DIR) as host: - await host.init_worker() - r = await host.get_functions_metadata() - self.assertIsInstance(r.response, protos.FunctionMetadataResponse) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - del sys.modules['function_app'] - - @testutils.retryable_test(3, 5) - async def test_deferred_bindings_disabled_log(self): - async with testutils.start_mockhost( - script_root=DEFERRED_BINDINGS_DISABLED_DIR) as host: - await host.init_worker() - r = await host.get_functions_metadata() - disabled_log_present = False - for log in r.logs: - message = log.message - if "Deferred bindings enabled: False" in message: - disabled_log_present = True - break - self.assertTrue(disabled_log_present) - del sys.modules['function_app'] - - -@unittest.skipIf(sys.version_info.minor <= 8, "The base extension" - "is only supported for 3.9+.") -class TestDeferredBindingsEnabledDual(testutils.AsyncTestCase): - - @testutils.retryable_test(3, 5) - async def test_deferred_bindings_dual_metadata(self): - async with testutils.start_mockhost( - script_root=DEFERRED_BINDINGS_ENABLED_DUAL_DIR) as host: - await host.init_worker() - r = await host.get_functions_metadata() - self.assertIsInstance(r.response, protos.FunctionMetadataResponse) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - del sys.modules['function_app'] - - @testutils.retryable_test(3, 5) - async def test_deferred_bindings_dual_enabled_log(self): - async with testutils.start_mockhost( - script_root=DEFERRED_BINDINGS_ENABLED_DUAL_DIR) as host: - await host.init_worker() - r = await host.get_functions_metadata() - enabled_log_present = False - for log in r.logs: - message = log.message - if "Deferred bindings enabled: True" in message: - enabled_log_present = True - break - self.assertTrue(enabled_log_present) - del sys.modules['function_app'] - - -@unittest.skipIf(sys.version_info.minor <= 8, "The base extension" - "is only supported for 3.9+.") -class TestDeferredBindingsHelpers(testutils.AsyncTestCase): - - def test_deferred_bindings_enabled_decode(self): - binding = BlobClientConverter - pb = protos.ParameterBinding(name='test', - data=protos.TypedData( - string='test')) - sample_mbd = MockMBD(version="1.0", - source="AzureStorageBlobs", - content_type="application/json", - content="{\"Connection\":\"AzureWebJobsStorage\"," - "\"ContainerName\":" - "\"python-worker-tests\"," - "\"BlobName\":" - "\"test-blobclient-trigger.txt\"}") - datum = datumdef.Datum(value=sample_mbd, type='model_binding_data') - - obj = meta.deferred_bindings_decode(binding=binding, pb=pb, - pytype=BlobClient, datum=datum, metadata={}, - function_name="test_function") - - self.assertIsNotNone(obj) - - async def test_check_deferred_bindings_enabled(self): - """ - check_deferred_bindings_enabled checks if deferred bindings is enabled at fx - and single binding level. - - The first bool represents if deferred bindings is enabled at a fx level. This - means that at least one binding in the function is a deferred binding type. - - The second represents if the current binding is deferred binding. If this is - True, then deferred bindings must also be enabled at the function level. - """ - async with testutils.start_mockhost( - script_root=DEFERRED_BINDINGS_ENABLED_DIR) as host: - await host.init_worker() - - # Type is not supported, deferred_bindings_enabled is not yet set - self.assertEqual(meta.check_deferred_bindings_enabled( - func.InputStream, False), (False, False)) - - # Type is not supported, deferred_bindings_enabled already set - self.assertEqual(meta.check_deferred_bindings_enabled( - func.InputStream, True), (True, False)) - - # Type is supported, deferred_bindings_enabled is not yet set - self.assertEqual(meta.check_deferred_bindings_enabled( - BlobClient, False), (True, True)) - self.assertEqual(meta.check_deferred_bindings_enabled( - ContainerClient, False), (True, True)) - self.assertEqual(meta.check_deferred_bindings_enabled( - StorageStreamDownloader, False), (True, True)) - - # Type is supported, deferred_bindings_enabled is already set - self.assertEqual(meta.check_deferred_bindings_enabled( - BlobClient, True), (True, True)) - self.assertEqual(meta.check_deferred_bindings_enabled( - ContainerClient, True), (True, True)) - self.assertEqual(meta.check_deferred_bindings_enabled( - StorageStreamDownloader, True), (True, True)) diff --git a/tests/extension_tests/deferred_bindings_tests/test_deferred_bindings_blob_functions.py b/tests/extension_tests/deferred_bindings_tests/test_deferred_bindings_blob_functions.py deleted file mode 100644 index ed441a077..000000000 --- a/tests/extension_tests/deferred_bindings_tests/test_deferred_bindings_blob_functions.py +++ /dev/null @@ -1,232 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import sys -import time -import unittest - -from tests.utils import testutils - - -@unittest.skipIf(sys.version_info.minor <= 8, "The base extension" - "is only supported for 3.9+.") -class TestDeferredBindingsBlobFunctions(testutils.WebHostTestCase): - - @classmethod - def get_script_dir(cls): - return testutils.EXTENSION_TESTS_FOLDER / 'deferred_bindings_tests' / \ - 'deferred_bindings_blob_functions' - - @classmethod - def get_libraries_to_install(cls): - return ['azurefunctions-extensions-bindings-blob'] - - def test_blob_str(self): - r = self.webhost.request('POST', 'put_blob_str', data='test-data') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - time.sleep(5) - - r = self.webhost.request('GET', 'get_bc_str') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'test-data') - - r = self.webhost.request('GET', 'get_cc_str') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'test-data') - - r = self.webhost.request('GET', 'get_ssd_str') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'test-data') - - def test_blob_bytes(self): - r = self.webhost.request('POST', 'put_blob_bytes', - data='test-dată'.encode('utf-8')) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - time.sleep(5) - - r = self.webhost.request('POST', 'get_bc_bytes') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'test-dată') - - r = self.webhost.request('POST', 'get_cc_bytes') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'test-dată') - - r = self.webhost.request('POST', 'get_ssd_bytes') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'test-dată') - - def test_bc_blob_trigger(self): - data = "DummyData" - - r = self.webhost.request('POST', 'put_bc_trigger', - data=data.encode('utf-8')) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - # Blob trigger may be processed after some delay - # We check it every 2 seconds to allow the trigger to be fired - max_retries = 10 - for try_no in range(max_retries): - time.sleep(5) - - try: - # Check that the trigger has fired - r = self.webhost.request('GET', 'get_bc_blob_triggered') - self.assertEqual(r.status_code, 200) - response = r.json() - - self.assertEqual(response['name'], - 'test-blobclient-trigger.txt') - self.assertEqual(response['content'], data) - - break - except AssertionError: - if try_no == max_retries - 1: - raise - - def test_cc_blob_trigger(self): - data = "DummyData" - - r = self.webhost.request('POST', 'put_cc_trigger', - data=data.encode('utf-8')) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - # Blob trigger may be processed after some delay - # We check it every 2 seconds to allow the trigger to be fired - max_retries = 10 - for try_no in range(max_retries): - time.sleep(5) - - try: - # Check that the trigger has fired - r = self.webhost.request('GET', 'get_cc_blob_triggered') - self.assertEqual(r.status_code, 200) - response = r.json() - - self.assertEqual(response['name'], - 'python-worker-tests') - self.assertEqual(response['content'], data) - - break - except AssertionError: - if try_no == max_retries - 1: - raise - - def test_ssd_blob_trigger(self): - data = "DummyData" - - r = self.webhost.request('POST', 'put_ssd_trigger', - data=data.encode('utf-8')) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - # Blob trigger may be processed after some delay - # We check it every 2 seconds to allow the trigger to be fired - max_retries = 10 - for try_no in range(max_retries): - time.sleep(5) - - try: - # Check that the trigger has fired - r = self.webhost.request('GET', 'get_ssd_blob_triggered') - self.assertEqual(r.status_code, 200) - response = r.json() - - self.assertEqual(response['content'], data) - - break - except AssertionError: - if try_no == max_retries - 1: - raise - - def test_bc_and_inputstream_input(self): - r = self.webhost.request('POST', 'put_blob_str', data='test-data') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - r = self.webhost.request('GET', 'bc_and_inputstream_input') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'test-data - input stream test-data - blob client') - - def test_inputstream_and_bc_input(self): - r = self.webhost.request('POST', 'put_blob_str', data='test-data') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - r = self.webhost.request('GET', 'inputstream_and_bc_input') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'test-data - input stream test-data - blob client') - - def test_type_undefined(self): - r = self.webhost.request('POST', 'put_blob_str', data='test-data') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - r = self.webhost.request('GET', 'type_undefined') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'test-data') - - def test_caching(self): - ''' - The cache returns the same type based on resource and function name. - Two different functions with clients that access the same resource - will have two different clients. This tests that the same client - is returned for each invocation and that the clients are different - between the two functions. - ''' - - r = self.webhost.request('GET', 'blob_cache') - r2 = self.webhost.request('GET', 'blob_cache2') - self.assertEqual(r.status_code, 200) - self.assertEqual(r2.status_code, 200) - client = r.text - client2 = r2.text - self.assertNotEqual(client, client2) - - r = self.webhost.request('GET', 'blob_cache') - r2 = self.webhost.request('GET', 'blob_cache2') - self.assertEqual(r.status_code, 200) - self.assertEqual(r2.status_code, 200) - self.assertEqual(r.text, client) - self.assertEqual(r2.text, client2) - self.assertNotEqual(r.text, r2.text) - - r = self.webhost.request('GET', 'blob_cache') - r2 = self.webhost.request('GET', 'blob_cache2') - self.assertEqual(r.status_code, 200) - self.assertEqual(r2.status_code, 200) - self.assertEqual(r.text, client) - self.assertEqual(r2.text, client2) - self.assertNotEqual(r.text, r2.text) - - def test_caching_same_resource(self): - ''' - The cache returns the same type based on param name. - One functions with two clients that access the same resource - will have two different clients. This tests that the same clients - are returned for each invocation and that the clients are different - between the two bindings. - ''' - - r = self.webhost.request('GET', 'blob_cache3') - self.assertEqual(r.status_code, 200) - clients = r.text.split(" | ") - self.assertNotEqual(clients[0], clients[1]) - - r2 = self.webhost.request('GET', 'blob_cache3') - self.assertEqual(r2.status_code, 200) - clients_second_call = r2.text.split(" | ") - self.assertEqual(clients[0], clients_second_call[0]) - self.assertEqual(clients[1], clients_second_call[1]) - self.assertNotEqual(clients_second_call[0], clients_second_call[1]) - - def test_failed_client_creation(self): - r = self.webhost.request('GET', 'invalid_connection_info') - # Without the http_v2_enabled default definition, this request would time out. - # Instead, it fails immediately - self.assertEqual(r.status_code, 500) diff --git a/tests/extension_tests/http_v2_tests/http_functions_v2/fastapi/function_app.py b/tests/extension_tests/http_v2_tests/http_functions_v2/fastapi/function_app.py deleted file mode 100644 index b20f5440c..000000000 --- a/tests/extension_tests/http_v2_tests/http_functions_v2/fastapi/function_app.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import logging -import time -from datetime import datetime - -import azure.functions as func -from azurefunctions.extensions.http.fastapi import ( - FileResponse, - HTMLResponse, - ORJSONResponse, - Request, - Response, - StreamingResponse, - UJSONResponse, -) - -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") diff --git a/tests/extension_tests/http_v2_tests/test_http_v2.py b/tests/extension_tests/http_v2_tests/test_http_v2.py deleted file mode 100644 index 8c1d5b48e..000000000 --- a/tests/extension_tests/http_v2_tests/test_http_v2.py +++ /dev/null @@ -1,194 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import concurrent -import os -import sys -import unittest -from concurrent.futures import ThreadPoolExecutor - -import requests -from tests.utils import testutils -from azure_functions_worker.utils.common import is_envvar_true -from tests.utils.constants import CONSUMPTION_DOCKER_TEST, DEDICATED_DOCKER_TEST - -from azure_functions_worker.constants import PYTHON_ENABLE_INIT_INDEXING - -REQUEST_TIMEOUT_SEC = 5 - - -@unittest.skipIf(is_envvar_true(DEDICATED_DOCKER_TEST) - or is_envvar_true(CONSUMPTION_DOCKER_TEST), - "Tests are flaky when running on Docker") -@unittest.skipIf(sys.version_info.minor < 8, "HTTPv2" - "is only supported for 3.8+.") -class TestHttpFunctionsWithInitIndexing(testutils.WebHostTestCase): - @classmethod - def setUpClass(cls): - cls.env_variables[PYTHON_ENABLE_INIT_INDEXING] = '1' - os.environ[PYTHON_ENABLE_INIT_INDEXING] = "1" - super().setUpClass() - - @classmethod - def tearDownClass(cls): - os.environ.pop(PYTHON_ENABLE_INIT_INDEXING) - super().tearDownClass() - - @classmethod - def get_environment_variables(cls): - return cls.env_variables - - @classmethod - def get_script_dir(cls): - return testutils.EXTENSION_TESTS_FOLDER / 'http_v2_tests' / \ - 'http_functions_v2' / \ - 'fastapi' - - @classmethod - def get_libraries_to_install(cls): - return ['azurefunctions-extensions-http-fastapi', 'orjson', 'ujson'] - - @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', b' chun', b'k\nSec', b'ond c', b'hunk\n'] - received_content = [] - for chunk in r.iter_content(chunk_size=5): - 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) diff --git a/tests/protos/FunctionRpc_pb2.py b/tests/protos/FunctionRpc_pb2.py new file mode 100644 index 000000000..df996ff4f --- /dev/null +++ b/tests/protos/FunctionRpc_pb2.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: FunctionRpc.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 +from .identity import ClaimsIdentityRpc_pb2 as identity_dot_ClaimsIdentityRpc__pb2 +from .shared import NullableTypes_pb2 as shared_dot_NullableTypes__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11\x46unctionRpc.proto\x12\x19\x41zureFunctionsRpcMessages\x1a\x1egoogle/protobuf/duration.proto\x1a identity/ClaimsIdentityRpc.proto\x1a\x1ashared/NullableTypes.proto\"\x8c\x11\n\x10StreamingMessage\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12>\n\x0cstart_stream\x18\x14 \x01(\x0b\x32&.AzureFunctionsRpcMessages.StartStreamH\x00\x12K\n\x13worker_init_request\x18\x11 \x01(\x0b\x32,.AzureFunctionsRpcMessages.WorkerInitRequestH\x00\x12M\n\x14worker_init_response\x18\x10 \x01(\x0b\x32-.AzureFunctionsRpcMessages.WorkerInitResponseH\x00\x12\x46\n\x10worker_heartbeat\x18\x0f \x01(\x0b\x32*.AzureFunctionsRpcMessages.WorkerHeartbeatH\x00\x12\x46\n\x10worker_terminate\x18\x0e \x01(\x0b\x32*.AzureFunctionsRpcMessages.WorkerTerminateH\x00\x12O\n\x15worker_status_request\x18\x0c \x01(\x0b\x32..AzureFunctionsRpcMessages.WorkerStatusRequestH\x00\x12Q\n\x16worker_status_response\x18\r \x01(\x0b\x32/.AzureFunctionsRpcMessages.WorkerStatusResponseH\x00\x12V\n\x19\x66ile_change_event_request\x18\x06 \x01(\x0b\x32\x31.AzureFunctionsRpcMessages.FileChangeEventRequestH\x00\x12Q\n\x16worker_action_response\x18\x07 \x01(\x0b\x32/.AzureFunctionsRpcMessages.WorkerActionResponseH\x00\x12O\n\x15\x66unction_load_request\x18\x08 \x01(\x0b\x32..AzureFunctionsRpcMessages.FunctionLoadRequestH\x00\x12Q\n\x16\x66unction_load_response\x18\t \x01(\x0b\x32/.AzureFunctionsRpcMessages.FunctionLoadResponseH\x00\x12J\n\x12invocation_request\x18\x04 \x01(\x0b\x32,.AzureFunctionsRpcMessages.InvocationRequestH\x00\x12L\n\x13invocation_response\x18\x05 \x01(\x0b\x32-.AzureFunctionsRpcMessages.InvocationResponseH\x00\x12H\n\x11invocation_cancel\x18\x15 \x01(\x0b\x32+.AzureFunctionsRpcMessages.InvocationCancelH\x00\x12\x34\n\x07rpc_log\x18\x02 \x01(\x0b\x32!.AzureFunctionsRpcMessages.RpcLogH\x00\x12j\n#function_environment_reload_request\x18\x19 \x01(\x0b\x32;.AzureFunctionsRpcMessages.FunctionEnvironmentReloadRequestH\x00\x12l\n$function_environment_reload_response\x18\x1a \x01(\x0b\x32<.AzureFunctionsRpcMessages.FunctionEnvironmentReloadResponseH\x00\x12m\n%close_shared_memory_resources_request\x18\x1b \x01(\x0b\x32<.AzureFunctionsRpcMessages.CloseSharedMemoryResourcesRequestH\x00\x12o\n&close_shared_memory_resources_response\x18\x1c \x01(\x0b\x32=.AzureFunctionsRpcMessages.CloseSharedMemoryResourcesResponseH\x00\x12Y\n\x1a\x66unctions_metadata_request\x18\x1d \x01(\x0b\x32\x33.AzureFunctionsRpcMessages.FunctionsMetadataRequestH\x00\x12Y\n\x1a\x66unction_metadata_response\x18\x1e \x01(\x0b\x32\x33.AzureFunctionsRpcMessages.FunctionMetadataResponseH\x00\x12\x64\n function_load_request_collection\x18\x1f \x01(\x0b\x32\x38.AzureFunctionsRpcMessages.FunctionLoadRequestCollectionH\x00\x12\x66\n!function_load_response_collection\x18 \x01(\x0b\x32\x39.AzureFunctionsRpcMessages.FunctionLoadResponseCollectionH\x00\x12O\n\x15worker_warmup_request\x18! \x01(\x0b\x32..AzureFunctionsRpcMessages.WorkerWarmupRequestH\x00\x12Q\n\x16worker_warmup_response\x18\" \x01(\x0b\x32/.AzureFunctionsRpcMessages.WorkerWarmupResponseH\x00\x42\t\n\x07\x63ontent\" \n\x0bStartStream\x12\x11\n\tworker_id\x18\x02 \x01(\t\"\xa6\x03\n\x11WorkerInitRequest\x12\x14\n\x0chost_version\x18\x01 \x01(\t\x12T\n\x0c\x63\x61pabilities\x18\x02 \x03(\x0b\x32>.AzureFunctionsRpcMessages.WorkerInitRequest.CapabilitiesEntry\x12W\n\x0elog_categories\x18\x03 \x03(\x0b\x32?.AzureFunctionsRpcMessages.WorkerInitRequest.LogCategoriesEntry\x12\x18\n\x10worker_directory\x18\x04 \x01(\t\x12\x1e\n\x16\x66unction_app_directory\x18\x05 \x01(\t\x1a\x33\n\x11\x43\x61pabilitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a]\n\x12LogCategoriesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0e\x32\'.AzureFunctionsRpcMessages.RpcLog.Level:\x02\x38\x01\"\xb5\x02\n\x12WorkerInitResponse\x12\x16\n\x0eworker_version\x18\x01 \x01(\t\x12U\n\x0c\x63\x61pabilities\x18\x02 \x03(\x0b\x32?.AzureFunctionsRpcMessages.WorkerInitResponse.CapabilitiesEntry\x12\x37\n\x06result\x18\x03 \x01(\x0b\x32\'.AzureFunctionsRpcMessages.StatusResult\x12\x42\n\x0fworker_metadata\x18\x04 \x01(\x0b\x32).AzureFunctionsRpcMessages.WorkerMetadata\x1a\x33\n\x11\x43\x61pabilitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x84\x02\n\x0eWorkerMetadata\x12\x14\n\x0cruntime_name\x18\x01 \x01(\t\x12\x17\n\x0fruntime_version\x18\x02 \x01(\t\x12\x16\n\x0eworker_version\x18\x03 \x01(\t\x12\x16\n\x0eworker_bitness\x18\x04 \x01(\t\x12Z\n\x11\x63ustom_properties\x18\x05 \x03(\x0b\x32?.AzureFunctionsRpcMessages.WorkerMetadata.CustomPropertiesEntry\x1a\x37\n\x15\x43ustomPropertiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xfe\x01\n\x0cStatusResult\x12>\n\x06status\x18\x04 \x01(\x0e\x32..AzureFunctionsRpcMessages.StatusResult.Status\x12\x0e\n\x06result\x18\x01 \x01(\t\x12:\n\texception\x18\x02 \x01(\x0b\x32\'.AzureFunctionsRpcMessages.RpcException\x12/\n\x04logs\x18\x03 \x03(\x0b\x32!.AzureFunctionsRpcMessages.RpcLog\"1\n\x06Status\x12\x0b\n\x07\x46\x61ilure\x10\x00\x12\x0b\n\x07Success\x10\x01\x12\r\n\tCancelled\x10\x02\"\x11\n\x0fWorkerHeartbeat\"B\n\x0fWorkerTerminate\x12/\n\x0cgrace_period\x18\x01 \x01(\x0b\x32\x19.google.protobuf.Duration\"\xd1\x01\n\x16\x46ileChangeEventRequest\x12\x44\n\x04type\x18\x01 \x01(\x0e\x32\x36.AzureFunctionsRpcMessages.FileChangeEventRequest.Type\x12\x11\n\tfull_path\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\"P\n\x04Type\x12\x0b\n\x07Unknown\x10\x00\x12\x0b\n\x07\x43reated\x10\x01\x12\x0b\n\x07\x44\x65leted\x10\x02\x12\x0b\n\x07\x43hanged\x10\x04\x12\x0b\n\x07Renamed\x10\x08\x12\x07\n\x03\x41ll\x10\x0f\"\x91\x01\n\x14WorkerActionResponse\x12\x46\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32\x36.AzureFunctionsRpcMessages.WorkerActionResponse.Action\x12\x0e\n\x06reason\x18\x02 \x01(\t\"!\n\x06\x41\x63tion\x12\x0b\n\x07Restart\x10\x00\x12\n\n\x06Reload\x10\x01\"\x15\n\x13WorkerStatusRequest\"\x16\n\x14WorkerStatusResponse\"\xf5\x01\n FunctionEnvironmentReloadRequest\x12t\n\x15\x65nvironment_variables\x18\x01 \x03(\x0b\x32U.AzureFunctionsRpcMessages.FunctionEnvironmentReloadRequest.EnvironmentVariablesEntry\x12\x1e\n\x16\x66unction_app_directory\x18\x02 \x01(\t\x1a;\n\x19\x45nvironmentVariablesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xbb\x02\n!FunctionEnvironmentReloadResponse\x12\x42\n\x0fworker_metadata\x18\x01 \x01(\x0b\x32).AzureFunctionsRpcMessages.WorkerMetadata\x12\x64\n\x0c\x63\x61pabilities\x18\x02 \x03(\x0b\x32N.AzureFunctionsRpcMessages.FunctionEnvironmentReloadResponse.CapabilitiesEntry\x12\x37\n\x06result\x18\x03 \x01(\x0b\x32\'.AzureFunctionsRpcMessages.StatusResult\x1a\x33\n\x11\x43\x61pabilitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"6\n!CloseSharedMemoryResourcesRequest\x12\x11\n\tmap_names\x18\x01 \x03(\t\"\xcb\x01\n\"CloseSharedMemoryResourcesResponse\x12m\n\x11\x63lose_map_results\x18\x01 \x03(\x0b\x32R.AzureFunctionsRpcMessages.CloseSharedMemoryResourcesResponse.CloseMapResultsEntry\x1a\x36\n\x14\x43loseMapResultsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x08:\x02\x38\x01\"o\n\x1d\x46unctionLoadRequestCollection\x12N\n\x16\x66unction_load_requests\x18\x01 \x03(\x0b\x32..AzureFunctionsRpcMessages.FunctionLoadRequest\"r\n\x1e\x46unctionLoadResponseCollection\x12P\n\x17\x66unction_load_responses\x18\x01 \x03(\x0b\x32/.AzureFunctionsRpcMessages.FunctionLoadResponse\"\x90\x01\n\x13\x46unctionLoadRequest\x12\x13\n\x0b\x66unction_id\x18\x01 \x01(\t\x12@\n\x08metadata\x18\x02 \x01(\x0b\x32..AzureFunctionsRpcMessages.RpcFunctionMetadata\x12\"\n\x1amanaged_dependency_enabled\x18\x03 \x01(\x08\"\x86\x01\n\x14\x46unctionLoadResponse\x12\x13\n\x0b\x66unction_id\x18\x01 \x01(\t\x12\x37\n\x06result\x18\x02 \x01(\x0b\x32\'.AzureFunctionsRpcMessages.StatusResult\x12 \n\x18is_dependency_downloaded\x18\x03 \x01(\x08\"\xff\x04\n\x13RpcFunctionMetadata\x12\x0c\n\x04name\x18\x04 \x01(\t\x12\x11\n\tdirectory\x18\x01 \x01(\t\x12\x13\n\x0bscript_file\x18\x02 \x01(\t\x12\x13\n\x0b\x65ntry_point\x18\x03 \x01(\t\x12N\n\x08\x62indings\x18\x06 \x03(\x0b\x32<.AzureFunctionsRpcMessages.RpcFunctionMetadata.BindingsEntry\x12\x10\n\x08is_proxy\x18\x07 \x01(\x08\x12\x37\n\x06status\x18\x08 \x01(\x0b\x32\'.AzureFunctionsRpcMessages.StatusResult\x12\x10\n\x08language\x18\t \x01(\t\x12\x14\n\x0craw_bindings\x18\n \x03(\t\x12\x13\n\x0b\x66unction_id\x18\r \x01(\t\x12\"\n\x1amanaged_dependency_enabled\x18\x0e \x01(\x08\x12\x41\n\rretry_options\x18\x0f \x01(\x0b\x32*.AzureFunctionsRpcMessages.RpcRetryOptions\x12R\n\nproperties\x18\x10 \x03(\x0b\x32>.AzureFunctionsRpcMessages.RpcFunctionMetadata.PropertiesEntry\x1aW\n\rBindingsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x35\n\x05value\x18\x02 \x01(\x0b\x32&.AzureFunctionsRpcMessages.BindingInfo:\x02\x38\x01\x1a\x31\n\x0fPropertiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\":\n\x18\x46unctionsMetadataRequest\x12\x1e\n\x16\x66unction_app_directory\x18\x01 \x01(\t\"\xcd\x01\n\x18\x46unctionMetadataResponse\x12Q\n\x19\x66unction_metadata_results\x18\x01 \x03(\x0b\x32..AzureFunctionsRpcMessages.RpcFunctionMetadata\x12\x37\n\x06result\x18\x02 \x01(\x0b\x32\'.AzureFunctionsRpcMessages.StatusResult\x12%\n\x1duse_default_metadata_indexing\x18\x03 \x01(\x08\"\xbe\x03\n\x11InvocationRequest\x12\x15\n\rinvocation_id\x18\x01 \x01(\t\x12\x13\n\x0b\x66unction_id\x18\x02 \x01(\t\x12?\n\ninput_data\x18\x03 \x03(\x0b\x32+.AzureFunctionsRpcMessages.ParameterBinding\x12[\n\x10trigger_metadata\x18\x04 \x03(\x0b\x32\x41.AzureFunctionsRpcMessages.InvocationRequest.TriggerMetadataEntry\x12\x41\n\rtrace_context\x18\x05 \x01(\x0b\x32*.AzureFunctionsRpcMessages.RpcTraceContext\x12>\n\rretry_context\x18\x06 \x01(\x0b\x32\'.AzureFunctionsRpcMessages.RetryContext\x1a\\\n\x14TriggerMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x33\n\x05value\x18\x02 \x01(\x0b\x32$.AzureFunctionsRpcMessages.TypedData:\x02\x38\x01\"\xbf\x01\n\x0fRpcTraceContext\x12\x14\n\x0ctrace_parent\x18\x01 \x01(\t\x12\x13\n\x0btrace_state\x18\x02 \x01(\t\x12N\n\nattributes\x18\x03 \x03(\x0b\x32:.AzureFunctionsRpcMessages.RpcTraceContext.AttributesEntry\x1a\x31\n\x0f\x41ttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"x\n\x0cRetryContext\x12\x13\n\x0bretry_count\x18\x01 \x01(\x05\x12\x17\n\x0fmax_retry_count\x18\x02 \x01(\x05\x12:\n\texception\x18\x03 \x01(\x0b\x32\'.AzureFunctionsRpcMessages.RpcException\"Z\n\x10InvocationCancel\x12\x15\n\rinvocation_id\x18\x02 \x01(\t\x12/\n\x0cgrace_period\x18\x01 \x01(\x0b\x32\x19.google.protobuf.Duration\"\xe2\x01\n\x12InvocationResponse\x12\x15\n\rinvocation_id\x18\x01 \x01(\t\x12@\n\x0boutput_data\x18\x02 \x03(\x0b\x32+.AzureFunctionsRpcMessages.ParameterBinding\x12:\n\x0creturn_value\x18\x04 \x01(\x0b\x32$.AzureFunctionsRpcMessages.TypedData\x12\x37\n\x06result\x18\x03 \x01(\x0b\x32\'.AzureFunctionsRpcMessages.StatusResult\"/\n\x13WorkerWarmupRequest\x12\x18\n\x10worker_directory\x18\x01 \x01(\t\"O\n\x14WorkerWarmupResponse\x12\x37\n\x06result\x18\x01 \x01(\x0b\x32\'.AzureFunctionsRpcMessages.StatusResult\"\xfe\x04\n\tTypedData\x12\x10\n\x06string\x18\x01 \x01(\tH\x00\x12\x0e\n\x04json\x18\x02 \x01(\tH\x00\x12\x0f\n\x05\x62ytes\x18\x03 \x01(\x0cH\x00\x12\x10\n\x06stream\x18\x04 \x01(\x0cH\x00\x12\x32\n\x04http\x18\x05 \x01(\x0b\x32\".AzureFunctionsRpcMessages.RpcHttpH\x00\x12\r\n\x03int\x18\x06 \x01(\x12H\x00\x12\x10\n\x06\x64ouble\x18\x07 \x01(\x01H\x00\x12\x46\n\x10\x63ollection_bytes\x18\x08 \x01(\x0b\x32*.AzureFunctionsRpcMessages.CollectionBytesH\x00\x12H\n\x11\x63ollection_string\x18\t \x01(\x0b\x32+.AzureFunctionsRpcMessages.CollectionStringH\x00\x12H\n\x11\x63ollection_double\x18\n \x01(\x0b\x32+.AzureFunctionsRpcMessages.CollectionDoubleH\x00\x12H\n\x11\x63ollection_sint64\x18\x0b \x01(\x0b\x32+.AzureFunctionsRpcMessages.CollectionSInt64H\x00\x12I\n\x12model_binding_data\x18\x0c \x01(\x0b\x32+.AzureFunctionsRpcMessages.ModelBindingDataH\x00\x12^\n\x1d\x63ollection_model_binding_data\x18\r \x01(\x0b\x32\x35.AzureFunctionsRpcMessages.CollectionModelBindingDataH\x00\x42\x06\n\x04\x64\x61ta\"t\n\x0fRpcSharedMemory\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06offset\x18\x02 \x01(\x03\x12\r\n\x05\x63ount\x18\x03 \x01(\x03\x12\x34\n\x04type\x18\x04 \x01(\x0e\x32&.AzureFunctionsRpcMessages.RpcDataType\"\"\n\x10\x43ollectionString\x12\x0e\n\x06string\x18\x01 \x03(\t\" \n\x0f\x43ollectionBytes\x12\r\n\x05\x62ytes\x18\x01 \x03(\x0c\"\"\n\x10\x43ollectionDouble\x12\x0e\n\x06\x64ouble\x18\x01 \x03(\x01\"\"\n\x10\x43ollectionSInt64\x12\x0e\n\x06sint64\x18\x01 \x03(\x12\"\xab\x01\n\x10ParameterBinding\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x34\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32$.AzureFunctionsRpcMessages.TypedDataH\x00\x12G\n\x11rpc_shared_memory\x18\x03 \x01(\x0b\x32*.AzureFunctionsRpcMessages.RpcSharedMemoryH\x00\x42\n\n\x08rpc_data\"\x8b\x03\n\x0b\x42indingInfo\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x43\n\tdirection\x18\x03 \x01(\x0e\x32\x30.AzureFunctionsRpcMessages.BindingInfo.Direction\x12\x42\n\tdata_type\x18\x04 \x01(\x0e\x32/.AzureFunctionsRpcMessages.BindingInfo.DataType\x12J\n\nproperties\x18\x05 \x03(\x0b\x32\x36.AzureFunctionsRpcMessages.BindingInfo.PropertiesEntry\x1a\x31\n\x0fPropertiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\'\n\tDirection\x12\x06\n\x02in\x10\x00\x12\x07\n\x03out\x10\x01\x12\t\n\x05inout\x10\x02\"=\n\x08\x44\x61taType\x12\r\n\tundefined\x10\x00\x12\n\n\x06string\x10\x01\x12\n\n\x06\x62inary\x10\x02\x12\n\n\x06stream\x10\x03\"\xe7\x04\n\x06RpcLog\x12\x15\n\rinvocation_id\x18\x01 \x01(\t\x12\x10\n\x08\x63\x61tegory\x18\x02 \x01(\t\x12\x36\n\x05level\x18\x03 \x01(\x0e\x32\'.AzureFunctionsRpcMessages.RpcLog.Level\x12\x0f\n\x07message\x18\x04 \x01(\t\x12\x10\n\x08\x65vent_id\x18\x05 \x01(\t\x12:\n\texception\x18\x06 \x01(\x0b\x32\'.AzureFunctionsRpcMessages.RpcException\x12\x12\n\nproperties\x18\x07 \x01(\t\x12\x46\n\x0clog_category\x18\x08 \x01(\x0e\x32\x30.AzureFunctionsRpcMessages.RpcLog.RpcLogCategory\x12K\n\rpropertiesMap\x18\t \x03(\x0b\x32\x34.AzureFunctionsRpcMessages.RpcLog.PropertiesMapEntry\x1aZ\n\x12PropertiesMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x33\n\x05value\x18\x02 \x01(\x0b\x32$.AzureFunctionsRpcMessages.TypedData:\x02\x38\x01\"^\n\x05Level\x12\t\n\x05Trace\x10\x00\x12\t\n\x05\x44\x65\x62ug\x10\x01\x12\x0f\n\x0bInformation\x10\x02\x12\x0b\n\x07Warning\x10\x03\x12\t\n\x05\x45rror\x10\x04\x12\x0c\n\x08\x43ritical\x10\x05\x12\x08\n\x04None\x10\x06\"8\n\x0eRpcLogCategory\x12\x08\n\x04User\x10\x00\x12\n\n\x06System\x10\x01\x12\x10\n\x0c\x43ustomMetric\x10\x02\"m\n\x0cRpcException\x12\x0e\n\x06source\x18\x03 \x01(\t\x12\x13\n\x0bstack_trace\x18\x01 \x01(\t\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x19\n\x11is_user_exception\x18\x04 \x01(\x08\x12\x0c\n\x04type\x18\x05 \x01(\t\"\xf7\x02\n\rRpcHttpCookie\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\x12\x1f\n\x06\x64omain\x18\x03 \x01(\x0b\x32\x0f.NullableString\x12\x1d\n\x04path\x18\x04 \x01(\x0b\x32\x0f.NullableString\x12#\n\x07\x65xpires\x18\x05 \x01(\x0b\x32\x12.NullableTimestamp\x12\x1d\n\x06secure\x18\x06 \x01(\x0b\x32\r.NullableBool\x12 \n\thttp_only\x18\x07 \x01(\x0b\x32\r.NullableBool\x12\x44\n\tsame_site\x18\x08 \x01(\x0e\x32\x31.AzureFunctionsRpcMessages.RpcHttpCookie.SameSite\x12 \n\x07max_age\x18\t \x01(\x0b\x32\x0f.NullableDouble\";\n\x08SameSite\x12\x08\n\x04None\x10\x00\x12\x07\n\x03Lax\x10\x01\x12\n\n\x06Strict\x10\x02\x12\x10\n\x0c\x45xplicitNone\x10\x03\"\xc5\x08\n\x07RpcHttp\x12\x0e\n\x06method\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12@\n\x07headers\x18\x03 \x03(\x0b\x32/.AzureFunctionsRpcMessages.RpcHttp.HeadersEntry\x12\x32\n\x04\x62ody\x18\x04 \x01(\x0b\x32$.AzureFunctionsRpcMessages.TypedData\x12>\n\x06params\x18\n \x03(\x0b\x32..AzureFunctionsRpcMessages.RpcHttp.ParamsEntry\x12\x13\n\x0bstatus_code\x18\x0c \x01(\t\x12<\n\x05query\x18\x0f \x03(\x0b\x32-.AzureFunctionsRpcMessages.RpcHttp.QueryEntry\x12\"\n\x1a\x65nable_content_negotiation\x18\x10 \x01(\x08\x12\x35\n\x07rawBody\x18\x11 \x01(\x0b\x32$.AzureFunctionsRpcMessages.TypedData\x12&\n\nidentities\x18\x12 \x03(\x0b\x32\x12.RpcClaimsIdentity\x12\x39\n\x07\x63ookies\x18\x13 \x03(\x0b\x32(.AzureFunctionsRpcMessages.RpcHttpCookie\x12Q\n\x10nullable_headers\x18\x14 \x03(\x0b\x32\x37.AzureFunctionsRpcMessages.RpcHttp.NullableHeadersEntry\x12O\n\x0fnullable_params\x18\x15 \x03(\x0b\x32\x36.AzureFunctionsRpcMessages.RpcHttp.NullableParamsEntry\x12M\n\x0enullable_query\x18\x16 \x03(\x0b\x32\x35.AzureFunctionsRpcMessages.RpcHttp.NullableQueryEntry\x1a.\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a-\n\x0bParamsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a,\n\nQueryEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1aG\n\x14NullableHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1e\n\x05value\x18\x02 \x01(\x0b\x32\x0f.NullableString:\x02\x38\x01\x1a\x46\n\x13NullableParamsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1e\n\x05value\x18\x02 \x01(\x0b\x32\x0f.NullableString:\x02\x38\x01\x1a\x45\n\x12NullableQueryEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1e\n\x05value\x18\x02 \x01(\x0b\x32\x0f.NullableString:\x02\x38\x01\"Z\n\x10ModelBindingData\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x14\n\x0c\x63ontent_type\x18\x03 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x04 \x01(\x0c\"e\n\x1a\x43ollectionModelBindingData\x12G\n\x12model_binding_data\x18\x01 \x03(\x0b\x32+.AzureFunctionsRpcMessages.ModelBindingData\"\xd4\x02\n\x0fRpcRetryOptions\x12\x17\n\x0fmax_retry_count\x18\x02 \x01(\x05\x12\x31\n\x0e\x64\x65lay_interval\x18\x03 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x33\n\x10minimum_interval\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x33\n\x10maximum_interval\x18\x05 \x01(\x0b\x32\x19.google.protobuf.Duration\x12P\n\x0eretry_strategy\x18\x06 \x01(\x0e\x32\x38.AzureFunctionsRpcMessages.RpcRetryOptions.RetryStrategy\"9\n\rRetryStrategy\x12\x17\n\x13\x65xponential_backoff\x10\x00\x12\x0f\n\x0b\x66ixed_delay\x10\x01*\xc1\x01\n\x0bRpcDataType\x12\x0b\n\x07unknown\x10\x00\x12\n\n\x06string\x10\x01\x12\x08\n\x04json\x10\x02\x12\t\n\x05\x62ytes\x10\x03\x12\n\n\x06stream\x10\x04\x12\x08\n\x04http\x10\x05\x12\x07\n\x03int\x10\x06\x12\n\n\x06\x64ouble\x10\x07\x12\x14\n\x10\x63ollection_bytes\x10\x08\x12\x15\n\x11\x63ollection_string\x10\t\x12\x15\n\x11\x63ollection_double\x10\n\x12\x15\n\x11\x63ollection_sint64\x10\x0b\x32|\n\x0b\x46unctionRpc\x12m\n\x0b\x45ventStream\x12+.AzureFunctionsRpcMessages.StreamingMessage\x1a+.AzureFunctionsRpcMessages.StreamingMessage\"\x00(\x01\x30\x01\x42\xa5\x01\n*com.microsoft.azure.functions.rpc.messagesB\rFunctionProtoP\x01Z7github.com/Azure/azure-functions-go-worker/internal/rpc\xaa\x02,Microsoft.Azure.WebJobs.Script.Grpc.Messagesb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'FunctionRpc_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n*com.microsoft.azure.functions.rpc.messagesB\rFunctionProtoP\001Z7github.com/Azure/azure-functions-go-worker/internal/rpc\252\002,Microsoft.Azure.WebJobs.Script.Grpc.Messages' + _WORKERINITREQUEST_CAPABILITIESENTRY._options = None + _WORKERINITREQUEST_CAPABILITIESENTRY._serialized_options = b'8\001' + _WORKERINITREQUEST_LOGCATEGORIESENTRY._options = None + _WORKERINITREQUEST_LOGCATEGORIESENTRY._serialized_options = b'8\001' + _WORKERINITRESPONSE_CAPABILITIESENTRY._options = None + _WORKERINITRESPONSE_CAPABILITIESENTRY._serialized_options = b'8\001' + _WORKERMETADATA_CUSTOMPROPERTIESENTRY._options = None + _WORKERMETADATA_CUSTOMPROPERTIESENTRY._serialized_options = b'8\001' + _FUNCTIONENVIRONMENTRELOADREQUEST_ENVIRONMENTVARIABLESENTRY._options = None + _FUNCTIONENVIRONMENTRELOADREQUEST_ENVIRONMENTVARIABLESENTRY._serialized_options = b'8\001' + _FUNCTIONENVIRONMENTRELOADRESPONSE_CAPABILITIESENTRY._options = None + _FUNCTIONENVIRONMENTRELOADRESPONSE_CAPABILITIESENTRY._serialized_options = b'8\001' + _CLOSESHAREDMEMORYRESOURCESRESPONSE_CLOSEMAPRESULTSENTRY._options = None + _CLOSESHAREDMEMORYRESOURCESRESPONSE_CLOSEMAPRESULTSENTRY._serialized_options = b'8\001' + _RPCFUNCTIONMETADATA_BINDINGSENTRY._options = None + _RPCFUNCTIONMETADATA_BINDINGSENTRY._serialized_options = b'8\001' + _RPCFUNCTIONMETADATA_PROPERTIESENTRY._options = None + _RPCFUNCTIONMETADATA_PROPERTIESENTRY._serialized_options = b'8\001' + _INVOCATIONREQUEST_TRIGGERMETADATAENTRY._options = None + _INVOCATIONREQUEST_TRIGGERMETADATAENTRY._serialized_options = b'8\001' + _RPCTRACECONTEXT_ATTRIBUTESENTRY._options = None + _RPCTRACECONTEXT_ATTRIBUTESENTRY._serialized_options = b'8\001' + _BINDINGINFO_PROPERTIESENTRY._options = None + _BINDINGINFO_PROPERTIESENTRY._serialized_options = b'8\001' + _RPCLOG_PROPERTIESMAPENTRY._options = None + _RPCLOG_PROPERTIESMAPENTRY._serialized_options = b'8\001' + _RPCHTTP_HEADERSENTRY._options = None + _RPCHTTP_HEADERSENTRY._serialized_options = b'8\001' + _RPCHTTP_PARAMSENTRY._options = None + _RPCHTTP_PARAMSENTRY._serialized_options = b'8\001' + _RPCHTTP_QUERYENTRY._options = None + _RPCHTTP_QUERYENTRY._serialized_options = b'8\001' + _RPCHTTP_NULLABLEHEADERSENTRY._options = None + _RPCHTTP_NULLABLEHEADERSENTRY._serialized_options = b'8\001' + _RPCHTTP_NULLABLEPARAMSENTRY._options = None + _RPCHTTP_NULLABLEPARAMSENTRY._serialized_options = b'8\001' + _RPCHTTP_NULLABLEQUERYENTRY._options = None + _RPCHTTP_NULLABLEQUERYENTRY._serialized_options = b'8\001' + _globals['_RPCDATATYPE']._serialized_start=11800 + _globals['_RPCDATATYPE']._serialized_end=11993 + _globals['_STREAMINGMESSAGE']._serialized_start=143 + _globals['_STREAMINGMESSAGE']._serialized_end=2331 + _globals['_STARTSTREAM']._serialized_start=2333 + _globals['_STARTSTREAM']._serialized_end=2365 + _globals['_WORKERINITREQUEST']._serialized_start=2368 + _globals['_WORKERINITREQUEST']._serialized_end=2790 + _globals['_WORKERINITREQUEST_CAPABILITIESENTRY']._serialized_start=2644 + _globals['_WORKERINITREQUEST_CAPABILITIESENTRY']._serialized_end=2695 + _globals['_WORKERINITREQUEST_LOGCATEGORIESENTRY']._serialized_start=2697 + _globals['_WORKERINITREQUEST_LOGCATEGORIESENTRY']._serialized_end=2790 + _globals['_WORKERINITRESPONSE']._serialized_start=2793 + _globals['_WORKERINITRESPONSE']._serialized_end=3102 + _globals['_WORKERINITRESPONSE_CAPABILITIESENTRY']._serialized_start=2644 + _globals['_WORKERINITRESPONSE_CAPABILITIESENTRY']._serialized_end=2695 + _globals['_WORKERMETADATA']._serialized_start=3105 + _globals['_WORKERMETADATA']._serialized_end=3365 + _globals['_WORKERMETADATA_CUSTOMPROPERTIESENTRY']._serialized_start=3310 + _globals['_WORKERMETADATA_CUSTOMPROPERTIESENTRY']._serialized_end=3365 + _globals['_STATUSRESULT']._serialized_start=3368 + _globals['_STATUSRESULT']._serialized_end=3622 + _globals['_STATUSRESULT_STATUS']._serialized_start=3573 + _globals['_STATUSRESULT_STATUS']._serialized_end=3622 + _globals['_WORKERHEARTBEAT']._serialized_start=3624 + _globals['_WORKERHEARTBEAT']._serialized_end=3641 + _globals['_WORKERTERMINATE']._serialized_start=3643 + _globals['_WORKERTERMINATE']._serialized_end=3709 + _globals['_FILECHANGEEVENTREQUEST']._serialized_start=3712 + _globals['_FILECHANGEEVENTREQUEST']._serialized_end=3921 + _globals['_FILECHANGEEVENTREQUEST_TYPE']._serialized_start=3841 + _globals['_FILECHANGEEVENTREQUEST_TYPE']._serialized_end=3921 + _globals['_WORKERACTIONRESPONSE']._serialized_start=3924 + _globals['_WORKERACTIONRESPONSE']._serialized_end=4069 + _globals['_WORKERACTIONRESPONSE_ACTION']._serialized_start=4036 + _globals['_WORKERACTIONRESPONSE_ACTION']._serialized_end=4069 + _globals['_WORKERSTATUSREQUEST']._serialized_start=4071 + _globals['_WORKERSTATUSREQUEST']._serialized_end=4092 + _globals['_WORKERSTATUSRESPONSE']._serialized_start=4094 + _globals['_WORKERSTATUSRESPONSE']._serialized_end=4116 + _globals['_FUNCTIONENVIRONMENTRELOADREQUEST']._serialized_start=4119 + _globals['_FUNCTIONENVIRONMENTRELOADREQUEST']._serialized_end=4364 + _globals['_FUNCTIONENVIRONMENTRELOADREQUEST_ENVIRONMENTVARIABLESENTRY']._serialized_start=4305 + _globals['_FUNCTIONENVIRONMENTRELOADREQUEST_ENVIRONMENTVARIABLESENTRY']._serialized_end=4364 + _globals['_FUNCTIONENVIRONMENTRELOADRESPONSE']._serialized_start=4367 + _globals['_FUNCTIONENVIRONMENTRELOADRESPONSE']._serialized_end=4682 + _globals['_FUNCTIONENVIRONMENTRELOADRESPONSE_CAPABILITIESENTRY']._serialized_start=2644 + _globals['_FUNCTIONENVIRONMENTRELOADRESPONSE_CAPABILITIESENTRY']._serialized_end=2695 + _globals['_CLOSESHAREDMEMORYRESOURCESREQUEST']._serialized_start=4684 + _globals['_CLOSESHAREDMEMORYRESOURCESREQUEST']._serialized_end=4738 + _globals['_CLOSESHAREDMEMORYRESOURCESRESPONSE']._serialized_start=4741 + _globals['_CLOSESHAREDMEMORYRESOURCESRESPONSE']._serialized_end=4944 + _globals['_CLOSESHAREDMEMORYRESOURCESRESPONSE_CLOSEMAPRESULTSENTRY']._serialized_start=4890 + _globals['_CLOSESHAREDMEMORYRESOURCESRESPONSE_CLOSEMAPRESULTSENTRY']._serialized_end=4944 + _globals['_FUNCTIONLOADREQUESTCOLLECTION']._serialized_start=4946 + _globals['_FUNCTIONLOADREQUESTCOLLECTION']._serialized_end=5057 + _globals['_FUNCTIONLOADRESPONSECOLLECTION']._serialized_start=5059 + _globals['_FUNCTIONLOADRESPONSECOLLECTION']._serialized_end=5173 + _globals['_FUNCTIONLOADREQUEST']._serialized_start=5176 + _globals['_FUNCTIONLOADREQUEST']._serialized_end=5320 + _globals['_FUNCTIONLOADRESPONSE']._serialized_start=5323 + _globals['_FUNCTIONLOADRESPONSE']._serialized_end=5457 + _globals['_RPCFUNCTIONMETADATA']._serialized_start=5460 + _globals['_RPCFUNCTIONMETADATA']._serialized_end=6099 + _globals['_RPCFUNCTIONMETADATA_BINDINGSENTRY']._serialized_start=5961 + _globals['_RPCFUNCTIONMETADATA_BINDINGSENTRY']._serialized_end=6048 + _globals['_RPCFUNCTIONMETADATA_PROPERTIESENTRY']._serialized_start=6050 + _globals['_RPCFUNCTIONMETADATA_PROPERTIESENTRY']._serialized_end=6099 + _globals['_FUNCTIONSMETADATAREQUEST']._serialized_start=6101 + _globals['_FUNCTIONSMETADATAREQUEST']._serialized_end=6159 + _globals['_FUNCTIONMETADATARESPONSE']._serialized_start=6162 + _globals['_FUNCTIONMETADATARESPONSE']._serialized_end=6367 + _globals['_INVOCATIONREQUEST']._serialized_start=6370 + _globals['_INVOCATIONREQUEST']._serialized_end=6816 + _globals['_INVOCATIONREQUEST_TRIGGERMETADATAENTRY']._serialized_start=6724 + _globals['_INVOCATIONREQUEST_TRIGGERMETADATAENTRY']._serialized_end=6816 + _globals['_RPCTRACECONTEXT']._serialized_start=6819 + _globals['_RPCTRACECONTEXT']._serialized_end=7010 + _globals['_RPCTRACECONTEXT_ATTRIBUTESENTRY']._serialized_start=6961 + _globals['_RPCTRACECONTEXT_ATTRIBUTESENTRY']._serialized_end=7010 + _globals['_RETRYCONTEXT']._serialized_start=7012 + _globals['_RETRYCONTEXT']._serialized_end=7132 + _globals['_INVOCATIONCANCEL']._serialized_start=7134 + _globals['_INVOCATIONCANCEL']._serialized_end=7224 + _globals['_INVOCATIONRESPONSE']._serialized_start=7227 + _globals['_INVOCATIONRESPONSE']._serialized_end=7453 + _globals['_WORKERWARMUPREQUEST']._serialized_start=7455 + _globals['_WORKERWARMUPREQUEST']._serialized_end=7502 + _globals['_WORKERWARMUPRESPONSE']._serialized_start=7504 + _globals['_WORKERWARMUPRESPONSE']._serialized_end=7583 + _globals['_TYPEDDATA']._serialized_start=7586 + _globals['_TYPEDDATA']._serialized_end=8224 + _globals['_RPCSHAREDMEMORY']._serialized_start=8226 + _globals['_RPCSHAREDMEMORY']._serialized_end=8342 + _globals['_COLLECTIONSTRING']._serialized_start=8344 + _globals['_COLLECTIONSTRING']._serialized_end=8378 + _globals['_COLLECTIONBYTES']._serialized_start=8380 + _globals['_COLLECTIONBYTES']._serialized_end=8412 + _globals['_COLLECTIONDOUBLE']._serialized_start=8414 + _globals['_COLLECTIONDOUBLE']._serialized_end=8448 + _globals['_COLLECTIONSINT64']._serialized_start=8450 + _globals['_COLLECTIONSINT64']._serialized_end=8484 + _globals['_PARAMETERBINDING']._serialized_start=8487 + _globals['_PARAMETERBINDING']._serialized_end=8658 + _globals['_BINDINGINFO']._serialized_start=8661 + _globals['_BINDINGINFO']._serialized_end=9056 + _globals['_BINDINGINFO_PROPERTIESENTRY']._serialized_start=6050 + _globals['_BINDINGINFO_PROPERTIESENTRY']._serialized_end=6099 + _globals['_BINDINGINFO_DIRECTION']._serialized_start=8954 + _globals['_BINDINGINFO_DIRECTION']._serialized_end=8993 + _globals['_BINDINGINFO_DATATYPE']._serialized_start=8995 + _globals['_BINDINGINFO_DATATYPE']._serialized_end=9056 + _globals['_RPCLOG']._serialized_start=9059 + _globals['_RPCLOG']._serialized_end=9674 + _globals['_RPCLOG_PROPERTIESMAPENTRY']._serialized_start=9430 + _globals['_RPCLOG_PROPERTIESMAPENTRY']._serialized_end=9520 + _globals['_RPCLOG_LEVEL']._serialized_start=9522 + _globals['_RPCLOG_LEVEL']._serialized_end=9616 + _globals['_RPCLOG_RPCLOGCATEGORY']._serialized_start=9618 + _globals['_RPCLOG_RPCLOGCATEGORY']._serialized_end=9674 + _globals['_RPCEXCEPTION']._serialized_start=9676 + _globals['_RPCEXCEPTION']._serialized_end=9785 + _globals['_RPCHTTPCOOKIE']._serialized_start=9788 + _globals['_RPCHTTPCOOKIE']._serialized_end=10163 + _globals['_RPCHTTPCOOKIE_SAMESITE']._serialized_start=10104 + _globals['_RPCHTTPCOOKIE_SAMESITE']._serialized_end=10163 + _globals['_RPCHTTP']._serialized_start=10166 + _globals['_RPCHTTP']._serialized_end=11259 + _globals['_RPCHTTP_HEADERSENTRY']._serialized_start=10904 + _globals['_RPCHTTP_HEADERSENTRY']._serialized_end=10950 + _globals['_RPCHTTP_PARAMSENTRY']._serialized_start=10952 + _globals['_RPCHTTP_PARAMSENTRY']._serialized_end=10997 + _globals['_RPCHTTP_QUERYENTRY']._serialized_start=10999 + _globals['_RPCHTTP_QUERYENTRY']._serialized_end=11043 + _globals['_RPCHTTP_NULLABLEHEADERSENTRY']._serialized_start=11045 + _globals['_RPCHTTP_NULLABLEHEADERSENTRY']._serialized_end=11116 + _globals['_RPCHTTP_NULLABLEPARAMSENTRY']._serialized_start=11118 + _globals['_RPCHTTP_NULLABLEPARAMSENTRY']._serialized_end=11188 + _globals['_RPCHTTP_NULLABLEQUERYENTRY']._serialized_start=11190 + _globals['_RPCHTTP_NULLABLEQUERYENTRY']._serialized_end=11259 + _globals['_MODELBINDINGDATA']._serialized_start=11261 + _globals['_MODELBINDINGDATA']._serialized_end=11351 + _globals['_COLLECTIONMODELBINDINGDATA']._serialized_start=11353 + _globals['_COLLECTIONMODELBINDINGDATA']._serialized_end=11454 + _globals['_RPCRETRYOPTIONS']._serialized_start=11457 + _globals['_RPCRETRYOPTIONS']._serialized_end=11797 + _globals['_RPCRETRYOPTIONS_RETRYSTRATEGY']._serialized_start=11740 + _globals['_RPCRETRYOPTIONS_RETRYSTRATEGY']._serialized_end=11797 + _globals['_FUNCTIONRPC']._serialized_start=11995 + _globals['_FUNCTIONRPC']._serialized_end=12119 +# @@protoc_insertion_point(module_scope) diff --git a/tests/protos/FunctionRpc_pb2_grpc.py b/tests/protos/FunctionRpc_pb2_grpc.py new file mode 100644 index 000000000..364658aa9 --- /dev/null +++ b/tests/protos/FunctionRpc_pb2_grpc.py @@ -0,0 +1,69 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +from . import FunctionRpc_pb2 as FunctionRpc__pb2 + + +class FunctionRpcStub(object): + """Interface exported by the server. + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.EventStream = channel.stream_stream( + '/AzureFunctionsRpcMessages.FunctionRpc/EventStream', + request_serializer=FunctionRpc__pb2.StreamingMessage.SerializeToString, + response_deserializer=FunctionRpc__pb2.StreamingMessage.FromString, + ) + + +class FunctionRpcServicer(object): + """Interface exported by the server. + """ + + def EventStream(self, request_iterator, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_FunctionRpcServicer_to_server(servicer, server): + rpc_method_handlers = { + 'EventStream': grpc.stream_stream_rpc_method_handler( + servicer.EventStream, + request_deserializer=FunctionRpc__pb2.StreamingMessage.FromString, + response_serializer=FunctionRpc__pb2.StreamingMessage.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'AzureFunctionsRpcMessages.FunctionRpc', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class FunctionRpc(object): + """Interface exported by the server. + """ + + @staticmethod + def EventStream(request_iterator, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.stream_stream(request_iterator, target, '/AzureFunctionsRpcMessages.FunctionRpc/EventStream', + FunctionRpc__pb2.StreamingMessage.SerializeToString, + FunctionRpc__pb2.StreamingMessage.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/azure_functions_worker/protos/__init__.py b/tests/protos/__init__.py similarity index 100% rename from azure_functions_worker/protos/__init__.py rename to tests/protos/__init__.py diff --git a/tests/protos/identity/ClaimsIdentityRpc_pb2.py b/tests/protos/identity/ClaimsIdentityRpc_pb2.py new file mode 100644 index 000000000..e4a2be477 --- /dev/null +++ b/tests/protos/identity/ClaimsIdentityRpc_pb2.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: identity/ClaimsIdentityRpc.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from ..shared import NullableTypes_pb2 as shared_dot_NullableTypes__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n identity/ClaimsIdentityRpc.proto\x1a\x1ashared/NullableTypes.proto\"\xb0\x01\n\x11RpcClaimsIdentity\x12,\n\x13\x61uthentication_type\x18\x01 \x01(\x0b\x32\x0f.NullableString\x12(\n\x0fname_claim_type\x18\x02 \x01(\x0b\x32\x0f.NullableString\x12(\n\x0frole_claim_type\x18\x03 \x01(\x0b\x32\x0f.NullableString\x12\x19\n\x06\x63laims\x18\x04 \x03(\x0b\x32\t.RpcClaim\"\'\n\x08RpcClaim\x12\r\n\x05value\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\tB,\n*com.microsoft.azure.functions.rpc.messagesb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'identity.ClaimsIdentityRpc_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n*com.microsoft.azure.functions.rpc.messages' + _globals['_RPCCLAIMSIDENTITY']._serialized_start=65 + _globals['_RPCCLAIMSIDENTITY']._serialized_end=241 + _globals['_RPCCLAIM']._serialized_start=243 + _globals['_RPCCLAIM']._serialized_end=282 +# @@protoc_insertion_point(module_scope) diff --git a/tests/protos/identity/ClaimsIdentityRpc_pb2_grpc.py b/tests/protos/identity/ClaimsIdentityRpc_pb2_grpc.py new file mode 100644 index 000000000..2daafffeb --- /dev/null +++ b/tests/protos/identity/ClaimsIdentityRpc_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/azure_functions_worker/_thirdparty/__init__.py b/tests/protos/identity/__init__.py similarity index 100% rename from azure_functions_worker/_thirdparty/__init__.py rename to tests/protos/identity/__init__.py diff --git a/tests/protos/shared/NullableTypes_pb2.py b/tests/protos/shared/NullableTypes_pb2.py new file mode 100644 index 000000000..0b5b96bf1 --- /dev/null +++ b/tests/protos/shared/NullableTypes_pb2.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: shared/NullableTypes.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ashared/NullableTypes.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"+\n\x0eNullableString\x12\x0f\n\x05value\x18\x01 \x01(\tH\x00\x42\x08\n\x06string\"+\n\x0eNullableDouble\x12\x0f\n\x05value\x18\x01 \x01(\x01H\x00\x42\x08\n\x06\x64ouble\"\'\n\x0cNullableBool\x12\x0f\n\x05value\x18\x01 \x01(\x08H\x00\x42\x06\n\x04\x62ool\"M\n\x11NullableTimestamp\x12+\n\x05value\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x00\x42\x0b\n\ttimestampB,\n*com.microsoft.azure.functions.rpc.messagesb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'shared.NullableTypes_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n*com.microsoft.azure.functions.rpc.messages' + _globals['_NULLABLESTRING']._serialized_start=63 + _globals['_NULLABLESTRING']._serialized_end=106 + _globals['_NULLABLEDOUBLE']._serialized_start=108 + _globals['_NULLABLEDOUBLE']._serialized_end=151 + _globals['_NULLABLEBOOL']._serialized_start=153 + _globals['_NULLABLEBOOL']._serialized_end=192 + _globals['_NULLABLETIMESTAMP']._serialized_start=194 + _globals['_NULLABLETIMESTAMP']._serialized_end=271 +# @@protoc_insertion_point(module_scope) diff --git a/tests/protos/shared/NullableTypes_pb2_grpc.py b/tests/protos/shared/NullableTypes_pb2_grpc.py new file mode 100644 index 000000000..2daafffeb --- /dev/null +++ b/tests/protos/shared/NullableTypes_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/azure_functions_worker/protos/identity/__init__.py b/tests/protos/shared/__init__.py similarity index 100% rename from azure_functions_worker/protos/identity/__init__.py rename to tests/protos/shared/__init__.py diff --git a/tests/test_setup.py b/tests/test_setup.py deleted file mode 100644 index fd6f0044e..000000000 --- a/tests/test_setup.py +++ /dev/null @@ -1,304 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -""" -Usage: -This file defines tasks for building Protos, webhost and extensions - -To use these tasks, you can run the following commands: - -1. Build protos: - invoke -c test_setup build-protos - -2. Set up the Azure Functions Web Host: - invoke -c test_setup webhost - -3. Install WebJobs extensions: - invoke -c test_setup extensions -""" - -import glob -import json -import os -import pathlib -import re -import shutil -import subprocess -import sys -import tempfile -import urllib.request -import zipfile -from distutils import dir_util - -from invoke import task - -from utils.constants import EXTENSIONS_CSPROJ_TEMPLATE, NUGET_CONFIG - -ROOT_DIR = pathlib.Path(__file__).parent.parent -BUILD_DIR = ROOT_DIR / 'build' -WEBHOST_GITHUB_API = "https://api.github.com/repos/Azure/azure-functions-host" -WEBHOST_GIT_REPO = "https://github.com/Azure/azure-functions-host/archive" -WEBHOST_TAG_PREFIX = "v4." - - -def get_webhost_version() -> str: - # Return the latest matched version (e.g. 4.39.1) - github_api_url = f"{WEBHOST_GITHUB_API}/tags?page=1&per_page=10" - print(f"Checking latest webhost version from {github_api_url}") - github_response = urllib.request.urlopen(github_api_url) - tags = json.loads(github_response.read()) - - # As tags are placed in time desending order, the latest v3 - # tag should be the first occurance starts with 'v3.' string - latest = [gt for gt in tags if gt["name"].startswith(WEBHOST_TAG_PREFIX)] - return latest[0]["name"].replace("v", "") - - -def download_webhost_zip(version, branch): - with tempfile.NamedTemporaryFile(delete=False) as temp_file: - if branch: - zip_url = f"{WEBHOST_GIT_REPO}/refs/heads/{branch}.zip" - else: - zip_url = f"{WEBHOST_GIT_REPO}/v{version}.zip" - - print(f"Downloading Functions Host from {zip_url}") - try: - urllib.request.urlretrieve(zip_url, temp_file.name) - except Exception as e: - print( - f"Failed to download Functions Host source code from {zip_url}: {e}", - file=sys.stderr) - sys.exit(1) - return temp_file.name - - -def create_webhost_folder(dest_folder): - if dest_folder.exists(): - shutil.rmtree(dest_folder) - os.makedirs(dest_folder, exist_ok=True) - print(f"Functions Host folder is created in {dest_folder}") - - -def extract_webhost_zip(version, src_zip, dest): - print(f"Extracting Functions Host from {src_zip}") - with zipfile.ZipFile(src_zip, 'r') as archive: - for archive_name in archive.namelist(): - prefix = f"azure-functions-host-{version}/" - if archive_name.startswith(prefix): - sanitized_name = archive_name.replace("\\", os.sep).replace( - prefix, "") - dest_filename = dest / sanitized_name - zipinfo = archive.getinfo(archive_name) - if not dest_filename.parent.exists(): - os.makedirs(dest_filename.parent, exist_ok=True) - if zipinfo.is_dir(): - os.makedirs(dest_filename, exist_ok=True) - else: - with archive.open(archive_name) as src, open(dest_filename, - "wb") as dst: - dst.write(src.read()) - print(f"Functions Host is extracted into {dest}") - - -def chmod_protobuf_generation_script(webhost_dir): - script_path = webhost_dir / "src" / "WebJobs.Script.Grpc" / "generate_protos.sh" - if sys.platform != "win32" and script_path.exists(): - print("Change generate_protos.sh script permission") - os.chmod(script_path, 0o555) - - -def compile_webhost(webhost_dir): - print(f"Compiling Functions Host from {webhost_dir}") - try: - subprocess.run( - ["dotnet", "build", "WebJobs.Script.sln", "-o", "bin", - "/p:TreatWarningsAsErrors=false"], - check=True, - cwd=str(webhost_dir), - stdout=sys.stdout, - stderr=sys.stderr, - ) - except subprocess.CalledProcessError: - print( - f"Failed to compile webhost in {webhost_dir}. " - ".NET Core SDK is required to build the solution. " - "Please visit https://aka.ms/dotnet-download", - file=sys.stderr, - ) - sys.exit(1) - print("Functions Host is compiled successfully") - - -def gen_grpc(): - proto_root_dir = ROOT_DIR / "azure_functions_worker" / "protos" - proto_src_dir = proto_root_dir / "_src" / "src" / "proto" - staging_root_dir = BUILD_DIR / "protos" - staging_dir = staging_root_dir / "azure_functions_worker" / "protos" - built_protos_dir = BUILD_DIR / "built_protos" - - if os.path.exists(BUILD_DIR): - shutil.rmtree(BUILD_DIR) - - shutil.copytree(proto_src_dir, staging_dir) - os.makedirs(built_protos_dir) - - protos = [ - os.sep.join(("shared", "NullableTypes.proto")), - os.sep.join(("identity", "ClaimsIdentityRpc.proto")), - "FunctionRpc.proto", - ] - - for proto in protos: - subprocess.run( - [ - sys.executable, - "-m", - "grpc_tools.protoc", - "-I", - os.sep.join(("azure_functions_worker", "protos")), - "--python_out", - str(built_protos_dir), - "--grpc_python_out", - str(built_protos_dir), - os.sep.join(("azure_functions_worker", "protos", proto)), - ], - check=True, - stdout=sys.stdout, - stderr=sys.stderr, - cwd=staging_root_dir, - ) - - compiled_files = glob.glob( - str(built_protos_dir / "**" / "*.py"), recursive=True - ) - - if not compiled_files: - print("grpc_tools.protoc produced no Python files", file=sys.stderr) - sys.exit(1) - - # Needed to support absolute imports in files. See - # https://github.com/protocolbuffers/protobuf/issues/1491 - make_absolute_imports(compiled_files) - - dir_util.copy_tree(str(built_protos_dir), str(proto_root_dir)) - - -def make_absolute_imports(compiled_files): - for compiled in compiled_files: - with open(compiled, "r+") as f: - content = f.read() - f.seek(0) - # Convert lines of the form: - # import xxx_pb2 as xxx__pb2 to - # from azure_functions_worker.protos import xxx_pb2 as.. - p1 = re.sub( - r"\nimport (.*?_pb2)", - r"\nfrom azure_functions_worker.protos import \g<1>", - content, - ) - # Convert lines of the form: - # from identity import xxx_pb2 as.. to - # from azure_functions_worker.protos.identity import xxx_pb2.. - p2 = re.sub( - r"from ([a-z]*) (import.*_pb2)", - r"from azure_functions_worker.protos.\g<1> \g<2>", - p1, - ) - f.write(p2) - f.truncate() - - -def install_extensions(extensions_dir): - if not extensions_dir.exists(): - os.makedirs(extensions_dir, exist_ok=True) - - if not (extensions_dir / "host.json").exists(): - with open(extensions_dir / "host.json", "w") as f: - f.write("{}") - - if not (extensions_dir / "extensions.csproj").exists(): - with open(extensions_dir / "extensions.csproj", "w") as f: - f.write(EXTENSIONS_CSPROJ_TEMPLATE) - - with open(extensions_dir / "NuGet.config", "w") as f: - f.write(NUGET_CONFIG) - - env = os.environ.copy() - env["TERM"] = "xterm" # ncurses 6.1 workaround - try: - subprocess.run( - args=["dotnet", "build", "-o", "."], - check=True, - cwd=str(extensions_dir), - stdout=sys.stdout, - stderr=sys.stderr, - env=env, - ) - except subprocess.CalledProcessError: - print( - ".NET Core SDK is required to build the extensions. " - "Please visit https://aka.ms/dotnet-download" - ) - sys.exit(1) - - -@task -def extensions(c, clean=False, extensions_dir=None): - """Build extensions.""" - extensions_dir = extensions_dir or BUILD_DIR / "extensions" - if clean: - print(f"Deleting Extensions Directory: {extensions_dir}") - shutil.rmtree(extensions_dir, ignore_errors=True) - print("Deleted Extensions Directory") - return - - print("Installing Extensions") - install_extensions(extensions_dir) - print("Extensions installed successfully.") - - -@task -def build_protos(c, clean=False): - """Build gRPC bindings.""" - - if clean: - shutil.rmtree(BUILD_DIR / 'protos') - return - print("Generating gRPC bindings...") - gen_grpc() - print("gRPC bindings generated successfully.") - - -@task -def webhost(c, clean=False, webhost_version=None, webhost_dir=None, - branch_name=None): - """Builds the webhost""" - - if webhost_dir is None: - webhost_dir = BUILD_DIR / "webhost" - else: - webhost_dir = pathlib.Path(webhost_dir) - - if clean: - print("Deleting webhost dir") - shutil.rmtree(webhost_dir, ignore_errors=True) - print("Deleted webhost dir") - return - - if webhost_version is None: - webhost_version = get_webhost_version() - - zip_path = download_webhost_zip(webhost_version, branch_name) - create_webhost_folder(webhost_dir) - version = branch_name or webhost_version - extract_webhost_zip(version.replace("/", "-"), zip_path, webhost_dir) - chmod_protobuf_generation_script(webhost_dir) - compile_webhost(webhost_dir) - - -@task -def clean(c): - """Clean build directory.""" - - print("Deleting build directory") - shutil.rmtree(BUILD_DIR, ignore_errors=True) - print("Deleted build directory") diff --git a/tests/emulator_tests/blob_functions/blob_functions_stein/function_app.py b/tests/unit_tests/function_app.py similarity index 81% rename from tests/emulator_tests/blob_functions/blob_functions_stein/function_app.py rename to tests/unit_tests/function_app.py index 24489b0e6..51349a631 100644 --- a/tests/emulator_tests/blob_functions/blob_functions_stein/function_app.py +++ b/tests/unit_tests/function_app.py @@ -52,11 +52,11 @@ def get_blob_as_bytes_return_http_response(req: func.HttpRequest, file: bytes) \ assert isinstance(file, bytes) content_size = len(file) - content_sha256 = hashlib.sha256(file).hexdigest() + content_md5 = hashlib.md5(file).hexdigest() response_dict = { 'content_size': content_size, - 'content_sha256': content_sha256 + 'content_md5': content_md5 } response_body = json.dumps(response_dict, indent=2) @@ -84,11 +84,11 @@ def get_blob_as_bytes_stream_return_http_response(req: func.HttpRequest, file_bytes = file.read() content_size = len(file_bytes) - content_sha256 = hashlib.sha256(file_bytes).hexdigest() + content_md5 = hashlib.md5(file_bytes).hexdigest() response_dict = { 'content_size': content_size, - 'content_sha256': content_sha256 + 'content_md5': content_md5 } response_body = json.dumps(response_dict, indent=2) @@ -127,11 +127,11 @@ def get_blob_as_str_return_http_response(req: func.HttpRequest, num_chars = len(file) content_bytes = file.encode('utf-8') - content_sha256 = hashlib.sha256(content_bytes).hexdigest() + content_md5 = hashlib.md5(content_bytes).hexdigest() response_dict = { 'num_chars': num_chars, - 'content_sha256': content_sha256 + 'content_md5': content_md5 } response_body = json.dumps(response_dict, indent=2) @@ -212,13 +212,13 @@ def put_blob_as_bytes_return_http_response(req: func.HttpRequest, content = b'\x01' * content_size else: content = bytearray(random.getrandbits(8) for _ in range(content_size)) - content_sha256 = hashlib.sha256(content).hexdigest() + content_md5 = hashlib.md5(content).hexdigest() file.set(content) response_dict = { 'content_size': content_size, - 'content_sha256': content_sha256 + 'content_md5': content_md5 } response_body = json.dumps(response_dict, indent=2) @@ -249,14 +249,14 @@ def put_blob_as_str_return_http_response(req: func.HttpRequest, file: func.Out[ k=num_chars)) content_bytes = content.encode('utf-8') content_size = len(content_bytes) - content_sha256 = hashlib.sha256(content_bytes).hexdigest() + content_md5 = hashlib.md5(content_bytes).hexdigest() file.set(content) response_dict = { 'num_chars': num_chars, 'content_size': content_size, - 'content_sha256': content_sha256 + 'content_md5': content_md5 } response_body = json.dumps(response_dict, indent=2) @@ -321,8 +321,8 @@ def put_blob_trigger(req: func.HttpRequest, file: func.Out[str]) -> str: def _generate_content_and_digest(content_size): content = bytearray(random.getrandbits(8) for _ in range(content_size)) - content_sha256 = hashlib.sha256(content).hexdigest() - return content, content_sha256 + content_md5 = hashlib.md5(content).hexdigest() + return content, content_md5 @app.function_name(name="put_get_multiple_blobs_as_bytes_return_http_response") @@ -359,15 +359,15 @@ def put_get_multiple_blobs_as_bytes_return_http_response( input_content_size_1 = len(inputfile1) input_content_size_2 = len(inputfile2) - input_content_sha256_1 = hashlib.sha256(inputfile1).hexdigest() - input_content_sha256_2 = hashlib.sha256(inputfile2).hexdigest() + input_content_md5_1 = hashlib.md5(inputfile1).hexdigest() + input_content_md5_2 = hashlib.md5(inputfile2).hexdigest() output_content_size_1 = int(req.params['output_content_size_1']) output_content_size_2 = int(req.params['output_content_size_2']) - output_content_1, output_content_sha256_1 = \ + output_content_1, output_content_md5_1 = \ _generate_content_and_digest(output_content_size_1) - output_content_2, output_content_sha256_2 = \ + output_content_2, output_content_md5_2 = \ _generate_content_and_digest(output_content_size_2) outputfile1.set(output_content_1) @@ -376,12 +376,12 @@ def put_get_multiple_blobs_as_bytes_return_http_response( response_dict = { 'input_content_size_1': input_content_size_1, 'input_content_size_2': input_content_size_2, - 'input_content_sha256_1': input_content_sha256_1, - 'input_content_sha256_2': input_content_sha256_2, + 'input_content_md5_1': input_content_md5_1, + 'input_content_md5_2': input_content_md5_2, 'output_content_size_1': output_content_size_1, 'output_content_size_2': output_content_size_2, - 'output_content_sha256_1': output_content_sha256_1, - 'output_content_sha256_2': output_content_sha256_2 + 'output_content_md5_1': output_content_md5_1, + 'output_content_md5_2': output_content_md5_2 } response_body = json.dumps(response_dict, indent=2) @@ -391,55 +391,3 @@ def put_get_multiple_blobs_as_bytes_return_http_response( mimetype="application/json", status_code=200 ) - - -@app.function_name(name="blob_trigger_default_source_enum") -@app.blob_trigger(arg_name="file", - path="python-worker-tests/test-blob-trigger.txt", - connection="AzureWebJobsStorage", - source=func.BlobSource.LOGS_AND_CONTAINER_SCAN) -def blob_trigger_default_source_enum(file: func.InputStream) -> str: - return json.dumps({ - 'name': file.name, - 'length': file.length, - 'content': file.read().decode('utf-8') - }) - - -@app.function_name(name="blob_trigger_eventgrid_source_enum") -@app.blob_trigger(arg_name="file", - path="python-worker-tests/test-blob-trigger.txt", - connection="AzureWebJobsStorage", - source=func.BlobSource.EVENT_GRID) -def blob_trigger_eventgrid_source_enum(file: func.InputStream) -> str: - return json.dumps({ - 'name': file.name, - 'length': file.length, - 'content': file.read().decode('utf-8') - }) - - -@app.function_name(name="blob_trigger_default_source_str") -@app.blob_trigger(arg_name="file", - path="python-worker-tests/test-blob-trigger.txt", - connection="AzureWebJobsStorage", - source="LogsAndContainerScan") -def blob_trigger_default_source_str(file: func.InputStream) -> str: - return json.dumps({ - 'name': file.name, - 'length': file.length, - 'content': file.read().decode('utf-8') - }) - - -@app.function_name(name="blob_trigger_eventgrid_source_str") -@app.blob_trigger(arg_name="file", - path="python-worker-tests/test-blob-trigger.txt", - connection="AzureWebJobsStorage", - source="EventGrid") -def blob_trigger_eventgrid_source_str(file: func.InputStream) -> str: - return json.dumps({ - 'name': file.name, - 'length': file.length, - 'content': file.read().decode('utf-8') - }) diff --git a/tests/unit_tests/test_handle_event.py b/tests/unit_tests/test_handle_event.py new file mode 100644 index 000000000..ed5c51b76 --- /dev/null +++ b/tests/unit_tests/test_handle_event.py @@ -0,0 +1,117 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import asyncio +import unittest + +from typing import Any + +from tests.utils import testutils +import tests.protos as protos + +from azure_functions_worker.handle_event import (worker_init_request, + functions_metadata_request, + function_environment_reload_request) + + +class WorkerRequest: + def __init__(self, name: str, request: Any, properties: dict): + self.name = name + self.request = request + self.properties = properties + + +class InnerRequest: + def __init__(self, name: Any): + self.worker_init_request = name + self.function_environment_reload_request = name + + +class InnerInnerRequest: + def __init__(self, name: Any): + self.capabilities = name + self.function_app_directory = "tests\\unit_tests" + + +class TestObjects(unittest.TestCase): + def test_stringify_enum(self): + pass + + def test_status(self): + pass + + def test_worker_response(self): + pass + + +class TestHandleEvent(testutils.AsyncTestCase): + async def test_worker_init_request(self): + worker_request = WorkerRequest(name='worker_init_request', + request=InnerRequest(InnerInnerRequest('hello')), + properties={'host': '123', + 'protos': protos}) + result = await worker_init_request(worker_request) + self.assertEqual(result.capabilities, {'WorkerStatus': 'true', 'RpcHttpBodyOnly': 'true', 'SharedMemoryDataTransfer': 'true', 'RpcHttpTriggerMetadataRemoved': 'true', 'RawHttpBodyBytes': 'true', 'TypedDataCollection': 'true'}) + self.assertEqual(result.worker_metadata.runtime_name, "python") + self.assertIsNotNone(result.worker_metadata.runtime_version) + self.assertIsNotNone(result.worker_metadata.worker_version) + self.assertIsNotNone(result.worker_metadata.worker_bitness) + self.assertEqual(result.result.status, 1) + + def test_worker_init_request_with_streaming(self): + pass + + def test_worker_init_request_with_exception(self): + pass + + async def test_functions_metadata_request(self): + result = await self.run_init_then_meta() + self.assertEqual(result.use_default_metadata_indexing, False) + self.assertIsNotNone(result.function_metadata_results) + self.assertEqual(result.result.status, 1) + + async def run_init_then_meta(self): + worker_request = WorkerRequest(name='worker_init_request', + request=InnerRequest(InnerInnerRequest('hello')), + properties={'host': '123', + 'protos': protos}) + _ = await worker_init_request(worker_request) + result = await functions_metadata_request(worker_request) + return result + + def test_functions_metadata_request_with_exception(self): + pass + + def test_invocation_request_sync(self): + pass + + def test_invocation_request_async(self): + pass + + def test_invocation_request_with_exception(self): + pass + + async def test_function_environment_reload_request(self): + worker_request = WorkerRequest(name='function_environment_reload_request', + request=InnerRequest(InnerInnerRequest('hello')), + properties={'host': '123', + 'protos': protos}) + result = await function_environment_reload_request(worker_request) + self.assertEqual(result.capabilities, {}) + self.assertEqual(result.worker_metadata.runtime_name, "python") + self.assertIsNotNone(result.worker_metadata.runtime_version) + self.assertIsNotNone(result.worker_metadata.worker_version) + self.assertIsNotNone(result.worker_metadata.worker_bitness) + self.assertEqual(result.result.status, 1) + + def test_function_environment_reload_request_with_streaming(self): + pass + + def test_function_environment_reload_request_with_exception(self): + pass + + def test_load_function_metadata(self): + pass + + def test_index_functions(self): + pass diff --git a/tests/unittests/azure_namespace_import/azure_namespace_import.py b/tests/unittests/azure_namespace_import/azure_namespace_import.py deleted file mode 100644 index a7490cf50..000000000 --- a/tests/unittests/azure_namespace_import/azure_namespace_import.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import asyncio -import os -import shutil -import sys - -from azure_functions_worker import protos - -from ...utils.testutils import UNIT_TESTS_ROOT, create_dummy_dispatcher - - -async def verify_nested_namespace_import(): - test_env = {} - request = protos.FunctionEnvironmentReloadRequest( - environment_variables=test_env) - - request_msg = protos.StreamingMessage( - request_id='0', - function_environment_reload_request=request) - - disp = create_dummy_dispatcher() - - # Mock intepreter starts in placeholder mode - import azure.module_a as mod_a # noqa: F401 - - # Mock function specialization, load customer's libraries and functionapps - ns_root = os.path.join( - UNIT_TESTS_ROOT, - 'azure_namespace_import', - 'namespace_location_b') - test_path = os.path.join(ns_root, 'azure', 'namespace_b', 'module_b') - test_mod_path = os.path.join(test_path, 'test_module.py') - - os.makedirs(test_path) - with open(test_mod_path, 'w') as f: - f.write('MESSAGE = "module_b is imported"') - - try: - # Mock a customer uses test_module - if sys.argv[1].lower() == 'true': - await disp._handle__function_environment_reload_request( - request_msg) - from azure.namespace_b.module_b import test_module - print(test_module.MESSAGE) - except ModuleNotFoundError: - print('module_b fails to import') - finally: - # Cleanup - shutil.rmtree(ns_root) - - -if __name__ == '__main__': - loop = asyncio.get_event_loop() - loop.run_until_complete(verify_nested_namespace_import()) - loop.close() diff --git a/tests/unittests/azure_namespace_import/namespace_location_a/azure/module_a/__init__.py b/tests/unittests/azure_namespace_import/namespace_location_a/azure/module_a/__init__.py deleted file mode 100644 index 30adb862c..000000000 --- a/tests/unittests/azure_namespace_import/namespace_location_a/azure/module_a/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -MESSAGE = "module_a is imported" diff --git a/tests/unittests/azure_namespace_import/test_azure_namespace_import.sh b/tests/unittests/azure_namespace_import/test_azure_namespace_import.sh deleted file mode 100644 index b6afe032f..000000000 --- a/tests/unittests/azure_namespace_import/test_azure_namespace_import.sh +++ /dev/null @@ -1,10 +0,0 @@ -#! /bin/bash - -# $1 controls whether we allow reload module ("true" or "false") - -SCRIPT_DIR="$(dirname $0)" -export PYTHONPATH="$SCRIPT_DIR/namespace_location_a:$SCRIPT_DIR/namespace_location_b" - -python $SCRIPT_DIR/azure_namespace_import.py $1 - -unset PYTHONPATH \ No newline at end of file diff --git a/tests/unittests/broken_functions/README.md b/tests/unittests/broken_functions/README.md deleted file mode 100644 index 9601a892a..000000000 --- a/tests/unittests/broken_functions/README.md +++ /dev/null @@ -1,3 +0,0 @@ -Functions in this directory are purposefully "broken". They either have -missing information in `function.json`, or invalid signatures, or even -syntax errors. They are tested in "test_broken_functions.py". diff --git a/tests/unittests/broken_functions/bad_out_annotation/function.json b/tests/unittests/broken_functions/bad_out_annotation/function.json deleted file mode 100644 index 736b93690..000000000 --- a/tests/unittests/broken_functions/bad_out_annotation/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "direction": "out", - "name": "foo", - "type": "int" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/broken_functions/bad_out_annotation/main.py b/tests/unittests/broken_functions/bad_out_annotation/main.py deleted file mode 100644 index 3c8cf73c4..000000000 --- a/tests/unittests/broken_functions/bad_out_annotation/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req, foo: azf.Out): - return 'trust me, it is OK!' diff --git a/tests/unittests/broken_functions/import_error/function.json b/tests/unittests/broken_functions/import_error/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/broken_functions/import_error/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/broken_functions/import_error/main.py b/tests/unittests/broken_functions/import_error/main.py deleted file mode 100644 index ade8ed183..000000000 --- a/tests/unittests/broken_functions/import_error/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from sys import __nonexistent # should raise ImportError - - -def main(req): - __nonexistent() diff --git a/tests/unittests/broken_functions/inout_param/function.json b/tests/unittests/broken_functions/inout_param/function.json deleted file mode 100644 index 6f5f71254..000000000 --- a/tests/unittests/broken_functions/inout_param/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "inout", - "name": "abc" - } - ] -} diff --git a/tests/unittests/broken_functions/inout_param/main.py b/tests/unittests/broken_functions/inout_param/main.py deleted file mode 100644 index 2ab233cef..000000000 --- a/tests/unittests/broken_functions/inout_param/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(req, abc): - return 'trust me, it is OK!' diff --git a/tests/unittests/broken_functions/invalid_app_stein/function_app.py b/tests/unittests/broken_functions/invalid_app_stein/function_app.py deleted file mode 100644 index 3454a59ed..000000000 --- a/tests/unittests/broken_functions/invalid_app_stein/function_app.py +++ /dev/null @@ -1,5 +0,0 @@ -import azure.functions as func - - -def main(req: func.HttpRequest): - pass diff --git a/tests/unittests/broken_functions/invalid_context_param/function.json b/tests/unittests/broken_functions/invalid_context_param/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/broken_functions/invalid_context_param/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/broken_functions/invalid_context_param/main.py b/tests/unittests/broken_functions/invalid_context_param/main.py deleted file mode 100644 index 290c270a7..000000000 --- a/tests/unittests/broken_functions/invalid_context_param/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(req, context: int): - return 'trust me, it is OK!' diff --git a/tests/unittests/broken_functions/invalid_datatype/function.json b/tests/unittests/broken_functions/invalid_datatype/function.json deleted file mode 100644 index 247beea27..000000000 --- a/tests/unittests/broken_functions/invalid_datatype/function.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "dataType" : "string", - "name": "req" - } - ] -} diff --git a/tests/unittests/broken_functions/invalid_datatype/main.py b/tests/unittests/broken_functions/invalid_datatype/main.py deleted file mode 100644 index 0fbe6b520..000000000 --- a/tests/unittests/broken_functions/invalid_datatype/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpResponse): - return 'This function should fail!!' diff --git a/tests/unittests/broken_functions/invalid_http_trigger_anno/function.json b/tests/unittests/broken_functions/invalid_http_trigger_anno/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/broken_functions/invalid_http_trigger_anno/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/broken_functions/invalid_http_trigger_anno/main.py b/tests/unittests/broken_functions/invalid_http_trigger_anno/main.py deleted file mode 100644 index 6f25e6b41..000000000 --- a/tests/unittests/broken_functions/invalid_http_trigger_anno/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(req: int): - return 'trust me, it is OK!' diff --git a/tests/unittests/broken_functions/invalid_in_anno/function.json b/tests/unittests/broken_functions/invalid_in_anno/function.json deleted file mode 100644 index da37649e4..000000000 --- a/tests/unittests/broken_functions/invalid_in_anno/function.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - } - ] -} diff --git a/tests/unittests/broken_functions/invalid_in_anno/main.py b/tests/unittests/broken_functions/invalid_in_anno/main.py deleted file mode 100644 index fc0ae8ad4..000000000 --- a/tests/unittests/broken_functions/invalid_in_anno/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpResponse): # should be azf.HttpRequest - return 'trust me, it is OK!' diff --git a/tests/unittests/broken_functions/invalid_in_anno_non_type/function.json b/tests/unittests/broken_functions/invalid_in_anno_non_type/function.json deleted file mode 100644 index da37649e4..000000000 --- a/tests/unittests/broken_functions/invalid_in_anno_non_type/function.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - } - ] -} diff --git a/tests/unittests/broken_functions/invalid_in_anno_non_type/main.py b/tests/unittests/broken_functions/invalid_in_anno_non_type/main.py deleted file mode 100644 index fa44422a1..000000000 --- a/tests/unittests/broken_functions/invalid_in_anno_non_type/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(req: 123): # annotations must be types! - return 'trust me, it is OK!' diff --git a/tests/unittests/broken_functions/invalid_out_anno/function.json b/tests/unittests/broken_functions/invalid_out_anno/function.json deleted file mode 100644 index 0c06cc22f..000000000 --- a/tests/unittests/broken_functions/invalid_out_anno/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "ret" - } - ] -} diff --git a/tests/unittests/broken_functions/invalid_out_anno/main.py b/tests/unittests/broken_functions/invalid_out_anno/main.py deleted file mode 100644 index b50a8d536..000000000 --- a/tests/unittests/broken_functions/invalid_out_anno/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req, ret: azf.Out[azf.HttpRequest]): - return 'trust me, it is OK!' diff --git a/tests/unittests/broken_functions/invalid_return_anno/function.json b/tests/unittests/broken_functions/invalid_return_anno/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/broken_functions/invalid_return_anno/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/broken_functions/invalid_return_anno/main.py b/tests/unittests/broken_functions/invalid_return_anno/main.py deleted file mode 100644 index e15ef70d7..000000000 --- a/tests/unittests/broken_functions/invalid_return_anno/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(req) -> int: - return 'trust me, it is OK!' diff --git a/tests/unittests/broken_functions/invalid_return_anno_non_type/function.json b/tests/unittests/broken_functions/invalid_return_anno_non_type/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/broken_functions/invalid_return_anno_non_type/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/broken_functions/invalid_return_anno_non_type/main.py b/tests/unittests/broken_functions/invalid_return_anno_non_type/main.py deleted file mode 100644 index b3fdb6842..000000000 --- a/tests/unittests/broken_functions/invalid_return_anno_non_type/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(req) -> 123: - return 'trust me, it is OK!' diff --git a/tests/unittests/broken_functions/invalid_stein/function_app.py b/tests/unittests/broken_functions/invalid_stein/function_app.py deleted file mode 100644 index d6ddd39d9..000000000 --- a/tests/unittests/broken_functions/invalid_stein/function_app.py +++ /dev/null @@ -1,8 +0,0 @@ -import azure.functions as func - -app = func.FunctionApp() - - -@app.route() -def main(): - pass diff --git a/tests/unittests/broken_functions/missing_json_param/function.json b/tests/unittests/broken_functions/missing_json_param/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/broken_functions/missing_json_param/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/broken_functions/missing_json_param/main.py b/tests/unittests/broken_functions/missing_json_param/main.py deleted file mode 100644 index 110dfad1f..000000000 --- a/tests/unittests/broken_functions/missing_json_param/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(req, spam): - return 'trust me, it is OK!' diff --git a/tests/unittests/broken_functions/missing_module/function.json b/tests/unittests/broken_functions/missing_module/function.json deleted file mode 100644 index 985453fcf..000000000 --- a/tests/unittests/broken_functions/missing_module/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] - } diff --git a/tests/unittests/broken_functions/missing_module/main.py b/tests/unittests/broken_functions/missing_module/main.py deleted file mode 100644 index 16e9e5d5f..000000000 --- a/tests/unittests/broken_functions/missing_module/main.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import logging - -import azure.functions -import does_not_exist # Noqa - -logger = logging.getLogger('my function') - - -def main(req: azure.functions.HttpRequest): - logger.info('Function should fail before hitting main') - return 'OK-async' diff --git a/tests/unittests/broken_functions/missing_py_param/function.json b/tests/unittests/broken_functions/missing_py_param/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/broken_functions/missing_py_param/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/broken_functions/missing_py_param/main.py b/tests/unittests/broken_functions/missing_py_param/main.py deleted file mode 100644 index 7ac88c6dc..000000000 --- a/tests/unittests/broken_functions/missing_py_param/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(): - return 'trust me, it is OK!' diff --git a/tests/unittests/broken_functions/module_not_found_error/function.json b/tests/unittests/broken_functions/module_not_found_error/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/broken_functions/module_not_found_error/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/broken_functions/module_not_found_error/main.py b/tests/unittests/broken_functions/module_not_found_error/main.py deleted file mode 100644 index 57f5f134f..000000000 --- a/tests/unittests/broken_functions/module_not_found_error/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from __nonexistent import foo # should raise ModuleNotFoundError - - -def main(req): - foo() diff --git a/tests/unittests/broken_functions/return_param_in/function.json b/tests/unittests/broken_functions/return_param_in/function.json deleted file mode 100644 index 2d96d3cf5..000000000 --- a/tests/unittests/broken_functions/return_param_in/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "in", - "name": "$return" - } - ] -} diff --git a/tests/unittests/broken_functions/return_param_in/main.py b/tests/unittests/broken_functions/return_param_in/main.py deleted file mode 100644 index cc865f340..000000000 --- a/tests/unittests/broken_functions/return_param_in/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(req): - return 'trust me, it is OK!' diff --git a/tests/unittests/broken_functions/syntax_error/function.json b/tests/unittests/broken_functions/syntax_error/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/broken_functions/syntax_error/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/broken_functions/syntax_error/main.py b/tests/unittests/broken_functions/syntax_error/main.py deleted file mode 100644 index 22df71a7a..000000000 --- a/tests/unittests/broken_functions/syntax_error/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(req): - 1 / # noqa diff --git a/tests/unittests/broken_functions/wrong_binding_dir/function.json b/tests/unittests/broken_functions/wrong_binding_dir/function.json deleted file mode 100644 index 47ebf1791..000000000 --- a/tests/unittests/broken_functions/wrong_binding_dir/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "direction": "in", - "name": "foo", - "type": "int" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/broken_functions/wrong_binding_dir/main.py b/tests/unittests/broken_functions/wrong_binding_dir/main.py deleted file mode 100644 index ed51e46cf..000000000 --- a/tests/unittests/broken_functions/wrong_binding_dir/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req, foo: azf.Out[str]): - return 'trust me, it is OK!' diff --git a/tests/unittests/broken_functions/wrong_param_dir/function.json b/tests/unittests/broken_functions/wrong_param_dir/function.json deleted file mode 100644 index 736b93690..000000000 --- a/tests/unittests/broken_functions/wrong_param_dir/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "direction": "out", - "name": "foo", - "type": "int" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/broken_functions/wrong_param_dir/main.py b/tests/unittests/broken_functions/wrong_param_dir/main.py deleted file mode 100644 index e52d77d34..000000000 --- a/tests/unittests/broken_functions/wrong_param_dir/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(req, foo: int): - return 'trust me, it is OK!' diff --git a/tests/unittests/dispatcher_functions/dispatcher_functions_stein/function_app.py b/tests/unittests/dispatcher_functions/dispatcher_functions_stein/function_app.py deleted file mode 100644 index fe9af2d32..000000000 --- a/tests/unittests/dispatcher_functions/dispatcher_functions_stein/function_app.py +++ /dev/null @@ -1,9 +0,0 @@ -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.route(route="http_trigger") -def http_trigger(req: func.HttpRequest) -> func.HttpResponse: - - return func.HttpResponse("Hello.") diff --git a/tests/unittests/dispatcher_functions/http_v2/fastapi/function_app.py b/tests/unittests/dispatcher_functions/http_v2/fastapi/function_app.py deleted file mode 100644 index 8a8982349..000000000 --- a/tests/unittests/dispatcher_functions/http_v2/fastapi/function_app.py +++ /dev/null @@ -1,9 +0,0 @@ -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="http_trigger") -def http_trigger(req: Request) -> Response: - return Response("ok") diff --git a/tests/unittests/dispatcher_functions/show_context/__init__.py b/tests/unittests/dispatcher_functions/show_context/__init__.py deleted file mode 100644 index 31f9766f9..000000000 --- a/tests/unittests/dispatcher_functions/show_context/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json - -import azure.functions as func - - -def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse: - result = { - 'function_directory': context.function_directory, - 'function_name': context.function_name - } - return func.HttpResponse(body=json.dumps(result), - mimetype='application/json') diff --git a/tests/unittests/dispatcher_functions/show_context/function.json b/tests/unittests/dispatcher_functions/show_context/function.json deleted file mode 100644 index 7239e0fcc..000000000 --- a/tests/unittests/dispatcher_functions/show_context/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} \ No newline at end of file diff --git a/tests/unittests/dispatcher_functions/show_context_async/__init__.py b/tests/unittests/dispatcher_functions/show_context_async/__init__.py deleted file mode 100644 index e43e8fa7d..000000000 --- a/tests/unittests/dispatcher_functions/show_context_async/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json - -import azure.functions as func - - -async def main(req: func.HttpRequest, - context: func.Context) -> func.HttpResponse: - result = { - 'function_directory': context.function_directory, - 'function_name': context.function_name - } - return func.HttpResponse(body=json.dumps(result), - mimetype='application/json') diff --git a/tests/unittests/dispatcher_functions/show_context_async/function.json b/tests/unittests/dispatcher_functions/show_context_async/function.json deleted file mode 100644 index 7239e0fcc..000000000 --- a/tests/unittests/dispatcher_functions/show_context_async/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} \ No newline at end of file diff --git a/tests/unittests/durable_functions/activity_trigger/function.json b/tests/unittests/durable_functions/activity_trigger/function.json deleted file mode 100644 index ebf8bfa62..000000000 --- a/tests/unittests/durable_functions/activity_trigger/function.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "activityTrigger", - "name": "input", - "direction": "in" - } - ] - } diff --git a/tests/unittests/durable_functions/activity_trigger/main.py b/tests/unittests/durable_functions/activity_trigger/main.py deleted file mode 100644 index b3fee32cc..000000000 --- a/tests/unittests/durable_functions/activity_trigger/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(input: str) -> str: - return input diff --git a/tests/unittests/durable_functions/activity_trigger_dict/function.json b/tests/unittests/durable_functions/activity_trigger_dict/function.json deleted file mode 100644 index cb44d98cc..000000000 --- a/tests/unittests/durable_functions/activity_trigger_dict/function.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "activityTrigger", - "name": "input", - "direction": "in" - } - ] -} diff --git a/tests/unittests/durable_functions/activity_trigger_dict/main.py b/tests/unittests/durable_functions/activity_trigger_dict/main.py deleted file mode 100644 index 0045c198c..000000000 --- a/tests/unittests/durable_functions/activity_trigger_dict/main.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from typing import Dict - - -def main(input: Dict[str, str]) -> Dict[str, str]: - result = input.copy() - if result.get('bird'): - result['bird'] = result['bird'][::-1] - - return result diff --git a/tests/unittests/durable_functions/activity_trigger_int_to_float/function.json b/tests/unittests/durable_functions/activity_trigger_int_to_float/function.json deleted file mode 100644 index cb44d98cc..000000000 --- a/tests/unittests/durable_functions/activity_trigger_int_to_float/function.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "activityTrigger", - "name": "input", - "direction": "in" - } - ] -} diff --git a/tests/unittests/durable_functions/activity_trigger_int_to_float/main.py b/tests/unittests/durable_functions/activity_trigger_int_to_float/main.py deleted file mode 100644 index 4faf3ef8b..000000000 --- a/tests/unittests/durable_functions/activity_trigger_int_to_float/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(input: int) -> float: - return float(input) * (-1.1) diff --git a/tests/unittests/durable_functions/activity_trigger_no_anno/function.json b/tests/unittests/durable_functions/activity_trigger_no_anno/function.json deleted file mode 100644 index ebf8bfa62..000000000 --- a/tests/unittests/durable_functions/activity_trigger_no_anno/function.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "activityTrigger", - "name": "input", - "direction": "in" - } - ] - } diff --git a/tests/unittests/durable_functions/activity_trigger_no_anno/main.py b/tests/unittests/durable_functions/activity_trigger_no_anno/main.py deleted file mode 100644 index 6a7f9c971..000000000 --- a/tests/unittests/durable_functions/activity_trigger_no_anno/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(input): - return input diff --git a/tests/unittests/durable_functions/orchestration_trigger/function.json b/tests/unittests/durable_functions/orchestration_trigger/function.json deleted file mode 100644 index c8ef14a94..000000000 --- a/tests/unittests/durable_functions/orchestration_trigger/function.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "orchestrationTrigger", - "name": "context", - "direction": "in" - } - ] - } diff --git a/tests/unittests/durable_functions/orchestration_trigger/main.py b/tests/unittests/durable_functions/orchestration_trigger/main.py deleted file mode 100644 index 40b5919c4..000000000 --- a/tests/unittests/durable_functions/orchestration_trigger/main.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -# import azure.durable_functions as df - - -def generator_function(context): - final_result = yield context.call_activity('activity_trigger', 'foobar') - return final_result - - -def main(context): - # orchestrate = df.Orchestrator.create(generator_function) - # result = orchestrate(context) - # return result - return f'{context} :)' diff --git a/tests/unittests/eventhub_mock_functions/eventhub_cardinality_many/__init__.py b/tests/unittests/eventhub_mock_functions/eventhub_cardinality_many/__init__.py deleted file mode 100644 index eb75a0012..000000000 --- a/tests/unittests/eventhub_mock_functions/eventhub_cardinality_many/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from typing import List - -import azure.functions as func - - -# This is testing the function load feature for the multiple events annotation -def main(events: List[func.EventHubEvent]) -> str: - return 'OK_MANY' diff --git a/tests/unittests/eventhub_mock_functions/eventhub_cardinality_many/function.json b/tests/unittests/eventhub_mock_functions/eventhub_cardinality_many/function.json deleted file mode 100644 index 39d2d0059..000000000 --- a/tests/unittests/eventhub_mock_functions/eventhub_cardinality_many/function.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "scriptFile": "__init__.py", - - "bindings": [ - { - "type": "eventHubTrigger", - "name": "events", - "direction": "in", - "eventHubName": "python-worker-iot-ci", - "connection": "AzureWebJobsEventHubConnectionString", - "cardinality": "many" - }, - { - "type": "blob", - "direction": "out", - "name": "$return", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-eventhub-iot-triggered.txt" - } - ] - } diff --git a/tests/unittests/eventhub_mock_functions/eventhub_cardinality_many_bad_anno/__init__.py b/tests/unittests/eventhub_mock_functions/eventhub_cardinality_many_bad_anno/__init__.py deleted file mode 100644 index ed0f9f118..000000000 --- a/tests/unittests/eventhub_mock_functions/eventhub_cardinality_many_bad_anno/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from typing import List - - -# This is testing the function load feature for the multiple events annotation -# The event shouldn't be List[str] -def main(events: List[str]) -> str: - return 'BAD' diff --git a/tests/unittests/eventhub_mock_functions/eventhub_cardinality_many_bad_anno/function.json b/tests/unittests/eventhub_mock_functions/eventhub_cardinality_many_bad_anno/function.json deleted file mode 100644 index 39d2d0059..000000000 --- a/tests/unittests/eventhub_mock_functions/eventhub_cardinality_many_bad_anno/function.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "scriptFile": "__init__.py", - - "bindings": [ - { - "type": "eventHubTrigger", - "name": "events", - "direction": "in", - "eventHubName": "python-worker-iot-ci", - "connection": "AzureWebJobsEventHubConnectionString", - "cardinality": "many" - }, - { - "type": "blob", - "direction": "out", - "name": "$return", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-eventhub-iot-triggered.txt" - } - ] - } diff --git a/tests/unittests/eventhub_mock_functions/eventhub_cardinality_one/__init__.py b/tests/unittests/eventhub_mock_functions/eventhub_cardinality_one/__init__.py deleted file mode 100644 index 0f6852a36..000000000 --- a/tests/unittests/eventhub_mock_functions/eventhub_cardinality_one/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func - - -# This is testing the function load feature for the single event annotation -def main(event: func.EventHubEvent) -> str: - return 'OK_ONE' diff --git a/tests/unittests/eventhub_mock_functions/eventhub_cardinality_one/function.json b/tests/unittests/eventhub_mock_functions/eventhub_cardinality_one/function.json deleted file mode 100644 index 4c9ae1e74..000000000 --- a/tests/unittests/eventhub_mock_functions/eventhub_cardinality_one/function.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "scriptFile": "__init__.py", - - "bindings": [ - { - "type": "eventHubTrigger", - "name": "event", - "direction": "in", - "eventHubName": "python-worker-iot-ci", - "connection": "AzureWebJobsEventHubConnectionString", - "cardinality": "one" - }, - { - "type": "blob", - "direction": "out", - "name": "$return", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-eventhub-iot-triggered.txt" - } - ] - } diff --git a/tests/unittests/eventhub_mock_functions/eventhub_cardinality_one_bad_anno/__init__.py b/tests/unittests/eventhub_mock_functions/eventhub_cardinality_one_bad_anno/__init__.py deleted file mode 100644 index 69b24e476..000000000 --- a/tests/unittests/eventhub_mock_functions/eventhub_cardinality_one_bad_anno/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -# This is testing the function load feature for the single event annotation -# The event shouldn't be int -def main(event: int) -> str: - return 'BAD' diff --git a/tests/unittests/eventhub_mock_functions/eventhub_cardinality_one_bad_anno/function.json b/tests/unittests/eventhub_mock_functions/eventhub_cardinality_one_bad_anno/function.json deleted file mode 100644 index 4c9ae1e74..000000000 --- a/tests/unittests/eventhub_mock_functions/eventhub_cardinality_one_bad_anno/function.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "scriptFile": "__init__.py", - - "bindings": [ - { - "type": "eventHubTrigger", - "name": "event", - "direction": "in", - "eventHubName": "python-worker-iot-ci", - "connection": "AzureWebJobsEventHubConnectionString", - "cardinality": "one" - }, - { - "type": "blob", - "direction": "out", - "name": "$return", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-eventhub-iot-triggered.txt" - } - ] - } diff --git a/tests/unittests/eventhub_mock_functions/eventhub_trigger_iot/__init__.py b/tests/unittests/eventhub_mock_functions/eventhub_trigger_iot/__init__.py deleted file mode 100644 index 918548403..000000000 --- a/tests/unittests/eventhub_mock_functions/eventhub_trigger_iot/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json - -import azure.functions as func - - -def main(event: func.EventHubEvent) -> str: - return json.dumps(event.iothub_metadata) diff --git a/tests/unittests/eventhub_mock_functions/eventhub_trigger_iot/function.json b/tests/unittests/eventhub_mock_functions/eventhub_trigger_iot/function.json deleted file mode 100644 index c14ae132f..000000000 --- a/tests/unittests/eventhub_mock_functions/eventhub_trigger_iot/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "__init__.py", - - "bindings": [ - { - "type": "eventHubTrigger", - "name": "event", - "direction": "in", - "eventHubName": "python-worker-iot-ci", - "connection": "AzureWebJobsEventHubConnectionString" - }, - { - "type": "blob", - "direction": "out", - "name": "$return", - "connection": "AzureWebJobsStorage", - "path": "python-worker-tests/test-eventhub-iot-triggered.txt" - } - ] -} diff --git a/tests/unittests/file_name_functions/default_file_name/function_app.py b/tests/unittests/file_name_functions/default_file_name/function_app.py deleted file mode 100644 index 7eeb55331..000000000 --- a/tests/unittests/file_name_functions/default_file_name/function_app.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import azure.functions as func - -app = func.FunctionApp() - - -@app.route(route="return_str") -def return_str(req: func.HttpRequest) -> str: - return 'Hello World!' diff --git a/tests/unittests/file_name_functions/invalid_file_name/main b/tests/unittests/file_name_functions/invalid_file_name/main deleted file mode 100644 index 7eeb55331..000000000 --- a/tests/unittests/file_name_functions/invalid_file_name/main +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import azure.functions as func - -app = func.FunctionApp() - - -@app.route(route="return_str") -def return_str(req: func.HttpRequest) -> str: - return 'Hello World!' diff --git a/tests/unittests/file_name_functions/new_file_name/test.py b/tests/unittests/file_name_functions/new_file_name/test.py deleted file mode 100644 index 7eeb55331..000000000 --- a/tests/unittests/file_name_functions/new_file_name/test.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import azure.functions as func - -app = func.FunctionApp() - - -@app.route(route="return_str") -def return_str(req: func.HttpRequest) -> str: - return 'Hello World!' diff --git a/tests/unittests/generic_functions/foobar_as_bytes/function.json b/tests/unittests/generic_functions/foobar_as_bytes/function.json deleted file mode 100644 index f0117f606..000000000 --- a/tests/unittests/generic_functions/foobar_as_bytes/function.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "foobar", - "name": "input", - "direction": "in", - "dataType": "binary" - }, - { - "direction": "out", - "name": "$return", - "type": "foobar", - "dataType": "binary" - } - ] -} diff --git a/tests/unittests/generic_functions/foobar_as_bytes/main.py b/tests/unittests/generic_functions/foobar_as_bytes/main.py deleted file mode 100644 index e4c9d11a9..000000000 --- a/tests/unittests/generic_functions/foobar_as_bytes/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(input: bytes) -> bytes: - return input diff --git a/tests/unittests/generic_functions/foobar_as_bytes_no_anno/function.json b/tests/unittests/generic_functions/foobar_as_bytes_no_anno/function.json deleted file mode 100644 index f0117f606..000000000 --- a/tests/unittests/generic_functions/foobar_as_bytes_no_anno/function.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "foobar", - "name": "input", - "direction": "in", - "dataType": "binary" - }, - { - "direction": "out", - "name": "$return", - "type": "foobar", - "dataType": "binary" - } - ] -} diff --git a/tests/unittests/generic_functions/foobar_as_bytes_no_anno/main.py b/tests/unittests/generic_functions/foobar_as_bytes_no_anno/main.py deleted file mode 100644 index c03a4f0fb..000000000 --- a/tests/unittests/generic_functions/foobar_as_bytes_no_anno/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -# Input as bytes, without annotation - - -def main(input): - return input diff --git a/tests/unittests/generic_functions/foobar_as_none/function.json b/tests/unittests/generic_functions/foobar_as_none/function.json deleted file mode 100644 index 7a458eb7f..000000000 --- a/tests/unittests/generic_functions/foobar_as_none/function.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "direction": "out", - "name": "$return", - "type": "foobar", - "dataType": "binary" - } - ] -} diff --git a/tests/unittests/generic_functions/foobar_as_none/main.py b/tests/unittests/generic_functions/foobar_as_none/main.py deleted file mode 100644 index b7acadcdd..000000000 --- a/tests/unittests/generic_functions/foobar_as_none/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(): - return "hello" diff --git a/tests/unittests/generic_functions/foobar_as_str/function.json b/tests/unittests/generic_functions/foobar_as_str/function.json deleted file mode 100644 index 144593c6a..000000000 --- a/tests/unittests/generic_functions/foobar_as_str/function.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "foobar", - "name": "input", - "direction": "in", - "dataType": "string" - }, - { - "direction": "out", - "name": "$return", - "type": "foobar", - "dataType": "string" - } - ] -} diff --git a/tests/unittests/generic_functions/foobar_as_str/main.py b/tests/unittests/generic_functions/foobar_as_str/main.py deleted file mode 100644 index b3fee32cc..000000000 --- a/tests/unittests/generic_functions/foobar_as_str/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(input: str) -> str: - return input diff --git a/tests/unittests/generic_functions/foobar_as_str_no_anno/function.json b/tests/unittests/generic_functions/foobar_as_str_no_anno/function.json deleted file mode 100644 index 144593c6a..000000000 --- a/tests/unittests/generic_functions/foobar_as_str_no_anno/function.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "foobar", - "name": "input", - "direction": "in", - "dataType": "string" - }, - { - "direction": "out", - "name": "$return", - "type": "foobar", - "dataType": "string" - } - ] -} diff --git a/tests/unittests/generic_functions/foobar_as_str_no_anno/main.py b/tests/unittests/generic_functions/foobar_as_str_no_anno/main.py deleted file mode 100644 index 9626e2aef..000000000 --- a/tests/unittests/generic_functions/foobar_as_str_no_anno/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -# Input as string, without annotation - - -def main(input): - return input diff --git a/tests/unittests/generic_functions/foobar_implicit_output/function.json b/tests/unittests/generic_functions/foobar_implicit_output/function.json deleted file mode 100644 index 6f8a83ec0..000000000 --- a/tests/unittests/generic_functions/foobar_implicit_output/function.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "foobar", - "name": "input", - "direction": "in", - "dataType": "string" - } - ] -} diff --git a/tests/unittests/generic_functions/foobar_implicit_output/main.py b/tests/unittests/generic_functions/foobar_implicit_output/main.py deleted file mode 100644 index 53124993e..000000000 --- a/tests/unittests/generic_functions/foobar_implicit_output/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -# Input as string, without annotation - - -def main(input: str): - return input diff --git a/tests/unittests/generic_functions/foobar_implicit_output_exemption/function.json b/tests/unittests/generic_functions/foobar_implicit_output_exemption/function.json deleted file mode 100644 index 82a015bbb..000000000 --- a/tests/unittests/generic_functions/foobar_implicit_output_exemption/function.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "durableClient", - "name": "input", - "direction": "in", - "dataType": "string" - } - ] - } - \ No newline at end of file diff --git a/tests/unittests/generic_functions/foobar_implicit_output_exemption/main.py b/tests/unittests/generic_functions/foobar_implicit_output_exemption/main.py deleted file mode 100644 index 53124993e..000000000 --- a/tests/unittests/generic_functions/foobar_implicit_output_exemption/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -# Input as string, without annotation - - -def main(input: str): - return input diff --git a/tests/unittests/generic_functions/foobar_nil_data/function.json b/tests/unittests/generic_functions/foobar_nil_data/function.json deleted file mode 100644 index 4cced7c56..000000000 --- a/tests/unittests/generic_functions/foobar_nil_data/function.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "generic", - "name": "input", - "direction": "in" - } - ] - } - \ No newline at end of file diff --git a/tests/unittests/generic_functions/foobar_nil_data/main.py b/tests/unittests/generic_functions/foobar_nil_data/main.py deleted file mode 100644 index a41823ddc..000000000 --- a/tests/unittests/generic_functions/foobar_nil_data/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import logging - - -def main(input) -> None: - logging.info("Hello World") diff --git a/tests/unittests/generic_functions/foobar_return_bool/function.json b/tests/unittests/generic_functions/foobar_return_bool/function.json deleted file mode 100644 index 6f8a83ec0..000000000 --- a/tests/unittests/generic_functions/foobar_return_bool/function.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "foobar", - "name": "input", - "direction": "in", - "dataType": "string" - } - ] -} diff --git a/tests/unittests/generic_functions/foobar_return_bool/main.py b/tests/unittests/generic_functions/foobar_return_bool/main.py deleted file mode 100644 index 4fadd2bff..000000000 --- a/tests/unittests/generic_functions/foobar_return_bool/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(input): - return True diff --git a/tests/unittests/generic_functions/foobar_return_dict/function.json b/tests/unittests/generic_functions/foobar_return_dict/function.json deleted file mode 100644 index 6f8a83ec0..000000000 --- a/tests/unittests/generic_functions/foobar_return_dict/function.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "foobar", - "name": "input", - "direction": "in", - "dataType": "string" - } - ] -} diff --git a/tests/unittests/generic_functions/foobar_return_dict/main.py b/tests/unittests/generic_functions/foobar_return_dict/main.py deleted file mode 100644 index c8aef81a3..000000000 --- a/tests/unittests/generic_functions/foobar_return_dict/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(input): - return {"hello": "world"} diff --git a/tests/unittests/generic_functions/foobar_return_double/function.json b/tests/unittests/generic_functions/foobar_return_double/function.json deleted file mode 100644 index 6f8a83ec0..000000000 --- a/tests/unittests/generic_functions/foobar_return_double/function.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "foobar", - "name": "input", - "direction": "in", - "dataType": "string" - } - ] -} diff --git a/tests/unittests/generic_functions/foobar_return_double/main.py b/tests/unittests/generic_functions/foobar_return_double/main.py deleted file mode 100644 index 42aac3fc0..000000000 --- a/tests/unittests/generic_functions/foobar_return_double/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(input): - return 12.34 diff --git a/tests/unittests/generic_functions/foobar_return_int/function.json b/tests/unittests/generic_functions/foobar_return_int/function.json deleted file mode 100644 index 6f8a83ec0..000000000 --- a/tests/unittests/generic_functions/foobar_return_int/function.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "foobar", - "name": "input", - "direction": "in", - "dataType": "string" - } - ] -} diff --git a/tests/unittests/generic_functions/foobar_return_int/main.py b/tests/unittests/generic_functions/foobar_return_int/main.py deleted file mode 100644 index 8beb85606..000000000 --- a/tests/unittests/generic_functions/foobar_return_int/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(input): - return 12 diff --git a/tests/unittests/generic_functions/foobar_return_list/function.json b/tests/unittests/generic_functions/foobar_return_list/function.json deleted file mode 100644 index 6f8a83ec0..000000000 --- a/tests/unittests/generic_functions/foobar_return_list/function.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "foobar", - "name": "input", - "direction": "in", - "dataType": "string" - } - ] -} diff --git a/tests/unittests/generic_functions/foobar_return_list/main.py b/tests/unittests/generic_functions/foobar_return_list/main.py deleted file mode 100644 index 1d1a4a5ea..000000000 --- a/tests/unittests/generic_functions/foobar_return_list/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(input): - return [1, 2, 3] diff --git a/tests/unittests/generic_functions/foobar_with_no_datatype/function.json b/tests/unittests/generic_functions/foobar_with_no_datatype/function.json deleted file mode 100644 index 4e49f1942..000000000 --- a/tests/unittests/generic_functions/foobar_with_no_datatype/function.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "foobar", - "name": "input", - "direction": "in" - } - ] -} diff --git a/tests/unittests/generic_functions/foobar_with_no_datatype/main.py b/tests/unittests/generic_functions/foobar_with_no_datatype/main.py deleted file mode 100644 index b3fee32cc..000000000 --- a/tests/unittests/generic_functions/foobar_with_no_datatype/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(input: str) -> str: - return input diff --git a/tests/unittests/http_functions/accept_json/function.json b/tests/unittests/http_functions/accept_json/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/accept_json/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/accept_json/main.py b/tests/unittests/http_functions/accept_json/main.py deleted file mode 100644 index 368a33741..000000000 --- a/tests/unittests/http_functions/accept_json/main.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json - -import azure.functions - - -def main(req: azure.functions.HttpRequest): - 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() - }) diff --git a/tests/unittests/http_functions/async_logging/function.json b/tests/unittests/http_functions/async_logging/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/async_logging/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/async_logging/main.py b/tests/unittests/http_functions/async_logging/main.py deleted file mode 100644 index e30b88660..000000000 --- a/tests/unittests/http_functions/async_logging/main.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import asyncio -import logging - -import azure.functions - -logger = logging.getLogger('my function') - - -async def main(req: azure.functions.HttpRequest): - 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' - - -async def nested(): - try: - 1 / 0 - except ZeroDivisionError: - logger.error('and another error', exc_info=True) diff --git a/tests/unittests/http_functions/async_return_str/function.json b/tests/unittests/http_functions/async_return_str/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/async_return_str/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/async_return_str/main.py b/tests/unittests/http_functions/async_return_str/main.py deleted file mode 100644 index a64f811f7..000000000 --- a/tests/unittests/http_functions/async_return_str/main.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import asyncio - -import azure.functions - - -async def main(req: azure.functions.HttpRequest, context): - await asyncio.sleep(0.1) - return 'Hello Async World!' diff --git a/tests/unittests/http_functions/create_task_with_context/function.json b/tests/unittests/http_functions/create_task_with_context/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/create_task_with_context/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/create_task_with_context/main.py b/tests/unittests/http_functions/create_task_with_context/main.py deleted file mode 100644 index f603acd1b..000000000 --- a/tests/unittests/http_functions/create_task_with_context/main.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import asyncio -import contextvars - -import azure.functions - -num = contextvars.ContextVar('num') - - -async def count(name: str): - # The number of times the loop is executed - # depends on the val set in context - val = num.get() - for i in range(val): - await asyncio.sleep(0.5) - return f"Finished {name} in {val}" - - -async def main(req: azure.functions.HttpRequest): - # Create first task with context num = 5 - num.set(5) - first_ctx = contextvars.copy_context() - first_count_task = asyncio.create_task(count("Hello World"), context=first_ctx) - - # Create second task with context num = 10 - num.set(10) - second_ctx = contextvars.copy_context() - second_count_task = asyncio.create_task(count("Hello World"), context=second_ctx) - - # Execute tasks - first_count_val = await first_count_task - second_count_val = await second_count_task - - return f'{first_count_val + " | " + second_count_val}' diff --git a/tests/unittests/http_functions/create_task_without_context/function.json b/tests/unittests/http_functions/create_task_without_context/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/create_task_without_context/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/create_task_without_context/main.py b/tests/unittests/http_functions/create_task_without_context/main.py deleted file mode 100644 index c7ee21f7b..000000000 --- a/tests/unittests/http_functions/create_task_without_context/main.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import asyncio - -import azure.functions - - -async def count(name: str, num: int): - # The number of times the loop executes is decided by a - # user-defined param - for i in range(num): - await asyncio.sleep(0.5) - return f"Finished {name} in {num}" - - -async def main(req: azure.functions.HttpRequest): - # No context is being sent into asyncio.create_task - count_task = asyncio.create_task(count("Hello World", 5)) - count_val = await count_task - return f'{count_val}' diff --git a/tests/unittests/http_functions/debug_logging/function.json b/tests/unittests/http_functions/debug_logging/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/debug_logging/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/debug_logging/main.py b/tests/unittests/http_functions/debug_logging/main.py deleted file mode 100644 index 628896cbc..000000000 --- a/tests/unittests/http_functions/debug_logging/main.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import logging - -import azure.functions - - -def main(req: azure.functions.HttpRequest): - 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' diff --git a/tests/unittests/http_functions/hijack_current_event_loop/function.json b/tests/unittests/http_functions/hijack_current_event_loop/function.json deleted file mode 100644 index 059791f31..000000000 --- a/tests/unittests/http_functions/hijack_current_event_loop/function.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req", - "methods": [ - "get" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] - } diff --git a/tests/unittests/http_functions/hijack_current_event_loop/main.py b/tests/unittests/http_functions/hijack_current_event_loop/main.py deleted file mode 100644 index 10856ca46..000000000 --- a/tests/unittests/http_functions/hijack_current_event_loop/main.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import asyncio -import logging -import sys - -import azure.functions as func - -logger = logging.getLogger('custom_logger') - -# 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') - - -async def main(req: func.HttpRequest) -> func.HttpResponse: - 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' diff --git a/tests/unittests/http_functions/http_functions_stein/function_app.py b/tests/unittests/http_functions/http_functions_stein/function_app.py deleted file mode 100644 index 112813de9..000000000 --- a/tests/unittests/http_functions/http_functions_stein/function_app.py +++ /dev/null @@ -1,455 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import asyncio -import contextvars -import hashlib -import json -import logging -import sys -import time -from urllib.request import urlopen - -import azure.functions as func - -app = func.FunctionApp() - -logger = logging.getLogger("my-function") - -num = contextvars.ContextVar('num') - - -async def count_with_context(name: str): - # The number of times the loop is executed - # depends on the val set in context - val = num.get() - for i in range(val): - await asyncio.sleep(0.5) - return f"Finished {name} in {val}" - - -async def count_without_context(name: str, number: int): - # The number of times the loop executes is decided by a - # user-defined param - for i in range(number): - await asyncio.sleep(0.5) - return f"Finished {name} in {number}" - - -@app.route(route="return_str") -def return_str(req: func.HttpRequest) -> str: - return 'Hello World!' - - -@app.route(route="accept_json") -def accept_json(req: func.HttpRequest): - 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: func.HttpRequest): - 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: func.HttpRequest): - await asyncio.sleep(0.1) - return 'Hello Async World!' - - -@app.route(route="debug_logging") -def debug_logging(req: func.HttpRequest): - 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: func.HttpRequest): - 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: func.HttpRequest) -> func.HttpResponse: - 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: func.HttpRequest): - 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: func.HttpRequest): - 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: func.HttpRequest) -> func.HttpResponse: - body = req.get_body() - body_len = str(len(body)) - - headers = {'body-len': body_len} - return func.HttpResponse(body=body, status_code=200, headers=headers) - - -@app.route(route="remapped_context") -def remapped_context(req: func.HttpRequest): - return req.method - - -@app.route(route="return_bytes") -def return_bytes(req: func.HttpRequest): - # 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: func.HttpRequest, 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: func.HttpRequest): - return func.HttpResponse('

    Hello Worldâ„¢

    ', - mimetype='text/html') - - -@app.route(route="return_http_404") -def return_http_404(req: func.HttpRequest): - return func.HttpResponse('bye', status_code=404) - - -@app.route(route="return_http_auth_admin", auth_level=func.AuthLevel.ADMIN) -def return_http_auth_admin(req: func.HttpRequest): - return func.HttpResponse('

    Hello Worldâ„¢

    ', - mimetype='text/html') - - -@app.route(route="return_http_no_body") -def return_http_no_body(req: func.HttpRequest): - return func.HttpResponse() - - -@app.route(route="return_http_redirect") -def return_http_redirect(req: func.HttpRequest): - location = 'return_http?code={}'.format(req.params['code']) - return func.HttpResponse( - status_code=302, - headers={'location': location}) - - -@app.route(route="return_out", binding_arg_name="foo") -def return_out(req: func.HttpRequest, foo: func.Out[func.HttpResponse]): - foo.set(func.HttpResponse(body='hello', status_code=201)) - - -@app.route(route="return_request") -def return_request(req: func.HttpRequest): - 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: func.HttpRequest) -> str: - return json.dumps(dict(req.route_params)) - - -@app.route(route="sync_logging") -def main(req: func.HttpRequest): - 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: func.HttpRequest): - 1 / 0 - - -@app.route(route="unhandled_urllib_error") -def unhandled_urllib_error(req: func.HttpRequest) -> 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: func.HttpRequest) -> str: - raise UnserializableException('foo') - - -async def try_log(): - logger.info("try_log") - - -@app.route(route="user_event_loop") -def user_event_loop(req: func.HttpRequest) -> func.HttpResponse: - 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: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - resp = func.HttpResponse( - "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: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - resp = func.HttpResponse( - "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: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - resp = func.HttpResponse( - "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: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - resp = func.HttpResponse( - "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: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - resp = func.HttpResponse( - "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: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - resp = func.HttpResponse( - "This HTTP triggered function executed successfully.") - - resp.headers.add("Set-Cookie", '') - - return resp - - -@app.route('create_task_with_context') -async def create_task_with_context(req: func.HttpRequest): - # Create first task with context num = 5 - num.set(5) - first_ctx = contextvars.copy_context() - first_count_task = asyncio.create_task( - count_with_context("Hello World"), context=first_ctx) - - # Create second task with context num = 10 - num.set(10) - second_ctx = contextvars.copy_context() - second_count_task = asyncio.create_task( - count_with_context("Hello World"), context=second_ctx) - - # Execute tasks - first_count_val = await first_count_task - second_count_val = await second_count_task - - return f'{first_count_val + " | " + second_count_val}' - - -@app.route('create_task_without_context') -async def create_task_without_context(req: func.HttpRequest): - # No context is being sent into asyncio.create_task - count_task = asyncio.create_task(count_without_context("Hello World", 5)) - count_val = await count_task - return f'{count_val}' 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 deleted file mode 100644 index c10bcb8ec..000000000 --- a/tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py +++ /dev/null @@ -1,438 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import asyncio -import hashlib -import logging -import sys -import time -from urllib.request import urlopen - -import azure.functions as func -from azurefunctions.extensions.http.fastapi import ( - HTMLResponse, - RedirectResponse, - Request, - Response, -) -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 diff --git a/tests/unittests/http_functions/multiple_set_cookie_resp_headers/function.json b/tests/unittests/http_functions/multiple_set_cookie_resp_headers/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/multiple_set_cookie_resp_headers/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/multiple_set_cookie_resp_headers/main.py b/tests/unittests/http_functions/multiple_set_cookie_resp_headers/main.py deleted file mode 100644 index 450496fb4..000000000 --- a/tests/unittests/http_functions/multiple_set_cookie_resp_headers/main.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import logging - -import azure.functions as func - - -def main(req: func.HttpRequest): - logging.info('Python HTTP trigger function processed a request.') - resp = func.HttpResponse( - "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 diff --git a/tests/unittests/http_functions/no_return/function.json b/tests/unittests/http_functions/no_return/function.json deleted file mode 100644 index da37649e4..000000000 --- a/tests/unittests/http_functions/no_return/function.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - } - ] -} diff --git a/tests/unittests/http_functions/no_return/main.py b/tests/unittests/http_functions/no_return/main.py deleted file mode 100644 index 1e9c228fc..000000000 --- a/tests/unittests/http_functions/no_return/main.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import logging - -logger = logging.getLogger('test') - - -def main(req): - logger.error('hi') diff --git a/tests/unittests/http_functions/no_return_returns/function.json b/tests/unittests/http_functions/no_return_returns/function.json deleted file mode 100644 index da37649e4..000000000 --- a/tests/unittests/http_functions/no_return_returns/function.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - } - ] -} diff --git a/tests/unittests/http_functions/no_return_returns/main.py b/tests/unittests/http_functions/no_return_returns/main.py deleted file mode 100644 index f6722b5f8..000000000 --- a/tests/unittests/http_functions/no_return_returns/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(req): - return 'ABC' diff --git a/tests/unittests/http_functions/print_logging/function.json b/tests/unittests/http_functions/print_logging/function.json deleted file mode 100644 index 985453fcf..000000000 --- a/tests/unittests/http_functions/print_logging/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] - } diff --git a/tests/unittests/http_functions/print_logging/main.py b/tests/unittests/http_functions/print_logging/main.py deleted file mode 100644 index 87fd693f6..000000000 --- a/tests/unittests/http_functions/print_logging/main.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import sys - -import azure.functions - - -def main(req: azure.functions.HttpRequest): - 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' diff --git a/tests/unittests/http_functions/raw_body_bytes/function.json b/tests/unittests/http_functions/raw_body_bytes/function.json deleted file mode 100644 index c8d851b10..000000000 --- a/tests/unittests/http_functions/raw_body_bytes/function.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "methods": [ - "post" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/raw_body_bytes/main.py b/tests/unittests/http_functions/raw_body_bytes/main.py deleted file mode 100644 index 02dc72d8f..000000000 --- a/tests/unittests/http_functions/raw_body_bytes/main.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func - - -def main(req: func.HttpRequest) -> func.HttpResponse: - body = req.get_body() - body_len = str(len(body)) - - headers = {'body-len': body_len} - return func.HttpResponse(body=body, status_code=200, headers=headers) diff --git a/tests/unittests/http_functions/remapped_context/function.json b/tests/unittests/http_functions/remapped_context/function.json deleted file mode 100644 index f6f7bc7e5..000000000 --- a/tests/unittests/http_functions/remapped_context/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "context" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/remapped_context/main.py b/tests/unittests/http_functions/remapped_context/main.py deleted file mode 100644 index 11faca410..000000000 --- a/tests/unittests/http_functions/remapped_context/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(context): - return context.method diff --git a/tests/unittests/http_functions/response_cookie_header_nullable_bool_err/function.json b/tests/unittests/http_functions/response_cookie_header_nullable_bool_err/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/response_cookie_header_nullable_bool_err/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/response_cookie_header_nullable_bool_err/main.py b/tests/unittests/http_functions/response_cookie_header_nullable_bool_err/main.py deleted file mode 100644 index 630a33dff..000000000 --- a/tests/unittests/http_functions/response_cookie_header_nullable_bool_err/main.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import logging - -import azure.functions as func - - -def main(req: func.HttpRequest): - logging.info('Python HTTP trigger function processed a request.') - resp = func.HttpResponse( - "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 diff --git a/tests/unittests/http_functions/response_cookie_header_nullable_double_err/function.json b/tests/unittests/http_functions/response_cookie_header_nullable_double_err/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/response_cookie_header_nullable_double_err/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/response_cookie_header_nullable_double_err/main.py b/tests/unittests/http_functions/response_cookie_header_nullable_double_err/main.py deleted file mode 100644 index 81601b8b9..000000000 --- a/tests/unittests/http_functions/response_cookie_header_nullable_double_err/main.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import logging - -import azure.functions as func - - -def main(req: func.HttpRequest): - logging.info('Python HTTP trigger function processed a request.') - resp = func.HttpResponse( - "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 diff --git a/tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/function.json b/tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/main.py b/tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/main.py deleted file mode 100644 index 6a7c8cfef..000000000 --- a/tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/main.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import logging - -import azure.functions as func - - -def main(req: func.HttpRequest): - logging.info('Python HTTP trigger function processed a request.') - resp = func.HttpResponse( - "This HTTP triggered function executed successfully.") - - resp.headers.add("Set-Cookie", 'foo=bar; Domain=123; Expires=Dummy') - - return resp diff --git a/tests/unittests/http_functions/return_bytes/function.json b/tests/unittests/http_functions/return_bytes/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/return_bytes/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/return_bytes/main.py b/tests/unittests/http_functions/return_bytes/main.py deleted file mode 100644 index bf2eda7cf..000000000 --- a/tests/unittests/http_functions/return_bytes/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(req): - # This function will fail, as we don't auto-convert "bytes" to "http". - return b'Hello World!' diff --git a/tests/unittests/http_functions/return_context/function.json b/tests/unittests/http_functions/return_context/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/return_context/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/return_context/main.py b/tests/unittests/http_functions/return_context/main.py deleted file mode 100644 index 2b17ef301..000000000 --- a/tests/unittests/http_functions/return_context/main.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json - -import azure.functions - - -def main(req: azure.functions.HttpRequest, context: azure.functions.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, - }) diff --git a/tests/unittests/http_functions/return_http/function.json b/tests/unittests/http_functions/return_http/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/return_http/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/return_http/main.py b/tests/unittests/http_functions/return_http/main.py deleted file mode 100644 index d2abfb7b4..000000000 --- a/tests/unittests/http_functions/return_http/main.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest): - return azf.HttpResponse('

    Hello Worldâ„¢

    ', - mimetype='text/html') diff --git a/tests/unittests/http_functions/return_http_404/function.json b/tests/unittests/http_functions/return_http_404/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/return_http_404/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/return_http_404/main.py b/tests/unittests/http_functions/return_http_404/main.py deleted file mode 100644 index bd4e254c2..000000000 --- a/tests/unittests/http_functions/return_http_404/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest): - return azf.HttpResponse('bye', status_code=404) diff --git a/tests/unittests/http_functions/return_http_auth_admin/function.json b/tests/unittests/http_functions/return_http_auth_admin/function.json deleted file mode 100644 index 1baa699c4..000000000 --- a/tests/unittests/http_functions/return_http_auth_admin/function.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "authLevel": "admin", - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/return_http_auth_admin/main.py b/tests/unittests/http_functions/return_http_auth_admin/main.py deleted file mode 100644 index d2abfb7b4..000000000 --- a/tests/unittests/http_functions/return_http_auth_admin/main.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest): - return azf.HttpResponse('

    Hello Worldâ„¢

    ', - mimetype='text/html') diff --git a/tests/unittests/http_functions/return_http_no_body/function.json b/tests/unittests/http_functions/return_http_no_body/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/return_http_no_body/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/return_http_no_body/main.py b/tests/unittests/http_functions/return_http_no_body/main.py deleted file mode 100644 index 8ac6c4a50..000000000 --- a/tests/unittests/http_functions/return_http_no_body/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest): - return azf.HttpResponse() diff --git a/tests/unittests/http_functions/return_http_redirect/function.json b/tests/unittests/http_functions/return_http_redirect/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/return_http_redirect/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/return_http_redirect/main.py b/tests/unittests/http_functions/return_http_redirect/main.py deleted file mode 100644 index 54fb8bbed..000000000 --- a/tests/unittests/http_functions/return_http_redirect/main.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest): - location = 'return_http?code={}'.format(req.params['code']) - return azf.HttpResponse( - status_code=302, - headers={'location': location}) diff --git a/tests/unittests/http_functions/return_out/function.json b/tests/unittests/http_functions/return_out/function.json deleted file mode 100644 index 1cbac7ad1..000000000 --- a/tests/unittests/http_functions/return_out/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "direction": "out", - "name": "foo", - "type": "http" - } - ] -} diff --git a/tests/unittests/http_functions/return_out/main.py b/tests/unittests/http_functions/return_out/main.py deleted file mode 100644 index 53e8cbb5d..000000000 --- a/tests/unittests/http_functions/return_out/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest, foo: azf.Out[azf.HttpResponse]): - foo.set(azf.HttpResponse(body='hello', status_code=201)) diff --git a/tests/unittests/http_functions/return_request/function.json b/tests/unittests/http_functions/return_request/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/return_request/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/return_request/main.py b/tests/unittests/http_functions/return_request/main.py deleted file mode 100644 index 842e18581..000000000 --- a/tests/unittests/http_functions/return_request/main.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import hashlib -import json - -import azure.functions - - -def main(req: azure.functions.HttpRequest): - 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(), - }) diff --git a/tests/unittests/http_functions/return_route_params/function.json b/tests/unittests/http_functions/return_route_params/function.json deleted file mode 100644 index 2ca2c9196..000000000 --- a/tests/unittests/http_functions/return_route_params/function.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "route": "return_route_params/{param1}/{param2}" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/return_route_params/main.py b/tests/unittests/http_functions/return_route_params/main.py deleted file mode 100644 index 1cc7f0740..000000000 --- a/tests/unittests/http_functions/return_route_params/main.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json - -import azure.functions - - -def main(req: azure.functions.HttpRequest) -> str: - return json.dumps(dict(req.route_params)) diff --git a/tests/unittests/http_functions/return_str/function.json b/tests/unittests/http_functions/return_str/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/return_str/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/return_str/main.py b/tests/unittests/http_functions/return_str/main.py deleted file mode 100644 index 9fa7e56cc..000000000 --- a/tests/unittests/http_functions/return_str/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions - - -def main(req: azure.functions.HttpRequest, context) -> str: - return 'Hello World!' diff --git a/tests/unittests/http_functions/set_cookie_resp_header_default_values/function.json b/tests/unittests/http_functions/set_cookie_resp_header_default_values/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/set_cookie_resp_header_default_values/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/set_cookie_resp_header_default_values/main.py b/tests/unittests/http_functions/set_cookie_resp_header_default_values/main.py deleted file mode 100644 index a29b693b9..000000000 --- a/tests/unittests/http_functions/set_cookie_resp_header_default_values/main.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import logging - -import azure.functions as func - - -def main(req: func.HttpRequest): - logging.info('Python HTTP trigger function processed a request.') - resp = func.HttpResponse( - "This HTTP triggered function executed successfully.") - - resp.headers.add("Set-Cookie", 'foo=bar') - - return resp diff --git a/tests/unittests/http_functions/set_cookie_resp_header_empty/function.json b/tests/unittests/http_functions/set_cookie_resp_header_empty/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/set_cookie_resp_header_empty/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/set_cookie_resp_header_empty/main.py b/tests/unittests/http_functions/set_cookie_resp_header_empty/main.py deleted file mode 100644 index 3e33bc8e4..000000000 --- a/tests/unittests/http_functions/set_cookie_resp_header_empty/main.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import logging - -import azure.functions as func - - -def main(req: func.HttpRequest): - logging.info('Python HTTP trigger function processed a request.') - resp = func.HttpResponse( - "This HTTP triggered function executed successfully.") - - resp.headers.add("Set-Cookie", '') - - return resp diff --git a/tests/unittests/http_functions/sync_logging/function.json b/tests/unittests/http_functions/sync_logging/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/sync_logging/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/sync_logging/main.py b/tests/unittests/http_functions/sync_logging/main.py deleted file mode 100644 index 9b4d89634..000000000 --- a/tests/unittests/http_functions/sync_logging/main.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import logging -import time - -import azure.functions - -logger = logging.getLogger('my function') - - -def main(req: azure.functions.HttpRequest): - 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' diff --git a/tests/unittests/http_functions/unhandled_error/function.json b/tests/unittests/http_functions/unhandled_error/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/unhandled_error/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/unhandled_error/main.py b/tests/unittests/http_functions/unhandled_error/main.py deleted file mode 100644 index 9fb1153c7..000000000 --- a/tests/unittests/http_functions/unhandled_error/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(req: azf.HttpRequest): - 1 / 0 diff --git a/tests/unittests/http_functions/unhandled_unserializable_error/function.json b/tests/unittests/http_functions/unhandled_unserializable_error/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/unhandled_unserializable_error/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/unhandled_unserializable_error/main.py b/tests/unittests/http_functions/unhandled_unserializable_error/main.py deleted file mode 100644 index aafa850f6..000000000 --- a/tests/unittests/http_functions/unhandled_unserializable_error/main.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as func - - -class UnserializableException(Exception): - def __str__(self): - raise RuntimeError('cannot serialize me') - - -def main(req: func.HttpRequest) -> str: - raise UnserializableException('foo') diff --git a/tests/unittests/http_functions/unhandled_urllib_error/function.json b/tests/unittests/http_functions/unhandled_urllib_error/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/http_functions/unhandled_urllib_error/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/unhandled_urllib_error/main.py b/tests/unittests/http_functions/unhandled_urllib_error/main.py deleted file mode 100644 index 6835fd631..000000000 --- a/tests/unittests/http_functions/unhandled_urllib_error/main.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from urllib.request import urlopen - -import azure.functions as func - - -def main(req: func.HttpRequest) -> str: - image_url = req.params.get('img') - urlopen(image_url).read() diff --git a/tests/unittests/http_functions/user_event_loop/function.json b/tests/unittests/http_functions/user_event_loop/function.json deleted file mode 100644 index 91360208a..000000000 --- a/tests/unittests/http_functions/user_event_loop/function.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req", - "methods": [ - "get" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/http_functions/user_event_loop/main.py b/tests/unittests/http_functions/user_event_loop/main.py deleted file mode 100644 index 879b1cf86..000000000 --- a/tests/unittests/http_functions/user_event_loop/main.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import asyncio -import logging - -import azure.functions as func - -logger = logging.getLogger('my function') - - -async def try_log(): - logger.info("try_log") - - -def main(req: func.HttpRequest) -> func.HttpResponse: - 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' diff --git a/tests/unittests/load_functions/absolute_thirdparty/function.json b/tests/unittests/load_functions/absolute_thirdparty/function.json deleted file mode 100644 index cb4469e61..000000000 --- a/tests/unittests/load_functions/absolute_thirdparty/function.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "scriptFile": "main.py", - "entryPoint": "main", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/load_functions/absolute_thirdparty/main.py b/tests/unittests/load_functions/absolute_thirdparty/main.py deleted file mode 100644 index e00bf2f3f..000000000 --- a/tests/unittests/load_functions/absolute_thirdparty/main.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -# Import a module from thirdparty package azure-eventhub -import azure.eventhub as eh - - -def main(req) -> str: - return f'eh = {eh.__name__}' diff --git a/tests/unittests/load_functions/entrypoint/function.json b/tests/unittests/load_functions/entrypoint/function.json deleted file mode 100644 index cb426f2f4..000000000 --- a/tests/unittests/load_functions/entrypoint/function.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "scriptFile": "main.py", - "entryPoint": "customentry", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/load_functions/entrypoint/main.py b/tests/unittests/load_functions/entrypoint/main.py deleted file mode 100644 index 4bbdf7b3d..000000000 --- a/tests/unittests/load_functions/entrypoint/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def customentry(req) -> str: - return __name__ diff --git a/tests/unittests/load_functions/implicit_import/function.json b/tests/unittests/load_functions/implicit_import/function.json deleted file mode 100644 index ec10c6de0..000000000 --- a/tests/unittests/load_functions/implicit_import/function.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "scriptFile": "main.py", - "entryPoint": "implicitinmport", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/load_functions/implicit_import/main.py b/tests/unittests/load_functions/implicit_import/main.py deleted file mode 100644 index 96b929ab9..000000000 --- a/tests/unittests/load_functions/implicit_import/main.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -# Import simple module with implicit statement should now be acceptable -# since sys.path is now appended with function script root -from simple.main import main as s_main - - -def implicitinmport(req) -> str: - return f's_main = {s_main(req)}' diff --git a/tests/unittests/load_functions/load_outside_main/function.json b/tests/unittests/load_functions/load_outside_main/function.json deleted file mode 100644 index 96d44a67a..000000000 --- a/tests/unittests/load_functions/load_outside_main/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} \ No newline at end of file diff --git a/tests/unittests/load_functions/load_outside_main/main.py b/tests/unittests/load_functions/load_outside_main/main.py deleted file mode 100644 index 3e5a5e132..000000000 --- a/tests/unittests/load_functions/load_outside_main/main.py +++ /dev/null @@ -1,23 +0,0 @@ -# This function app is to ensure the code outside main() function -# should only get loaded once in __init__.py - -import azure.functions as func - - -def main(req: func.HttpRequest): - if req.params['from'] == 'init': - # Ensure the module can still be loaded from package.__init__ - from __app__.stub_http_trigger.__init__ import main # NoQA - - from ..stub_http_trigger.__init__ import main # NoQA - - elif req.params['from'] == 'package': - # Ensure the module can still be loaded from package - from __app__.stub_http_trigger import main # NoQA - - # Ensure submodules can also be imported - from __app__.stub_http_trigger.stub_tools import FOO # NoQA - - from ..stub_http_trigger.stub_tools import FOO # NoQA - - return 'OK' diff --git a/tests/unittests/load_functions/module_not_found/function.json b/tests/unittests/load_functions/module_not_found/function.json deleted file mode 100644 index 77ad6ecb1..000000000 --- a/tests/unittests/load_functions/module_not_found/function.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "scriptFile": "main.py", - "entryPoint": "modulenotfound", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/load_functions/module_not_found/main.py b/tests/unittests/load_functions/module_not_found/main.py deleted file mode 100644 index 82e43227b..000000000 --- a/tests/unittests/load_functions/module_not_found/main.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -# Import simple module with implicit statement should now be acceptable -import notfound - - -def modulenotfound(req) -> str: - return f'notfound = {notfound.__name__}' diff --git a/tests/unittests/load_functions/name_collision/function.json b/tests/unittests/load_functions/name_collision/function.json deleted file mode 100644 index cb4469e61..000000000 --- a/tests/unittests/load_functions/name_collision/function.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "scriptFile": "main.py", - "entryPoint": "main", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/load_functions/name_collision/main.py b/tests/unittests/load_functions/name_collision/main.py deleted file mode 100644 index 41924a4c7..000000000 --- a/tests/unittests/load_functions/name_collision/main.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -# Both customer code and third-party package has the same name pytest. -# Worker should pick the pytest from the third-party package -import pytest as pt - - -def main(req) -> str: - return f'pt.__version__ = {pt.__version__}' diff --git a/tests/unittests/load_functions/name_collision_app_import/function.json b/tests/unittests/load_functions/name_collision_app_import/function.json deleted file mode 100644 index cb4469e61..000000000 --- a/tests/unittests/load_functions/name_collision_app_import/function.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "scriptFile": "main.py", - "entryPoint": "main", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/load_functions/name_collision_app_import/main.py b/tests/unittests/load_functions/name_collision_app_import/main.py deleted file mode 100644 index 4767a1c7d..000000000 --- a/tests/unittests/load_functions/name_collision_app_import/main.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -# Both customer code and third-party package has the same name pytest. -# When using absolute import, should pick customer's package. -import __app__.pytest as pt - - -def main(req) -> str: - return f'pt.__version__ = {pt.__version__}' diff --git a/tests/unittests/load_functions/no_script_file/function.json b/tests/unittests/load_functions/no_script_file/function.json deleted file mode 100644 index af28e6613..000000000 --- a/tests/unittests/load_functions/no_script_file/function.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/load_functions/no_script_file/main.py b/tests/unittests/load_functions/no_script_file/main.py deleted file mode 100644 index d0e930835..000000000 --- a/tests/unittests/load_functions/no_script_file/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(req) -> str: - return __name__ diff --git a/tests/unittests/load_functions/outside_main_code_in_init/__init__.py b/tests/unittests/load_functions/outside_main_code_in_init/__init__.py deleted file mode 100644 index d1046410c..000000000 --- a/tests/unittests/load_functions/outside_main_code_in_init/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# This function app is to ensure the code outside main() function -# should only get loaded once in __init__.py - -from .count import get_invoke_count, invoke, reset_count - -invoke() - - -def main(req): - count = get_invoke_count() - reset_count() - return f'executed count = {count}' diff --git a/tests/unittests/load_functions/outside_main_code_in_init/count.py b/tests/unittests/load_functions/outside_main_code_in_init/count.py deleted file mode 100644 index 4183ebd4d..000000000 --- a/tests/unittests/load_functions/outside_main_code_in_init/count.py +++ /dev/null @@ -1,19 +0,0 @@ -# This function app is to ensure the code outside main() function -# should only get loaded once in __init__.py - -_INVOCATION_COUNT: int = 0 - - -def invoke(): - global _INVOCATION_COUNT - _INVOCATION_COUNT += 1 - - -def get_invoke_count() -> int: - global _INVOCATION_COUNT - return _INVOCATION_COUNT - - -def reset_count(): - global _INVOCATION_COUNT - _INVOCATION_COUNT = 0 diff --git a/tests/unittests/load_functions/outside_main_code_in_init/function.json b/tests/unittests/load_functions/outside_main_code_in_init/function.json deleted file mode 100644 index 7239e0fcc..000000000 --- a/tests/unittests/load_functions/outside_main_code_in_init/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} \ No newline at end of file diff --git a/tests/unittests/load_functions/outside_main_code_in_main/count.py b/tests/unittests/load_functions/outside_main_code_in_main/count.py deleted file mode 100644 index 4183ebd4d..000000000 --- a/tests/unittests/load_functions/outside_main_code_in_main/count.py +++ /dev/null @@ -1,19 +0,0 @@ -# This function app is to ensure the code outside main() function -# should only get loaded once in __init__.py - -_INVOCATION_COUNT: int = 0 - - -def invoke(): - global _INVOCATION_COUNT - _INVOCATION_COUNT += 1 - - -def get_invoke_count() -> int: - global _INVOCATION_COUNT - return _INVOCATION_COUNT - - -def reset_count(): - global _INVOCATION_COUNT - _INVOCATION_COUNT = 0 diff --git a/tests/unittests/load_functions/outside_main_code_in_main/function.json b/tests/unittests/load_functions/outside_main_code_in_main/function.json deleted file mode 100644 index 96d44a67a..000000000 --- a/tests/unittests/load_functions/outside_main_code_in_main/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} \ No newline at end of file diff --git a/tests/unittests/load_functions/outside_main_code_in_main/main.py b/tests/unittests/load_functions/outside_main_code_in_main/main.py deleted file mode 100644 index d1046410c..000000000 --- a/tests/unittests/load_functions/outside_main_code_in_main/main.py +++ /dev/null @@ -1,12 +0,0 @@ -# This function app is to ensure the code outside main() function -# should only get loaded once in __init__.py - -from .count import get_invoke_count, invoke, reset_count - -invoke() - - -def main(req): - count = get_invoke_count() - reset_count() - return f'executed count = {count}' diff --git a/tests/unittests/load_functions/parentmodule/function.json b/tests/unittests/load_functions/parentmodule/function.json deleted file mode 100644 index 12ff3cc01..000000000 --- a/tests/unittests/load_functions/parentmodule/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "sub_module/main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/load_functions/parentmodule/module.py b/tests/unittests/load_functions/parentmodule/module.py deleted file mode 100644 index 0cf4eb5be..000000000 --- a/tests/unittests/load_functions/parentmodule/module.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -MODULE_NAME = 'PARENTMODULE' diff --git a/tests/unittests/load_functions/parentmodule/sub_module/__init__.py b/tests/unittests/load_functions/parentmodule/sub_module/__init__.py deleted file mode 100644 index 5b7f7a925..000000000 --- a/tests/unittests/load_functions/parentmodule/sub_module/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. diff --git a/tests/unittests/load_functions/parentmodule/sub_module/main.py b/tests/unittests/load_functions/parentmodule/sub_module/main.py deleted file mode 100644 index 46afb8d76..000000000 --- a/tests/unittests/load_functions/parentmodule/sub_module/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from .. import module - - -def main(req) -> str: - return module.__name__ diff --git a/tests/unittests/load_functions/pytest/__init__.py b/tests/unittests/load_functions/pytest/__init__.py deleted file mode 100644 index ab7de2090..000000000 --- a/tests/unittests/load_functions/pytest/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -"""This module pytest is provided inside customer's code, -used for checking module name collision""" -__version__ = 'from.customer.code' diff --git a/tests/unittests/load_functions/relimport/function.json b/tests/unittests/load_functions/relimport/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/load_functions/relimport/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/load_functions/relimport/main.py b/tests/unittests/load_functions/relimport/main.py deleted file mode 100644 index a83fa4696..000000000 --- a/tests/unittests/load_functions/relimport/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from . import relative - - -def main(req) -> str: - return relative.__name__ diff --git a/tests/unittests/load_functions/relimport/relative.py b/tests/unittests/load_functions/relimport/relative.py deleted file mode 100644 index 5b7f7a925..000000000 --- a/tests/unittests/load_functions/relimport/relative.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. diff --git a/tests/unittests/load_functions/simple/function.json b/tests/unittests/load_functions/simple/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/load_functions/simple/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/load_functions/simple/main.py b/tests/unittests/load_functions/simple/main.py deleted file mode 100644 index d0e930835..000000000 --- a/tests/unittests/load_functions/simple/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(req) -> str: - return __name__ diff --git a/tests/unittests/load_functions/stub_http_trigger/__init__.py b/tests/unittests/load_functions/stub_http_trigger/__init__.py deleted file mode 100644 index bccd1fe4a..000000000 --- a/tests/unittests/load_functions/stub_http_trigger/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# This function app is to ensure the code outside main() function -# should only get loaded once in __init__.py - - -def main(req): - return 'OK' diff --git a/tests/unittests/load_functions/stub_http_trigger/function.json b/tests/unittests/load_functions/stub_http_trigger/function.json deleted file mode 100644 index 7239e0fcc..000000000 --- a/tests/unittests/load_functions/stub_http_trigger/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} \ No newline at end of file diff --git a/tests/unittests/load_functions/stub_http_trigger/stub_tools.py b/tests/unittests/load_functions/stub_http_trigger/stub_tools.py deleted file mode 100644 index 3be88dd3a..000000000 --- a/tests/unittests/load_functions/stub_http_trigger/stub_tools.py +++ /dev/null @@ -1,4 +0,0 @@ -# This function app is to ensure the code outside main() function -# should only get loaded once in __init__.py - -FOO = 'BAR' diff --git a/tests/unittests/load_functions/subdir/function.json b/tests/unittests/load_functions/subdir/function.json deleted file mode 100644 index 245cdb9c8..000000000 --- a/tests/unittests/load_functions/subdir/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "sub/main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/load_functions/subdir/sub/main.py b/tests/unittests/load_functions/subdir/sub/main.py deleted file mode 100644 index d0e930835..000000000 --- a/tests/unittests/load_functions/subdir/sub/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(req) -> str: - return __name__ diff --git a/tests/unittests/load_functions/submodule/function.json b/tests/unittests/load_functions/submodule/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/load_functions/submodule/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/load_functions/submodule/main.py b/tests/unittests/load_functions/submodule/main.py deleted file mode 100644 index 0dc4ecb60..000000000 --- a/tests/unittests/load_functions/submodule/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from .sub_module import module - - -def main(req) -> str: - return module.__name__ diff --git a/tests/unittests/load_functions/submodule/sub_module/__init__.py b/tests/unittests/load_functions/submodule/sub_module/__init__.py deleted file mode 100644 index 5b7f7a925..000000000 --- a/tests/unittests/load_functions/submodule/sub_module/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. diff --git a/tests/unittests/load_functions/submodule/sub_module/module.py b/tests/unittests/load_functions/submodule/sub_module/module.py deleted file mode 100644 index bd01f6df8..000000000 --- a/tests/unittests/load_functions/submodule/sub_module/module.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -MODULE_NAME = 'SUB_MODULE' diff --git a/tests/unittests/log_filtering_functions/debug_logging/function.json b/tests/unittests/log_filtering_functions/debug_logging/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/log_filtering_functions/debug_logging/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/log_filtering_functions/debug_logging/main.py b/tests/unittests/log_filtering_functions/debug_logging/main.py deleted file mode 100644 index be3e2d506..000000000 --- a/tests/unittests/log_filtering_functions/debug_logging/main.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import logging - -import azure.functions - - -def main(req: azure.functions.HttpRequest): - 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' diff --git a/tests/unittests/log_filtering_functions/debug_user_logging/function.json b/tests/unittests/log_filtering_functions/debug_user_logging/function.json deleted file mode 100644 index 5d4d8285f..000000000 --- a/tests/unittests/log_filtering_functions/debug_user_logging/function.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} diff --git a/tests/unittests/log_filtering_functions/debug_user_logging/main.py b/tests/unittests/log_filtering_functions/debug_user_logging/main.py deleted file mode 100644 index 1f669b236..000000000 --- a/tests/unittests/log_filtering_functions/debug_user_logging/main.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import logging - -import azure.functions - -logger = logging.getLogger('my function') - - -def main(req: azure.functions.HttpRequest): - 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' diff --git a/tests/unittests/log_filtering_functions/sdk_logging/__init__.py b/tests/unittests/log_filtering_functions/sdk_logging/__init__.py deleted file mode 100644 index b45da85a5..000000000 --- a/tests/unittests/log_filtering_functions/sdk_logging/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import logging - -import azure.functions as func - -sdk_logger = logging.getLogger('azure.functions') - - -def main(req: func.HttpRequest): - sdk_logger.info('sdk_logger info') - sdk_logger.warning('sdk_logger warning') - sdk_logger.debug('sdk_logger debug') - sdk_logger.error('sdk_logger error', exc_info=True) - return 'OK-sdk-logger' diff --git a/tests/unittests/log_filtering_functions/sdk_logging/function.json b/tests/unittests/log_filtering_functions/sdk_logging/function.json deleted file mode 100644 index 9f7c2ae61..000000000 --- a/tests/unittests/log_filtering_functions/sdk_logging/function.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ], - "entryPoint": "main" -} diff --git a/tests/unittests/log_filtering_functions/sdk_submodule_logging/__init__.py b/tests/unittests/log_filtering_functions/sdk_submodule_logging/__init__.py deleted file mode 100644 index 5950d0432..000000000 --- a/tests/unittests/log_filtering_functions/sdk_submodule_logging/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import logging - -import azure.functions as func - -sdk_submodule_logger = logging.getLogger('azure.functions.submodule') - - -def main(req: func.HttpRequest): - sdk_submodule_logger.info('sdk_submodule_logger info') - sdk_submodule_logger.warning('sdk_submodule_logger warning') - sdk_submodule_logger.debug('sdk_submodule_logger debug') - sdk_submodule_logger.error('sdk_submodule_logger error', exc_info=True) - return 'OK-sdk-submodule-logging' diff --git a/tests/unittests/log_filtering_functions/sdk_submodule_logging/function.json b/tests/unittests/log_filtering_functions/sdk_submodule_logging/function.json deleted file mode 100644 index 9f7c2ae61..000000000 --- a/tests/unittests/log_filtering_functions/sdk_submodule_logging/function.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req" - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ], - "entryPoint": "main" -} diff --git a/tests/unittests/path_import/path_import.py b/tests/unittests/path_import/path_import.py deleted file mode 100644 index bc22de775..000000000 --- a/tests/unittests/path_import/path_import.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import asyncio -import os -import shutil -import sys - -from tests.utils import testutils - -from azure_functions_worker import protos - - -async def verify_path_imports(): - test_env = {} - request = protos.FunctionEnvironmentReloadRequest( - environment_variables=test_env) - - request_msg = protos.StreamingMessage( - request_id='0', - function_environment_reload_request=request) - - disp = testutils.create_dummy_dispatcher() - - test_path = 'test_module_dir' - test_mod_path = os.path.join(test_path, 'test_module.py') - - os.mkdir(test_path) - with open(test_mod_path, 'w') as f: - f.write('CONSTANT = "This module was imported!"') - - if (sys.argv[1] == 'success'): - await disp._handle__function_environment_reload_request(request_msg) - - try: - import test_module - print(test_module.CONSTANT) - finally: - # Cleanup - shutil.rmtree(test_path) - - -if __name__ == '__main__': - loop = asyncio.get_event_loop() - loop.run_until_complete(verify_path_imports()) - loop.close() diff --git a/tests/unittests/path_import/test_path_import.sh b/tests/unittests/path_import/test_path_import.sh deleted file mode 100644 index 881b4de02..000000000 --- a/tests/unittests/path_import/test_path_import.sh +++ /dev/null @@ -1,9 +0,0 @@ -#! /bin/bash - -# $2 is sys.path from caller -export PYTHONPATH="test_module_dir:$2" -SCRIPT_DIR="$(dirname $0)" - -python $SCRIPT_DIR/path_import.py $1 - -unset PYTHONPATH \ No newline at end of file diff --git a/tests/unittests/resources/customer_deps_path/azure/__init__.py b/tests/unittests/resources/customer_deps_path/azure/__init__.py deleted file mode 100644 index 649cbaa5f..000000000 --- a/tests/unittests/resources/customer_deps_path/azure/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/tests/unittests/resources/customer_deps_path/azure/functions/__init__.py b/tests/unittests/resources/customer_deps_path/azure/functions/__init__.py deleted file mode 100644 index e5e3779b1..000000000 --- a/tests/unittests/resources/customer_deps_path/azure/functions/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -__version__: str = 'customer' - -import os - -# ./tests/unittests/resources/customer_deps_path/common_module -package_location: str = os.path.dirname(__file__) diff --git a/tests/unittests/resources/customer_deps_path/common_module/__init__.py b/tests/unittests/resources/customer_deps_path/common_module/__init__.py deleted file mode 100644 index e5e3779b1..000000000 --- a/tests/unittests/resources/customer_deps_path/common_module/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -__version__: str = 'customer' - -import os - -# ./tests/unittests/resources/customer_deps_path/common_module -package_location: str = os.path.dirname(__file__) diff --git a/tests/unittests/resources/customer_deps_path/common_namespace/__init__.py b/tests/unittests/resources/customer_deps_path/common_namespace/__init__.py deleted file mode 100644 index 649cbaa5f..000000000 --- a/tests/unittests/resources/customer_deps_path/common_namespace/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/tests/unittests/resources/customer_deps_path/common_namespace/nested_module/__init__.py b/tests/unittests/resources/customer_deps_path/common_namespace/nested_module/__init__.py deleted file mode 100644 index 07afaec18..000000000 --- a/tests/unittests/resources/customer_deps_path/common_namespace/nested_module/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -__version__: str = 'customer' - -import os - -# ./tests/unittests/resources/customer_deps_path/common_namespace/nested_module -package_location: str = os.path.dirname(__file__) diff --git a/tests/unittests/resources/customer_deps_path/readme.md b/tests/unittests/resources/customer_deps_path/readme.md deleted file mode 100644 index fc7905c7f..000000000 --- a/tests/unittests/resources/customer_deps_path/readme.md +++ /dev/null @@ -1,9 +0,0 @@ -This is a folder for containing a common_module in customer dependencies. - -It is used for testing import behavior with worker_deps_path. - -Adding this folder to sys.path and importing common_module, printing out the -common_module.__version__ will show which module is loaded. - -To test if the namespace is reloaded properly, printing out the -common_namespace.nested_common.__version__ will show which namespace is loaded. diff --git a/tests/unittests/resources/customer_func_path/HttpTrigger/__init__.py b/tests/unittests/resources/customer_func_path/HttpTrigger/__init__.py deleted file mode 100644 index d5d57e3eb..000000000 --- a/tests/unittests/resources/customer_func_path/HttpTrigger/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -import azure.functions as func # NoQA - - -def main(): - return os.path.abspath(os.path.dirname(func.__file__)) diff --git a/tests/unittests/resources/customer_func_path/HttpTrigger/function.json b/tests/unittests/resources/customer_func_path/HttpTrigger/function.json deleted file mode 100644 index 4667f0aca..000000000 --- a/tests/unittests/resources/customer_func_path/HttpTrigger/function.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "scriptFile": "__init__.py", - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req", - "methods": [ - "get", - "post" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ] -} \ No newline at end of file diff --git a/tests/unittests/resources/customer_func_path/common_module/__init__.py b/tests/unittests/resources/customer_func_path/common_module/__init__.py deleted file mode 100644 index 0784c9b6f..000000000 --- a/tests/unittests/resources/customer_func_path/common_module/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import os - -FUNCTION_APP = "function_app" -__version__: str == FUNCTION_APP - -# This module should be shadowed from customer_deps_path/common_module -# ./tests/unittests/resources/customer_func_path/common_module -package_location: str = os.path.dirname(__file__) diff --git a/tests/unittests/resources/customer_func_path/func_specific_module/__init__.py b/tests/unittests/resources/customer_func_path/func_specific_module/__init__.py deleted file mode 100644 index ed832cbbc..000000000 --- a/tests/unittests/resources/customer_func_path/func_specific_module/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import os - -FUNCTION_APP = "function_app" -__version__: str == FUNCTION_APP - -# ./tests/unittests/resources/customer_func_path/func_specific_module -package_location: str = os.path.dirname(__file__) diff --git a/tests/unittests/resources/customer_func_path/host.json b/tests/unittests/resources/customer_func_path/host.json deleted file mode 100644 index 05291ed43..000000000 --- a/tests/unittests/resources/customer_func_path/host.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "version": "2.0", - "logging": { - "applicationInsights": { - "samplingSettings": { - "isEnabled": true, - "excludedTypes": "Request" - } - } - }, - "extensionBundle": { - "id": "Microsoft.Azure.Functions.ExtensionBundle", - "version": "[1.*, 2.0.0)" - } -} \ No newline at end of file diff --git a/tests/unittests/resources/customer_func_path/requirements.txt b/tests/unittests/resources/customer_func_path/requirements.txt deleted file mode 100644 index f86a15a6a..000000000 --- a/tests/unittests/resources/customer_func_path/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -azure-functions \ No newline at end of file diff --git a/tests/unittests/resources/functions.png b/tests/unittests/resources/functions.png deleted file mode 100644 index 42ea4bbf5c1c1867f987bffff173340dd5465731..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1976 zcmV;p2S@mcP)_5}I)J7=KTznH!%Hy>((`GrjHo&t_1qn zHEpUFG|&e)^Q&vxOy?N$7-#YXRs z!c@<+K>y{M6AH8cNT9t+j%bcRBMZylt~ohj2G6_X*iMp3pZyavcw)UfEqVt1Ns?^a zX70r+C5##~cw(Sm^S4iv_22g#Fvs!x;)N?ppjkudefsw{sr!fNcgBbLD-Wc4wX2kxN0DlPBL zP-~gR{;tVoD0sgns3%%x(f%6*8rc{C>Iw37jWCP-U6VjBkq;czq$V(n2G5}0y`rC3 zkD9`~tWob|o$5N&Jp!|6@aXk!Q##!S9c2s5SbrDQI2D9eSdyyLG-hD$x3m6H_J6Xq0^P`~iyM#c~2gO*Vcj*5;^^Y>&%XL5Gr96P4 z4~l^+QRke%fm>A{9tm4V7HIm2spOxuc(+9z=2C_j zQO~hJQ;a{)T&bH^nHpwCzOJ0o?$fHvT$vhXN3pKZrZwv`M_j9yP7SjoFLyNK-w1Ox z>Y*6uGtyEBCI;%AUzk%bkwBA-e`C!2icX>iGYNUQBN_h&nR#l;SsBO<6XV|~Gw=EI z)JrkY6yx78Gw-jp)Q>`-DaOBXW;1HmvYye6KVa50TMR^}W-aS6-T1e{%x&k@)GRv= z$i}}VW;*p%DbNJt-x~93kG?~pekcW+VEkKTCQ^Scl{>xhZ!fOZ2Y`Y7RdQhw@dgDKG!JJcrJKXMAxzii} z?BLHbb9(*vz}-vdMY*GN{HMJ(?zYgYKZQ43rN^CPddGh{wRWefR4+xK{dt6rR_A%S z^XK_WTBX@R@A!M6mn_i4j=vvz&I0Y%^BL57JMNr0e?EhmPSSEm>-hVk=d|2iy5=y^ zIy=UGJrhc=`CFi=hu@y){;k}RM}54|;^Vs9dGj1fn&#_3cWEhHYn2`JQ6EpVT(2u$ zJ;#cmr&YQ0=^N9ZQZ?c8t4Z{8K$`9CXO|XBzHyt=g@RJ3D3kQ$6l>$@piw z>Xh+Mb@QWZ)o!K7P8t79bJqYBwBNaE7Zel}6ciK`6ciNHMg0%UvV9Y4bvWVx0000< KMNUMnLSTZ5JlK2y diff --git a/tests/unittests/resources/mock_azure_functions/azure/__init__.py b/tests/unittests/resources/mock_azure_functions/azure/__init__.py deleted file mode 100644 index 649cbaa5f..000000000 --- a/tests/unittests/resources/mock_azure_functions/azure/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/tests/unittests/resources/mock_azure_functions/azure/functions/__init__.py b/tests/unittests/resources/mock_azure_functions/azure/functions/__init__.py deleted file mode 100644 index 9f561659c..000000000 --- a/tests/unittests/resources/mock_azure_functions/azure/functions/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -__version__ = "dummy" diff --git a/tests/unittests/resources/mock_azure_functions/readme.md b/tests/unittests/resources/mock_azure_functions/readme.md deleted file mode 100644 index c40015fb4..000000000 --- a/tests/unittests/resources/mock_azure_functions/readme.md +++ /dev/null @@ -1,3 +0,0 @@ -# Instruction - -This is a dummy azure.functions SDK for testing the backward compatibility \ No newline at end of file diff --git a/tests/unittests/resources/worker_deps_path/azure/__init__.py b/tests/unittests/resources/worker_deps_path/azure/__init__.py deleted file mode 100644 index 649cbaa5f..000000000 --- a/tests/unittests/resources/worker_deps_path/azure/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/tests/unittests/resources/worker_deps_path/azure/functions/__init__.py b/tests/unittests/resources/worker_deps_path/azure/functions/__init__.py deleted file mode 100644 index abdc27afc..000000000 --- a/tests/unittests/resources/worker_deps_path/azure/functions/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -__version__: str = 'worker' - -import os - -# ./tests/unittests/resources/worker_deps_path/common_module -package_location: str = os.path.dirname(__file__) diff --git a/tests/unittests/resources/worker_deps_path/common_module/__init__.py b/tests/unittests/resources/worker_deps_path/common_module/__init__.py deleted file mode 100644 index abdc27afc..000000000 --- a/tests/unittests/resources/worker_deps_path/common_module/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -__version__: str = 'worker' - -import os - -# ./tests/unittests/resources/worker_deps_path/common_module -package_location: str = os.path.dirname(__file__) diff --git a/tests/unittests/resources/worker_deps_path/common_namespace/__init__.py b/tests/unittests/resources/worker_deps_path/common_namespace/__init__.py deleted file mode 100644 index 649cbaa5f..000000000 --- a/tests/unittests/resources/worker_deps_path/common_namespace/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/tests/unittests/resources/worker_deps_path/common_namespace/nested_module/__init__.py b/tests/unittests/resources/worker_deps_path/common_namespace/nested_module/__init__.py deleted file mode 100644 index 3211c9534..000000000 --- a/tests/unittests/resources/worker_deps_path/common_namespace/nested_module/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -__version__: str = 'worker' - -import os - -# ./tests/unittests/resources/worker_deps_path/common_namespace/nested_module -package_location: str = os.path.dirname(__file__) diff --git a/tests/unittests/resources/worker_deps_path/readme.md b/tests/unittests/resources/worker_deps_path/readme.md deleted file mode 100644 index a8e3154f9..000000000 --- a/tests/unittests/resources/worker_deps_path/readme.md +++ /dev/null @@ -1,9 +0,0 @@ -This is a folder for containing a common_module in worker dependencies. - -It is used for testing import behavior with customer_deps_path. - -Adding this folder to sys.path and importing common_module, printing out the -common_module.__version__ will show which module is loaded. - -To test if the namespace is reloaded properly, printing out the -common_namespace.nested_common.__version__ will show which namespace is loaded. diff --git a/tests/unittests/test-binding/foo/__init__.py b/tests/unittests/test-binding/foo/__init__.py deleted file mode 100644 index 5b7f7a925..000000000 --- a/tests/unittests/test-binding/foo/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. diff --git a/tests/unittests/test-binding/foo/binding.py b/tests/unittests/test-binding/foo/binding.py deleted file mode 100644 index 68b0d80af..000000000 --- a/tests/unittests/test-binding/foo/binding.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from azure.functions import meta - - -class Binding(meta.InConverter, meta.OutConverter, - binding='fooType'): - pass diff --git a/tests/unittests/test-binding/functions/foo/function.json b/tests/unittests/test-binding/functions/foo/function.json deleted file mode 100644 index fb00b1207..000000000 --- a/tests/unittests/test-binding/functions/foo/function.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "scriptFile": "main.py", - - "bindings": [ - { - "type": "fooType", - "direction": "in", - "name": "req" - } - ] -} diff --git a/tests/unittests/test-binding/functions/foo/main.py b/tests/unittests/test-binding/functions/foo/main.py deleted file mode 100644 index 160eb0bd8..000000000 --- a/tests/unittests/test-binding/functions/foo/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def main(req): - pass diff --git a/tests/unittests/test-binding/setup.py b/tests/unittests/test-binding/setup.py deleted file mode 100644 index e0e248693..000000000 --- a/tests/unittests/test-binding/setup.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from setuptools import setup - -setup( - name='foo-binding', - version='1.0', - packages=['foo'], - entry_points={ - 'azure.functions.bindings': [ - 'foo=foo.binding:Binding', - ] - }, -) diff --git a/tests/unittests/test_app_setting_manager.py b/tests/unittests/test_app_setting_manager.py deleted file mode 100644 index d203704f9..000000000 --- a/tests/unittests/test_app_setting_manager.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import collections as col -import os -from unittest.mock import patch - -from tests.utils import testutils - -from azure_functions_worker.constants import ( - PYTHON_ENABLE_DEBUG_LOGGING, - PYTHON_ENABLE_INIT_INDEXING, - PYTHON_THREADPOOL_THREAD_COUNT, -) -from azure_functions_worker.utils.app_setting_manager import get_python_appsetting_state - -SysVersionInfo = col.namedtuple("VersionInfo", ["major", "minor", "micro", - "releaselevel", "serial"]) -DISPATCHER_FUNCTIONS_DIR = testutils.UNIT_TESTS_FOLDER / 'dispatcher_functions' -DISPATCHER_STEIN_FUNCTIONS_DIR = testutils.UNIT_TESTS_FOLDER / \ - 'dispatcher_functions' / \ - 'dispatcher_functions_stein' -DISPATCHER_STEIN_INVALID_FUNCTIONS_DIR = testutils.UNIT_TESTS_FOLDER / \ - 'broken_functions' / \ - 'invalid_stein' - - -class TestDefaultAppSettingsLogs(testutils.AsyncTestCase): - """Tests for default app settings logs.""" - - @classmethod - def setUpClass(cls): - cls._ctrl = testutils.start_mockhost( - script_root=DISPATCHER_FUNCTIONS_DIR) - os_environ = os.environ.copy() - cls._patch_environ = patch.dict('os.environ', os_environ) - cls._patch_environ.start() - super().setUpClass() - - @classmethod - def tearDownClass(cls): - super().tearDownClass() - cls._patch_environ.stop() - - async def test_initialize_worker_logging(self): - """Test if the dispatcher's log can be flushed out during worker - initialization - """ - async with self._ctrl as host: - r = await host.init_worker('3.0.12345') - self.assertTrue('App Settings state: ' in log for log in r.logs) - self.assertTrue('PYTHON_ENABLE_WORKER_EXTENSIONS: ' - in log for log in r.logs) - - def test_get_python_appsetting_state(self): - app_setting_state = get_python_appsetting_state() - expected_string = "PYTHON_ENABLE_WORKER_EXTENSIONS: " - self.assertIn(expected_string, app_setting_state) - - -class TestNonDefaultAppSettingsLogs(testutils.AsyncTestCase): - """Tests for non-default app settings logs.""" - - @classmethod - def setUpClass(cls): - cls._ctrl = testutils.start_mockhost( - script_root=DISPATCHER_FUNCTIONS_DIR) - os_environ = os.environ.copy() - os_environ[PYTHON_THREADPOOL_THREAD_COUNT] = '20' - os_environ[PYTHON_ENABLE_DEBUG_LOGGING] = '1' - 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): - super().tearDownClass() - cls._patch_environ.stop() - - async def test_initialize_worker_logging(self): - """Test if the dispatcher's log can be flushed out during worker - initialization - """ - async with self._ctrl as host: - r = await host.init_worker('3.0.12345') - self.assertTrue('App Settings state: ' in log for log in r.logs) - self.assertTrue('PYTHON_THREADPOOL_THREAD_COUNT: ' - in log for log in r.logs) - self.assertTrue('PYTHON_ENABLE_DEBUG_LOGGING: ' - in log for log in r.logs) - self.assertTrue('PYTHON_ENABLE_INIT_INDEXING: ' - in log for log in r.logs) - - def test_get_python_appsetting_state(self): - app_setting_state = get_python_appsetting_state() - self.assertIn("PYTHON_THREADPOOL_THREAD_COUNT: 20 | ", - app_setting_state) - self.assertIn("PYTHON_ENABLE_DEBUG_LOGGING: 1 | ", app_setting_state) - self.assertIn("PYTHON_ENABLE_WORKER_EXTENSIONS: ", app_setting_state) diff --git a/tests/unittests/test_broken_functions.py b/tests/unittests/test_broken_functions.py deleted file mode 100644 index 508122c92..000000000 --- a/tests/unittests/test_broken_functions.py +++ /dev/null @@ -1,299 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from tests.utils import testutils - -from azure_functions_worker import protos - - -class TestMockHost(testutils.AsyncTestCase): - broken_funcs_dir = testutils.UNIT_TESTS_FOLDER / 'broken_functions' - - async def test_load_broken__missing_py_param(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('missing_py_param') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r".*cannot load the missing_py_param function" - r".*parameters are declared in function.json" - r".*'req'.*") - - async def test_load_broken__missing_json_param(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('missing_json_param') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r".*cannot load the missing_json_param function" - r".*parameters are declared in Python" - r".*'spam'.*") - - async def test_load_broken__wrong_param_dir(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('wrong_param_dir') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r'.*cannot load the wrong_param_dir function' - r'.*binding foo is declared to have the "out".*') - - async def test_load_broken__bad_out_annotation(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('bad_out_annotation') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r'.*cannot load the bad_out_annotation function' - r'.*binding foo has invalid Out annotation.*') - - async def test_load_broken__wrong_binding_dir(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('wrong_binding_dir') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r'.*cannot load the wrong_binding_dir function' - r'.* binding foo is declared to have the "in" direction' - r'.*but its annotation is.*Out.*') - - async def test_load_broken__invalid_context_param(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('invalid_context_param') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r'.*cannot load the invalid_context_param function' - r'.*the "context" parameter.*') - - async def test_load_broken__syntax_error(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('syntax_error') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertIn('SyntaxError', r.response.result.exception.message) - - async def test_load_broken__module_not_found_error(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('module_not_found_error') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertIn('ModuleNotFoundError', - r.response.result.exception.message) - - async def test_load_broken__import_error(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('import_error') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertIn('ImportError', - r.response.result.exception.message) - self.assertNotIn('', - r.response.result.exception.message) - self.assertNotIn('', - r.response.result.exception.message) - - async def test_load_broken__inout_param(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('inout_param') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r'.*cannot load the inout_param function' - r'.*"inout" bindings.*') - - async def test_load_broken__return_param_in(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('return_param_in') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r'.*cannot load the return_param_in function' - r'.*"\$return" .* set to "out"') - - async def test_load_broken__invalid_return_anno(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('invalid_return_anno') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r'.*cannot load the invalid_return_anno function' - r'.*Python return annotation "int" does not match ' - r'binding type "http"') - - async def test_load_broken__invalid_return_anno_non_type(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function( - 'invalid_return_anno_non_type') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r'.*cannot load the invalid_return_anno_non_type function: ' - r'has invalid non-type return annotation 123') - - async def test_load_broken__invalid_http_trigger_anno(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('invalid_http_trigger_anno') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertEqual( - r.response.result.exception.message, - 'FunctionLoadError: cannot load the invalid_http_trigger_anno' - ' function: type of req binding in function.json "httpTrigger" ' - 'does not match its Python annotation "int"') - - async def test_load_broken__invalid_out_anno(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('invalid_out_anno') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertEqual( - r.response.result.exception.message, - 'FunctionLoadError: cannot load the invalid_out_anno function: ' - r'type of ret binding in function.json "http" ' - r'does not match its Python annotation "HttpRequest"') - - async def test_load_broken__invalid_in_anno(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('invalid_in_anno') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertEqual( - r.response.result.exception.message, - 'FunctionLoadError: cannot load the invalid_in_anno function: ' - r'type of req binding in function.json "httpTrigger" ' - r'does not match its Python annotation "HttpResponse"') - - async def test_load_broken__invalid_datatype(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('invalid_datatype') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r'.*cannot load the invalid_datatype function: ' - r'.*binding type "httpTrigger" and dataType "1" in ' - r'function.json do not match the corresponding function ' - r'parameter.* Python type annotation "HttpResponse"') - - async def test_load_broken__invalid_in_anno_non_type(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('invalid_in_anno_non_type') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r'.*cannot load the invalid_in_anno_non_type function: ' - r'binding req has invalid non-type annotation 123') - - async def test_import_module_troubleshooting_url(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('missing_module') - - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r'.*ModuleNotFoundError') diff --git a/tests/unittests/test_code_quality.py b/tests/unittests/test_code_quality.py deleted file mode 100644 index 54d1cc725..000000000 --- a/tests/unittests/test_code_quality.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import pathlib -import subprocess -import sys -import unittest - -ROOT_PATH = pathlib.Path(__file__).parent.parent.parent - - -class TestCodeQuality(unittest.TestCase): - def test_mypy(self): - try: - import mypy # NoQA - except ImportError as e: - raise unittest.SkipTest('mypy module is missing') from e - - try: - subprocess.run( - [sys.executable, '-m', 'mypy', '-m', 'azure_functions_worker'], - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - cwd=str(ROOT_PATH)) - except subprocess.CalledProcessError as ex: - if (sys.version_info[1] == 7 - and sys.version_info[2] == 3): - raise unittest.SkipTest('Subprocess start failing for 3.7.3') \ - from ex - output = ex.output.decode() - raise AssertionError( - f'mypy validation failed:\n{output}') from None - - def test_flake8(self): - try: - import flake8 # NoQA - except ImportError as e: - raise unittest.SkipTest('flake8 module is missing') from e - - config_path = ROOT_PATH / '.flake8' - if not config_path.exists(): - raise unittest.SkipTest('could not locate the .flake8 file') - - try: - subprocess.run( - [sys.executable, '-m', 'flake8', '--config', str(config_path)], - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - cwd=str(ROOT_PATH)) - except subprocess.CalledProcessError as ex: - output = ex.output.decode() - raise AssertionError( - f'flake8 validation failed:\n{output}') from None diff --git a/tests/unittests/test_datumref.py b/tests/unittests/test_datumref.py deleted file mode 100644 index 3db946467..000000000 --- a/tests/unittests/test_datumref.py +++ /dev/null @@ -1,148 +0,0 @@ -import sys -import unittest -from http.cookies import SimpleCookie -from unittest import skipIf - -from dateutil import parser -from dateutil.parser import ParserError - -from azure_functions_worker import protos -from azure_functions_worker.bindings.datumdef import ( - Datum, - parse_cookie_attr_expires, - parse_cookie_attr_same_site, - parse_to_rpc_http_cookie_list, -) -from azure_functions_worker.bindings.nullable_converters import ( - to_nullable_bool, - to_nullable_double, - to_nullable_string, - to_nullable_timestamp, -) -from azure_functions_worker.protos import RpcHttpCookie - - -class TestDatumRef(unittest.TestCase): - def test_parse_cookie_attr_expires_none(self): - self.assertEqual(parse_cookie_attr_expires({"expires": None}), None) - - def test_parse_cookie_attr_expires_zero_length(self): - self.assertEqual(parse_cookie_attr_expires({"expires": ""}), None) - - def test_parse_cookie_attr_expires_valid(self): - self.assertEqual(parse_cookie_attr_expires( - {"expires": "Thu, 12-Jan-2017 13:55:08 GMT"}), - parser.parse("Thu, 12-Jan-2017 13:55:08 GMT")) - - def test_parse_cookie_attr_expires_parse_error(self): - with self.assertRaises(ParserError): - parse_cookie_attr_expires( - {"expires": "Thu, 12-Jan-2017 13:550:08 GMT"}) - - def test_parse_cookie_attr_expires_overflow_error(self): - with self.assertRaises(OverflowError): - parse_cookie_attr_expires( - {"expires": "Thu, 12-Jan-9999999999999999 13:550:08 GMT"}) - - def test_parse_cookie_attr_same_site_default(self): - self.assertEqual(parse_cookie_attr_same_site( - {}), - getattr(protos.RpcHttpCookie.SameSite, "None")) - - def test_parse_cookie_attr_same_site_lax(self): - self.assertEqual(parse_cookie_attr_same_site( - {'samesite': 'lax'}), - getattr(protos.RpcHttpCookie.SameSite, "Lax")) - - def test_parse_cookie_attr_same_site_strict(self): - self.assertEqual(parse_cookie_attr_same_site( - {'samesite': 'strict'}), - getattr(protos.RpcHttpCookie.SameSite, "Strict")) - - def test_parse_cookie_attr_same_site_explicit_none(self): - self.assertEqual(parse_cookie_attr_same_site( - {'samesite': 'none'}), - getattr(protos.RpcHttpCookie.SameSite, "ExplicitNone")) - - def test_parse_to_rpc_http_cookie_list_none(self): - self.assertEqual(parse_to_rpc_http_cookie_list(None), None) - - @skipIf(sys.version_info < (3, 8, 0), - "Skip the tests for Python 3.7 and below") - def test_parse_to_rpc_http_cookie_list_valid(self): - headers = [ - 'foo=bar; Path=/some/path; Secure; HttpOnly; Domain=123; ' - 'SameSite=Lax; Max-Age=12345; Expires=Thu, 12-Jan-2017 13:55:08 ' - 'GMT;', - 'foo2=bar; Path=/some/path2; Secure; HttpOnly; Domain=123; ' - 'SameSite=Lax; Max-Age=12345; Expires=Thu, 12-Jan-2017 13:55:08 ' - 'GMT;'] - - cookies = SimpleCookie('\r\n'.join(headers)) - - cookie1 = RpcHttpCookie(name="foo", - value="bar", - domain=to_nullable_string("123", - "cookie.domain"), - path=to_nullable_string("/some/path", - "cookie.path"), - expires=to_nullable_timestamp( - parse_cookie_attr_expires( - { - "expires": "Thu, " - "12-Jan-2017 13:55:08" - " GMT"}), - 'cookie.expires'), - secure=to_nullable_bool( - bool("True"), - 'cookie.secure'), - http_only=to_nullable_bool( - bool("True"), - 'cookie.httpOnly'), - same_site=parse_cookie_attr_same_site( - {"samesite": "Lax"}), - max_age=to_nullable_double( - 12345, - 'cookie.maxAge')) - - cookie2 = RpcHttpCookie(name="foo2", - value="bar", - domain=to_nullable_string("123", - "cookie.domain"), - path=to_nullable_string("/some/path2", - "cookie.path"), - expires=to_nullable_timestamp( - parse_cookie_attr_expires( - { - "expires": "Thu, " - "12-Jan-2017 13:55:08" - " GMT"}), - 'cookie.expires'), - secure=to_nullable_bool( - bool("True"), - 'cookie.secure'), - http_only=to_nullable_bool( - bool("True"), - 'cookie.httpOnly'), - same_site=parse_cookie_attr_same_site( - {"samesite": "Lax"}), - max_age=to_nullable_double( - 12345, - 'cookie.maxAge')) - - rpc_cookies = parse_to_rpc_http_cookie_list([cookies]) - self.assertEqual(cookie1, rpc_cookies[0]) - self.assertEqual(cookie2, rpc_cookies[1]) - - def test_parse_to_rpc_http_cookie_list_no_cookie(self): - datum = Datum( - type='http', - value=dict( - status_code=None, - headers=None, - body=None, - ) - ) - - self.assertIsNone( - parse_to_rpc_http_cookie_list(datum.value.get('cookies'))) diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py deleted file mode 100644 index ebaac2ced..000000000 --- a/tests/unittests/test_dispatcher.py +++ /dev/null @@ -1,1127 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import asyncio -import collections as col -import contextvars -import os -import sys -import unittest -from typing import Optional, Tuple -from unittest.mock import patch - -from tests.utils import testutils -from tests.utils.testutils import UNIT_TESTS_ROOT - -from azure_functions_worker import protos -from azure_functions_worker.constants import ( - HTTP_URI, - METADATA_PROPERTIES_WORKER_INDEXED, - PYTHON_ENABLE_DEBUG_LOGGING, - PYTHON_ENABLE_INIT_INDEXING, - PYTHON_THREADPOOL_THREAD_COUNT, - PYTHON_THREADPOOL_THREAD_COUNT_DEFAULT, - PYTHON_THREADPOOL_THREAD_COUNT_MAX_37, - PYTHON_THREADPOOL_THREAD_COUNT_MIN, - REQUIRES_ROUTE_PARAMETERS -) -from azure_functions_worker.dispatcher import Dispatcher, ContextEnabledTask -from azure_functions_worker.version import VERSION - -SysVersionInfo = col.namedtuple("VersionInfo", ["major", "minor", "micro", - "releaselevel", "serial"]) -DISPATCHER_FUNCTIONS_DIR = testutils.UNIT_TESTS_FOLDER / 'dispatcher_functions' -DISPATCHER_STEIN_FUNCTIONS_DIR = testutils.UNIT_TESTS_FOLDER / \ - 'dispatcher_functions' / \ - 'dispatcher_functions_stein' -FUNCTION_APP_DIRECTORY = UNIT_TESTS_ROOT / 'dispatcher_functions' / \ - 'dispatcher_functions_stein' -HTTPV2_FUNCTION_APP_DIRECTORY = UNIT_TESTS_ROOT / 'dispatcher_functions' / \ - 'http_v2' / 'fastapi' - - -class TestThreadPoolSettingsPython37(testutils.AsyncTestCase): - """Base test class for testing thread pool settings for sync threadpool - worker count. This class specifically sets sys.version_info to return as - Python 3.7 and extended classes change this value and other platform - specific values to test the behavior across the different python versions. - - Ref: - NEW_TYPING = sys.version_info[:3] >= (3, 7, 0) # PEP 560 - """ - - def setUp(self, version=SysVersionInfo(3, 7, 0, 'final', 0)): - self._ctrl = testutils.start_mockhost( - script_root=DISPATCHER_FUNCTIONS_DIR) - self._default_workers: Optional[ - int] = PYTHON_THREADPOOL_THREAD_COUNT_DEFAULT - self._over_max_workers: int = 10000 - self._allowed_max_workers: int = PYTHON_THREADPOOL_THREAD_COUNT_MAX_37 - self._pre_env = dict(os.environ) - self.mock_version_info = patch( - 'azure_functions_worker.dispatcher.sys.version_info', - version) - 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_initialize_worker(self): - """Test if the dispatcher can be initialized worker successfully - """ - async with self._ctrl as host: - r = await host.init_worker('3.0.12345') - self.assertIsInstance(r.response, protos.WorkerInitResponse) - 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) - - async def test_dispatcher_environment_reload(self): - """Test function environment reload response - """ - async with self._ctrl as host: - # Reload environment variable on specialization - r = await host.reload_environment(environment={}) - 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) - - async def test_dispatcher_initialize_worker_logging(self): - """Test if the dispatcher's log can be flushed out during worker - initialization - """ - async with self._ctrl as host: - r = await host.init_worker('3.0.12345') - self.assertEqual( - len([log for log in r.logs if log.message.startswith( - 'Received WorkerInitRequest' - )]), - 1 - ) - - async def test_dispatcher_initialize_worker_settings_logs(self): - """Test if the dispatcher's log can be flushed out during worker - initialization - """ - async with self._ctrl as host: - r = await host.init_worker('3.0.12345') - self.assertTrue('PYTHON_ENABLE_WORKER_EXTENSIONS: ' - in log for log in r.logs) - - async def test_dispatcher_environment_reload_logging(self): - """Test if the sync threadpool will pick up app setting in placeholder - mode (Linux Consumption) - """ - async with self._ctrl as host: - await host.init_worker() - await self._check_if_function_is_ok(host) - - # Reload environment variable on specialization - r = await host.reload_environment(environment={}) - self.assertEqual( - len([log for log in r.logs if log.message.startswith( - 'Received FunctionEnvironmentReloadRequest' - )]), - 1 - ) - - async def test_dispatcher_environment_reload_settings_logs(self): - """Test if the sync threadpool will pick up app setting in placeholder - mode (Linux Consumption) - """ - async with self._ctrl as host: - await host.init_worker() - await self._check_if_function_is_ok(host) - - # Reload environment variable on specialization - r = await host.reload_environment(environment={}) - self.assertTrue('PYTHON_ENABLE_WORKER_EXTENSIONS: ' - in log for log in r.logs) - - async def test_dispatcher_send_worker_request(self): - """Test if the worker status response will be sent correctly when - a worker status request is received - """ - async with self._ctrl as host: - r = await host.get_worker_status() - self.assertIsInstance(r.response, protos.WorkerStatusResponse) - - async def test_dispatcher_sync_threadpool_default_worker(self): - """Test if the sync threadpool has maximum worker count set the - correct default value - """ - async with self._ctrl as host: - await host.init_worker() - await self._check_if_function_is_ok(host) - await self._assert_workers_threadpool(self._ctrl, host, - self._default_workers) - - async def test_dispatcher_sync_threadpool_set_worker(self): - """Test if the sync threadpool maximum worker can be set - """ - # Configure thread pool max worker - os.environ.update({PYTHON_THREADPOOL_THREAD_COUNT: - f'{self._allowed_max_workers}'}) - async with self._ctrl as host: - await host.init_worker() - await self._check_if_function_is_ok(host) - await self._assert_workers_threadpool(self._ctrl, host, - self._allowed_max_workers) - - async def test_dispatcher_sync_threadpool_invalid_worker_count(self): - """Test when sync threadpool maximum worker is set to an invalid value, - the host should fallback to default value - """ - # The @patch decorator does not work as expected and will suppress - # any assertion failures in the async test cases. - # Thus we're moving the patch() method to use the with syntax - - with patch('azure_functions_worker.dispatcher.logger') as mock_logger: - # Configure thread pool max worker to an invalid value - os.environ.update({PYTHON_THREADPOOL_THREAD_COUNT: 'invalid'}) - - async with self._ctrl as host: - await host.init_worker() - await self._check_if_function_is_ok(host) - await self._assert_workers_threadpool(self._ctrl, host, - self._default_workers) - mock_logger.warning.assert_any_call( - '%s must be an integer', PYTHON_THREADPOOL_THREAD_COUNT) - - async def test_dispatcher_sync_threadpool_below_min_setting(self): - """Test if the sync threadpool will pick up default value when the - setting is below minimum - """ - with patch('azure_functions_worker.dispatcher.logger') as mock_logger: - # Configure thread pool max worker to an invalid value - os.environ.update({PYTHON_THREADPOOL_THREAD_COUNT: '0'}) - async with self._ctrl as host: - await host.init_worker() - await self._check_if_function_is_ok(host) - await self._assert_workers_threadpool(self._ctrl, host, - self._default_workers) - mock_logger.warning.assert_any_call( - '%s must be set to a value between %s and sys.maxint. ' - 'Reverting to default value for max_workers', - PYTHON_THREADPOOL_THREAD_COUNT, - PYTHON_THREADPOOL_THREAD_COUNT_MIN) - - async def test_dispatcher_sync_threadpool_exceed_max_setting(self): - """Test if the sync threadpool will pick up default max value when the - setting is above maximum - """ - with patch('azure_functions_worker.dispatcher.logger'): - # Configure thread pool max worker to an invalid value - os.environ.update({PYTHON_THREADPOOL_THREAD_COUNT: - f'{self._over_max_workers}'}) - async with self._ctrl as host: - await host.init_worker('4.15.1') - await self._check_if_function_is_ok(host) - - # Ensure the dispatcher sync threadpool should fallback to max - await self._assert_workers_threadpool(self._ctrl, host, - self._allowed_max_workers) - - async def test_dispatcher_sync_threadpool_in_placeholder(self): - """Test if the sync threadpool will pick up app setting in placeholder - mode (Linux Consumption) - """ - async with self._ctrl as host: - await host.init_worker() - await self._check_if_function_is_ok(host) - - # Reload environment variable on specialization - await host.reload_environment(environment={ - PYTHON_THREADPOOL_THREAD_COUNT: f'{self._allowed_max_workers}' - }) - await self._assert_workers_threadpool(self._ctrl, host, - self._allowed_max_workers) - - async def test_dispatcher_sync_threadpool_in_placeholder_invalid(self): - """Test if the sync threadpool will use the default setting when the - app setting is invalid - """ - with patch('azure_functions_worker.dispatcher.logger') as mock_logger: - async with self._ctrl as host: - await host.init_worker() - await self._check_if_function_is_ok(host) - - # Reload environment variable on specialization - await host.reload_environment(environment={ - PYTHON_THREADPOOL_THREAD_COUNT: 'invalid' - }) - await self._assert_workers_threadpool(self._ctrl, host, - self._default_workers) - - # Check warning message - mock_logger.warning.assert_any_call( - '%s must be an integer', PYTHON_THREADPOOL_THREAD_COUNT) - - async def test_dispatcher_sync_threadpool_in_placeholder_above_max(self): - """Test if the sync threadpool will use the default max setting when - the app setting is above maximum. - - Note: This is designed for Linux Consumption. - """ - with patch('azure_functions_worker.dispatcher.logger'): - async with self._ctrl as host: - await host.init_worker() - await self._check_if_function_is_ok(host) - - # Reload environment variable on specialization - await host.reload_environment(environment={ - PYTHON_THREADPOOL_THREAD_COUNT: f'{self._over_max_workers}' - }) - await self._assert_workers_threadpool(self._ctrl, host, - self._allowed_max_workers) - - async def test_dispatcher_sync_threadpool_in_placeholder_below_min(self): - """Test if the sync threadpool will use the default setting when the - app setting is below minimum - """ - with patch('azure_functions_worker.dispatcher.logger') as mock_logger: - async with self._ctrl as host: - await host.init_worker() - await self._check_if_function_is_ok(host) - - # Reload environment variable on specialization - await host.reload_environment(environment={ - PYTHON_THREADPOOL_THREAD_COUNT: '0' - }) - - await self._assert_workers_threadpool(self._ctrl, host, - self._default_workers) - - mock_logger.warning.assert_any_call( - '%s must be set to a value between %s and sys.maxint. ' - 'Reverting to default value for max_workers', - PYTHON_THREADPOOL_THREAD_COUNT, - PYTHON_THREADPOOL_THREAD_COUNT_MIN) - - async def test_sync_invocation_request_log(self): - with patch('azure_functions_worker.dispatcher.logger') as mock_logger: - async with self._ctrl as host: - await host.init_worker() - request_id: str = self._ctrl._worker._request_id - func_id, invoke_id, func_name = ( - await self._check_if_function_is_ok(host) - ) - - logs, _ = mock_logger.info.call_args - self.assertRegex(logs[0], - 'Received FunctionInvocationRequest, ' - f'request ID: {request_id}, ' - f'function ID: {func_id}, ' - f'function name: {func_name}, ' - f'invocation ID: {invoke_id}, ' - 'function type: sync, ' - r'timestamp \(UTC\): ' - r'(\d{4}-\d{2}-\d{2} ' - r'\d{2}:\d{2}:\d{2}.\d{6}), ' - 'sync threadpool max workers: ' - f'{self._default_workers}' - ) - - async def test_async_invocation_request_log(self): - with patch('azure_functions_worker.dispatcher.logger') as mock_logger: - async with self._ctrl as host: - await host.init_worker() - request_id: str = self._ctrl._worker._request_id - func_id, invoke_id, func_name = ( - await self._check_if_async_function_is_ok(host) - ) - - logs, _ = mock_logger.info.call_args - self.assertRegex(logs[0], - 'Received FunctionInvocationRequest, ' - f'request ID: {request_id}, ' - f'function ID: {func_id}, ' - f'function name: {func_name}, ' - f'invocation ID: {invoke_id}, ' - 'function type: async, ' - r'timestamp \(UTC\): ' - r'(\d{4}-\d{2}-\d{2} ' - r'\d{2}:\d{2}:\d{2}.\d{6})' - ) - - async def test_sync_invocation_request_log_threads(self): - with patch('azure_functions_worker.dispatcher.logger') as mock_logger: - os.environ.update({PYTHON_THREADPOOL_THREAD_COUNT: '5'}) - - async with self._ctrl as host: - await host.init_worker() - request_id: str = self._ctrl._worker._request_id - func_id, invoke_id, func_name = ( - await self._check_if_function_is_ok(host) - ) - - logs, _ = mock_logger.info.call_args - self.assertRegex(logs[0], - 'Received FunctionInvocationRequest, ' - f'request ID: {request_id}, ' - f'function ID: {func_id}, ' - f'function name: {func_name}, ' - f'invocation ID: {invoke_id}, ' - 'function type: sync, ' - r'timestamp \(UTC\): ' - r'(\d{4}-\d{2}-\d{2} ' - r'\d{2}:\d{2}:\d{2}.\d{6}), ' - 'sync threadpool max workers: 5' - ) - - async def test_async_invocation_request_log_threads(self): - with patch('azure_functions_worker.dispatcher.logger') as mock_logger: - os.environ.update({PYTHON_THREADPOOL_THREAD_COUNT: '4'}) - - async with self._ctrl as host: - await host.init_worker() - request_id: str = self._ctrl._worker._request_id - func_id, invoke_id, func_name = ( - await self._check_if_async_function_is_ok(host) - ) - - logs, _ = mock_logger.info.call_args - self.assertRegex(logs[0], - 'Received FunctionInvocationRequest, ' - f'request ID: {request_id}, ' - f'function ID: {func_id}, ' - f'function name: {func_name}, ' - f'invocation ID: {invoke_id}, ' - 'function type: async, ' - r'timestamp \(UTC\): ' - r'(\d{4}-\d{2}-\d{2} ' - r'\d{2}:\d{2}:\d{2}.\d{6})' - ) - - async def test_sync_invocation_request_log_in_placeholder_threads(self): - with patch('azure_functions_worker.dispatcher.logger') as mock_logger: - async with self._ctrl as host: - await host.reload_environment(environment={ - PYTHON_THREADPOOL_THREAD_COUNT: '5' - }) - - request_id: str = self._ctrl._worker._request_id - func_id, invoke_id, func_name = ( - await self._check_if_function_is_ok(host) - ) - - logs, _ = mock_logger.info.call_args - self.assertRegex(logs[0], - 'Received FunctionInvocationRequest, ' - f'request ID: {request_id}, ' - f'function ID: {func_id}, ' - f'function name: {func_name}, ' - f'invocation ID: {invoke_id}, ' - 'function type: sync, ' - r'timestamp \(UTC\): ' - r'(\d{4}-\d{2}-\d{2} ' - r'\d{2}:\d{2}:\d{2}.\d{6}), ' - 'sync threadpool max workers: 5' - ) - - async def test_async_invocation_request_log_in_placeholder_threads(self): - with patch('azure_functions_worker.dispatcher.logger') as mock_logger: - async with self._ctrl as host: - await host.reload_environment(environment={ - PYTHON_THREADPOOL_THREAD_COUNT: '5' - }) - - request_id: str = self._ctrl._worker._request_id - func_id, invoke_id, func_name = ( - await self._check_if_async_function_is_ok(host) - ) - - logs, _ = mock_logger.info.call_args - self.assertRegex(logs[0], - 'Received FunctionInvocationRequest, ' - f'request ID: {request_id}, ' - f'function ID: {func_id}, ' - f'function name: {func_name}, ' - f'invocation ID: {invoke_id}, ' - 'function type: async, ' - r'timestamp \(UTC\): ' - r'(\d{4}-\d{2}-\d{2} ' - r'\d{2}:\d{2}:\d{2}.\d{6})' - ) - - async def _assert_workers_threadpool(self, ctrl, host, - expected_worker_count): - self.assertIsNotNone(ctrl._worker._sync_call_tp) - self.assertEqual(ctrl._worker.get_sync_tp_workers_set(), - expected_worker_count) - # Check if the dispatcher still function - await self._check_if_function_is_ok(host) - - async def _check_if_function_is_ok(self, host) -> Tuple[str, str, str]: - # Ensure the function can be properly loaded - function_name = "show_context" - func_id, load_r = await host.load_function(function_name) - self.assertEqual(load_r.response.function_id, func_id) - ex = load_r.response.result.exception - self.assertEqual(load_r.response.result.status, - protos.StatusResult.Success, msg=ex) - - # Ensure the function can be properly invoked - invoke_id, call_r = await host.invoke_function( - 'show_context', [ - protos.ParameterBinding( - name='req', - data=protos.TypedData( - http=protos.RpcHttp( - method='GET' - ) - ) - ) - ]) - self.assertIsNotNone(invoke_id) - self.assertEqual(call_r.response.result.status, - protos.StatusResult.Success) - - return func_id, invoke_id, function_name - - async def _check_if_async_function_is_ok(self, host) -> Tuple[str, str]: - # Ensure the function can be properly loaded - function_name = "show_context_async" - func_id, load_r = await host.load_function('show_context_async') - self.assertEqual(load_r.response.function_id, func_id) - self.assertEqual(load_r.response.result.status, - protos.StatusResult.Success) - - # Ensure the function can be properly invoked - invoke_id, call_r = await host.invoke_function( - 'show_context_async', [ - protos.ParameterBinding( - name='req', - data=protos.TypedData( - http=protos.RpcHttp( - method='GET' - ) - ) - ) - ]) - self.assertIsNotNone(invoke_id) - self.assertEqual(call_r.response.result.status, - protos.StatusResult.Success) - - return func_id, invoke_id, function_name - - -@unittest.skipIf(sys.version_info.minor != 8, - "Run the tests only for Python 3.8. In other platforms, " - "as the default passed is None, the cpu_count determines the " - "number of max_workers and we cannot mock the os.cpu_count() " - "in the concurrent.futures.ThreadPoolExecutor") -class TestThreadPoolSettingsPython38(TestThreadPoolSettingsPython37): - def setUp(self, version=SysVersionInfo(3, 8, 0, 'final', 0)): - super(TestThreadPoolSettingsPython38, self).setUp(version) - self._allowed_max_workers: int = self._over_max_workers - - def tearDown(self): - super(TestThreadPoolSettingsPython38, self).tearDown() - - async def test_dispatcher_sync_threadpool_in_placeholder_above_max(self): - """Test if the sync threadpool will use any value and there isn't any - artificial max value set. - """ - with patch('azure_functions_worker.dispatcher.logger'): - async with self._ctrl as host: - await self._check_if_function_is_ok(host) - - # Reload environment variable on specialization - await host.reload_environment(environment={ - PYTHON_THREADPOOL_THREAD_COUNT: f'{self._over_max_workers}' - }) - await self._assert_workers_threadpool(self._ctrl, host, - self._allowed_max_workers) - self.assertNotEqual( - self._ctrl._worker.get_sync_tp_workers_set(), - self._default_workers) - - -@unittest.skipIf(sys.version_info.minor != 9, - "Run the tests only for Python 3.9. In other platforms, " - "as the default passed is None, the cpu_count determines the " - "number of max_workers and we cannot mock the os.cpu_count() " - "in the concurrent.futures.ThreadPoolExecutor") -class TestThreadPoolSettingsPython39(TestThreadPoolSettingsPython37): - def setUp(self, version=SysVersionInfo(3, 9, 0, 'final', 0)): - super(TestThreadPoolSettingsPython39, self).setUp(version) - self.mock_os_cpu = patch( - 'os.cpu_count', return_value=2) - # 6 - based on 2 cores - min(32, (os.cpu_count() or 1) + 4) - 2 + 4 - self._default_workers: Optional[int] = 6 - self.mock_os_cpu.start() - self._allowed_max_workers: int = self._over_max_workers - - def tearDown(self): - self.mock_os_cpu.stop() - super(TestThreadPoolSettingsPython39, self).tearDown() - - -@unittest.skipIf(sys.version_info.minor != 10, - "Run the tests only for Python 3.10. In other platforms, " - "as the default passed is None, the cpu_count determines the " - "number of max_workers and we cannot mock the os.cpu_count() " - "in the concurrent.futures.ThreadPoolExecutor") -class TestThreadPoolSettingsPython310(TestThreadPoolSettingsPython37): - def setUp(self, version=SysVersionInfo(3, 10, 0, 'final', 0)): - super(TestThreadPoolSettingsPython310, self).setUp(version) - self._allowed_max_workers: int = self._over_max_workers - self.mock_os_cpu = patch( - 'os.cpu_count', return_value=2) - # 6 - based on 2 cores - min(32, (os.cpu_count() or 1) + 4) - 2 + 4 - self._default_workers: Optional[int] = 6 - self.mock_os_cpu.start() - self._allowed_max_workers: int = self._over_max_workers - - def tearDown(self): - self.mock_os_cpu.stop() - super(TestThreadPoolSettingsPython310, self).tearDown() - - -@unittest.skipIf(sys.version_info.minor != 11, - "Run the tests only for Python 3.11. In other platforms, " - "as the default passed is None, the cpu_count determines the " - "number of max_workers and we cannot mock the os.cpu_count() " - "in the concurrent.futures.ThreadPoolExecutor") -class TestThreadPoolSettingsPython311(TestThreadPoolSettingsPython37): - def setUp(self, version=SysVersionInfo(3, 11, 0, 'final', 0)): - super(TestThreadPoolSettingsPython311, self).setUp(version) - self._allowed_max_workers: int = self._over_max_workers - self.mock_os_cpu = patch( - 'os.cpu_count', return_value=2) - # 6 - based on 2 cores - min(32, (os.cpu_count() or 1) + 4) - 2 + 4 - self._default_workers: Optional[int] = 6 - self.mock_os_cpu.start() - self._allowed_max_workers: int = self._over_max_workers - - def tearDown(self): - self.mock_os_cpu.stop() - super(TestThreadPoolSettingsPython311, self).tearDown() - - -@unittest.skipIf(sys.version_info.minor != 12, - "Run the tests only for Python 3.12. In other platforms, " - "as the default passed is None, the cpu_count determines the " - "number of max_workers and we cannot mock the os.cpu_count() " - "in the concurrent.futures.ThreadPoolExecutor") -class TestThreadPoolSettingsPython312(TestThreadPoolSettingsPython37): - def setUp(self, version=SysVersionInfo(3, 12, 0, 'final', 0)): - super(TestThreadPoolSettingsPython312, self).setUp(version) - self._allowed_max_workers: int = self._over_max_workers - self.mock_os_cpu = patch( - 'os.cpu_count', return_value=2) - # 6 - based on 2 cores - min(32, (os.cpu_count() or 1) + 4) - 2 + 4 - self._default_workers: Optional[int] = 6 - self.mock_os_cpu.start() - self._allowed_max_workers: int = self._over_max_workers - - def tearDown(self): - self.mock_os_cpu.stop() - super(TestThreadPoolSettingsPython312, self).tearDown() - - -class TestDispatcherStein(testutils.AsyncTestCase): - - def setUp(self): - self._ctrl = testutils.start_mockhost( - script_root=DISPATCHER_STEIN_FUNCTIONS_DIR) - - async def test_dispatcher_functions_metadata_request(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.Success) - del sys.modules['function_app'] - - async def test_dispatcher_functions_metadata_request_with_retry(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.Success) - del sys.modules['function_app'] - - -class TestDispatcherSteinLegacyFallback(testutils.AsyncTestCase): - - def setUp(self): - self._ctrl = testutils.start_mockhost( - script_root=DISPATCHER_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_legacy_fallback(self): - """Test if the functions metadata response will be sent correctly - when a functions metadata request is received - """ - async with self._ctrl as host: - r = await host.get_functions_metadata() - self.assertIsInstance(r.response, protos.FunctionMetadataResponse) - self.assertTrue(r.response.use_default_metadata_indexing) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - -class TestDispatcherInitRequest(testutils.AsyncTestCase): - - def setUp(self): - self._ctrl = testutils.start_mockhost( - script_root=DISPATCHER_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_load_azfunc_in_init(self): - """Test if azure functions is loaded during init - """ - async with self._ctrl as host: - r = await host.init_worker() - self.assertEqual( - len([log for log in r.logs if log.message.startswith( - 'Received WorkerInitRequest' - )]), - 1 - ) - self.assertEqual( - 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) - - async def test_dispatcher_indexing_in_init_request(self): - """Test if azure functions is loaded during init - """ - env = {PYTHON_ENABLE_INIT_INDEXING: "1", - PYTHON_ENABLE_DEBUG_LOGGING: "1"} - with patch.dict(os.environ, env): - async with self._ctrl as host: - r = await host.init_worker() - self.assertEqual( - len([log for log in r.logs if log.message.startswith( - "Received WorkerInitRequest" - )]), - 1 - ) - - self.assertEqual( - len([log for log in r.logs if log.message.startswith( - "Received load metadata request from " - "worker_init_request" - )]), - 1 - ) - - async def test_dispatcher_load_modules_dedicated_app(self): - """Test modules are loaded in dedicated apps - """ - os.environ["PYTHON_ISOLATE_WORKER_DEPENDENCIES"] = "1" - - # Dedicated Apps where placeholder mode is not set - async with self._ctrl as host: - r = await host.init_worker() - logs = [log.message for log in r.logs] - self.assertIn( - "Applying prioritize_customer_dependencies: " - "worker_dependencies_path: , customer_dependencies_path: , " - "working_directory: , Linux Consumption: False," - " Placeholder: False", logs - ) - - async def test_dispatcher_load_modules_con_placeholder_enabled(self): - """Test modules are loaded in consumption apps with placeholder mode - enabled. - """ - # Consumption apps with placeholder mode enabled - os.environ["PYTHON_ISOLATE_WORKER_DEPENDENCIES"] = "1" - os.environ["CONTAINER_NAME"] = "test" - os.environ["WEBSITE_PLACEHOLDER_MODE"] = "1" - async with self._ctrl as host: - r = await host.init_worker() - logs = [log.message for log in r.logs] - self.assertNotIn( - "Applying prioritize_customer_dependencies: " - "worker_dependencies_path: , customer_dependencies_path: , " - "working_directory: , Linux Consumption: True,", logs) - - async def test_dispatcher_load_modules_con_app_placeholder_disabled(self): - """Test modules are loaded in consumption apps with placeholder mode - disabled. - """ - # Consumption apps with placeholder mode disabled i.e. worker - # is specialized - os.environ["PYTHON_ISOLATE_WORKER_DEPENDENCIES"] = "1" - os.environ["WEBSITE_PLACEHOLDER_MODE"] = "0" - os.environ["CONTAINER_NAME"] = "test" - async with self._ctrl as host: - r = await host.init_worker() - logs = [log.message for log in r.logs] - self.assertIn( - "Applying prioritize_customer_dependencies: " - "worker_dependencies_path: , customer_dependencies_path: , " - "working_directory: , Linux Consumption: True," - " Placeholder: False", logs) - - -class TestDispatcherIndexingInInit(unittest.TestCase): - - def setUp(self): - self.loop = asyncio.new_event_loop() - asyncio.set_event_loop(self.loop) - self.dispatcher = testutils.create_dummy_dispatcher() - sys.path.append(str(FUNCTION_APP_DIRECTORY)) - sys.path.append(str(HTTPV2_FUNCTION_APP_DIRECTORY)) - - def tearDown(self): - self.loop.close() - - @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", - function_app_directory=str(FUNCTION_APP_DIRECTORY) - ) - ) - - self.loop.run_until_complete( - self.dispatcher._handle__worker_init_request(request)) - - self.assertIsNotNone(self.dispatcher._function_metadata_result) - self.assertIsNone(self.dispatcher._function_metadata_exception) - - del sys.modules['function_app'] - - @patch.dict(os.environ, {PYTHON_ENABLE_INIT_INDEXING: 'false'}) - def test_worker_init_request_with_indexing_disabled(self): - request = protos.StreamingMessage( - worker_init_request=protos.WorkerInitRequest( - host_version="2.3.4", - function_app_directory=str(FUNCTION_APP_DIRECTORY) - ) - ) - - self.loop.run_until_complete( - self.dispatcher._handle__worker_init_request(request)) - - self.assertIsNone(self.dispatcher._function_metadata_result) - self.assertIsNone(self.dispatcher._function_metadata_exception) - - @patch.dict(os.environ, {PYTHON_ENABLE_INIT_INDEXING: 'true'}) - @patch.object(Dispatcher, 'index_functions') - def test_worker_init_request_with_indexing_exception(self, - mock_index_functions): - mock_index_functions.side_effect = Exception("Mocked Exception") - - request = protos.StreamingMessage( - worker_init_request=protos.WorkerInitRequest( - host_version="2.3.4", - function_app_directory=str(FUNCTION_APP_DIRECTORY) - ) - ) - - self.loop.run_until_complete( - self.dispatcher._handle__worker_init_request(request)) - - self.assertIsNone(self.dispatcher._function_metadata_result) - self.assertIsNotNone(self.dispatcher._function_metadata_exception) - - @patch.dict(os.environ, {PYTHON_ENABLE_INIT_INDEXING: 'true'}) - def test_functions_metadata_request_with_init_indexing_enabled(self): - init_request = protos.StreamingMessage( - worker_init_request=protos.WorkerInitRequest( - host_version="2.3.4", - function_app_directory=str(FUNCTION_APP_DIRECTORY) - ) - ) - - metadata_request = protos.StreamingMessage( - functions_metadata_request=protos.FunctionsMetadataRequest( - function_app_directory=str(FUNCTION_APP_DIRECTORY) - ) - ) - - init_response = self.loop.run_until_complete( - self.dispatcher._handle__worker_init_request(init_request)) - 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)) - - 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) - - del sys.modules['function_app'] - - @patch.dict(os.environ, {PYTHON_ENABLE_INIT_INDEXING: 'false'}) - def test_functions_metadata_request_with_init_indexing_disabled(self): - init_request = protos.StreamingMessage( - worker_init_request=protos.WorkerInitRequest( - host_version="2.3.4", - function_app_directory=str(FUNCTION_APP_DIRECTORY) - ) - ) - - metadata_request = protos.StreamingMessage( - functions_metadata_request=protos.FunctionsMetadataRequest( - function_app_directory=str(str(FUNCTION_APP_DIRECTORY)) - ) - ) - - init_response = self.loop.run_until_complete( - self.dispatcher._handle__worker_init_request(init_request)) - self.assertEqual(init_response.worker_init_response.result.status, - protos.StatusResult.Success) - self.assertIsNone(self.dispatcher._function_metadata_result) - self.assertIsNone(self.dispatcher._function_metadata_exception) - - 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.assertIsNotNone(self.dispatcher._function_metadata_result) - self.assertIsNone(self.dispatcher._function_metadata_exception) - - del sys.modules['function_app'] - - @patch.dict(os.environ, {PYTHON_ENABLE_INIT_INDEXING: 'true'}) - @patch.object(Dispatcher, 'index_functions') - def test_functions_metadata_request_with_indexing_exception( - self, - mock_index_functions): - mock_index_functions.side_effect = Exception("Mocked Exception") - - request = protos.StreamingMessage( - worker_init_request=protos.WorkerInitRequest( - host_version="2.3.4", - function_app_directory=str(FUNCTION_APP_DIRECTORY) - ) - ) - - metadata_request = protos.StreamingMessage( - functions_metadata_request=protos.FunctionsMetadataRequest( - function_app_directory=str(FUNCTION_APP_DIRECTORY) - ) - ) - - self.loop.run_until_complete( - self.dispatcher._handle__worker_init_request(request)) - - self.assertIsNone(self.dispatcher._function_metadata_result) - self.assertIsNotNone(self.dispatcher._function_metadata_exception) - - 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.Failure) - - @patch.dict(os.environ, {PYTHON_ENABLE_INIT_INDEXING: 'false'}) - def test_dispatcher_indexing_in_load_request(self): - init_request = protos.StreamingMessage( - worker_init_request=protos.WorkerInitRequest( - host_version="2.3.4", - function_app_directory=str(FUNCTION_APP_DIRECTORY) - ) - ) - - self.loop.run_until_complete( - self.dispatcher._handle__worker_init_request(init_request)) - - self.assertIsNone(self.dispatcher._function_metadata_result) - - load_request = protos.StreamingMessage( - function_load_request=protos.FunctionLoadRequest( - function_id="http_trigger", - metadata=protos.RpcFunctionMetadata( - directory=str(FUNCTION_APP_DIRECTORY), - properties={METADATA_PROPERTIES_WORKER_INDEXED: "True"} - ))) - - self.loop.run_until_complete( - self.dispatcher._handle__function_load_request(load_request)) - - self.assertIsNotNone(self.dispatcher._function_metadata_result) - self.assertIsNone(self.dispatcher._function_metadata_exception) - - del sys.modules['function_app'] - - @patch.dict(os.environ, {PYTHON_ENABLE_INIT_INDEXING: 'true'}) - @patch.object(Dispatcher, 'index_functions') - def test_dispatcher_indexing_in_load_request_with_exception( - self, - mock_index_functions): - # This is the case when the second worker has an exception in indexing. - # In this case, we save the error in _function_metadata_exception in - # the init request and throw the error when load request is called. - - mock_index_functions.side_effect = Exception("Mocked Exception") - - init_request = protos.StreamingMessage( - worker_init_request=protos.WorkerInitRequest( - host_version="2.3.4", - function_app_directory=str(FUNCTION_APP_DIRECTORY) - ) - ) - - self.loop.run_until_complete( - self.dispatcher._handle__worker_init_request(init_request)) - - self.assertIsNone(self.dispatcher._function_metadata_result) - - load_request = protos.StreamingMessage( - function_load_request=protos.FunctionLoadRequest( - function_id="http_trigger", - metadata=protos.RpcFunctionMetadata( - directory=str(FUNCTION_APP_DIRECTORY), - properties={METADATA_PROPERTIES_WORKER_INDEXED: "True"} - ))) - - response = self.loop.run_until_complete( - self.dispatcher._handle__function_load_request(load_request)) - - self.assertIsNotNone(self.dispatcher._function_metadata_exception) - self.assertEqual( - response.function_load_response.result.exception.message, - "Exception: Mocked Exception") - - @patch.dict(os.environ, {PYTHON_ENABLE_INIT_INDEXING: 'true'}) - @patch("azure_functions_worker.http_v2.HttpV2Registry.http_v2_enabled", - return_value=True) - def test_dispatcher_http_v2_init_request_fail(self, mock_http_v2_enabled): - request = protos.StreamingMessage( - worker_init_request=protos.WorkerInitRequest( - host_version="2.3.4", - function_app_directory=str(HTTPV2_FUNCTION_APP_DIRECTORY) - ) - ) - - resp = self.loop.run_until_complete( - self.dispatcher._handle__worker_init_request(request) - ) - - mock_http_v2_enabled.assert_called_once() - self.assertIsNotNone(self.dispatcher._function_metadata_exception) - - capabilities = resp.worker_init_response.capabilities - self.assertNotIn(HTTP_URI, capabilities) - self.assertNotIn(REQUIRES_ROUTE_PARAMETERS, capabilities) - - # Cleanup - del sys.modules['function_app'] - - @patch.dict(os.environ, {PYTHON_ENABLE_INIT_INDEXING: 'true'}) - @patch("azure_functions_worker.http_v2.HttpV2Registry.http_v2_enabled", - return_value=True) - @patch("azure_functions_worker.dispatcher.initialize_http_server", - return_value="http://localhost:8080") - @patch("azure_functions_worker.dispatcher.Dispatcher" - ".load_function_metadata") - def test_dispatcher_http_v2_init_request_pass(self, mock_http_v2_enabled, - mock_init_http_server, - mock_load_func_metadata): - request = protos.StreamingMessage( - worker_init_request=protos.WorkerInitRequest( - host_version="2.3.4", - function_app_directory=str(HTTPV2_FUNCTION_APP_DIRECTORY) - ) - ) - - resp = self.loop.run_until_complete( - self.dispatcher._handle__worker_init_request(request) - ) - - mock_http_v2_enabled.assert_called_once() - mock_init_http_server.assert_called_once() - mock_load_func_metadata.assert_called_once() - self.assertIsNone(self.dispatcher._function_metadata_exception) - - capabilities = resp.worker_init_response.capabilities - self.assertIn(HTTP_URI, capabilities) - self.assertEqual(capabilities[HTTP_URI], "http://localhost:8080") - self.assertIn(REQUIRES_ROUTE_PARAMETERS, capabilities) - self.assertEqual(capabilities[REQUIRES_ROUTE_PARAMETERS], "true") - - -class TestContextEnabledTask(unittest.TestCase): - def setUp(self): - self.loop = asyncio.new_event_loop() - asyncio.set_event_loop(self.loop) - - def tearDown(self): - self.loop.close() - - def test_init_with_context(self): - # Since ContextEnabledTask accepts the context param, - # no errors will be thrown here - num = contextvars.ContextVar('num') - num.set(5) - ctx = contextvars.copy_context() - exception_raised = False - try: - self.loop.set_task_factory( - lambda loop, coro, context=None: ContextEnabledTask( - coro, loop=loop, context=ctx)) - except TypeError: - exception_raised = True - self.assertFalse(exception_raised) - - async def test_init_without_context(self): - # If the context param is not defined, - # no errors will be thrown for backwards compatibility - exception_raised = False - try: - self.loop.set_task_factory( - lambda loop, coro: ContextEnabledTask( - coro, loop=loop)) - except TypeError: - exception_raised = True - self.assertFalse(exception_raised) diff --git a/tests/unittests/test_enable_debug_logging_functions.py b/tests/unittests/test_enable_debug_logging_functions.py deleted file mode 100644 index c39e7b60e..000000000 --- a/tests/unittests/test_enable_debug_logging_functions.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import os -import typing - -from tests.utils import testutils -from tests.utils.testutils import TESTS_ROOT, remove_path - -from azure_functions_worker.constants import PYTHON_ENABLE_DEBUG_LOGGING - -HOST_JSON_TEMPLATE_WITH_LOGLEVEL_INFO = """\ -{ - "version": "2.0", - "logging": { - "logLevel": { - "default": "Information" - } - }, - "functionTimeout": "00:05:00" -} -""" - - -@testutils.retryable_test(4, 5) -class TestDebugLoggingEnabledFunctions(testutils.WebHostTestCase): - """ - Tests for cx debug logging enabled case. - """ - @classmethod - def setUpClass(cls): - os.environ["PYTHON_ENABLE_DEBUG_LOGGING"] = "1" - super().setUpClass() - - @classmethod - def tearDownClass(cls): - os.environ.pop(PYTHON_ENABLE_DEBUG_LOGGING) - super().tearDownClass() - - @classmethod - def get_script_dir(cls): - return testutils.UNIT_TESTS_FOLDER / 'log_filtering_functions' - - def test_debug_logging_enabled(self): - """ - Verify when cx debug logging is enabled, cx function debug logs - are recorded in host logs. - """ - r = self.webhost.request('GET', 'debug_logging') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK-debug') - - def check_log_debug_logging_enabled(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) - - -class TestDebugLoggingDisabledFunctions(testutils.WebHostTestCase): - """ - Tests for cx debug logging disabled case. - """ - @classmethod - def setUpClass(cls): - os.environ["PYTHON_ENABLE_DEBUG_LOGGING"] = "0" - super().setUpClass() - - @classmethod - def tearDownClass(cls): - os.environ.pop(PYTHON_ENABLE_DEBUG_LOGGING) - super().tearDownClass() - - @classmethod - def get_script_dir(cls): - return testutils.UNIT_TESTS_FOLDER / 'log_filtering_functions' - - def test_debug_logging_disabled(self): - """ - Verify when cx debug logging is disabled, cx function debug logs - are not written to host logs. - """ - r = self.webhost.request('GET', 'debug_logging') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK-debug') - - def check_log_debug_logging_disabled(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) - - -class TestDebugLogEnabledHostFilteringFunctions(testutils.WebHostTestCase): - """ - Tests for enable debug logging flag enabled and host log level is - Information case. - """ - @classmethod - def setUpClass(cls): - host_json = TESTS_ROOT / cls.get_script_dir() / 'host.json' - - with open(host_json, 'w+') as f: - f.write(HOST_JSON_TEMPLATE_WITH_LOGLEVEL_INFO) - - os.environ["PYTHON_ENABLE_DEBUG_LOGGING"] = "1" - super().setUpClass() - - @classmethod - def tearDownClass(cls): - host_json = TESTS_ROOT / cls.get_script_dir() / 'host.json' - remove_path(host_json) - - os.environ.pop(PYTHON_ENABLE_DEBUG_LOGGING) - super().tearDownClass() - - @classmethod - def get_script_dir(cls): - return testutils.UNIT_TESTS_FOLDER / 'log_filtering_functions' - - def test_debug_logging_filtered(self): - """ - Verify when cx debug logging is enabled and host logging level - is Information, cx function debug logs are not written to host logs. - """ - r = self.webhost.request('GET', 'debug_logging') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK-debug') - - def check_log_debug_logging_filtered(self, host_out: typing.List[str]): - self.assertIn('logging info', host_out) - self.assertIn('logging warning', host_out) - self.assertNotIn('logging debug', host_out) - self.assertIn('logging error', host_out) diff --git a/tests/unittests/test_extension.py b/tests/unittests/test_extension.py deleted file mode 100644 index 62569fefd..000000000 --- a/tests/unittests/test_extension.py +++ /dev/null @@ -1,864 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import asyncio -import importlib -import logging -import os -import pathlib -import sys -import unittest -from importlib import import_module -from unittest.mock import Mock, call, patch - -from azure_functions_worker.constants import ( - CUSTOMER_PACKAGES_PATH, - PYTHON_ENABLE_WORKER_EXTENSIONS, -) -from azure_functions_worker.extension import ( - APP_EXT_POST_FUNCTION_LOAD, - APP_EXT_POST_INVOCATION, - APP_EXT_PRE_INVOCATION, - FUNC_EXT_POST_FUNCTION_LOAD, - FUNC_EXT_POST_INVOCATION, - FUNC_EXT_PRE_INVOCATION, - ExtensionManager, -) -from azure_functions_worker.utils.common import get_sdk_from_sys_path - - -class MockContext: - def __init__(self, function_name: str, function_directory: str): - self.function_name = function_name - self.function_directory = function_directory - - -class TestExtension(unittest.TestCase): - - def setUp(self): - # Patch sys.modules and sys.path to avoid pollution between tests - self.mock_environ = patch.dict('os.environ', os.environ.copy()) - self.mock_sys_module = patch.dict('sys.modules', sys.modules.copy()) - self.mock_sys_path = patch('sys.path', sys.path.copy()) - self.mock_environ.start() - self.mock_sys_module.start() - self.mock_sys_path.start() - - # Initialize Extension Manager Instance - self._instance = ExtensionManager - self._instance._is_sdk_detected = False - self._instance._extension_enabled_sdk = None - - # Initialize Azure Functions SDK and clear cache - self._sdk = import_module('azure.functions') - self._sdk.ExtensionMeta._func_exts = {} - self._sdk.ExtensionMeta._app_exts = None - self._sdk.ExtensionMeta._info = {} - sys.modules.pop('azure.functions') - sys.modules.pop('azure') - - # Derived dummy SDK Python system path - self._dummy_sdk_sys_path = os.path.join( - os.path.dirname(__file__), - 'resources', - 'mock_azure_functions' - ) - self._dummy_sdk = Mock(__file__="test") - - # Initialize mock context - self._mock_arguments = {'req': 'request'} - self._mock_func_name = 'HttpTrigger' - self._mock_func_dir = '/home/site/wwwroot/HttpTrigger' - self._mock_context = MockContext( - function_name=self._mock_func_name, - function_directory=self._mock_func_dir - ) - - # Set feature flag to on - os.environ[PYTHON_ENABLE_WORKER_EXTENSIONS] = 'true' - - def tearDown(self) -> None: - os.environ.pop(PYTHON_ENABLE_WORKER_EXTENSIONS) - - self.mock_sys_path.stop() - self.mock_sys_module.stop() - self.mock_environ.stop() - - def test_extension_is_supported_by_latest_sdk(self): - """Test if extension interface supports check as expected on - new version of azure.functions SDK - """ - module = get_sdk_from_sys_path() - sdk_enabled = self._instance._is_extension_enabled_in_sdk(module) - self.assertTrue(sdk_enabled) - - def test_extension_is_not_supported_by_mock_sdk(self): - """Test if the detection works when an azure.functions SDK does not - support extension management. - """ - sys.path.insert(0, self._dummy_sdk_sys_path) - module = get_sdk_from_sys_path() - sdk_enabled = self._instance._is_extension_enabled_in_sdk(module) - self.assertFalse(sdk_enabled) - - def test_extension_in_worker(self): - """Test if worker contains support for extensions - """ - sys.path.insert(0, pathlib.Path.home()) - module = importlib.import_module('azure.functions') - sdk_enabled = self._instance._is_extension_enabled_in_sdk(module) - self.assertTrue(sdk_enabled) - - def test_extension_if_sdk_not_in_path(self): - """Test if the detection works when an azure.functions SDK does not - support extension management. - """ - - module = get_sdk_from_sys_path() - self.assertIn(CUSTOMER_PACKAGES_PATH, sys.path) - sdk_enabled = self._instance._is_extension_enabled_in_sdk(module) - self.assertTrue(sdk_enabled) - - @patch('azure_functions_worker.extension.get_sdk_from_sys_path', - return_value=importlib.import_module('azure.functions')) - def test_function_load_extension_enable_when_feature_flag_is_on( - self, - get_sdk_from_sys_path_mock: Mock - ): - """When turning off the feature flag PYTHON_ENABLE_WORKER_EXTENSIONS, - the post_function_load extension should be disabled - """ - self._instance.function_load_extension( - func_name=self._mock_func_name, - func_directory=self._mock_func_dir - ) - get_sdk_from_sys_path_mock.assert_called_once() - - @patch('azure_functions_worker.extension.get_sdk_from_sys_path') - def test_function_load_extension_disable_when_feature_flag_is_off( - self, - get_sdk_from_sys_path_mock: Mock - ): - """When turning off the feature flag PYTHON_ENABLE_WORKER_EXTENSIONS, - the post_function_load extension should be disabled - """ - os.environ[PYTHON_ENABLE_WORKER_EXTENSIONS] = 'false' - self._instance.function_load_extension( - func_name=self._mock_func_name, - func_directory=self._mock_func_dir - ) - get_sdk_from_sys_path_mock.assert_not_called() - - @patch('azure_functions_worker.extension.ExtensionManager.' - '_warn_sdk_not_support_extension') - def test_function_load_extension_warns_when_sdk_does_not_support( - self, - _warn_sdk_not_support_extension_mock: Mock - ): - """When customer is using an old version of sdk which does not have - extension support and turning on the feature flag, we should warn them - """ - sys.path.insert(0, self._dummy_sdk_sys_path) - self._instance.function_load_extension( - func_name=self._mock_func_name, - func_directory=self._mock_func_dir - ) - _warn_sdk_not_support_extension_mock.assert_called_once() - - @patch('azure_functions_worker.extension.ExtensionManager.' - '_safe_execute_function_load_hooks') - def test_function_load_extension_should_invoke_extension_call( - self, - safe_execute_function_load_hooks_mock: Mock - ): - """Should invoke extension if SDK suports extension interface - """ - self._instance.function_load_extension( - func_name=self._mock_func_name, - func_directory=self._mock_func_dir - ) - # No registered hooks - safe_execute_function_load_hooks_mock.assert_has_calls( - calls=[ - call( - None, APP_EXT_POST_FUNCTION_LOAD, - self._mock_func_name, self._mock_func_dir - ), - call( - None, FUNC_EXT_POST_FUNCTION_LOAD, - self._mock_func_name, self._mock_func_dir - ) - ], - any_order=True - ) - - @patch('azure_functions_worker.extension.get_sdk_from_sys_path', - return_value=importlib.import_module('azure.functions')) - def test_invocation_extension_enable_when_feature_flag_is_on( - self, - get_sdk_from_sys_path_mock: Mock - ): - """When turning off the feature flag PYTHON_ENABLE_WORKER_EXTENSIONS, - the pre_invocation and post_invocation extension should be disabled - """ - self._instance._invocation_extension( - ctx=self._mock_context, - hook_name=FUNC_EXT_PRE_INVOCATION, - func_args=[], - func_ret=None - ) - get_sdk_from_sys_path_mock.assert_called_once() - - @patch('azure_functions_worker.extension.get_sdk_from_sys_path') - def test_invocation_extension_extension_disable_when_feature_flag_is_off( - self, - get_sdk_from_sys_path_mock: Mock - ): - """When turning off the feature flag PYTHON_ENABLE_WORKER_EXTENSIONS, - the pre_invocation and post_invocation extension should be disabled - """ - os.environ[PYTHON_ENABLE_WORKER_EXTENSIONS] = 'false' - self._instance._invocation_extension( - ctx=self._mock_context, - hook_name=FUNC_EXT_PRE_INVOCATION, - func_args=[], - func_ret=None - ) - get_sdk_from_sys_path_mock.assert_not_called() - - @patch('azure_functions_worker.extension.ExtensionManager.' - '_warn_sdk_not_support_extension') - def test_invocation_extension_warns_when_sdk_does_not_support( - self, - _warn_sdk_not_support_extension_mock: Mock - ): - """When customer is using an old version of sdk which does not have - extension support and turning on the feature flag, we should warn them - """ - sys.path.insert(0, self._dummy_sdk_sys_path) - self._instance._invocation_extension( - ctx=self._mock_context, - hook_name=FUNC_EXT_PRE_INVOCATION, - func_args=[], - func_ret=None - ) - _warn_sdk_not_support_extension_mock.assert_called_once() - - @patch('azure_functions_worker.extension.ExtensionManager.' - '_safe_execute_invocation_hooks') - def test_invocation_extension_should_invoke_extension_call( - self, - safe_execute_invocation_hooks_mock: Mock - ): - """Should invoke extension if SDK suports extension interface - """ - for hook_name in (APP_EXT_PRE_INVOCATION, FUNC_EXT_PRE_INVOCATION, - APP_EXT_POST_INVOCATION, FUNC_EXT_POST_INVOCATION): - self._instance._invocation_extension( - ctx=self._mock_context, - hook_name=hook_name, - func_args=[], - func_ret=None - ) - - safe_execute_invocation_hooks_mock.assert_has_calls( - calls=[ - call( - None, hook_name, self._mock_context, - [], None - ) - ], - any_order=True - ) - - @patch('azure_functions_worker.extension.ExtensionManager.' - '_is_pre_invocation_hook') - @patch('azure_functions_worker.extension.ExtensionManager.' - '_is_post_invocation_hook') - def test_empty_hooks_should_not_receive_any_invocation( - self, - _is_post_invocation_hook_mock: Mock, - _is_pre_invocation_hook_mock: Mock - ): - """If there is no life-cycle hooks implemented under a function, - then we should skip it - """ - for hook_name in (APP_EXT_PRE_INVOCATION, FUNC_EXT_PRE_INVOCATION, - APP_EXT_POST_INVOCATION, FUNC_EXT_POST_INVOCATION): - self._instance._safe_execute_invocation_hooks( - hooks=[], - hook_name=hook_name, - ctx=self._mock_context, - fargs=[], - fret=None - ) - _is_pre_invocation_hook_mock.assert_not_called() - _is_post_invocation_hook_mock.assert_not_called() - - def test_invocation_hooks_should_be_executed(self): - """If there is an extension implemented the pre_invocation and - post_invocation life-cycle hooks, it should be invoked in - safe_execute_invocation_hooks - """ - FuncExtClass = self._generate_new_func_extension_class( - base=self._sdk.FuncExtensionBase, - trigger=self._mock_func_name - ) - func_ext_instance = FuncExtClass() - hook_instances = ( - self._sdk.ExtensionMeta.get_function_hooks(self._mock_func_name) - ) - for hook_name in (FUNC_EXT_PRE_INVOCATION, FUNC_EXT_POST_INVOCATION): - self._instance._safe_execute_invocation_hooks( - hooks=hook_instances, - hook_name=hook_name, - ctx=self._mock_context, - fargs=[], - fret=None - ) - self.assertFalse(func_ext_instance._post_function_load_executed) - self.assertTrue(func_ext_instance._pre_invocation_executed) - self.assertTrue(func_ext_instance._post_invocation_executed) - - def test_post_function_load_hook_should_be_executed(self): - """If there is an extension implemented the post_function_load - life-cycle hook, it invokes in safe_execute_function_load_hooks - """ - FuncExtClass = self._generate_new_func_extension_class( - base=self._sdk.FuncExtensionBase, - trigger=self._mock_func_name - ) - func_ext_instance = FuncExtClass() - hook_instances = ( - self._sdk.ExtensionMeta.get_function_hooks(self._mock_func_name) - ) - for hook_name in (FUNC_EXT_POST_FUNCTION_LOAD,): - self._instance._safe_execute_function_load_hooks( - hooks=hook_instances, - hook_name=hook_name, - fname=self._mock_func_name, - fdir=self._mock_func_dir - ) - self.assertTrue(func_ext_instance._post_function_load_executed) - self.assertFalse(func_ext_instance._pre_invocation_executed) - self.assertFalse(func_ext_instance._post_invocation_executed) - - def test_invocation_hooks_app_level_should_be_executed(self): - """If there is an extension implemented the pre_invocation and - post_invocation life-cycle hooks, it should be invoked in - safe_execute_invocation_hooks - """ - AppExtClass = self._generate_new_app_extension( - base=self._sdk.AppExtensionBase - ) - hook_instances = ( - self._sdk.ExtensionMeta.get_application_hooks() - ) - for hook_name in (APP_EXT_PRE_INVOCATION, APP_EXT_POST_INVOCATION): - self._instance._safe_execute_invocation_hooks( - hooks=hook_instances, - hook_name=hook_name, - ctx=self._mock_context, - fargs=[], - fret=None - ) - self.assertFalse(AppExtClass._post_function_load_app_level_executed) - self.assertTrue(AppExtClass._pre_invocation_app_level_executed) - self.assertTrue(AppExtClass._post_invocation_app_level_executed) - - def test_post_function_load_app_level_hook_should_be_executed(self): - """If there is an extension implemented the post_function_load - life-cycle hook, it invokes in safe_execute_function_load_hooks - """ - AppExtClass = self._generate_new_app_extension( - base=self._sdk.AppExtensionBase - ) - hook_instances = ( - self._sdk.ExtensionMeta.get_application_hooks() - ) - for hook_name in (APP_EXT_POST_FUNCTION_LOAD,): - self._instance._safe_execute_function_load_hooks( - hooks=hook_instances, - hook_name=hook_name, - fname=self._mock_func_name, - fdir=self._mock_func_dir - ) - self.assertTrue(AppExtClass._post_function_load_app_level_executed) - self.assertFalse(AppExtClass._pre_invocation_app_level_executed) - self.assertFalse(AppExtClass._post_invocation_app_level_executed) - - def test_raw_invocation_wrapper(self): - """This wrapper should automatically invoke all invocation extensions - """ - # Instantiate extensions - AppExtClass = self._generate_new_app_extension( - base=self._sdk.AppExtensionBase - ) - FuncExtClass = self._generate_new_func_extension_class( - base=self._sdk.FuncExtensionBase, - trigger=self._mock_func_name - ) - func_ext_instance = FuncExtClass() - - # Invoke with wrapper - self._instance._raw_invocation_wrapper( - self._mock_context, self._mock_function_main, self._mock_arguments - ) - - # Assert: invocation hooks should be executed - self.assertTrue(func_ext_instance._pre_invocation_executed) - self.assertTrue(func_ext_instance._post_invocation_executed) - self.assertTrue(AppExtClass._pre_invocation_app_level_executed) - self.assertTrue(AppExtClass._post_invocation_app_level_executed) - - # Assert: arguments should be passed into the extension - comparisons = ( - func_ext_instance._pre_invocation_executed_fargs, - func_ext_instance._post_invocation_executed_fargs, - AppExtClass._pre_invocation_app_level_executed_fargs, - AppExtClass._post_invocation_app_level_executed_fargs - ) - for current_argument in comparisons: - self.assertEqual(current_argument, self._mock_arguments) - - # Assert: returns should be passed into the extension - comparisons = ( - func_ext_instance._post_invocation_executed_fret, - AppExtClass._post_invocation_app_level_executed_fret - ) - for current_return in comparisons: - self.assertEqual(current_return, 'request_ok') - - @patch('azure_functions_worker.extension.logger.error') - def test_exception_handling_in_post_function_load_app_level( - self, - error_mock: Mock - ): - """When there's a chain breaks in the extension chain, it should not - pause other executions. For post_function_load_app_level, becasue the - logger is not fully initialized, the exception will be suppressed. - """ - # Create an customized exception - expt = Exception('Exception in post_function_load_app_level') - - # Register an application extension - class BadAppExtension(self._sdk.AppExtensionBase): - post_function_load_app_level_executed = False - - @classmethod - def post_function_load_app_level(cls, - function_name, - function_directory, - *args, - **kwargs): - cls.post_function_load_app_level_executed = True - raise expt - - # Execute function with a broken extension - hooks = self._sdk.ExtensionMeta.get_application_hooks() - self._instance._safe_execute_function_load_hooks( - hooks=hooks, - hook_name=APP_EXT_POST_FUNCTION_LOAD, - fname=self._mock_func_name, - fdir=self._mock_func_dir - ) - - # Ensure the extension is executed, but the exception shouldn't surface - self.assertTrue(BadAppExtension.post_function_load_app_level_executed) - - # Ensure errors are reported from system logger - error_mock.assert_called_with(expt, exc_info=True) - - def test_exception_handling_in_pre_invocation_app_level(self): - """When there's a chain breaks in the extension chain, it should not - pause other executions, but report with a system logger, so that the - error is accessible to customers and ours. - """ - # Create an customized exception - expt = Exception('Exception in pre_invocation_app_level') - - # Register an application extension - class BadAppExtension(self._sdk.AppExtensionBase): - @classmethod - def pre_invocation_app_level(cls, logger, context, func_args, - *args, **kwargs): - raise expt - - # Create a mocked customer_function - wrapped = self._instance.get_sync_invocation_wrapper( - self._mock_context, - self._mock_function_main - ) - - # Mock logger - ext_logger = logging.getLogger( - 'azure_functions_worker.extension.BadAppExtension' - ) - ext_logger_error_mock = Mock() - ext_logger.error = ext_logger_error_mock - - # Invocation with arguments. This will throw an exception, but should - # not break the execution chain. - result = wrapped(self._mock_arguments) - - # Ensure the customer's function is executed - self.assertEqual(result, 'request_ok') - - # Ensure the error is reported - ext_logger_error_mock.assert_called_with(expt, exc_info=True) - - def test_get_sync_invocation_wrapper_no_extension(self): - """The wrapper is using functools.partial() to expose the arguments - for synchronous execution in dispatcher. - """ - # Create a mocked customer_function - wrapped = self._instance.get_sync_invocation_wrapper( - self._mock_context, - self._mock_function_main - ) - - # Invocation with arguments - result = wrapped(self._mock_arguments) - - # Ensure the return value matches the function method - self.assertEqual(result, 'request_ok') - - def test_get_sync_invocation_wrapper_with_func_extension(self): - """The wrapper is using functools.partial() to expose the arguments. - Ensure the func extension can be executed along with customer's funcs. - """ - # Register a function extension - FuncExtClass = self._generate_new_func_extension_class( - self._sdk.FuncExtensionBase, - self._mock_func_name - ) - _func_ext_instance = FuncExtClass() - - # Create a mocked customer_function - wrapped = self._instance.get_sync_invocation_wrapper( - self._mock_context, - self._mock_function_main - ) - - # Invocation via wrapper with arguments - result = wrapped(self._mock_arguments) - - # Ensure the extension is executed - self.assertTrue(_func_ext_instance._pre_invocation_executed) - - # Ensure the customer's function is executed - self.assertEqual(result, 'request_ok') - - def test_get_sync_invocation_wrapper_disabled_with_flag(self): - """The wrapper should still exist, customer's functions should still - be executed, but not the extension - """ - # Turn off feature flag - os.environ[PYTHON_ENABLE_WORKER_EXTENSIONS] = 'false' - - # Register a function extension - FuncExtClass = self._generate_new_func_extension_class( - self._sdk.FuncExtensionBase, - self._mock_func_name - ) - _func_ext_instance = FuncExtClass() - - # Create a mocked customer_function - wrapped = self._instance.get_sync_invocation_wrapper( - self._mock_context, - self._mock_function_main - ) - - # Invocation via wrapper with arguments - result = wrapped(self._mock_arguments) - - # The extension SHOULD NOT be executed, since the feature flag is off - self.assertFalse(_func_ext_instance._pre_invocation_executed) - - # Ensure the customer's function is executed - self.assertEqual(result, 'request_ok') - - def test_get_async_invocation_wrapper_no_extension(self): - """The async wrapper will wrap an asynchronous function with a - coroutine interface. When there is no extension, it should only invoke - the customer's function. - """ - # Create a mocked customer_function with async wrapper - result = asyncio.run( - self._instance.get_async_invocation_wrapper( - self._mock_context, - self._mock_function_main_async, - self._mock_arguments - ) - ) - - # Ensure the return value matches the function method - self.assertEqual(result, 'request_ok') - - def test_get_async_invocation_wrapper_with_func_extension(self): - """The async wrapper will wrap an asynchronous function with a - coroutine interface. When there is registered extension, it should - execute the extension as well. - """ - # Register a function extension - FuncExtClass = self._generate_new_func_extension_class( - self._sdk.FuncExtensionBase, - self._mock_func_name - ) - _func_ext_instance = FuncExtClass() - - # Create a mocked customer_function with async wrapper - result = asyncio.run( - self._instance.get_async_invocation_wrapper( - self._mock_context, - self._mock_function_main_async, - self._mock_arguments - ) - ) - - # Ensure the extension is executed - self.assertTrue(_func_ext_instance._pre_invocation_executed) - - # Ensure the customer's function is executed - self.assertEqual(result, 'request_ok') - - def test_get_invocation_async_disabled_with_flag(self): - """The async wrapper will only execute customer's function. This - should not execute the extension. - """ - # Turn off feature flag - os.environ[PYTHON_ENABLE_WORKER_EXTENSIONS] = 'false' - - # Register a function extension - FuncExtClass = self._generate_new_func_extension_class( - self._sdk.FuncExtensionBase, - self._mock_func_name - ) - _func_ext_instance = FuncExtClass() - - # Create a mocked customer_function with async wrapper - result = asyncio.run( - self._instance.get_async_invocation_wrapper( - self._mock_context, - self._mock_function_main_async, - self._mock_arguments - ) - ) - - # The extension SHOULD NOT be executed - self.assertFalse(_func_ext_instance._pre_invocation_executed) - - # Ensure the customer's function is executed - self.assertEqual(result, 'request_ok') - - def test_is_pre_invocation_hook(self): - for name in (FUNC_EXT_PRE_INVOCATION, APP_EXT_PRE_INVOCATION): - self.assertTrue( - self._instance._is_pre_invocation_hook(name) - ) - - def test_is_pre_invocation_hook_negative(self): - for name in (FUNC_EXT_POST_INVOCATION, APP_EXT_POST_INVOCATION, - FUNC_EXT_POST_FUNCTION_LOAD, APP_EXT_POST_FUNCTION_LOAD): - self.assertFalse( - self._instance._is_pre_invocation_hook(name) - ) - - def test_is_post_invocation_hook(self): - for name in (FUNC_EXT_POST_INVOCATION, APP_EXT_POST_INVOCATION): - self.assertTrue( - self._instance._is_post_invocation_hook(name) - ) - - def test_is_post_invocation_hook_negative(self): - for name in (FUNC_EXT_PRE_INVOCATION, APP_EXT_PRE_INVOCATION, - FUNC_EXT_POST_FUNCTION_LOAD, APP_EXT_POST_FUNCTION_LOAD): - self.assertFalse( - self._instance._is_post_invocation_hook(name) - ) - - @patch('azure_functions_worker.extension.' - 'ExtensionManager._info_extension_is_enabled') - def test_try_get_sdk_with_extension_enabled_should_execute_once( - self, - info_extension_is_enabled_mock: Mock - ): - """The result of an extension enabled SDK should be cached. No need - to be derived multiple times. - """ - # Call twice the function - self._instance._try_get_sdk_with_extension_enabled() - sdk = self._instance._try_get_sdk_with_extension_enabled() - - # The actual execution will only process once (e.g. list extensions) - info_extension_is_enabled_mock.assert_called_once() - - # Ensure the SDK is returned correctly - self.assertIsNotNone(sdk) - - @patch('azure_functions_worker.extension.' - 'ExtensionManager._warn_sdk_not_support_extension') - def test_try_get_sdk_with_extension_disabled_should_execute_once( - self, - warn_sdk_not_support_extension_mock: Mock - ): - """When SDK does not support extension interface, it should return - None and throw a warning. - """ - # Point to dummy SDK - sys.path.insert(0, self._dummy_sdk_sys_path) - - # Call twice the function - self._instance._try_get_sdk_with_extension_enabled() - sdk = self._instance._try_get_sdk_with_extension_enabled() - - # The actual execution will only process once (e.g. warning) - warn_sdk_not_support_extension_mock.assert_called_once() - - # The SDK does not support Extension Interface, should be None - self.assertIsNone(sdk) - - @patch('azure_functions_worker.extension.logger.info') - def test_info_extension_is_enabled(self, info_mock: Mock): - # Get SDK from sys.path - sdk = get_sdk_from_sys_path() - - # Check logs - self._instance._info_extension_is_enabled(sdk) - info_mock.assert_called_once_with( - 'Python Worker Extension is enabled in azure.functions ' - '(%s). Sdk path: %s', sdk.__version__, sdk.__file__ - ) - - @patch('azure_functions_worker.extension.logger.info') - def test_info_discover_extension_list_func_ext(self, info_mock: Mock): - # Get SDK from sys.path - sdk = get_sdk_from_sys_path() - - # Register a function extension class - FuncExtClass = self._generate_new_func_extension_class( - sdk.FuncExtensionBase, - self._mock_func_name - ) - - # Instantiate a function extension - FuncExtClass() - - # Check logs - self._instance._info_discover_extension_list(self._mock_func_name, sdk) - info_mock.assert_called_once_with( - 'Python Worker Extension Manager is loading %s, ' - 'current registered extensions: %s', 'HttpTrigger', - '{"FuncExtension": {"HttpTrigger": ["NewFuncExtension"]}}' - ) - - @patch('azure_functions_worker.extension.logger.info') - def test_info_discover_extension_list_app_ext(self, info_mock: Mock): - # Get SDK from sys.path - sdk = get_sdk_from_sys_path() - - # Register a function extension class - self._generate_new_app_extension(sdk.AppExtensionBase) - - # Check logs - self._instance._info_discover_extension_list(self._mock_func_name, sdk) - info_mock.assert_called_once_with( - 'Python Worker Extension Manager is loading %s, current ' - 'registered extensions: %s', - 'HttpTrigger', '{"AppExtension": ["NewAppExtension"]}' - ) - - @patch('azure_functions_worker.extension.logger.warning') - def test_warn_sdk_not_support_extension(self, warning_mock: Mock): - # Get SDK from dummy - sys.path.insert(0, self._dummy_sdk_sys_path) - sdk = get_sdk_from_sys_path() - - # Check logs - self._instance._warn_sdk_not_support_extension(sdk) - warning_mock.assert_called_once_with( - 'The azure.functions (%s) does not support Python worker ' - 'extensions. If you believe extensions are correctly installed, ' - 'please set the %s and %s to "true"', - 'dummy', 'PYTHON_ISOLATE_WORKER_DEPENDENCIES', - 'PYTHON_ENABLE_WORKER_EXTENSIONS' - ) - - def _generate_new_func_extension_class(self, base: type, trigger: str): - class NewFuncExtension(base): - def __init__(self): - self._trigger_name = trigger - self._post_function_load_executed = False - self._pre_invocation_executed = False - self._post_invocation_executed = False - - self._pre_invocation_executed_fargs = {} - self._post_invocation_executed_fargs = {} - self._post_invocation_executed_fret = None - - def post_function_load(self, - function_name, - function_directory, - *args, - **kwargs): - self._post_function_load_executed = True - - def pre_invocation(self, logger, context, fargs, - *args, **kwargs): - self._pre_invocation_executed = True - self._pre_invocation_executed_fargs = fargs - - def post_invocation(self, logger, context, fargs, fret, - *args, **kwargs): - self._post_invocation_executed = True - self._post_invocation_executed_fargs = fargs - self._post_invocation_executed_fret = fret - - return NewFuncExtension - - def _generate_new_app_extension(self, base: type): - class NewAppExtension(base): - _init_executed = False - - _post_function_load_app_level_executed = False - _pre_invocation_app_level_executed = False - _post_invocation_app_level_executed = False - - _pre_invocation_app_level_executed_fargs = {} - _post_invocation_app_level_executed_fargs = {} - _post_invocation_app_level_executed_fret = None - - @classmethod - def init(cls): - cls._init_executed = True - - @classmethod - def post_function_load_app_level(cls, - function_name, - function_directory, - *args, - **kwargs): - cls._post_function_load_app_level_executed = True - - @classmethod - def pre_invocation_app_level(cls, logger, context, func_args, - *args, **kwargs): - cls._pre_invocation_app_level_executed = True - cls._pre_invocation_app_level_executed_fargs = func_args - - @classmethod - def post_invocation_app_level(cls, logger, context, - func_args, func_ret, - *args, **kwargs): - cls._post_invocation_app_level_executed = True - cls._post_invocation_app_level_executed_fargs = func_args - cls._post_invocation_app_level_executed_fret = func_ret - - return NewAppExtension - - def _mock_function_main(self, req): - assert req == 'request' - return req + '_ok' - - async def _mock_function_main_async(self, req): - assert req == 'request' - return req + '_ok' diff --git a/tests/unittests/test_file_accessor.py b/tests/unittests/test_file_accessor.py deleted file mode 100644 index 97381229f..000000000 --- a/tests/unittests/test_file_accessor.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os -import sys -import unittest -from unittest import skipIf - -from tests.utils import testutils - -from azure_functions_worker.bindings.shared_memory_data_transfer import ( - SharedMemoryException, -) - - -@skipIf(sys.platform == 'darwin', 'MacOS M1 machines do not correctly test the' - 'shared memory filesystems and thus skipping' - ' these tests for the time being') -class TestFileAccessor(testutils.SharedMemoryTestCase): - """ - Tests for FileAccessor. - """ - def test_create_and_delete_mem_map(self): - """ - Verify if memory maps were created and deleted. - """ - for mem_map_size in [1, 10, 1024, 2 * 1024 * 1024, 10 * 1024 * 1024]: - mem_map_name = self.get_new_mem_map_name() - mem_map = self.file_accessor.create_mem_map(mem_map_name, - mem_map_size) - self.assertIsNotNone(mem_map) - delete_status = self.file_accessor.delete_mem_map(mem_map_name, - mem_map) - self.assertTrue(delete_status) - - def test_create_mem_map_invalid_inputs(self): - """ - Attempt to create memory maps with invalid inputs (size and name) and - verify that an SharedMemoryException is raised. - """ - mem_map_name = self.get_new_mem_map_name() - inv_mem_map_size = 0 - with self.assertRaisesRegex(SharedMemoryException, 'Invalid size'): - self.file_accessor.create_mem_map(mem_map_name, inv_mem_map_size) - inv_mem_map_name = None - mem_map_size = 1024 - with self.assertRaisesRegex(SharedMemoryException, 'Invalid name'): - self.file_accessor.create_mem_map(inv_mem_map_name, mem_map_size) - inv_mem_map_name = '' - with self.assertRaisesRegex(SharedMemoryException, 'Invalid name'): - self.file_accessor.create_mem_map(inv_mem_map_name, mem_map_size) - - def test_open_existing_mem_map(self): - """ - Verify that an existing memory map can be opened. - """ - mem_map_size = 1024 - mem_map_name = self.get_new_mem_map_name() - mem_map = self.file_accessor.create_mem_map(mem_map_name, mem_map_size) - o_mem_map = self.file_accessor.open_mem_map(mem_map_name, mem_map_size) - self.assertIsNotNone(o_mem_map) - o_mem_map.close() - delete_status = self.file_accessor.delete_mem_map(mem_map_name, mem_map) - self.assertTrue(delete_status) - - def test_open_mem_map_invalid_inputs(self): - """ - Attempt to open a memory map with invalid inputs (size and name) and - verify that an SharedMemoryException is raised. - """ - mem_map_name = self.get_new_mem_map_name() - inv_mem_map_size = -1 - with self.assertRaisesRegex(SharedMemoryException, 'Invalid size'): - self.file_accessor.open_mem_map(mem_map_name, inv_mem_map_size) - inv_mem_map_name = None - mem_map_size = 1024 - with self.assertRaisesRegex(SharedMemoryException, 'Invalid name'): - self.file_accessor.open_mem_map(inv_mem_map_name, mem_map_size) - inv_mem_map_name = '' - with self.assertRaisesRegex(SharedMemoryException, 'Invalid name'): - self.file_accessor.open_mem_map(inv_mem_map_name, mem_map_size) - - @unittest.skipIf(os.name == 'nt', - 'Windows will create an mmap if one does not exist') - def test_open_deleted_mem_map(self): - """ - Attempt to open a deleted memory map and verify that it fails. - Note: Windows creates a new memory map if one does not exist when - opening a memory map, so we skip this test on Windows. - """ - mem_map_size = 1024 - mem_map_name = self.get_new_mem_map_name() - mem_map = self.file_accessor.create_mem_map(mem_map_name, mem_map_size) - o_mem_map = self.file_accessor.open_mem_map(mem_map_name, mem_map_size) - self.assertIsNotNone(o_mem_map) - o_mem_map.close() - delete_status = self.file_accessor.delete_mem_map(mem_map_name, mem_map) - self.assertTrue(delete_status) - d_mem_map = self.file_accessor.open_mem_map(mem_map_name, mem_map_size) - self.assertIsNone(d_mem_map) diff --git a/tests/unittests/test_file_accessor_factory.py b/tests/unittests/test_file_accessor_factory.py deleted file mode 100644 index 13f8fbada..000000000 --- a/tests/unittests/test_file_accessor_factory.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os -import sys -import unittest -from unittest.mock import patch - -from azure_functions_worker.bindings.shared_memory_data_transfer import ( - FileAccessorFactory, -) -from azure_functions_worker.bindings.shared_memory_data_transfer.file_accessor_unix import ( # NoQA - FileAccessorUnix, -) -from azure_functions_worker.bindings.shared_memory_data_transfer.file_accessor_windows import ( # NoQA - FileAccessorWindows, -) - - -class TestFileAccessorFactory(unittest.TestCase): - """ - Tests for FileAccessorFactory. - """ - def setUp(self): - env = os.environ.copy() - env['FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED'] = "true" - self.mock_environ = patch.dict('os.environ', env) - self.mock_environ.start() - - def tearDown(self): - self.mock_environ.stop() - - @unittest.skipIf(os.name != 'nt', - 'FileAccessorWindows is only valid on Windows') - def test_file_accessor_windows_created(self): - """ - Verify that FileAccessorWindows was created when running on Windows. - """ - file_accessor = FileAccessorFactory.create_file_accessor() - self.assertTrue(type(file_accessor) is FileAccessorWindows) - - @unittest.skipIf(os.name == 'nt' or sys.platform == 'darwin', - 'FileAccessorUnix is only valid on Unix') - def test_file_accessor_unix_created(self): - """ - Verify that FileAccessorUnix was created when running on Windows. - """ - file_accessor = FileAccessorFactory.create_file_accessor() - self.assertTrue(type(file_accessor) is FileAccessorUnix) diff --git a/tests/unittests/test_functions_registry.py b/tests/unittests/test_functions_registry.py deleted file mode 100644 index 4f28541d0..000000000 --- a/tests/unittests/test_functions_registry.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from azure.functions import Function -from azure.functions.decorators.blob import BlobInput -from azure.functions.decorators.http import HttpTrigger -from tests.utils import testutils - -from azure_functions_worker import functions -from azure_functions_worker.functions import FunctionLoadError - - -class TestFunctionsRegistry(testutils.AsyncTestCase): - - def setUp(self): - def dummy(): - return "test" - - self.dummy = dummy - self.func = Function(self.dummy, "test.py") - self.function_registry = functions.Registry() - - async def test_add_indexed_function_invalid_direction(self): - # Ensures that azure-functions is loaded and BINDING_REGISTRY - # is not None - async with testutils.start_mockhost() as host: - await host.init_worker() - - trigger1 = HttpTrigger(name="req1", route="test") - binding = BlobInput(name="$return", path="testpath", - connection="testconnection") - self.func.add_trigger(trigger=trigger1) - self.func.add_binding(binding=binding) - - with self.assertRaises(FunctionLoadError) as ex: - self.function_registry.add_indexed_function(function=self.func) - - self.assertEqual(str(ex.exception), - 'cannot load the dummy function: \"$return\" ' - 'binding must have direction set to \"out\"') diff --git a/tests/unittests/test_http_functions.py b/tests/unittests/test_http_functions.py deleted file mode 100644 index cfa46be57..000000000 --- a/tests/unittests/test_http_functions.py +++ /dev/null @@ -1,480 +0,0 @@ -# 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 tests.utils import testutils - - -class TestHttpFunctions(testutils.WebHostTestCase): - - @classmethod - def get_script_dir(cls): - return testutils.UNIT_TESTS_FOLDER / 'http_functions' - - def test_return_str(self): - r = self.webhost.request('GET', 'return_str') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'Hello World!') - self.assertTrue(r.headers['content-type'].startswith('text/plain')) - - def test_return_out(self): - r = self.webhost.request('GET', 'return_out') - self.assertEqual(r.status_code, 201) - self.assertEqual(r.text, 'hello') - self.assertTrue(r.headers['content-type'].startswith('text/plain')) - - def test_return_bytes(self): - r = self.webhost.request('GET', 'return_bytes') - self.assertEqual(r.status_code, 500) - # https://github.com/Azure/azure-functions-host/issues/2706 - # self.assertRegex( - # r.text, r'.*unsupported type .*http.* for Python type .*bytes.*') - - 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') - self.assertEqual(r.headers['content-type'], - 'text/plain; charset=utf-8') - - 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_no_return(self): - r = self.webhost.request('GET', 'no_return') - self.assertEqual(r.status_code, 204) - - def test_no_return_returns(self): - r = self.webhost.request('GET', 'no_return_returns') - self.assertEqual(r.status_code, 500) - # https://github.com/Azure/azure-functions-host/issues/2706 - # self.assertRegex(r.text, - # r'.*function .+no_return_returns.+ without a ' - # r'\$return binding returned a non-None value.*') - - 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 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['get_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( - 'POST', 'accept_json', - json={'a': 'abc', 'd': 42}) - - req = r.json() - - self.assertEqual(req['method'], 'POST') - self.assertEqual(req['get_json'], {'a': 'abc', 'd': 42}) - - self.assertIn('accept_json', req['url']) - - 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) - - @testutils.retryable_test(3, 5) - 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') - - @testutils.retryable_test(3, 5) - def check_log_print_logging_no_flush(self, host_out: typing.List[str]): - self.assertIn('Secret42', host_out) - - @testutils.retryable_test(3, 5) - 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') - - @testutils.retryable_test(3, 5) - 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; expires=Thu, 12 Jan 2017 13:55:08 GMT; " - "max-age=10000000; domain=example.com; path=/; secure; httponly, " - "foo3=43; expires=Fri, 12 Jan 2018 13:55:08 GMT; " - "max-age=10000000; domain=example.com; path=/; secure; httponly") - - @skipIf(sys.version_info < (3, 8, 0), - "Skip the tests for Python 3.7 and below") - def test_set_cookie_header_in_response_empty_value(self): - r = self.webhost.request('GET', 'set_cookie_resp_header_empty') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.headers.get('Set-Cookie'), None) - - @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'), - 'foo=bar; domain=; path=') - - @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, 500) - - def check_log_response_cookie_header_nullable_timestamp_err(self, - host_out: - typing.List[ - str]): - self.assertIn( - "Can not parse value Dummy of expires in the cookie due to " - "invalid format.", - host_out) - - @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.assertFalse("Set-Cookie" in r.headers) - - @skipIf(sys.version_info < (3, 8, 0), - "Skip the tests for Python 3.7 and below") - def test_response_cookie_header_nullable_double_err(self): - r = self.webhost.request( - 'GET', - 'response_cookie_header_nullable_double_err') - self.assertEqual(r.status_code, 200) - self.assertFalse("Set-Cookie" in r.headers) - - def check_log_print_to_console_stdout(self, host_out: typing.List[str]): - # System logs stdout should exist in host_out - self.assertIn('Secret42', host_out) - - @skipIf(sys.version_info < (3, 9, 0), - "Skip the tests for Python 3.8 and below") - 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 exist in host_out - self.assertIn('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 exist in host_out - self.assertIn('parallelly_log_system at disguised_logger', host_out) - - @skipIf(sys.version_info.minor < 11, - "The context param is only available for 3.11+") - def test_create_task_with_context(self): - r = self.webhost.request('GET', 'create_task_with_context') - - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'Finished Hello World in 5' - ' | Finished Hello World in 10') - - def test_create_task_without_context(self): - r = self.webhost.request('GET', 'create_task_without_context') - - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'Finished Hello World in 5') - - -class TestHttpFunctionsStein(TestHttpFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.UNIT_TESTS_FOLDER / 'http_functions' / \ - 'http_functions_stein' - - def test_no_return(self): - r = self.webhost.request('GET', 'no_return') - self.assertEqual(r.status_code, 500) - - def test_no_return_returns(self): - r = self.webhost.request('GET', 'no_return_returns') - self.assertEqual(r.status_code, 200) diff --git a/tests/unittests/test_http_functions_v2.py b/tests/unittests/test_http_functions_v2.py deleted file mode 100644 index b7e456671..000000000 --- a/tests/unittests/test_http_functions_v2.py +++ /dev/null @@ -1,472 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import filecmp -import hashlib -import os -import pathlib -import sys -import typing -import unittest -from unittest import skipIf -from unittest.mock import patch - -from tests.utils import testutils - -from azure_functions_worker.constants import PYTHON_ENABLE_INIT_INDEXING - - -@unittest.skipIf(sys.version_info.minor <= 7, "Skipping tests <= Python 3.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' - cls._patch_environ = patch.dict('os.environ', os_environ) - cls._patch_environ.start() - - super().setUpClass() - - @classmethod - def tearDownClass(cls): - os.environ.clear() - os.environ.update(cls._pre_env) - 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) - - @unittest.skipIf(sys.version_info.minor >= 7, "Skipping for ADO") - 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) - - @unittest.skipIf(sys.version_info.minor >= 7, "Skipping for ADO") - 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') - - @testutils.retryable_test(3, 5) - 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]): - 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( - '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) - - @testutils.retryable_test(3, 5) - 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) - - @skipIf(sys.version_info < (3, 9, 0), - "Skip the tests for Python 3.8 and below") - 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 now exist in host_out - self.assertIn('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 now exist in host_out - self.assertIn('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) diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py deleted file mode 100644 index b2b1852fd..000000000 --- a/tests/unittests/test_http_v2.py +++ /dev/null @@ -1,253 +0,0 @@ -import asyncio -import socket -import sys -import unittest -from unittest.mock import MagicMock, patch - -from azure_functions_worker.http_v2 import ( - AsyncContextReference, - SingletonMeta, - get_unused_tcp_port, - http_coordinator, -) - - -class MockHttpRequest: - pass - - -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" - 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 - 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, 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 " - f"{self.invoc_id}") - - 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) - 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" - - with self.assertRaises(asyncio.TimeoutError): - self.loop.run_until_complete( - asyncio.wait_for( - http_coordinator.get_http_request_async( - invalid_invoc_id), - timeout=1 - ) - ) - - def test_await_http_response_async_valid_invocation(self): - invoc_id = "valid_invocation" - expected_response = self.http_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 = 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) - - 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: - 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}'") - - 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 = 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( - 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 setUp(self): - 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() - 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 - - -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) diff --git a/tests/unittests/test_invalid_stein.py b/tests/unittests/test_invalid_stein.py deleted file mode 100644 index 1f65389d4..000000000 --- a/tests/unittests/test_invalid_stein.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from tests.utils import testutils - -from azure_functions_worker import protos - -STEIN_INVALID_APP_FUNCTIONS_DIR = testutils.UNIT_TESTS_FOLDER / \ - 'broken_functions' / \ - 'invalid_app_stein' -STEIN_INVALID_FUNCTIONS_DIR = testutils.UNIT_TESTS_FOLDER / \ - 'broken_functions' / \ - 'invalid_stein' - - -class TestInvalidAppStein(testutils.AsyncTestCase): - - @testutils.retryable_test(4, 5) - async def test_indexing_not_app(self): - """Test if the functions metadata status will be - Failure when an invalid app is provided - """ - async with testutils.start_mockhost( - script_root=STEIN_INVALID_APP_FUNCTIONS_DIR) as host: - await host.init_worker() - r = await host.get_functions_metadata() - self.assertIsInstance(r.response, protos.FunctionMetadataResponse) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - self.assertIsNotNone(r.response.result.exception.message) - - @testutils.retryable_test(4, 5) - async def test_indexing_invalid_app(self): - """Test if the functions metadata status will be - Failure when an invalid app is provided - """ - async with testutils.start_mockhost( - script_root=STEIN_INVALID_FUNCTIONS_DIR) as host: - await host.init_worker() - r = await host.get_functions_metadata() - self.assertIsInstance(r.response, protos.FunctionMetadataResponse) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - self.assertIsNotNone(r.response.result.exception.message) diff --git a/tests/unittests/test_loader.py b/tests/unittests/test_loader.py deleted file mode 100644 index a6af8faa5..000000000 --- a/tests/unittests/test_loader.py +++ /dev/null @@ -1,281 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import asyncio -import os -import pathlib -import subprocess -import sys -import textwrap -from unittest import skipIf -from unittest.mock import Mock, patch - -from azure.functions import Function -from azure.functions.decorators.retry_policy import RetryPolicy -from azure.functions.decorators.timer import TimerTrigger -from tests.utils import testutils - -from azure_functions_worker import functions -from azure_functions_worker.constants import ( - PYTHON_SCRIPT_FILE_NAME, - PYTHON_SCRIPT_FILE_NAME_DEFAULT, -) -from azure_functions_worker.loader import build_retry_protos - - -class TestLoader(testutils.WebHostTestCase): - - def setUp(self) -> None: - def test_function(): - return "Test" - - self.test_function = test_function - self.func = Function(self.test_function, script_file="test.py") - self.function_registry = functions.Registry() - - @classmethod - def get_script_dir(cls): - return testutils.UNIT_TESTS_FOLDER / 'load_functions' - - def test_loader_building_fixed_retry_protos(self): - trigger = TimerTrigger(schedule="*/1 * * * * *", arg_name="mytimer", - name="mytimer") - self.func.add_trigger(trigger=trigger) - setting = RetryPolicy(strategy="fixed_delay", max_retry_count="1", - delay_interval="00:02:00") - self.func.add_setting(setting=setting) - - protos = build_retry_protos(self.func) - self.assertEqual(protos.max_retry_count, 1) - self.assertEqual(protos.retry_strategy, 1) # 1 enum for fixed delay - self.assertEqual(protos.delay_interval.seconds, 120) - - def test_loader_building_exponential_retry_protos(self): - trigger = TimerTrigger(schedule="*/1 * * * * *", arg_name="mytimer", - name="mytimer") - self.func.add_trigger(trigger=trigger) - setting = RetryPolicy(strategy="exponential_backoff", - max_retry_count="1", - minimum_interval="00:01:00", - maximum_interval="00:02:00") - self.func.add_setting(setting=setting) - - protos = build_retry_protos(self.func) - self.assertEqual(protos.max_retry_count, 1) - self.assertEqual(protos.retry_strategy, - 0) # 0 enum for exponential backoff - self.assertEqual(protos.minimum_interval.seconds, 60) - self.assertEqual(protos.maximum_interval.seconds, 120) - - @patch('azure_functions_worker.logging.logger.warning') - def test_loader_retry_policy_attribute_error(self, mock_logger): - self.func = Mock() - self.func.get_settings_dict.side_effect = AttributeError('DummyError') - - result = build_retry_protos(self.func) - self.assertIsNone(result) - - # Check if the logged message starts with the expected string - logged_message = mock_logger.call_args[0][ - 0] # Get the first argument of the logger.warning call - self.assertTrue(logged_message.startswith( - 'AttributeError while loading retry policy.')) - - def test_loader_simple(self): - r = self.webhost.request('GET', 'simple') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, '__app__.simple.main') - - def test_loader_custom_entrypoint(self): - r = self.webhost.request('GET', 'entrypoint') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, '__app__.entrypoint.main') - - def test_loader_no_script_file(self): - r = self.webhost.request('GET', 'no_script_file') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, '__app__.no_script_file.main') - - def test_loader_subdir(self): - r = self.webhost.request('GET', 'subdir') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, '__app__.subdir.sub.main') - - def test_loader_relimport(self): - r = self.webhost.request('GET', 'relimport') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, '__app__.relimport.relative') - - def test_loader_submodule(self): - r = self.webhost.request('GET', 'submodule') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, '__app__.submodule.sub_module.module') - - def test_loader_parentmodule(self): - r = self.webhost.request('GET', 'parentmodule') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, '__app__.parentmodule.module') - - def test_loader_absolute_thirdparty(self): - """Allow third-party package import from .python_packages - and worker_venv - """ - - r = self.webhost.request('GET', 'absolute_thirdparty') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'eh = azure.eventhub') - - def test_loader_prioritize_customer_module(self): - """When a module in customer code has the same name with a third-party - package, the worker should prioritize third-party package - """ - - r = self.webhost.request('GET', 'name_collision') - self.assertEqual(r.status_code, 200) - self.assertRegex(r.text, r'pt.__version__ = \d+.\d+.\d+') - - def test_loader_fix_customer_module_with_app_import(self): - """When a module in customer code has the same name with a third-party - package, if customer uses "import __app__." statement, - the worker should load customer package - """ - - r = self.webhost.request('GET', 'name_collision_app_import') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'pt.__version__ = from.customer.code') - - def test_loader_implicit_import(self): - """Since sys.path is now fixed with script root appended, - implicit import statement is now acceptable. - """ - - r = self.webhost.request('GET', 'implicit_import') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 's_main = simple.main') - - def test_loader_module_not_found(self): - """If a module cannot be found, should throw an exception with - trouble shooting link https://aka.ms/functions-modulenotfound - """ - r = self.webhost.request('GET', 'module_not_found') - self.assertEqual(r.status_code, 500) - - def test_loader_init_should_only_invoke_outside_main_once(self): - """Check if the code in __init__.py outside of main() function - is only executed once - """ - r = self.webhost.request('GET', 'outside_main_code_in_init') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'executed count = 1') - - def test_loader_main_should_only_invoke_outside_main_once(self): - """Check if the code in main.py outside of main() function - is only executed once - """ - r = self.webhost.request('GET', 'outside_main_code_in_main') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'executed count = 1') - - def test_loader_outside_main_package_should_be_loaded_from_init(self): - """Check if the package can still be loaded from __init__ module - """ - r = self.webhost.request('GET', 'load_outside_main?from=init') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - def test_loader_outside_main_package_should_be_loaded_from_package(self): - """Check if the package can still be loaded from package - """ - r = self.webhost.request('GET', - 'load_outside_main?from=package') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK') - - def check_log_loader_module_not_found(self, host_out): - passed = False - exception_message = "Exception: ModuleNotFoundError: "\ - "No module named 'notfound'. "\ - "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) - - -class TestPluginLoader(testutils.AsyncTestCase): - - @skipIf(sys.version_info.minor <= 7, "Skipping tests <= Python 3.7") - async def test_entry_point_plugin(self): - test_binding = pathlib.Path(__file__).parent / 'test-binding' - subprocess.run([ - sys.executable, '-m', 'pip', - '--disable-pip-version-check', - 'install', '--quiet', - '-e', test_binding - ], check=True) - - # This test must be run in a subprocess so that - # pkg_resources picks up the newly installed package. - code = textwrap.dedent(''' -import asyncio -from azure_functions_worker import protos -from tests.utils import testutils - -async def _runner(): - async with testutils.start_mockhost( - script_root='unittests/test-binding/functions') as host: - await host.init_worker() - func_id, r = await host.load_function('foo') - - print(r.response.function_id == func_id) - print(r.response.result.status == protos.StatusResult.Success) - -asyncio.get_event_loop().run_until_complete(_runner()) -''') - - try: - proc = await asyncio.create_subprocess_exec( - sys.executable, '-c', code, - stdout=asyncio.subprocess.PIPE) - - stdout, stderr = await proc.communicate() - - # Trimming off carriage return charater when testing on Windows - stdout_lines = [ - line.replace(b'\r', b'') for line in stdout.strip().split(b'\n') - ] - self.assertEqual(stdout_lines, [b'True', b'True']) - - finally: - subprocess.run([ - sys.executable, '-m', 'pip', - '--disable-pip-version-check', - 'uninstall', '-y', '--quiet', 'foo-binding' - ], check=True) - - -class TestConfigurableFileName(testutils.WebHostTestCase): - - def setUp(self) -> None: - def test_function(): - return "Test" - - self.file_name = PYTHON_SCRIPT_FILE_NAME_DEFAULT - self.test_function = test_function - self.func = Function(self.test_function, script_file="function_app.py") - self.function_registry = functions.Registry() - - @classmethod - def get_script_dir(cls): - return testutils.UNIT_TESTS_FOLDER / 'http_functions' / \ - 'http_functions_stein' - - def test_correct_file_name(self): - os.environ.update({PYTHON_SCRIPT_FILE_NAME: self.file_name}) - self.assertIsNotNone(os.environ.get(PYTHON_SCRIPT_FILE_NAME)) - self.assertEqual(os.environ.get(PYTHON_SCRIPT_FILE_NAME), - 'function_app.py') diff --git a/tests/unittests/test_log_filtering_functions.py b/tests/unittests/test_log_filtering_functions.py deleted file mode 100644 index 3d074316b..000000000 --- a/tests/unittests/test_log_filtering_functions.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import typing - -from tests.utils import testutils - -HOST_JSON_TEMPLATE_WITH_LOGLEVEL_INFO = """\ -{ - "version": "2.0", - "logging": { - "logLevel": { - "default": "Information" - } - }, - "functionTimeout": "00:05:00" -} -""" - - -class TestLogFilteringFunctions(testutils.WebHostTestCase): - """This class is for testing the logger behavior in Python Worker when - dealing with customer's log and system's log. Here's a list of expected - behaviors: - local_console customer_app_insight functions_kusto_table - system_log false false true - customer_log true true false - - Please ensure the following unit test cases align with the expectations - """ - - @classmethod - def setUpClass(cls): - host_json = testutils.TESTS_ROOT / cls.get_script_dir() / 'host.json' - - with open(host_json, 'w+') as f: - f.write(HOST_JSON_TEMPLATE_WITH_LOGLEVEL_INFO) - - super(TestLogFilteringFunctions, cls).setUpClass() - - @classmethod - def tearDownClass(cls): - host_json = testutils.TESTS_ROOT / cls.get_script_dir() / 'host.json' - testutils.remove_path(host_json) - - super(TestLogFilteringFunctions, cls).tearDownClass() - - @classmethod - def get_script_dir(cls): - return testutils.UNIT_TESTS_FOLDER / 'log_filtering_functions' - - 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) - # See HOST_JSON_TEMPLATE_WITH_LOGLEVEL_INFO, debug log is disabled - 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 error', host_out) - # See HOST_JSON_TEMPLATE_WITH_LOGLEVEL_INFO, debug log is disabled - self.assertNotIn('logging debug', host_out) - - def test_info_with_sdk_logging(self): - """Invoke a HttpTrigger sdk_logging which contains logging invocation - via the azure.functions logger. This should be treated as system logs, - which means the log should not be displayed in local console. - """ - r = self.webhost.request('GET', 'sdk_logging') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK-sdk-logger') - - def check_log_info_with_sdk_logging(self, host_out: typing.List[str]): - # See TestLogFilteringFunctions docstring - # System log should be captured in console - self.assertIn('sdk_logger info', host_out) - self.assertIn('sdk_logger warning', host_out) - self.assertIn('sdk_logger error', host_out) - self.assertNotIn('sdk_logger debug', host_out) - - def test_info_with_sdk_submodule_logging(self): - """Invoke a HttpTrigger sdk_submodule_logging which contains logging - invocation via the azure.functions logger. This should be treated as - system logs. - """ - r = self.webhost.request('GET', 'sdk_submodule_logging') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK-sdk-submodule-logging') - - def check_log_info_with_sdk_submodule_logging(self, - host_out: typing.List[str]): - # See TestLogFilteringFunctions docstring - # System log should be captured in console - self.assertIn('sdk_submodule_logger info', host_out) - self.assertIn('sdk_submodule_logger warning', host_out) - self.assertIn('sdk_submodule_logger error', host_out) - self.assertNotIn('sdk_submodule_logger debug', host_out) diff --git a/tests/unittests/test_logging.py b/tests/unittests/test_logging.py deleted file mode 100644 index b7c4f5f4a..000000000 --- a/tests/unittests/test_logging.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import unittest - -from azure_functions_worker import logging as flog -from azure_functions_worker.logging import format_exception - - -class TestLogging(unittest.TestCase): - """This class is for testing the grpc logging behavior in Python Worker. - Here's a list of expected behaviors: - local_console customer_app_insight functions_kusto_table - system_log false false true - customer_log true true false - - Please ensure the following unit test cases align with the expectations - """ - - def test_system_log_namespace(self): - """Ensure the following list is part of the system's log - """ - self.assertTrue(flog.is_system_log_category('azure_functions_worker')) - self.assertTrue( - flog.is_system_log_category('azure_functions_worker_error') - ) - self.assertTrue(flog.is_system_log_category('azure.functions')) - self.assertTrue(flog.is_system_log_category('azure.functions.module')) - - def test_customer_log_namespace(self): - """Ensure the following list is part of the customer's log - """ - self.assertFalse(flog.is_system_log_category('customer_logger')) - self.assertFalse(flog.is_system_log_category('azure')) - self.assertFalse(flog.is_system_log_category('protobuf')) - self.assertFalse(flog.is_system_log_category('root')) - self.assertFalse(flog.is_system_log_category('')) - - def test_format_exception(self): - def call0(fn): - call1(fn) - - def call1(fn): - call2(fn) - - def call2(fn): - fn() - - def raising_function(): - raise ValueError("Value error being raised.", ) - - try: - call0(raising_function) - except ValueError as e: - processed_exception = format_exception(e) - self.assertIn("call0", processed_exception) - self.assertIn("call1", processed_exception) - self.assertIn("call2", processed_exception) - self.assertIn("f", processed_exception) - self.assertRegex(processed_exception, - r".*tests\\unittests\\test_logging.py.*") diff --git a/tests/unittests/test_main.py b/tests/unittests/test_main.py deleted file mode 100644 index 688b4b0c1..000000000 --- a/tests/unittests/test_main.py +++ /dev/null @@ -1,80 +0,0 @@ -import sys -import unittest -from unittest.mock import patch - -from azure_functions_worker.main import parse_args - - -class TestMain(unittest.TestCase): - - @patch.object(sys, 'argv', - ['xxx', '--host', '127.0.0.1', - '--port', '50821', - '--workerId', 'e9efd817-47a1-45dc-9e20-e6f975d7a025', - '--requestId', 'cbef5957-cdb3-4462-9ee7-ac9f91be0a51', - '--grpcMaxMessageLength', '2147483647', - '--functions-uri', 'http://127.0.0.1:50821', - '--functions-worker-id', - 'e9efd817-47a1-45dc-9e20-e6f975d7a025', - '--functions-request-id', - 'cbef5957-cdb3-4462-9ee7-ac9f91be0a51', - '--functions-grpc-max-message-length', '2147483647']) - def test_all_args(self): - args = parse_args() - self.assertEqual(args.host, '127.0.0.1') - self.assertEqual(args.port, 50821) - self.assertEqual(args.worker_id, - 'e9efd817-47a1-45dc-9e20-e6f975d7a025') - self.assertEqual(args.request_id, - 'cbef5957-cdb3-4462-9ee7-ac9f91be0a51') - self.assertEqual(args.grpc_max_msg_len, 2147483647) - self.assertEqual(args.functions_uri, 'http://127.0.0.1:50821') - self.assertEqual(args.functions_worker_id, - 'e9efd817-47a1-45dc-9e20-e6f975d7a025') - self.assertEqual(args.functions_request_id, - 'cbef5957-cdb3-4462-9ee7-ac9f91be0a51') - self.assertEqual(args.functions_grpc_max_msg_len, 2147483647) - - @patch.object(sys, 'argv', - ['xxx', '--host', '127.0.0.1', - '--port', '50821', - '--workerId', 'e9efd817-47a1-45dc-9e20-e6f975d7a025', - '--requestId', 'cbef5957-cdb3-4462-9ee7-ac9f91be0a51', - '--grpcMaxMessageLength', '2147483647']) - def test_old_args(self): - args = parse_args() - self.assertEqual(args.host, '127.0.0.1') - self.assertEqual(args.port, 50821) - self.assertEqual(args.worker_id, - 'e9efd817-47a1-45dc-9e20-e6f975d7a025') - self.assertEqual(args.request_id, - 'cbef5957-cdb3-4462-9ee7-ac9f91be0a51') - self.assertEqual(args.grpc_max_msg_len, 2147483647) - self.assertIsNone(args.functions_uri) - self.assertIsNone(args.functions_worker_id) - self.assertIsNone(args.functions_request_id) - self.assertIsNone(args.functions_grpc_max_msg_len) - - @patch.object(sys, 'argv', - ['xxx', '--functions-uri', 'http://127.0.0.1:50821', - '--functions-worker-id', - 'e9efd817-47a1-45dc-9e20-e6f975d7a025', - '--functions-request-id', - 'cbef5957-cdb3-4462-9ee7-ac9f91be0a51', - '--functions-grpc-max-message-length', '2147483647']) - def test_new_args(self): - args = parse_args() - self.assertEqual(args.functions_uri, 'http://127.0.0.1:50821') - self.assertEqual(args.functions_worker_id, - 'e9efd817-47a1-45dc-9e20-e6f975d7a025') - self.assertEqual(args.functions_request_id, - 'cbef5957-cdb3-4462-9ee7-ac9f91be0a51') - self.assertEqual(args.functions_grpc_max_msg_len, 2147483647) - - @patch.object(sys, 'argv', ['xxx', '--host', 'dummy_host', - '--port', '12345', - '--invalid-arg', 'invalid_value']) - def test_invalid_args(self): - with self.assertRaises(SystemExit) as context: - parse_args() - self.assertEqual(context.exception.code, 2) diff --git a/tests/unittests/test_mock_blob_shared_memory_functions.py b/tests/unittests/test_mock_blob_shared_memory_functions.py deleted file mode 100644 index 63b06ca12..000000000 --- a/tests/unittests/test_mock_blob_shared_memory_functions.py +++ /dev/null @@ -1,620 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import hashlib -import json -import sys -import time -from unittest import skipIf - -from tests.utils import testutils - -from azure_functions_worker import protos -from azure_functions_worker.bindings.shared_memory_data_transfer import ( - SharedMemoryConstants as consts, -) -from azure_functions_worker.bindings.shared_memory_data_transfer import SharedMemoryMap - - -@skipIf(sys.platform == 'darwin', 'MacOS M1 machines do not correctly test the' - 'shared memory filesystems and thus skipping' - ' these tests for the time being') -class TestMockBlobSharedMemoryFunctions(testutils.SharedMemoryTestCase, - testutils.AsyncTestCase): - """ - Test the use of shared memory to transfer input and output data to and from - the host/worker. - """ - def setUp(self): - super().setUp() - self.blob_funcs_dir = testutils.EMULATOR_TESTS_FOLDER / 'blob_functions' - - async def test_binary_blob_read_as_bytes_function(self): - """ - Read a blob with binary input that was transferred between the host and - worker over shared memory. - The function's input data type will be bytes. - """ - func_name = 'get_blob_as_bytes_return_http_response' - await self._test_binary_blob_read_function(func_name) - - async def test_binary_blob_read_as_stream_function(self): - """ - Read a blob with binary input that was transferred between the host and - worker over shared memory. - The function's input data type will be InputStream. - """ - func_name = 'get_blob_as_bytes_stream_return_http_response' - await self._test_binary_blob_read_function(func_name) - - async def test_binary_blob_write_function(self): - """ - Write a blob with binary output that was transferred between the worker - and host over shared memory. - """ - func_name = 'put_blob_as_bytes_return_http_response' - async with testutils.start_mockhost(script_root=self.blob_funcs_dir) \ - as host: - await host.init_worker("4.17.1") - await host.load_function(func_name) - - content_size = consts.MIN_BYTES_FOR_SHARED_MEM_TRANSFER + 10 - http_params = {'content_size': str(content_size)} - - # Invoke the function; it should read the input blob from shared - # memory and respond back in the HTTP body with the number of bytes - # it read in the input - _, response_msg = await host.invoke_function( - func_name, [ - protos.ParameterBinding( - name='req', - data=protos.TypedData( - http=protos.RpcHttp( - method='GET', - query=http_params))), - ]) - - # Verify if the function executed successfully - self.assertEqual(protos.StatusResult.Success, - response_msg.response.result.status) - - # The function responds back in the HTTP body with the sha256 digest of - # the output it created along with its size - response_bytes = response_msg.response.return_value.http.body.bytes - json_response = json.loads(response_bytes) - func_created_content_size = json_response['content_size'] - func_created_content_sha256 = json_response['content_sha256'] - - # Verify if the worker produced an output blob which was written - # in shared memory - output_data = response_msg.response.output_data - self.assertEqual(1, len(output_data)) - - output_binding = output_data[0] - binding_type = output_binding.WhichOneof('rpc_data') - self.assertEqual('rpc_shared_memory', binding_type) - - # Get the information about the shared memory region in which the - # worker wrote the function's output blob - shmem = output_binding.rpc_shared_memory - mem_map_name = shmem.name - offset = shmem.offset - count = shmem.count - data_type = shmem.type - - # Verify if the shared memory region's information is valid - self.assertTrue(self.is_valid_uuid(mem_map_name)) - self.assertEqual(0, offset) - self.assertEqual(func_created_content_size, count) - self.assertEqual(protos.RpcDataType.bytes, data_type) - - # Read data from the shared memory region - mem_map_size = consts.CONTENT_HEADER_TOTAL_BYTES + count - mem_map = self.file_accessor.open_mem_map(mem_map_name, - mem_map_size) - shared_mem_map = SharedMemoryMap(self.file_accessor, mem_map_name, - mem_map) - read_content = shared_mem_map.get_bytes() - - # Dispose the shared memory map since we have read the function's - # output now - shared_mem_map.dispose() - - # Verify if we were able to read the correct output that the - # function has produced - read_content_sha256 = hashlib.sha256(read_content).hexdigest() - self.assertEqual(func_created_content_sha256, read_content_sha256) - self.assertEqual(len(read_content), func_created_content_size) - - async def test_str_blob_read_function(self): - """ - Read a blob with binary input that was transferred between the host and - worker over shared memory. - The function's input data type will be str. - """ - func_name = 'get_blob_as_str_return_http_response' - async with testutils.start_mockhost(script_root=self.blob_funcs_dir) \ - as host: - await host.init_worker("4.17.1") - await host.load_function(func_name) - - # Write binary content into shared memory - mem_map_name = self.get_new_mem_map_name() - content_size = consts.MIN_BYTES_FOR_SHARED_MEM_TRANSFER + 10 - num_chars = int(content_size / consts.SIZE_OF_CHAR_BYTES) - content = self.get_random_string(num_chars) - content_bytes = content.encode('utf-8') - content_sha256 = hashlib.sha256(content_bytes).hexdigest() - mem_map_size = consts.CONTENT_HEADER_TOTAL_BYTES + content_size - mem_map = self.file_accessor.create_mem_map(mem_map_name, - mem_map_size) - shared_mem_map = SharedMemoryMap(self.file_accessor, mem_map_name, - mem_map) - num_bytes_written = shared_mem_map.put_bytes(content_bytes) - - # Create a message to send to the worker containing info about the - # shared memory region to read input from - value = protos.RpcSharedMemory( - name=mem_map_name, - offset=0, - count=num_bytes_written, - type=protos.RpcDataType.string - ) - - # Invoke the function; it should read the input blob from shared - # memory and respond back in the HTTP body with the number of bytes - # it read in the input - _, response_msg = await host.invoke_function( - func_name, [ - protos.ParameterBinding( - name='req', - data=protos.TypedData( - http=protos.RpcHttp( - method='GET'))), - protos.ParameterBinding( - name='file', - rpc_shared_memory=value - ) - ]) - - # Dispose the shared memory map since the function is done using it - shared_mem_map.dispose() - - # Verify if the function executed successfully - self.assertEqual(protos.StatusResult.Success, - response_msg.response.result.status) - - response_bytes = response_msg.response.return_value.http.body.bytes - json_response = json.loads(response_bytes) - func_received_num_chars = json_response['num_chars'] - func_received_content_sha256 = json_response['content_sha256'] - - # Check the function response to ensure that it read the complete - # input that we provided and the sha256 matches - self.assertEqual(num_chars, func_received_num_chars) - self.assertEqual(content_sha256, func_received_content_sha256) - - async def test_str_blob_write_function(self): - """ - Write a blob with string output that was transferred between the worker - and host over shared memory. - """ - func_name = 'put_blob_as_str_return_http_response' - async with testutils.start_mockhost(script_root=self.blob_funcs_dir) \ - as host: - await host.init_worker("4.17.1") - await host.load_function(func_name) - - content_size = consts.MIN_BYTES_FOR_SHARED_MEM_TRANSFER + 10 - num_chars = int(content_size / consts.SIZE_OF_CHAR_BYTES) - http_params = {'num_chars': str(num_chars)} - - # Invoke the function; it should read the input blob from shared - # memory and respond back in the HTTP body with the number of bytes - # it read in the input - _, response_msg = await host.invoke_function( - func_name, [ - protos.ParameterBinding( - name='req', - data=protos.TypedData( - http=protos.RpcHttp( - method='GET', - query=http_params))), - ]) - - # Verify if the function executed successfully - self.assertEqual(protos.StatusResult.Success, - response_msg.response.result.status) - - # The function responds back in the HTTP body with the sha256 digest of - # the output it created along with its size - response_bytes = response_msg.response.return_value.http.body.bytes - json_response = json.loads(response_bytes) - func_created_num_chars = json_response['num_chars'] - func_created_content_sha256 = json_response['content_sha256'] - - # Verify if the worker produced an output blob which was written - # in shared memory - output_data = response_msg.response.output_data - self.assertEqual(1, len(output_data)) - - output_binding = output_data[0] - binding_type = output_binding.WhichOneof('rpc_data') - self.assertEqual('rpc_shared_memory', binding_type) - - # Get the information about the shared memory region in which the - # worker wrote the function's output blob - shmem = output_binding.rpc_shared_memory - mem_map_name = shmem.name - offset = shmem.offset - count = shmem.count - data_type = shmem.type - - # Verify if the shared memory region's information is valid - self.assertTrue(self.is_valid_uuid(mem_map_name)) - self.assertEqual(0, offset) - self.assertEqual(func_created_num_chars, count) - self.assertEqual(protos.RpcDataType.string, data_type) - - # Read data from the shared memory region - mem_map_size = consts.CONTENT_HEADER_TOTAL_BYTES + count - mem_map = self.file_accessor.open_mem_map(mem_map_name, - mem_map_size) - shared_mem_map = SharedMemoryMap(self.file_accessor, mem_map_name, - mem_map) - read_content_bytes = shared_mem_map.get_bytes() - - # Dispose the shared memory map since we have read the function's - # output now - shared_mem_map.dispose() - - # Verify if we were able to read the correct output that the - # function has produced - read_content_sha256 = hashlib.sha256(read_content_bytes).hexdigest() - self.assertEqual(func_created_content_sha256, read_content_sha256) - read_content = read_content_bytes.decode('utf-8') - self.assertEqual(len(read_content), func_created_num_chars) - - async def test_close_shared_memory_maps(self): - """ - Close the shared memory maps created by the worker to transfer output - blob to the host after the host is done processing the response. - """ - func_name = 'put_blob_as_bytes_return_http_response' - async with testutils.start_mockhost(script_root=self.blob_funcs_dir) \ - as host: - await host.init_worker("4.17.1") - await host.load_function(func_name) - - content_size = consts.MIN_BYTES_FOR_SHARED_MEM_TRANSFER + 10 - http_params = {'content_size': str(content_size)} - - # Invoke the function; it should read the input blob from shared - # memory and respond back in the HTTP body with the number of bytes - # it read in the input - _, response_msg = await host.invoke_function( - func_name, [ - protos.ParameterBinding( - name='req', - data=protos.TypedData( - http=protos.RpcHttp( - method='GET', - query=http_params))), - ]) - - # Verify if the function executed successfully - self.assertEqual(protos.StatusResult.Success, - response_msg.response.result.status) - - # Verify if the worker produced an output blob which was written - # in shared memory - output_data = response_msg.response.output_data - output_binding = output_data[0] - - # Get the information about the shared memory region in which the - # worker wrote the function's output blob - shmem = output_binding.rpc_shared_memory - mem_map_name = shmem.name - - # Request the worker to close the memory maps - mem_map_names = [mem_map_name] - response_msg = \ - await host.close_shared_memory_resources(mem_map_names) - - # Verify that the worker responds with a successful status after - # closing the requested memory map - mem_map_statuses = response_msg.response.close_map_results - self.assertEqual(len(mem_map_names), len(mem_map_statuses.keys())) - for mem_map_name in mem_map_names: - self.assertTrue(mem_map_name in mem_map_statuses) - status = mem_map_statuses[mem_map_name] - self.assertTrue(status) - - async def test_shared_memory_not_used_with_small_output(self): - """ - Even though shared memory is enabled, small inputs will not be - transferred over shared memory (in this case from the worker to the - host.) - """ - func_name = 'put_blob_as_bytes_return_http_response' - async with testutils.start_mockhost(script_root=self.blob_funcs_dir) \ - as host: - await host.init_worker("4.17.1") - await host.load_function(func_name) - - content_size = consts.MIN_BYTES_FOR_SHARED_MEM_TRANSFER - 10 - http_params = {'content_size': str(content_size)} - - # Invoke the function; it should read the input blob from shared - # memory and respond back in the HTTP body with the number of bytes - # it read in the input - _, response_msg = await host.invoke_function( - func_name, [ - protos.ParameterBinding( - name='req', - data=protos.TypedData( - http=protos.RpcHttp( - method='GET', - query=http_params))), - ]) - - # Verify if the function executed successfully - self.assertEqual(protos.StatusResult.Success, - response_msg.response.result.status) - - # Verify if the worker produced an output blob which was sent over - # RPC instead of shared memory - output_data = response_msg.response.output_data - self.assertEqual(1, len(output_data)) - - output_binding = output_data[0] - binding_type = output_binding.WhichOneof('rpc_data') - self.assertEqual('data', binding_type) - - async def test_multiple_input_output_blobs(self): - """ - Read two blobs and write two blobs, all over shared memory. - """ - func_name = 'put_get_multiple_blobs_as_bytes_return_http_response' - async with testutils.start_mockhost(script_root=self.blob_funcs_dir) \ - as host: - await host.init_worker("4.17.1") - await host.load_function(func_name) - - # Input 1 - # Write binary content into shared memory - mem_map_name_1 = self.get_new_mem_map_name() - input_content_size_1 = consts.MIN_BYTES_FOR_SHARED_MEM_TRANSFER + 10 - input_content_1 = self.get_random_bytes(input_content_size_1) - input_content_sha256_1 = hashlib.sha256(input_content_1).hexdigest() - input_mem_map_size_1 = \ - consts.CONTENT_HEADER_TOTAL_BYTES + input_content_size_1 - input_mem_map_1 = \ - self.file_accessor.create_mem_map(mem_map_name_1, - input_mem_map_size_1) - input_shared_mem_map_1 = \ - SharedMemoryMap(self.file_accessor, mem_map_name_1, - input_mem_map_1) - input_num_bytes_written_1 = \ - input_shared_mem_map_1.put_bytes(input_content_1) - - # Create a message to send to the worker containing info about the - # shared memory region to read input from - input_value_1 = protos.RpcSharedMemory( - name=mem_map_name_1, - offset=0, - count=input_num_bytes_written_1, - type=protos.RpcDataType.bytes - ) - - # Input 2 - # Write binary content into shared memory - mem_map_name_2 = self.get_new_mem_map_name() - input_content_size_2 = consts.MIN_BYTES_FOR_SHARED_MEM_TRANSFER + 20 - input_content_2 = self.get_random_bytes(input_content_size_2) - input_content_sha256_2 = hashlib.sha256(input_content_2).hexdigest() - input_mem_map_size_2 = \ - consts.CONTENT_HEADER_TOTAL_BYTES + input_content_size_2 - input_mem_map_2 = \ - self.file_accessor.create_mem_map(mem_map_name_2, - input_mem_map_size_2) - input_shared_mem_map_2 = \ - SharedMemoryMap(self.file_accessor, mem_map_name_2, - input_mem_map_2) - input_num_bytes_written_2 = \ - input_shared_mem_map_2.put_bytes(input_content_2) - - # Outputs - output_content_size_1 = \ - consts.MIN_BYTES_FOR_SHARED_MEM_TRANSFER + 11 - output_content_size_2 = \ - consts.MIN_BYTES_FOR_SHARED_MEM_TRANSFER + 22 - http_params = { - 'output_content_size_1': str(output_content_size_1), - 'output_content_size_2': str(output_content_size_2)} - - # Create a message to send to the worker containing info about the - # shared memory region to read input from - input_value_2 = protos.RpcSharedMemory( - name=mem_map_name_2, - offset=0, - count=input_num_bytes_written_2, - type=protos.RpcDataType.bytes - ) - - # Invoke the function; it should read the input blob from shared - # memory and respond back in the HTTP body with the number of bytes - # it read in the input - _, response_msg = await host.invoke_function( - func_name, [ - protos.ParameterBinding( - name='req', - data=protos.TypedData( - http=protos.RpcHttp( - method='GET', - query=http_params))), - protos.ParameterBinding( - name='inputfile1', - rpc_shared_memory=input_value_1 - ), - protos.ParameterBinding( - name='inputfile2', - rpc_shared_memory=input_value_2 - ) - ]) - time.sleep(1) - - # Dispose the shared memory map since the function is done using it - input_shared_mem_map_1.dispose() - input_shared_mem_map_2.dispose() - - # Verify if the function executed successfully - self.assertEqual(protos.StatusResult.Success, - response_msg.response.result.status) - - response_bytes = response_msg.response.return_value.http.body.bytes - json_response = json.loads(response_bytes) - - func_received_content_size_1 = json_response['input_content_size_1'] - func_received_content_sha256_1 = json_response['input_content_sha256_1'] - func_received_content_size_2 = json_response['input_content_size_2'] - func_received_content_sha256_2 = json_response['input_content_sha256_2'] - func_created_content_size_1 = json_response['output_content_size_1'] - func_created_content_size_2 = json_response['output_content_size_2'] - func_created_content_sha256_1 = json_response['output_content_sha256_1'] - func_created_content_sha256_2 = json_response['output_content_sha256_2'] - - # Check the function response to ensure that it read the complete - # input that we provided and the sha256 matches - self.assertEqual(input_content_size_1, func_received_content_size_1) - self.assertEqual(input_content_sha256_1, func_received_content_sha256_1) - self.assertEqual(input_content_size_2, func_received_content_size_2) - self.assertEqual(input_content_sha256_2, func_received_content_sha256_2) - - # Verify if the worker produced two output blobs which were written - # in shared memory - output_data = response_msg.response.output_data - self.assertEqual(2, len(output_data)) - - # Output 1 - output_binding_1 = output_data[0] - binding_type = output_binding_1.WhichOneof('rpc_data') - self.assertEqual('rpc_shared_memory', binding_type) - - shmem_1 = output_binding_1.rpc_shared_memory - self._verify_function_output(shmem_1, func_created_content_size_1, - func_created_content_sha256_1) - - # Output 2 - output_binding_2 = output_data[1] - binding_type = output_binding_2.WhichOneof('rpc_data') - self.assertEqual('rpc_shared_memory', binding_type) - - shmem_2 = output_binding_2.rpc_shared_memory - self._verify_function_output(shmem_2, func_created_content_size_2, - func_created_content_sha256_2) - - async def _test_binary_blob_read_function(self, func_name): - """ - Verify that the function executed successfully when the worker received - inputs for the function over shared memory. - """ - async with testutils.start_mockhost(script_root=self.blob_funcs_dir) \ - as host: - await host.init_worker("4.17.1") - await host.load_function(func_name) - - # Write binary content into shared memory - mem_map_name = self.get_new_mem_map_name() - content_size = consts.MIN_BYTES_FOR_SHARED_MEM_TRANSFER + 10 - content = self.get_random_bytes(content_size) - content_sha256 = hashlib.sha256(content).hexdigest() - mem_map_size = consts.CONTENT_HEADER_TOTAL_BYTES + content_size - mem_map = self.file_accessor.create_mem_map(mem_map_name, - mem_map_size) - shared_mem_map = SharedMemoryMap(self.file_accessor, mem_map_name, - mem_map) - num_bytes_written = shared_mem_map.put_bytes(content) - - # Create a message to send to the worker containing info about the - # shared memory region to read input from - value = protos.RpcSharedMemory( - name=mem_map_name, - offset=0, - count=num_bytes_written, - type=protos.RpcDataType.bytes - ) - - # Invoke the function; it should read the input blob from shared - # memory and respond back in the HTTP body with the number of bytes - # it read in the input - _, response_msg = await host.invoke_function( - func_name, [ - protos.ParameterBinding( - name='req', - data=protos.TypedData( - http=protos.RpcHttp( - method='GET'))), - protos.ParameterBinding( - name='file', - rpc_shared_memory=value - ) - ]) - - # Dispose the shared memory map since the function is done using it - shared_mem_map.dispose() - - # Verify if the function executed successfully - self.assertEqual(protos.StatusResult.Success, - response_msg.response.result.status) - - response_bytes = response_msg.response.return_value.http.body.bytes - json_response = json.loads(response_bytes) - func_received_content_size = json_response['content_size'] - func_received_content_sha256 = json_response['content_sha256'] - - # Check the function response to ensure that it read the complete - # input that we provided and the sha256 matches - self.assertEqual(content_size, func_received_content_size) - self.assertEqual(content_sha256, func_received_content_sha256) - - def _verify_function_output( - self, - shmem: protos.RpcSharedMemory, - expected_size: int, - expected_sha256: str): - """ - Verify if the output produced by the worker is what we expect it to be - based on the size and MD5 digest. - """ - output_mem_map_name = shmem.name - output_offset = shmem.offset - output_count = shmem.count - output_data_type = shmem.type - - # Verify if the shared memory region's information is valid - self.assertTrue(self.is_valid_uuid(output_mem_map_name)) - self.assertEqual(0, output_offset) - self.assertEqual(expected_size, output_count) - self.assertEqual(protos.RpcDataType.bytes, output_data_type) - - # Read data from the shared memory region - output_mem_map_size = \ - consts.CONTENT_HEADER_TOTAL_BYTES + output_count - output_mem_map = \ - self.file_accessor.open_mem_map(output_mem_map_name, - output_mem_map_size) - output_shared_mem_map = \ - SharedMemoryMap(self.file_accessor, output_mem_map_name, - output_mem_map) - output_read_content = output_shared_mem_map.get_bytes() - - # Dispose the shared memory map since we have read the function's - # output now - output_shared_mem_map.dispose() - - # Verify if we were able to read the correct output that the - # function has produced - output_read_content_sha256 = hashlib.sha256(output_read_content).hexdigest() - self.assertEqual(expected_sha256, output_read_content_sha256) - self.assertEqual(len(output_read_content), expected_size) diff --git a/tests/unittests/test_mock_durable_functions.py b/tests/unittests/test_mock_durable_functions.py deleted file mode 100644 index ce19c613f..000000000 --- a/tests/unittests/test_mock_durable_functions.py +++ /dev/null @@ -1,158 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from tests.utils import testutils - -from azure_functions_worker import protos - - -class TestDurableFunctions(testutils.AsyncTestCase): - durable_functions_dir = testutils.UNIT_TESTS_FOLDER / 'durable_functions' - - async def test_mock_activity_trigger(self): - async with testutils.start_mockhost( - script_root=self.durable_functions_dir) as host: - - await host.init_worker("4.17.1") - func_id, r = await host.load_function('activity_trigger') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - _, r = await host.invoke_function( - 'activity_trigger', [ - # According to Durable Python - # Activity Trigger's input must be json serializable - protos.ParameterBinding( - name='input', - data=protos.TypedData( - string='test single_word' - ) - ) - ] - ) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - self.assertEqual( - r.response.return_value, - protos.TypedData(json='"test single_word"') - ) - - async def test_mock_activity_trigger_no_anno(self): - async with testutils.start_mockhost( - script_root=self.durable_functions_dir) as host: - - await host.init_worker("4.17.1") - func_id, r = await host.load_function('activity_trigger_no_anno') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - _, r = await host.invoke_function( - 'activity_trigger_no_anno', [ - # According to Durable Python - # Activity Trigger's input must be json serializable - protos.ParameterBinding( - name='input', - data=protos.TypedData( - string='test multiple words' - ) - ) - ] - ) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - self.assertEqual( - r.response.return_value, - protos.TypedData(json='"test multiple words"') - ) - - async def test_mock_activity_trigger_dict(self): - async with testutils.start_mockhost( - script_root=self.durable_functions_dir) as host: - - await host.init_worker("4.17.1") - func_id, r = await host.load_function('activity_trigger_dict') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - _, r = await host.invoke_function( - 'activity_trigger_dict', [ - # According to Durable Python - # Activity Trigger's input must be json serializable - protos.ParameterBinding( - name='input', - data=protos.TypedData( - json='{"bird": "Crane"}' - ) - ) - ] - ) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - self.assertEqual( - r.response.return_value, - protos.TypedData(json='{"bird": "enarC"}') - ) - - async def test_mock_activity_trigger_int_to_float(self): - async with testutils.start_mockhost( - script_root=self.durable_functions_dir) as host: - - await host.init_worker("4.17.1") - func_id, r = await host.load_function( - 'activity_trigger_int_to_float') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - _, r = await host.invoke_function( - 'activity_trigger_int_to_float', [ - # According to Durable Python - # Activity Trigger's input must be json serializable - protos.ParameterBinding( - name='input', - data=protos.TypedData( - json=str(int(10)) - ) - ) - ] - ) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - self.assertEqual( - r.response.return_value, - protos.TypedData(json='-11.0') - ) - - async def test_mock_orchestration_trigger(self): - async with testutils.start_mockhost( - script_root=self.durable_functions_dir) as host: - - await host.init_worker("4.17.1") - func_id, r = await host.load_function('orchestration_trigger') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - _, r = await host.invoke_function( - 'orchestration_trigger', [ - protos.ParameterBinding( - name='context', - data=protos.TypedData( - string='Durable functions coming soon' - ) - ) - ] - ) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - self.assertEqual( - r.response.return_value, - protos.TypedData(json='Durable functions coming soon :)') - ) diff --git a/tests/unittests/test_mock_eventhub_functions.py b/tests/unittests/test_mock_eventhub_functions.py deleted file mode 100644 index f93dfa994..000000000 --- a/tests/unittests/test_mock_eventhub_functions.py +++ /dev/null @@ -1,155 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json - -from tests.utils import testutils - -from azure_functions_worker import protos - - -class TestEventHubMockFunctions(testutils.AsyncTestCase): - mock_funcs_dir = testutils.UNIT_TESTS_FOLDER / 'eventhub_mock_functions' - - async def test_mock_eventhub_trigger_iot(self): - async with testutils.start_mockhost( - script_root=self.mock_funcs_dir) as host: - - await host.init_worker("4.17.1") - func_id, r = await host.load_function('eventhub_trigger_iot') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - async def call_and_check(): - _, r = await host.invoke_function( - 'eventhub_trigger_iot', - [ - protos.ParameterBinding( - name='event', - data=protos.TypedData( - json=json.dumps({ - 'id': 'foo' - }) - ), - ), - ], - metadata={ - 'SystemProperties': protos.TypedData(json=json.dumps({ - 'iothub-device-id': 'mock-iothub-device-id', - 'iothub-auth-data': 'mock-iothub-auth-data', - 'EnqueuedTimeUtc': '2020-02-18T21:28:42.5888539Z' - })) - } - ) - - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - res_json_string = r.response.return_value.string - self.assertIn('device-id', res_json_string) - self.assertIn('mock-iothub-device-id', res_json_string) - self.assertIn('auth-data', res_json_string) - self.assertIn('mock-iothub-auth-data', res_json_string) - - await call_and_check() - - async def test_mock_eventhub_cardinality_one(self): - async with testutils.start_mockhost( - script_root=self.mock_funcs_dir) as host: - - await host.init_worker("4.17.1") - func_id, r = await host.load_function('eventhub_cardinality_one') - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - _, r = await host.invoke_function( - 'eventhub_cardinality_one', - [ - protos.ParameterBinding( - name='event', - data=protos.TypedData( - json=json.dumps({ - 'id': 'cardinality_one' - }) - ), - ), - ], - metadata={ - 'SystemProperties': protos.TypedData(json=json.dumps({ - 'iothub-device-id': 'mock-iothub-device-id', - 'iothub-auth-data': 'mock-iothub-auth-data', - 'EnqueuedTimeUtc': '2020-02-18T21:28:42.5888539Z' - })) - } - ) - - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - self.assertEqual(r.response.return_value.string, 'OK_ONE') - - async def test_mock_eventhub_cardinality_one_bad_annotation(self): - async with testutils.start_mockhost( - script_root=self.mock_funcs_dir) as host: - - await host.init_worker("4.17.1") - # This suppose to fail since the event should not be int - func_id, r = await host.load_function( - 'eventhub_cardinality_one_bad_anno' - ) - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - async def test_mock_eventhub_cardinality_many(self): - async with testutils.start_mockhost( - script_root=self.mock_funcs_dir) as host: - - await host.init_worker("4.17.1") - - func_id, r = await host.load_function('eventhub_cardinality_many') - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - _, r = await host.invoke_function( - 'eventhub_cardinality_many', - [ - protos.ParameterBinding( - name='events', - data=protos.TypedData( - json=json.dumps([{ - 'id': 'cardinality_many' - }]) - ), - ), - ], - metadata={ - 'SystemPropertiesArray': protos.TypedData(json=json.dumps([ - { - 'iothub-device-id': 'mock-iothub-device-id', - 'iothub-auth-data': 'mock-iothub-auth-data', - 'EnqueuedTimeUtc': '2020-02-18T21:28:42.5888539Z' - } - ])) - } - ) - - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - self.assertEqual(r.response.return_value.string, 'OK_MANY') - - async def test_mock_eventhub_cardinality_many_bad_annotation(self): - async with testutils.start_mockhost( - script_root=self.mock_funcs_dir) as host: - - # This suppose to fail since the event should not be List[str] - await host.init_worker("4.17.1") - - func_id, r = await host.load_function( - 'eventhub_cardinality_many_bad_anno' - ) - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) diff --git a/tests/unittests/test_mock_generic_functions.py b/tests/unittests/test_mock_generic_functions.py deleted file mode 100644 index 5ae199bbf..000000000 --- a/tests/unittests/test_mock_generic_functions.py +++ /dev/null @@ -1,390 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from tests.utils import testutils - -from azure_functions_worker import protos - - -class TestGenericFunctions(testutils.AsyncTestCase): - generic_funcs_dir = testutils.UNIT_TESTS_FOLDER / 'generic_functions' - - async def test_mock_generic_as_str(self): - async with testutils.start_mockhost( - script_root=self.generic_funcs_dir) as host: - - await host.init_worker("4.17.1") - func_id, r = await host.load_function('foobar_as_str') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - _, r = await host.invoke_function( - 'foobar_as_str', [ - protos.ParameterBinding( - name='input', - data=protos.TypedData( - string='test' - ) - ) - ] - ) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - self.assertEqual( - r.response.return_value, - protos.TypedData(string='test') - ) - - async def test_mock_generic_as_bytes(self): - async with testutils.start_mockhost( - script_root=self.generic_funcs_dir) as host: - - await host.init_worker("4.17.1") - func_id, r = await host.load_function('foobar_as_bytes') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - _, r = await host.invoke_function( - 'foobar_as_bytes', [ - protos.ParameterBinding( - name='input', - data=protos.TypedData( - bytes=b'\x00\x01' - ) - ) - ] - ) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - self.assertEqual( - r.response.return_value, - protos.TypedData(bytes=b'\x00\x01') - ) - - async def test_mock_generic_as_str_no_anno(self): - async with testutils.start_mockhost( - script_root=self.generic_funcs_dir) as host: - - await host.init_worker("4.17.1") - func_id, r = await host.load_function('foobar_as_str_no_anno') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - _, r = await host.invoke_function( - 'foobar_as_str_no_anno', [ - protos.ParameterBinding( - name='input', - data=protos.TypedData( - string='test' - ) - ) - ] - ) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - self.assertEqual( - r.response.return_value, - protos.TypedData(string='test') - ) - - async def test_mock_generic_as_bytes_no_anno(self): - async with testutils.start_mockhost( - script_root=self.generic_funcs_dir) as host: - - await host.init_worker("4.17.1") - func_id, r = await host.load_function('foobar_as_bytes_no_anno') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - _, r = await host.invoke_function( - 'foobar_as_bytes_no_anno', [ - protos.ParameterBinding( - name='input', - data=protos.TypedData( - bytes=b'\x00\x01' - ) - ) - ] - ) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - self.assertEqual( - r.response.return_value, - protos.TypedData(bytes=b'\x00\x01') - ) - - async def test_mock_generic_should_support_implicit_output(self): - async with testutils.start_mockhost( - script_root=self.generic_funcs_dir) as host: - - await host.init_worker("4.17.1") - func_id, r = await host.load_function('foobar_implicit_output') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - _, r = await host.invoke_function( - 'foobar_implicit_output', [ - protos.ParameterBinding( - name='input', - data=protos.TypedData( - bytes=b'\x00\x01' - ) - ) - ] - ) - # It passes now as we are enabling generic binding to return output - # implicitly - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - self.assertEqual( - r.response.return_value, - protos.TypedData(bytes=b'\x00\x01')) - - async def test_mock_generic_should_support_without_datatype(self): - async with testutils.start_mockhost( - script_root=self.generic_funcs_dir) as host: - - await host.init_worker("4.17.1") - func_id, r = await host.load_function('foobar_with_no_datatype') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - _, r = await host.invoke_function( - 'foobar_with_no_datatype', [ - protos.ParameterBinding( - name='input', - data=protos.TypedData( - bytes=b'\x00\x01' - ) - ) - ] - ) - # It passes now as we are enabling generic binding to return output - # implicitly - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - self.assertEqual( - r.response.return_value, - protos.TypedData(bytes=b'\x00\x01')) - - async def test_mock_generic_implicit_output_exemption(self): - async with testutils.start_mockhost( - script_root=self.generic_funcs_dir) as host: - await host.init_worker("4.17.1") - func_id, r = await host.load_function( - 'foobar_implicit_output_exemption') - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - _, r = await host.invoke_function( - 'foobar_implicit_output_exemption', [ - protos.ParameterBinding( - name='input', - data=protos.TypedData( - bytes=b'\x00\x01' - ) - ) - ] - ) - # It should fail here, since implicit output is False - # For the Durable Functions durableClient case - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - async def test_mock_generic_as_nil_data(self): - async with testutils.start_mockhost( - script_root=self.generic_funcs_dir) as host: - - await host.init_worker("4.17.1") - func_id, r = await host.load_function('foobar_nil_data') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - _, r = await host.invoke_function( - 'foobar_nil_data', [ - protos.ParameterBinding( - name='input', - data=protos.TypedData() - ) - ] - ) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - self.assertEqual( - r.response.return_value, - protos.TypedData()) - - async def test_mock_generic_as_none(self): - async with testutils.start_mockhost( - script_root=self.generic_funcs_dir) as host: - - await host.init_worker("4.17.1") - func_id, r = await host.load_function('foobar_as_none') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - _, r = await host.invoke_function( - 'foobar_as_none', [ - ] - ) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - self.assertEqual( - r.response.return_value, - protos.TypedData(string="hello")) - - async def test_mock_generic_return_dict(self): - async with testutils.start_mockhost( - script_root=self.generic_funcs_dir) as host: - - await host.init_worker("4.17.1") - func_id, r = await host.load_function('foobar_return_dict') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - _, r = await host.invoke_function( - 'foobar_return_dict', [ - protos.ParameterBinding( - name='input', - data=protos.TypedData( - string='test' - ) - ) - ] - ) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - self.assertEqual( - r.response.return_value, - protos.TypedData(json="{\"hello\": \"world\"}") - ) - - async def test_mock_generic_return_list(self): - async with testutils.start_mockhost( - script_root=self.generic_funcs_dir) as host: - - await host.init_worker("4.17.1") - func_id, r = await host.load_function('foobar_return_list') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - _, r = await host.invoke_function( - 'foobar_return_list', [ - protos.ParameterBinding( - name='input', - data=protos.TypedData( - string='test' - ) - ) - ] - ) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - self.assertEqual( - r.response.return_value, - protos.TypedData(json="[1, 2, 3]") - ) - - async def test_mock_generic_return_int(self): - async with testutils.start_mockhost( - script_root=self.generic_funcs_dir) as host: - - await host.init_worker("4.17.1") - func_id, r = await host.load_function('foobar_return_int') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - _, r = await host.invoke_function( - 'foobar_return_int', [ - protos.ParameterBinding( - name='input', - data=protos.TypedData( - string='test' - ) - ) - ] - ) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - self.assertEqual( - r.response.return_value, - protos.TypedData(int=12) - ) - - async def test_mock_generic_return_double(self): - async with testutils.start_mockhost( - script_root=self.generic_funcs_dir) as host: - - await host.init_worker("4.17.1") - func_id, r = await host.load_function('foobar_return_double') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - _, r = await host.invoke_function( - 'foobar_return_double', [ - protos.ParameterBinding( - name='input', - data=protos.TypedData( - string='test' - ) - ) - ] - ) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - self.assertEqual( - r.response.return_value, - protos.TypedData(double=12.34) - ) - - async def test_mock_generic_return_bool(self): - async with testutils.start_mockhost( - script_root=self.generic_funcs_dir) as host: - - await host.init_worker("4.17.1") - func_id, r = await host.load_function('foobar_return_bool') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - _, r = await host.invoke_function( - 'foobar_return_bool', [ - protos.ParameterBinding( - name='input', - data=protos.TypedData( - string='test' - ) - ) - ] - ) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - self.assertEqual( - r.response.return_value, - protos.TypedData(int=1) - ) diff --git a/tests/unittests/test_mock_http_functions.py b/tests/unittests/test_mock_http_functions.py deleted file mode 100644 index 849134038..000000000 --- a/tests/unittests/test_mock_http_functions.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from tests.utils import testutils - -from azure_functions_worker import protos - - -class TestMockHost(testutils.AsyncTestCase): - - async def test_call_sync_function_check_logs(self): - async with testutils.start_mockhost() as host: - - await host.init_worker("4.17.1") - await host.load_function('sync_logging') - - invoke_id, r = await host.invoke_function( - 'sync_logging', [ - protos.ParameterBinding( - name='req', - data=protos.TypedData( - http=protos.RpcHttp( - method='GET'))) - ]) - - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - user_logs = [line for line in r.logs - if line.category == 'my function'] - # 2 log statements added (critical and error) in sync_logging - self.assertEqual(len(user_logs), 2) - - log = user_logs[0] - self.assertEqual(log.invocation_id, invoke_id) - self.assertTrue(log.message.startswith( - 'a gracefully handled error')) - - self.assertEqual(r.response.return_value.string, 'OK-sync') - - async def test_call_async_function_check_logs(self): - async with testutils.start_mockhost() as host: - - await host.init_worker("4.17.1") - await host.load_function('async_logging') - - invoke_id, r = await host.invoke_function( - 'async_logging', [ - protos.ParameterBinding( - name='req', - data=protos.TypedData( - http=protos.RpcHttp( - method='GET'))) - ]) - - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - user_logs = [line for line in r.logs if - line.category == 'my function'] - self.assertEqual(len(user_logs), 2) - - first_msg = user_logs[0] - self.assertEqual(first_msg.invocation_id, invoke_id) - self.assertEqual(first_msg.message, 'hello info') - self.assertEqual(first_msg.level, protos.RpcLog.Information) - - second_msg = user_logs[1] - self.assertEqual(second_msg.invocation_id, invoke_id) - self.assertTrue(second_msg.message.startswith('and another error')) - self.assertEqual(second_msg.level, protos.RpcLog.Error) - - self.assertEqual(r.response.return_value.string, 'OK-async') - - async def test_handles_unsupported_messages_gracefully(self): - async with testutils.start_mockhost() as host: - # Intentionally send a message to worker that isn't - # going to be ever supported by it. The idea is that - # workers should survive such messages and continue - # their operation. If anything, the host can always - # terminate the worker. - await host.send( - protos.StreamingMessage( - worker_heartbeat=protos.WorkerHeartbeat())) - - await host.init_worker("4.17.1") - _, r = await host.load_function('return_out') - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) diff --git a/tests/unittests/test_mock_log_filtering_functions.py b/tests/unittests/test_mock_log_filtering_functions.py deleted file mode 100644 index 022499502..000000000 --- a/tests/unittests/test_mock_log_filtering_functions.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from unittest.mock import call, patch - -from tests.utils import testutils - -from azure_functions_worker import protos -from azure_functions_worker.logging import is_system_log_category - - -class TestMockLogFilteringFunctions(testutils.AsyncTestCase): - dir = testutils.UNIT_TESTS_FOLDER / 'log_filtering_functions' - - async def test_root_logger_should_be_customer_log(self): - """When customer use the root logger to send logs, the 'root' namespace - should be treated as customer log, only sending to our customers. - """ - with patch( - 'azure_functions_worker.dispatcher.is_system_log_category' - ) as islc_mock: - async with testutils.start_mockhost(script_root=self.dir) as host: - await host.init_worker("4.17.1") - await host.load_function('debug_logging') - await self._invoke_function(host, 'debug_logging') - - self.assertIn(call('root'), islc_mock.call_args_list) - self.assertFalse(is_system_log_category('root')) - - async def test_customer_logging_should_not_be_system_log(self): - """When sdk uses the 'azure' logger to send logs - (e.g. 'azure.servicebus'), the namespace should be treated as customer - log, only sends to our customers. - """ - with patch( - 'azure_functions_worker.dispatcher.is_system_log_category' - ) as islc_mock: - async with testutils.start_mockhost(script_root=self.dir) as host: - await host.init_worker("4.17.1") - await host.load_function('debug_user_logging') - await self._invoke_function(host, 'debug_user_logging') - - self.assertIn(call('my function'), islc_mock.call_args_list) - self.assertFalse(is_system_log_category('my function')) - - async def test_sdk_logger_should_be_system_log(self): - """When sdk uses the 'azure.functions' logger to send logs, the - namespace should be treated as system log, sending to our customers and - our kusto table. - """ - with patch( - 'azure_functions_worker.dispatcher.is_system_log_category' - ) as islc_mock: - async with testutils.start_mockhost(script_root=self.dir) as host: - await host.init_worker("4.17.1") - await host.load_function('sdk_logging') - await self._invoke_function(host, 'sdk_logging') - - self.assertIn( - call('azure.functions'), islc_mock.call_args_list - ) - self.assertTrue(is_system_log_category('azure.functions')) - - async def test_sdk_submodule_logger_should_be_system_log(self): - """When sdk uses the 'azure.functions.submodule' logger to send logs, - the namespace should be treated as system log, sending to our customers - and our kusto table. - """ - with patch( - 'azure_functions_worker.dispatcher.is_system_log_category' - ) as islc_mock: - async with testutils.start_mockhost(script_root=self.dir) as host: - await host.init_worker("4.17.1") - await host.load_function('sdk_submodule_logging') - await self._invoke_function(host, 'sdk_submodule_logging') - - self.assertIn( - call('azure.functions.submodule'), islc_mock.call_args_list - ) - self.assertTrue( - is_system_log_category('azure.functions.submodule') - ) - - async def _invoke_function(self, - host: testutils._MockWebHost, - function_name: str): - _, r = await host.invoke_function( - function_name, [ - protos.ParameterBinding( - name='req', - data=protos.TypedData( - http=protos.RpcHttp(method='GET') - ) - ) - ] - ) - - self.assertEqual(r.response.result.status, protos.StatusResult.Success) diff --git a/tests/unittests/test_mock_timer_functions.py b/tests/unittests/test_mock_timer_functions.py deleted file mode 100644 index d4f11e644..000000000 --- a/tests/unittests/test_mock_timer_functions.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import json - -from tests.utils import testutils - -from azure_functions_worker import protos - - -class TestTimerFunctions(testutils.AsyncTestCase): - timer_funcs_dir = testutils.UNIT_TESTS_FOLDER / 'timer_functions' - - async def test_mock_timer__return_pastdue(self): - async with testutils.start_mockhost( - script_root=self.timer_funcs_dir) as host: - - await host.init_worker("4.17.1") - func_id, r = await host.load_function('return_pastdue') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - async def call_and_check(due: bool): - _, r = await host.invoke_function( - 'return_pastdue', [ - protos.ParameterBinding( - name='timer', - data=protos.TypedData( - json=json.dumps({ - 'IsPastDue': due - }))) - ]) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - self.assertEqual( - list(r.response.output_data), [ - protos.ParameterBinding( - name='pastdue', - data=protos.TypedData(string=str(due))) - ]) - - await call_and_check(True) - await call_and_check(False) - - async def test_mock_timer__user_event_loop(self): - async with testutils.start_mockhost( - script_root=self.timer_funcs_dir) as host: - await host.init_worker("4.17.1") - func_id, r = await host.load_function('user_event_loop_timer') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - async def call_and_check(): - _, r = await host.invoke_function( - 'user_event_loop_timer', [ - protos.ParameterBinding( - name='timer', - data=protos.TypedData( - json=json.dumps({ - 'IsPastDue': False - }))) - ]) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - await call_and_check() diff --git a/tests/unittests/test_nullable_converters.py b/tests/unittests/test_nullable_converters.py deleted file mode 100644 index 913c80fb7..000000000 --- a/tests/unittests/test_nullable_converters.py +++ /dev/null @@ -1,110 +0,0 @@ -import datetime -import unittest - -import pytest -from google.protobuf.timestamp_pb2 import Timestamp - -from azure_functions_worker import protos -from azure_functions_worker.bindings.nullable_converters import ( - to_nullable_bool, - to_nullable_double, - to_nullable_string, - to_nullable_timestamp, -) - -try: - from http.cookies import SimpleCookie -except ImportError: - from Cookie import SimpleCookie - -headers = ['foo=bar; Path=/some/path; Secure', - 'foo2=42; Domain=123; Expires=Thu, 12-Jan-2017 13:55:08 GMT; ' - 'Path=/; Max-Age=dd;'] - -cookies = SimpleCookie('\r\n'.join(headers)) - - -class TestNullableConverters(unittest.TestCase): - def test_to_nullable_string_none(self): - self.assertEqual(to_nullable_string(None, "name"), None) - - def test_to_nullable_string_valid(self): - self.assertEqual(to_nullable_string("dummy", "name"), - protos.NullableString(value="dummy")) - - def test_to_nullable_string_wrong_type(self): - with pytest.raises(Exception) as e: - self.assertEqual(to_nullable_string(123, "name"), - protos.NullableString(value="dummy")) - self.assertEqual(type(e), TypeError) - - def test_to_nullable_bool_none(self): - self.assertEqual(to_nullable_bool(None, "name"), None) - - def test_to_nullable_bool_valid(self): - self.assertEqual(to_nullable_bool(True, "name"), - protos.NullableBool(value=True)) - - def test_to_nullable_bool_wrong_type(self): - with pytest.raises(Exception) as e: - to_nullable_bool("True", "name") - - self.assertEqual(e.type, TypeError) - self.assertEqual(e.value.args[0], - "A 'bool' type was expected instead of a '' type. " - "Cannot parse value True of 'name'.") - - def test_to_nullable_double_str(self): - self.assertEqual(to_nullable_double("12", "name"), - protos.NullableDouble(value=12)) - - def test_to_nullable_double_empty_str(self): - self.assertEqual(to_nullable_double("", "name"), None) - - def test_to_nullable_double_invalid_str(self): - with pytest.raises(TypeError) as e: - to_nullable_double("222d", "name") - - self.assertEqual(e.type, TypeError) - self.assertEqual(e.value.args[0], - "Cannot parse value 222d of 'name' to float.") - - def test_to_nullable_double_int(self): - self.assertEqual(to_nullable_double(12, "name"), - protos.NullableDouble(value=12)) - - def test_to_nullable_double_float(self): - self.assertEqual(to_nullable_double(12.0, "name"), - protos.NullableDouble(value=12)) - - def test_to_nullable_double_none(self): - self.assertEqual(to_nullable_double(None, "name"), None) - - def test_to_nullable_double_wrong_type(self): - with pytest.raises(Exception) as e: - to_nullable_double(object(), "name") - - self.assertIn( - "A 'int' or 'float' type was expected instead of a '' type", - e.value.args[0]) - self.assertEqual(e.type, TypeError) - - def test_to_nullable_timestamp_int(self): - self.assertEqual(to_nullable_timestamp(1000, "datetime"), - protos.NullableTimestamp( - value=Timestamp(seconds=int(1000)))) - - def test_to_nullable_timestamp_datetime(self): - now = datetime.datetime.now() - self.assertEqual(to_nullable_timestamp(now, "datetime"), - protos.NullableTimestamp( - value=Timestamp(seconds=int(now.timestamp())))) - - def test_to_nullable_timestamp_wrong_type(self): - with self.assertRaises(TypeError): - to_nullable_timestamp("now", "datetime") - - def test_to_nullable_timestamp_none(self): - self.assertEqual(to_nullable_timestamp(None, "timestamp"), None) diff --git a/tests/unittests/test_opentelemetry.py b/tests/unittests/test_opentelemetry.py deleted file mode 100644 index b26334bdf..000000000 --- a/tests/unittests/test_opentelemetry.py +++ /dev/null @@ -1,110 +0,0 @@ -import asyncio -import os -import unittest -from unittest.mock import MagicMock, patch - -from tests.unittests.test_dispatcher import FUNCTION_APP_DIRECTORY -from tests.utils import testutils - -from azure_functions_worker import protos - - -class TestOpenTelemetry(unittest.TestCase): - - def setUp(self): - self.loop = asyncio.new_event_loop() - asyncio.set_event_loop(self.loop) - self.dispatcher = testutils.create_dummy_dispatcher() - - def tearDown(self): - self.loop.close() - - def test_update_opentelemetry_status_import_error(self): - # Patch the built-in import mechanism - with patch('builtins.__import__', side_effect=ImportError): - self.dispatcher.update_opentelemetry_status() - # Verify that otel_libs_available is set to False due to ImportError - self.assertFalse(self.dispatcher._azure_monitor_available) - - @patch('builtins.__import__') - def test_update_opentelemetry_status_success( - self, mock_imports): - mock_imports.return_value = MagicMock() - self.dispatcher.update_opentelemetry_status() - self.assertIsNotNone(self.dispatcher._context_api) - self.assertIsNotNone(self.dispatcher._trace_context_propagator) - - @patch('builtins.__import__') - @patch("azure_functions_worker.dispatcher.Dispatcher.update_opentelemetry_status") - def test_initialize_azure_monitor_success( - self, - mock_update_ot, - mock_imports, - ): - mock_imports.return_value = MagicMock() - self.dispatcher.initialize_azure_monitor() - mock_update_ot.assert_called_once() - self.assertTrue(self.dispatcher._azure_monitor_available) - - @patch("azure_functions_worker.dispatcher.Dispatcher.update_opentelemetry_status") - def test_initialize_azure_monitor_import_error( - self, - mock_update_ot, - ): - with patch('builtins.__import__', side_effect=ImportError): - self.dispatcher.initialize_azure_monitor() - mock_update_ot.assert_called_once() - # Verify that otel_libs_available is set to False due to ImportError - self.assertFalse(self.dispatcher._azure_monitor_available) - - @patch.dict(os.environ, {'PYTHON_ENABLE_OPENTELEMETRY': 'true'}) - @patch('builtins.__import__') - def test_init_request_otel_capability_enabled_app_setting( - self, - mock_imports, - ): - mock_imports.return_value = MagicMock() - - init_request = protos.StreamingMessage( - worker_init_request=protos.WorkerInitRequest( - host_version="2.3.4", - function_app_directory=str(FUNCTION_APP_DIRECTORY) - ) - ) - - init_response = self.loop.run_until_complete( - self.dispatcher._handle__worker_init_request(init_request)) - - self.assertEqual(init_response.worker_init_response.result.status, - protos.StatusResult.Success) - - # Verify that WorkerOpenTelemetryEnabled capability is set to _TRUE - capabilities = init_response.worker_init_response.capabilities - self.assertIn("WorkerOpenTelemetryEnabled", capabilities) - self.assertEqual(capabilities["WorkerOpenTelemetryEnabled"], "true") - - @patch("azure_functions_worker.dispatcher.Dispatcher.initialize_azure_monitor") - def test_init_request_otel_capability_disabled_app_setting( - self, - mock_initialize_azmon, - ): - - init_request = protos.StreamingMessage( - worker_init_request=protos.WorkerInitRequest( - host_version="2.3.4", - function_app_directory=str(FUNCTION_APP_DIRECTORY) - ) - ) - - init_response = self.loop.run_until_complete( - self.dispatcher._handle__worker_init_request(init_request)) - - self.assertEqual(init_response.worker_init_response.result.status, - protos.StatusResult.Success) - - # Azure monitor initialized not called - mock_initialize_azmon.assert_not_called() - - # Verify that WorkerOpenTelemetryEnabled capability is not set - capabilities = init_response.worker_init_response.capabilities - self.assertNotIn("WorkerOpenTelemetryEnabled", capabilities) diff --git a/tests/unittests/test_rpc_messages.py b/tests/unittests/test_rpc_messages.py deleted file mode 100644 index 4e3a6c23d..000000000 --- a/tests/unittests/test_rpc_messages.py +++ /dev/null @@ -1,147 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import os -import subprocess -import sys -import tempfile -import typing -import unittest - -from tests.utils import testutils - -from azure_functions_worker import protos -from azure_functions_worker.utils.common import is_python_version - - -class TestGRPC(testutils.AsyncTestCase): - pre_test_env = os.environ.copy() - pre_test_cwd = os.getcwd() - - def _reset_environ(self): - for key, value in self.pre_test_env.items(): - os.environ[key] = value - os.chdir(self.pre_test_cwd) - - async def _verify_environment_reloaded( - self, - test_env: typing.Dict[str, str] = {}, - test_cwd: str = os.getcwd()): - request = protos.FunctionEnvironmentReloadRequest( - environment_variables=test_env, - function_app_directory=test_cwd) - - request_msg = protos.StreamingMessage( - request_id='0', - function_environment_reload_request=request) - - disp = testutils.create_dummy_dispatcher() - - try: - r = await disp._handle__function_environment_reload_request( - request_msg) - status = r.function_environment_reload_response.result.status - exp = r.function_environment_reload_response.result.exception - self.assertEqual(status, protos.StatusResult.Success, - f"Exception in Reload request: {exp}") - - environ_dict = os.environ.copy() - self.assertDictEqual(environ_dict, test_env) - self.assertEqual(os.getcwd(), test_cwd) - - finally: - self._reset_environ() - - async def test_multiple_env_vars_load(self): - test_env = {'TEST_KEY': 'foo', 'HELLO': 'world'} - await self._verify_environment_reloaded(test_env=test_env) - - async def test_empty_env_vars_load(self): - test_env = {} - await self._verify_environment_reloaded(test_env=test_env) - - @unittest.skipIf(sys.platform == 'darwin', - 'MacOS creates the processes specific var folder in ' - '/private filesystem and not in /var like in linux ' - 'systems.') - async def test_changing_current_working_directory(self): - test_cwd = tempfile.gettempdir() - await self._verify_environment_reloaded(test_cwd=test_cwd) - - @unittest.skipIf(sys.platform == 'darwin', - 'MacOS creates the processes specific var folder in ' - '/private filesystem and not in /var like in linux ' - 'systems.') - async def test_reload_env_message(self): - test_env = {'TEST_KEY': 'foo', 'HELLO': 'world'} - test_cwd = tempfile.gettempdir() - await self._verify_environment_reloaded(test_env, test_cwd) - - def _verify_sys_path_import(self, result, expected_output): - path_import_script = os.path.join(testutils.UNIT_TESTS_ROOT, - 'path_import', 'test_path_import.sh') - try: - subprocess.run(['chmod +x ' + path_import_script], shell=True) - - exported_path = ":".join(sys.path) - output = subprocess.check_output( - [path_import_script, result, exported_path], - stderr=subprocess.STDOUT) - decoded_output = output.decode(sys.stdout.encoding).strip() - self.assertTrue(expected_output in decoded_output) - finally: - subprocess.run(['chmod -x ' + path_import_script], shell=True) - self._reset_environ() - - @unittest.skipIf(sys.platform == 'win32', - 'Linux .sh script only works on Linux') - def test_failed_sys_path_import(self): - self._verify_sys_path_import( - 'fail', - "No module named 'test_module'") - - @unittest.skipIf(sys.platform == 'win32', - 'Linux .sh script only works on Linux') - def test_successful_sys_path_import(self): - self._verify_sys_path_import( - 'success', - 'This module was imported!') - - def _verify_azure_namespace_import(self, result, expected_output): - print(os.getcwd()) - path_import_script = os.path.join(testutils.UNIT_TESTS_ROOT, - 'azure_namespace_import', - 'test_azure_namespace_import.sh') - try: - subprocess.run(['chmod +x ' + path_import_script], shell=True) - - output = subprocess.check_output( - [path_import_script, result], - stderr=subprocess.STDOUT) - decoded_output = output.decode(sys.stdout.encoding).strip() - self.assertTrue(expected_output in decoded_output, - f"Decoded Output: {decoded_output}") # DNM - finally: - subprocess.run(['chmod -x ' + path_import_script], shell=True) - self._reset_environ() - - @unittest.skipIf(sys.platform == 'win32', - 'Linux .sh script only works on Linux') - @unittest.skip("TODO: fix this tests. Failing with ImportError.") - def test_failed_azure_namespace_import(self): - self._verify_azure_namespace_import( - 'false', - 'module_b fails to import') - - @unittest.skipIf(sys.platform == 'win32', - 'Linux .sh script only works on Linux') - @unittest.skipIf( - is_python_version('3.10'), - 'In Python 3.10, isolate worker dependencies is turned on by default.' - ' Reloading all customer dependencies on specialization is a must.' - ' This partially reloading namespace feature is no longer needed.' - ) - @unittest.skip("TODO: fix this tests. Failing with ImportError.") - def test_successful_azure_namespace_import(self): - self._verify_azure_namespace_import( - 'true', - 'module_b is imported') diff --git a/tests/unittests/test_script_file_name.py b/tests/unittests/test_script_file_name.py deleted file mode 100644 index 24327249a..000000000 --- a/tests/unittests/test_script_file_name.py +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import os - -from tests.utils import testutils - -from azure_functions_worker.constants import ( - PYTHON_SCRIPT_FILE_NAME, - PYTHON_SCRIPT_FILE_NAME_DEFAULT, -) - -DEFAULT_SCRIPT_FILE_NAME_DIR = testutils.UNIT_TESTS_FOLDER / \ - 'file_name_functions' / \ - 'default_file_name' - -NEW_SCRIPT_FILE_NAME_DIR = testutils.UNIT_TESTS_FOLDER / \ - 'file_name_functions' / \ - 'new_file_name' - -INVALID_SCRIPT_FILE_NAME_DIR = testutils.UNIT_TESTS_FOLDER / \ - 'file_name_functions' / \ - 'invalid_file_name' - - -class TestDefaultScriptFileName(testutils.WebHostTestCase): - """ - Tests for default file name - """ - - @classmethod - def setUpClass(cls): - os.environ["PYTHON_SCRIPT_FILE_NAME"] = "function_app.py" - super().setUpClass() - - @classmethod - def tearDownClass(cls): - # Remove the PYTHON_SCRIPT_FILE_NAME environment variable - os.environ.pop('PYTHON_SCRIPT_FILE_NAME') - super().tearDownClass() - - @classmethod - def get_script_dir(cls): - return DEFAULT_SCRIPT_FILE_NAME_DIR - - def test_default_file_name(self): - """ - Test the default file name - """ - self.assertIsNotNone(os.environ.get(PYTHON_SCRIPT_FILE_NAME)) - self.assertEqual(os.environ.get(PYTHON_SCRIPT_FILE_NAME), - PYTHON_SCRIPT_FILE_NAME_DEFAULT) - - -class TestNewScriptFileName(testutils.WebHostTestCase): - """ - Tests for changed file name - """ - - @classmethod - def setUpClass(cls): - os.environ["PYTHON_SCRIPT_FILE_NAME"] = "test.py" - super().setUpClass() - - @classmethod - def tearDownClass(cls): - # Remove the PYTHON_SCRIPT_FILE_NAME environment variable - os.environ.pop('PYTHON_SCRIPT_FILE_NAME') - super().tearDownClass() - - @classmethod - def get_script_dir(cls): - return NEW_SCRIPT_FILE_NAME_DIR - - def test_new_file_name(self): - """ - Test the new file name - """ - self.assertIsNotNone(os.environ.get(PYTHON_SCRIPT_FILE_NAME)) - self.assertEqual(os.environ.get(PYTHON_SCRIPT_FILE_NAME), - 'test.py') - - -class TestInvalidScriptFileName(testutils.WebHostTestCase): - """ - Tests for invalid file name - """ - - @classmethod - def setUpClass(cls): - os.environ["PYTHON_SCRIPT_FILE_NAME"] = "main" - super().setUpClass() - - @classmethod - def tearDownClass(cls): - # Remove the PYTHON_SCRIPT_FILE_NAME environment variable - os.environ.pop('PYTHON_SCRIPT_FILE_NAME') - super().tearDownClass() - - @classmethod - def get_script_dir(cls): - return INVALID_SCRIPT_FILE_NAME_DIR - - def test_invalid_file_name(self): - """ - Test the invalid file name - """ - self.assertIsNotNone(os.environ.get(PYTHON_SCRIPT_FILE_NAME)) - self.assertEqual(os.environ.get(PYTHON_SCRIPT_FILE_NAME), - 'main') diff --git a/tests/unittests/test_shared_memory_manager.py b/tests/unittests/test_shared_memory_manager.py deleted file mode 100644 index ca3cb6088..000000000 --- a/tests/unittests/test_shared_memory_manager.py +++ /dev/null @@ -1,394 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import json -import math -import os -import sys -from unittest import skipIf -from unittest.mock import patch - -from azure.functions import meta as bind_meta -from tests.utils import testutils - -from azure_functions_worker.bindings.shared_memory_data_transfer import ( - SharedMemoryConstants as consts, -) -from azure_functions_worker.bindings.shared_memory_data_transfer import ( - SharedMemoryManager, -) -from azure_functions_worker.constants import ( - FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED, -) -from azure_functions_worker.utils.common import is_envvar_true - - -@skipIf(sys.platform == 'darwin', 'MacOS M1 machines do not correctly test the' - 'shared memory filesystems and thus skipping' - ' these tests for the time being') -class TestSharedMemoryManager(testutils.SharedMemoryTestCase): - """ - Tests for SharedMemoryManager. - """ - def setUp(self): - env = os.environ.copy() - env['FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED'] = "true" - self.mock_environ = patch.dict('os.environ', env) - self.mock_sys_module = patch.dict('sys.modules', sys.modules.copy()) - self.mock_sys_path = patch('sys.path', sys.path.copy()) - self.mock_environ.start() - self.mock_sys_module.start() - self.mock_sys_path.start() - - def tearDown(self): - self.mock_sys_path.stop() - self.mock_sys_module.stop() - self.mock_environ.stop() - - def test_is_enabled(self): - """ - Verify that when the AppSetting is enabled, SharedMemoryManager is - enabled. - """ - - # Make sure shared memory data transfer is enabled - was_shmem_env_true = is_envvar_true( - FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED) - os.environ.update( - {FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED: '1'}) - manager = SharedMemoryManager() - self.assertTrue(manager.is_enabled()) - # Restore the env variable to original value - if not was_shmem_env_true: - os.environ.update( - {FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED: '0'}) - - def test_is_disabled(self): - """ - Verify that when the AppSetting is disabled, SharedMemoryManager is - disabled. - """ - # Make sure shared memory data transfer is disabled - was_shmem_env_true = is_envvar_true( - FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED) - os.environ.update( - {FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED: '0'}) - manager = SharedMemoryManager() - self.assertFalse(manager.is_enabled()) - # Restore the env variable to original value - if was_shmem_env_true: - os.environ.update( - {FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED: '1'}) - - def test_bytes_input_support(self): - """ - Verify that the given input is supported by SharedMemoryManager to be - transfered over shared memory. - The input is bytes. - """ - manager = SharedMemoryManager() - content_size = consts.MIN_BYTES_FOR_SHARED_MEM_TRANSFER + 10 - content = self.get_random_bytes(content_size) - bytes_datum = bind_meta.Datum(type='bytes', value=content) - is_supported = manager.is_supported(bytes_datum) - self.assertTrue(is_supported) - - def test_string_input_support(self): - """ - Verify that the given input is supported by SharedMemoryManager to be - transfered over shared memory. - The input is string. - """ - manager = SharedMemoryManager() - content_size = consts.MIN_BYTES_FOR_SHARED_MEM_TRANSFER + 10 - num_chars = math.floor(content_size / consts.SIZE_OF_CHAR_BYTES) - content = self.get_random_string(num_chars) - bytes_datum = bind_meta.Datum(type='string', value=content) - is_supported = manager.is_supported(bytes_datum) - self.assertTrue(is_supported) - - def test_int_input_unsupported(self): - """ - Verify that the given input is unsupported by SharedMemoryManager. - This input is int. - """ - manager = SharedMemoryManager() - datum = bind_meta.Datum(type='int', value=1) - is_supported = manager.is_supported(datum) - self.assertFalse(is_supported) - - def test_double_input_unsupported(self): - """ - Verify that the given input is unsupported by SharedMemoryManager. - This input is double. - """ - manager = SharedMemoryManager() - datum = bind_meta.Datum(type='double', value=1.0) - is_supported = manager.is_supported(datum) - self.assertFalse(is_supported) - - def test_json_input_unsupported(self): - """ - Verify that the given input is unsupported by SharedMemoryManager. - This input is json. - """ - manager = SharedMemoryManager() - content = { - 'name': 'foo', - 'val': 'bar' - } - datum = bind_meta.Datum(type='json', value=json.dumps(content)) - is_supported = manager.is_supported(datum) - self.assertFalse(is_supported) - - def test_collection_string_unsupported(self): - """ - Verify that the given input is unsupported by SharedMemoryManager. - This input is collection_string. - """ - manager = SharedMemoryManager() - content = ['foo', 'bar'] - datum = bind_meta.Datum(type='collection_string', value=content) - is_supported = manager.is_supported(datum) - self.assertFalse(is_supported) - - def test_collection_bytes_unsupported(self): - """ - Verify that the given input is unsupported by SharedMemoryManager. - This input is collection_bytes. - """ - manager = SharedMemoryManager() - content = [b'x01', b'x02'] - datum = bind_meta.Datum(type='collection_bytes', value=content) - is_supported = manager.is_supported(datum) - self.assertFalse(is_supported) - - def test_collection_double_unsupported(self): - """ - Verify that the given input is unsupported by SharedMemoryManager. - This input is collection_double. - """ - manager = SharedMemoryManager() - content = [1.0, 2.0] - datum = bind_meta.Datum(type='collection_double', value=content) - is_supported = manager.is_supported(datum) - self.assertFalse(is_supported) - - def test_collection_sint64_unsupported(self): - """ - Verify that the given input is unsupported by SharedMemoryManager. - This input is collection_sint64. - """ - manager = SharedMemoryManager() - content = [1, 2] - datum = bind_meta.Datum(type='collection_sint64', value=content) - is_supported = manager.is_supported(datum) - self.assertFalse(is_supported) - - def test_large_invalid_bytes_input_support(self): - """ - Verify that the given input is NOT supported by SharedMemoryManager to - be transfered over shared memory. - The input is bytes of larger than the allowed size. - """ - manager = SharedMemoryManager() - content_size = consts.MAX_BYTES_FOR_SHARED_MEM_TRANSFER + 10 - # Not using get_random_bytes to avoid slowing down for creating a large - # random input - content = b'x01' * content_size - bytes_datum = bind_meta.Datum(type='bytes', value=content) - is_supported = manager.is_supported(bytes_datum) - self.assertFalse(is_supported) - - def test_small_invalid_bytes_input_support(self): - """ - Verify that the given input is NOT supported by SharedMemoryManager to - be transfered over shared memory. - The input is bytes of smaller than the allowed size. - """ - manager = SharedMemoryManager() - content_size = consts.MIN_BYTES_FOR_SHARED_MEM_TRANSFER - 10 - content = self.get_random_bytes(content_size) - bytes_datum = bind_meta.Datum(type='bytes', value=content) - is_supported = manager.is_supported(bytes_datum) - self.assertFalse(is_supported) - - def test_large_invalid_string_input_support(self): - """ - Verify that the given input is NOT supported by SharedMemoryManager to - be transfered over shared memory. - The input is string of larger than the allowed size. - """ - manager = SharedMemoryManager() - content_size = consts.MAX_BYTES_FOR_SHARED_MEM_TRANSFER + 10 - num_chars = math.floor(content_size / consts.SIZE_OF_CHAR_BYTES) - # Not using get_random_string to avoid slowing down for creating a large - # random input - content = 'a' * num_chars - string_datum = bind_meta.Datum(type='string', value=content) - is_supported = manager.is_supported(string_datum) - self.assertFalse(is_supported) - - def test_small_invalid_string_input_support(self): - """ - Verify that the given input is NOT supported by SharedMemoryManager to - be transfered over shared memory. - The input is string of smaller than the allowed size. - """ - manager = SharedMemoryManager() - content_size = consts.MIN_BYTES_FOR_SHARED_MEM_TRANSFER - 10 - num_chars = math.floor(content_size / consts.SIZE_OF_CHAR_BYTES) - content = self.get_random_string(num_chars) - string_datum = bind_meta.Datum(type='string', value=content) - is_supported = manager.is_supported(string_datum) - self.assertFalse(is_supported) - - def test_put_bytes(self): - """ - Verify that the given input was successfully put into shared memory. - The input is bytes. - """ - manager = SharedMemoryManager() - content_size = consts.MIN_BYTES_FOR_SHARED_MEM_TRANSFER + 10 - content = self.get_random_bytes(content_size) - shared_mem_meta = manager.put_bytes(content) - self.assertIsNotNone(shared_mem_meta) - self.assertTrue(self.is_valid_uuid(shared_mem_meta.mem_map_name)) - self.assertEqual(content_size, shared_mem_meta.count_bytes) - free_success = manager.free_mem_map(shared_mem_meta.mem_map_name) - self.assertTrue(free_success) - - def test_invalid_put_bytes(self): - """ - Attempt to put bytes using an invalid input and verify that it fails. - """ - manager = SharedMemoryManager() - shared_mem_meta = manager.put_bytes(None) - self.assertIsNone(shared_mem_meta) - - def test_get_bytes(self): - """ - Verify that the output object was successfully gotten from shared - memory. - The output is bytes. - """ - manager = SharedMemoryManager() - content_size = consts.MIN_BYTES_FOR_SHARED_MEM_TRANSFER + 10 - content = self.get_random_bytes(content_size) - shared_mem_meta = manager.put_bytes(content) - mem_map_name = shared_mem_meta.mem_map_name - num_bytes_written = shared_mem_meta.count_bytes - read_content = manager.get_bytes(mem_map_name, offset=0, - count=num_bytes_written) - self.assertEqual(content, read_content) - free_success = manager.free_mem_map(mem_map_name) - self.assertTrue(free_success) - - def test_put_string(self): - """ - Verify that the given input was successfully put into shared memory. - The input is string. - """ - manager = SharedMemoryManager() - content_size = consts.MIN_BYTES_FOR_SHARED_MEM_TRANSFER + 10 - num_chars = math.floor(content_size / consts.SIZE_OF_CHAR_BYTES) - content = self.get_random_string(num_chars) - expected_size = len(content.encode('utf-8')) - shared_mem_meta = manager.put_string(content) - self.assertIsNotNone(shared_mem_meta) - self.assertTrue(self.is_valid_uuid(shared_mem_meta.mem_map_name)) - self.assertEqual(expected_size, shared_mem_meta.count_bytes) - free_success = manager.free_mem_map(shared_mem_meta.mem_map_name) - self.assertTrue(free_success) - - def test_invalid_put_string(self): - """ - Attempt to put a string using an invalid input and verify that it fails. - """ - manager = SharedMemoryManager() - shared_mem_meta = manager.put_string(None) - self.assertIsNone(shared_mem_meta) - - def test_get_string(self): - """ - Verify that the output object was successfully gotten from shared - memory. - The output is string. - """ - manager = SharedMemoryManager() - content_size = consts.MIN_BYTES_FOR_SHARED_MEM_TRANSFER + 10 - num_chars = math.floor(content_size / consts.SIZE_OF_CHAR_BYTES) - content = self.get_random_string(num_chars) - shared_mem_meta = manager.put_string(content) - mem_map_name = shared_mem_meta.mem_map_name - num_bytes_written = shared_mem_meta.count_bytes - read_content = manager.get_string(mem_map_name, offset=0, - count=num_bytes_written) - self.assertEqual(content, read_content) - free_success = manager.free_mem_map(mem_map_name) - self.assertTrue(free_success) - - def test_allocated_mem_maps(self): - """ - Verify that the SharedMemoryManager is tracking the shared memory maps - it has allocated after put operations. - Verify that those shared memory maps are freed and no longer tracked - after attempting to free them. - """ - manager = SharedMemoryManager() - content_size = consts.MIN_BYTES_FOR_SHARED_MEM_TRANSFER + 10 - content = self.get_random_bytes(content_size) - shared_mem_meta = manager.put_bytes(content) - self.assertIsNotNone(shared_mem_meta) - mem_map_name = shared_mem_meta.mem_map_name - is_mem_map_found = mem_map_name in manager.allocated_mem_maps - self.assertTrue(is_mem_map_found) - self.assertEqual(1, len(manager.allocated_mem_maps.keys())) - free_success = manager.free_mem_map(mem_map_name) - self.assertTrue(free_success) - is_mem_map_found = mem_map_name in manager.allocated_mem_maps - self.assertFalse(is_mem_map_found) - self.assertEqual(0, len(manager.allocated_mem_maps.keys())) - - def test_do_not_free_resources_on_dispose(self): - """ - Verify that when the allocated shared memory maps are freed, - their backing resources are not freed. - Note: The shared memory map should no longer be tracked by the - SharedMemoryManager, though. - """ - manager = SharedMemoryManager() - content_size = consts.MIN_BYTES_FOR_SHARED_MEM_TRANSFER + 10 - content = self.get_random_bytes(content_size) - shared_mem_meta = manager.put_bytes(content) - self.assertIsNotNone(shared_mem_meta) - mem_map_name = shared_mem_meta.mem_map_name - is_mem_map_found = mem_map_name in manager.allocated_mem_maps - self.assertTrue(is_mem_map_found) - self.assertEqual(1, len(manager.allocated_mem_maps.keys())) - free_success = manager.free_mem_map(mem_map_name, False) - self.assertTrue(free_success) - is_mem_map_found = mem_map_name in manager.allocated_mem_maps - self.assertFalse(is_mem_map_found) - self.assertEqual(0, len(manager.allocated_mem_maps.keys())) - - def test_invalid_put_allocated_mem_maps(self): - """ - Verify that after an invalid put operation, no shared memory maps were - added to the list of allocated/tracked shared memory maps. - i.e. no resources were leaked for invalid operations. - """ - manager = SharedMemoryManager() - shared_mem_meta = manager.put_bytes(None) - self.assertIsNone(shared_mem_meta) - self.assertEqual(0, len(manager.allocated_mem_maps.keys())) - - def test_invalid_free_mem_map(self): - """ - Attempt to free a shared memory map that does not exist in the list of - allocated/tracked shared memory maps and verify that it fails. - """ - manager = SharedMemoryManager() - mem_map_name = self.get_new_mem_map_name() - free_success = manager.free_mem_map(mem_map_name) - self.assertFalse(free_success) diff --git a/tests/unittests/test_shared_memory_map.py b/tests/unittests/test_shared_memory_map.py deleted file mode 100644 index ecaeaacc0..000000000 --- a/tests/unittests/test_shared_memory_map.py +++ /dev/null @@ -1,139 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os -import sys -import unittest -from unittest import skipIf - -from tests.utils import testutils - -from azure_functions_worker.bindings.shared_memory_data_transfer import ( - SharedMemoryConstants as consts, -) -from azure_functions_worker.bindings.shared_memory_data_transfer import ( - SharedMemoryException, - SharedMemoryMap, -) - - -@skipIf(sys.platform == 'darwin', 'MacOS M1 machines do not correctly test the' - 'shared memory filesystems and thus skipping' - ' these tests for the time being') -class TestSharedMemoryMap(testutils.SharedMemoryTestCase): - """ - Tests for SharedMemoryMap. - """ - def test_init(self): - """ - Verify the initialization of a SharedMemoryMap. - """ - mem_map_name = self.get_new_mem_map_name() - mem_map_size = 1024 - mem_map = self.file_accessor.create_mem_map(mem_map_name, mem_map_size) - shared_mem_map = SharedMemoryMap(self.file_accessor, mem_map_name, - mem_map) - self.assertIsNotNone(shared_mem_map) - dispose_status = shared_mem_map.dispose() - self.assertTrue(dispose_status) - - def test_init_with_invalid_inputs(self): - """ - Attempt to initialize a SharedMemoryMap from invalid inputs (name and - mmap) and verify that an SharedMemoryException is raised. - """ - inv_mem_map_name = None - mem_map_name = self.get_new_mem_map_name() - mem_map_size = 1024 - mem_map = self.file_accessor.create_mem_map(mem_map_name, mem_map_size) - with self.assertRaisesRegex(SharedMemoryException, 'Invalid name'): - SharedMemoryMap(self.file_accessor, inv_mem_map_name, mem_map) - inv_mem_map_name = '' - with self.assertRaisesRegex(SharedMemoryException, 'Invalid name'): - SharedMemoryMap(self.file_accessor, inv_mem_map_name, mem_map) - with self.assertRaisesRegex(SharedMemoryException, - 'Invalid memory map'): - SharedMemoryMap(self.file_accessor, mem_map_name, None) - - def test_put_bytes(self): - """ - Create a SharedMemoryMap and write bytes to it. - """ - for content_size in [1, 10, 1024, 2 * 1024 * 1024, 20 * 1024 * 1024]: - mem_map_name = self.get_new_mem_map_name() - mem_map_size = content_size + consts.CONTENT_HEADER_TOTAL_BYTES - mem_map = self.file_accessor.create_mem_map(mem_map_name, - mem_map_size) - shared_mem_map = SharedMemoryMap(self.file_accessor, mem_map_name, - mem_map) - content = self.get_random_bytes(content_size) - num_bytes_written = shared_mem_map.put_bytes(content) - self.assertEqual(content_size, num_bytes_written) - dispose_status = shared_mem_map.dispose() - self.assertTrue(dispose_status) - - def test_get_bytes(self): - """ - Create a SharedMemoryMap, write bytes to it and then read them back. - Verify that the bytes written and read match. - """ - for content_size in [1, 10, 1024, 2 * 1024 * 1024, 20 * 1024 * 1024]: - mem_map_name = self.get_new_mem_map_name() - mem_map_size = content_size + consts.CONTENT_HEADER_TOTAL_BYTES - mem_map = self.file_accessor.create_mem_map(mem_map_name, - mem_map_size) - shared_mem_map = SharedMemoryMap(self.file_accessor, mem_map_name, - mem_map) - content = self.get_random_bytes(content_size) - num_bytes_written = shared_mem_map.put_bytes(content) - self.assertEqual(content_size, num_bytes_written) - read_content = shared_mem_map.get_bytes() - self.assertEqual(content, read_content) - dispose_status = shared_mem_map.dispose() - self.assertTrue(dispose_status) - - def test_put_bytes_more_than_capacity(self): - """ - Attempt to put more bytes into the created SharedMemoryMap than the - size with which it was created. Verify that an SharedMemoryException is - raised. - """ - mem_map_name = self.get_new_mem_map_name() - mem_map_size = 1024 + consts.CONTENT_HEADER_TOTAL_BYTES - mem_map = self.file_accessor.create_mem_map(mem_map_name, - mem_map_size) - shared_mem_map = SharedMemoryMap(self.file_accessor, mem_map_name, - mem_map) - # Attempt to write more bytes than the size of the memory map we created - # earlier (1024). - content_size = 2048 - content = self.get_random_bytes(content_size) - with self.assertRaisesRegex(ValueError, 'out of range'): - shared_mem_map.put_bytes(content) - dispose_status = shared_mem_map.dispose() - self.assertTrue(dispose_status) - - @unittest.skipIf(os.name == 'nt', - 'Windows will create an mmap if one does not exist') - def test_dispose_without_delete_file(self): - """ - Dispose a SharedMemoryMap without making it dispose the backing file - resources (on Unix). Verify that the same memory map can be opened again - as the backing file was still present. - """ - mem_map_name = self.get_new_mem_map_name() - mem_map_size = 1024 + consts.CONTENT_HEADER_TOTAL_BYTES - mem_map = self.file_accessor.create_mem_map(mem_map_name, - mem_map_size) - shared_mem_map = SharedMemoryMap(self.file_accessor, mem_map_name, - mem_map) - # Close the memory map but do not delete the backing file - dispose_status = shared_mem_map.dispose(is_delete_file=False) - self.assertTrue(dispose_status) - # Attempt to open the memory map again, it should still open since the - # backing file is present - mem_map_op = self.file_accessor.open_mem_map(mem_map_name, mem_map_size) - self.assertIsNotNone(mem_map_op) - delete_status = \ - self.file_accessor.delete_mem_map(mem_map_name, mem_map_op) - self.assertTrue(delete_status) diff --git a/tests/unittests/test_third_party_http_functions.py b/tests/unittests/test_third_party_http_functions.py deleted file mode 100644 index 7dd57e88d..000000000 --- a/tests/unittests/test_third_party_http_functions.py +++ /dev/null @@ -1,237 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License -import filecmp -import os -import pathlib -import re -import typing -import base64 -import sys - -from unittest import skipIf -from unittest.mock import patch - -from tests.utils import testutils -from tests.utils.testutils import UNIT_TESTS_ROOT - -HOST_JSON_TEMPLATE = """\ -{ - "version": "2.0", - "logging": { - "logLevel": { - "default": "Trace" - } - }, - "extensions": { - "http": { - "routePrefix": "" - } - }, - "functionTimeout": "00:05:00" -} -""" - - -class ThirdPartyHttpFunctionsTestBase: - class TestThirdPartyHttpFunctions(testutils.WebHostTestCase): - - @classmethod - def setUpClass(cls): - host_json = cls.get_script_dir() / 'host.json' - with open(host_json, 'w+') as f: - f.write(HOST_JSON_TEMPLATE) - os_environ = os.environ.copy() - # Turn on feature flag - os_environ['AzureWebJobsFeatureFlags'] = 'EnableWorkerIndexing' - cls._patch_environ = patch.dict('os.environ', os_environ) - cls._patch_environ.start() - super().setUpClass() - - @classmethod - def tearDownClass(cls): - super().tearDownClass() - cls._patch_environ.stop() - - @classmethod - def get_script_dir(cls): - pass - - def test_debug_logging(self): - r = self.webhost.request('GET', 'debug_logging', no_prefix=True) - 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', - no_prefix=True) - 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) - - @testutils.retryable_test(3, 5) - def test_print_logging_no_flush(self): - r = self.webhost.request('GET', 'print_logging?message=Secret42', - no_prefix=True) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK-print-logging') - - @testutils.retryable_test(3, 5) - def check_log_print_logging_no_flush(self, host_out: typing.List[str]): - self.assertIn('Secret42', host_out) - - @testutils.retryable_test(3, 5) - def test_print_logging_with_flush(self): - r = self.webhost.request('GET', - 'print_logging?flush=true&message' - '=Secret42', - no_prefix=True) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK-print-logging') - - @testutils.retryable_test(3, 5) - 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', - no_prefix=True) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK-print-logging') - - def check_log_print_to_console_stdout(self, - host_out: typing.List[str]): - # System logs stdout now exist in host_out - self.assertIn('Secret42', host_out) - - @skipIf(sys.version_info < (3, 9, 0), - "Skip the tests for Python 3.8 and below") - def test_print_to_console_stderr(self): - r = self.webhost.request('GET', 'print_logging?console=true' - '&message=Secret42&is_stderr=true', - no_prefix=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 now exist in host_out - self.assertIn('Secret42', host_out) - - def test_return_http_no_body(self): - r = self.webhost.request('GET', 'return_http_no_body', - no_prefix=True) - self.assertEqual(r.text, '') - self.assertEqual(r.status_code, 200) - - def test_unhandled_error(self): - r = self.webhost.request('GET', 'unhandled_error', no_prefix=True) - self.assertEqual(r.status_code, 500) - # https://github.com/Azure/azure-functions-host/issues/2706 - # self.assertIn('ZeroDivisionError', r.text) - - def check_log_unhandled_error(self, - host_out: typing.List[str]): - r = re.compile(".*ZeroDivisionError: division by zero.*") - error_log = list(filter(r.match, host_out)) - self.assertGreaterEqual(len(error_log), 1) - - def test_unhandled_unserializable_error(self): - r = self.webhost.request( - 'GET', 'unhandled_unserializable_error', no_prefix=True) - self.assertEqual(r.status_code, 500) - - def test_unhandled_urllib_error(self): - r = self.webhost.request( - 'GET', 'unhandled_urllib_error', - params={'img': 'http://example.com/nonexistent.jpg'}, - no_prefix=True) - self.assertEqual(r.status_code, 500) - - -class TestAsgiHttpFunctions( - ThirdPartyHttpFunctionsTestBase.TestThirdPartyHttpFunctions): - @classmethod - def get_script_dir(cls): - return UNIT_TESTS_ROOT / 'third_party_http_functions' / 'stein' / \ - 'asgi_function' - - def test_hijack_current_event_loop(self): - r = self.webhost.request('GET', 'hijack_current_event_loop', - no_prefix=True) - 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 now exist in host_out - self.assertIn('parallelly_log_system at disguised_logger', - host_out) - - def test_raw_body_bytes(self): - parent_dir = pathlib.Path(__file__).parent.parent - image_file = parent_dir / 'unittests/resources/functions.png' - with open(image_file, 'rb') as image: - img = image.read() - encoded_image = base64.b64encode(img).decode('utf-8') - html_img_tag = \ - f'PNG Image' # noqa - sanitized_img_len = len(html_img_tag) - r = self.webhost.request('POST', 'raw_body_bytes', data=img, - no_prefix=True) - - received_body_len = int(r.headers['body-len']) - self.assertEqual(received_body_len, sanitized_img_len) - - encoded_image_data = encoded_image.split(",")[0] - body = base64.b64decode(encoded_image_data) - 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) - - -class TestWsgiHttpFunctions( - ThirdPartyHttpFunctionsTestBase.TestThirdPartyHttpFunctions): - @classmethod - def get_script_dir(cls): - return UNIT_TESTS_ROOT / 'third_party_http_functions' / 'stein' / \ - 'wsgi_function' - - def test_return_http_redirect(self): - r = self.webhost.request('GET', 'return_http_redirect', - no_prefix=True) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, '

    Hello Worldâ„¢

    ') - - r = self.webhost.request('GET', 'return_http_redirect', - allow_redirects=False, no_prefix=True) - self.assertEqual(r.status_code, 302) diff --git a/tests/unittests/test_types.py b/tests/unittests/test_types.py deleted file mode 100644 index 963f26914..000000000 --- a/tests/unittests/test_types.py +++ /dev/null @@ -1,196 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import unittest - -from azure import functions as azf -from azure.functions import http as bind_http -from azure.functions import meta as bind_meta - -from azure_functions_worker import protos -from azure_functions_worker.bindings import datumdef - - -class MockMBD: - def __init__(self, version: str, source: str, - content_type: str, content: str): - self.version = version - self.source = source - self.content_type = content_type - self.content = content - - -class TestFunctions(unittest.TestCase): - - def test_http_request_bytes(self): - r = bind_http.HttpRequest( - 'get', - 'http://example.com/abc?a=1', - headers=dict(aaa='zzz', bAb='xYz'), - params=dict(a='b'), - route_params={'route': 'param'}, - body_type='bytes', - body=b'abc') - - self.assertEqual(r.method, 'GET') - self.assertEqual(r.url, 'http://example.com/abc?a=1') - self.assertEqual(r.params, {'a': 'b'}) - self.assertEqual(r.route_params, {'route': 'param'}) - - with self.assertRaises(TypeError): - r.params['a'] = 'z' - - self.assertEqual(r.get_body(), b'abc') - - with self.assertRaisesRegex(ValueError, 'does not contain valid JSON'): - r.get_json() - - h = r.headers - with self.assertRaises(AttributeError): - r.headers = dict() - - self.assertEqual(h['aaa'], 'zzz') - self.assertEqual(h['aaA'], 'zzz') - self.assertEqual(h['bab'], 'xYz') - self.assertEqual(h['BaB'], 'xYz') - - # test that request headers are read-only - with self.assertRaises(TypeError): - h['zzz'] = '123' - - def test_http_request_json(self): - r = bind_http.HttpRequest( - 'POST', - 'http://example.com/abc?a=1', - headers={}, - params={}, - route_params={}, - body_type='json', - body='{"a":1}') - - self.assertEqual(r.method, 'POST') - self.assertEqual(r.url, 'http://example.com/abc?a=1') - self.assertEqual(r.params, {}) - self.assertEqual(r.route_params, {}) - - self.assertEqual(r.get_body(), b'{"a":1}') - self.assertEqual(r.get_json(), {'a': 1}) - - def test_http_response(self): - r = azf.HttpResponse( - 'bodyâ„¢', - status_code=201, - headers=dict(aaa='zzz', bAb='xYz')) - - self.assertEqual(r.status_code, 201) - self.assertEqual(r.get_body(), b'body\xe2\x84\xa2') - - self.assertEqual(r.mimetype, 'text/plain') - self.assertEqual(r.charset, 'utf-8') - - h = r.headers - with self.assertRaises(AttributeError): - r.headers = dict() - - self.assertEqual(h['aaa'], 'zzz') - self.assertEqual(h['aaA'], 'zzz') - self.assertEqual(h['bab'], 'xYz') - self.assertEqual(h['BaB'], 'xYz') - - # test that response headers are mutable - h['zZz'] = '123' - self.assertEqual(h['zzz'], '123') - - -class Converter(bind_meta.InConverter, binding='foo'): - pass - - -class TestTriggerMetadataDecoder(unittest.TestCase): - - def test_scalar_typed_data_decoder_ok(self): - metadata = { - 'int_as_json': bind_meta.Datum(type='json', value='1'), - 'int_as_string': bind_meta.Datum(type='string', value='1'), - 'int_as_int': bind_meta.Datum(type='int', value=1), - 'string_as_json': bind_meta.Datum(type='json', value='"aaa"'), - 'string_as_string': bind_meta.Datum(type='string', value='aaa'), - 'dict_as_json': bind_meta.Datum(type='json', value='{"foo":"bar"}') - } - - cases = [ - ('int_as_json', int, 1), - ('int_as_string', int, 1), - ('int_as_int', int, 1), - ('string_as_json', str, 'aaa'), - ('string_as_string', str, 'aaa'), - ('dict_as_json', dict, {'foo': 'bar'}), - ] - - for field, pytype, expected in cases: - with self.subTest(field=field): - value = Converter._decode_trigger_metadata_field( - metadata, field, python_type=pytype) - - self.assertIsInstance(value, pytype) - self.assertEqual(value, expected) - - def test_scalar_typed_data_decoder_not_ok(self): - metadata = { - 'unsupported_type': - bind_meta.Datum(type='bytes', value=b'aaa'), - 'unexpected_json': - bind_meta.Datum(type='json', value='[1, 2, 3]'), - 'unexpected_data': - bind_meta.Datum(type='json', value='"foo"'), - } - - cases = [ - ( - 'unsupported_type', int, ValueError, - "unsupported type of field 'unsupported_type' in " - "trigger metadata: bytes" - ), - ( - 'unexpected_json', int, ValueError, - "cannot convert value of field 'unexpected_json' in " - "trigger metadata into int" - ), - ( - 'unexpected_data', int, ValueError, - "cannot convert value of field " - "'unexpected_data' in trigger metadata into int: " - "invalid literal for int" - ), - ( - 'unexpected_data', (int, float), ValueError, - "unexpected value type in field " - "'unexpected_data' in trigger metadata: str, " - "expected one of: int, float" - ), - ] - - for field, pytype, exc, msg in cases: - with self.subTest(field=field): - with self.assertRaisesRegex(exc, msg): - Converter._decode_trigger_metadata_field( - metadata, field, python_type=pytype) - - def test_model_binding_data_datum_ok(self): - sample_mbd = MockMBD(version="1.0", - source="AzureStorageBlobs", - content_type="application/json", - content="{\"Connection\":\"python-worker-tests\"," - "\"ContainerName\":\"test-blob\"," - "\"BlobName\":\"test.txt\"}") - - datum: bind_meta.Datum = bind_meta.Datum(value=sample_mbd, - type='model_binding_data') - - self.assertEqual(datum.value, sample_mbd) - self.assertEqual(datum.type, "model_binding_data") - - def test_model_binding_data_td_ok(self): - mock_mbd = protos.TypedData(model_binding_data={'version': '1.0'}) - mbd_datum = datumdef.Datum.from_typed_data(mock_mbd) - - self.assertEqual(mbd_datum.type, 'model_binding_data') diff --git a/tests/unittests/test_typing_inspect.py b/tests/unittests/test_typing_inspect.py deleted file mode 100644 index 4f01e4c73..000000000 --- a/tests/unittests/test_typing_inspect.py +++ /dev/null @@ -1,144 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -# Imported from https://github.com/ilevkivskyi/typing_inspect/blob/168fa6f7c5c55f720ce6282727211cf4cf6368f6/test_typing_inspect.py -# Author: Ivan Levkivskyi -# License: MIT - -from typing import ( - Any, - Callable, - ClassVar, - Dict, - Generic, - Iterable, - List, - Mapping, - MutableMapping, - NamedTuple, - Optional, - Sequence, - Tuple, - TypeVar, - Union, -) -from unittest import TestCase, main, skipIf - -from azure_functions_worker._thirdparty.typing_inspect import ( - get_args, - get_generic_bases, - get_generic_type, - get_last_args, - get_last_origin, - get_origin, - get_parameters, - is_callable_type, - is_classvar, - is_generic_type, - is_tuple_type, - is_typevar, - is_union_type, -) - - -class IsUtilityTestCase(TestCase): - def sample_test(self, fun, samples, nonsamples): - for s in samples: - self.assertTrue(fun(s), f"{s} type expected in {samples}") - for s in nonsamples: - self.assertFalse(fun(s), f"{s} type expected in {nonsamples}") - - def test_generic(self): - T = TypeVar('T') - samples = [Generic, Generic[T], Iterable[int], Mapping, - MutableMapping[T, List[int]], Sequence[Union[str, bytes]]] - nonsamples = [int, Union[int, str], Union[int, T], ClassVar[List[int]], - Callable[..., T], ClassVar, Optional, bytes, list] - self.sample_test(is_generic_type, samples, nonsamples) - - def test_callable(self): - samples = [Callable, Callable[..., int], - Callable[[int, int], Iterable[str]]] - nonsamples = [int, type, 42, [], List[int], - Union[callable, Callable[..., int]]] - self.sample_test(is_callable_type, samples, nonsamples) - class MyClass(Callable[[int], int]): - pass - self.assertTrue(is_callable_type(MyClass)) - - def test_tuple(self): - samples = [Tuple, Tuple[str, int], Tuple[Iterable, ...]] - nonsamples = [int, tuple, 42, List[int], NamedTuple('N', [('x', int)])] - self.sample_test(is_tuple_type, samples, nonsamples) - class MyClass(Tuple[str, int]): - pass - self.assertTrue(is_tuple_type(MyClass)) - - def test_union(self): - T = TypeVar('T') - S = TypeVar('S') - samples = [Union, Union[T, int], Union[int, Union[T, S]]] - nonsamples = [int, Union[int, int], [], Iterable[Any]] - self.sample_test(is_union_type, samples, nonsamples) - - def test_typevar(self): - T = TypeVar('T') - S_co = TypeVar('S_co', covariant=True) - samples = [T, S_co] - nonsamples = [int, Union[T, int], Union[T, S_co], type, ClassVar[int]] - self.sample_test(is_typevar, samples, nonsamples) - - def test_classvar(self): - T = TypeVar('T') - samples = [ClassVar, ClassVar[int], ClassVar[List[T]]] - nonsamples = [int, 42, Iterable, List[int], type, T] - self.sample_test(is_classvar, samples, nonsamples) - - -class GetUtilityTestCase(TestCase): - - def test_origin(self): - T = TypeVar('T') - self.assertEqual(get_origin(int), None) - self.assertEqual(get_origin(ClassVar[int]), None) - self.assertEqual(get_origin(Generic), Generic) - self.assertEqual(get_origin(Generic[T]), Generic) - self.assertEqual(get_origin(List[Tuple[T, T]][int]), list) - - def test_parameters(self): - T = TypeVar('T') - S_co = TypeVar('S_co', covariant=True) - U = TypeVar('U') - self.assertEqual(get_parameters(int), ()) - self.assertEqual(get_parameters(Generic), ()) - self.assertEqual(get_parameters(Union), ()) - self.assertEqual(get_parameters(List[int]), ()) - self.assertEqual(get_parameters(Generic[T]), (T,)) - self.assertEqual(get_parameters(Tuple[List[T], List[S_co]]), (T, S_co)) - self.assertEqual(get_parameters(Union[S_co, Tuple[T, T]][int, U]), (U,)) - self.assertEqual(get_parameters(Mapping[T, Tuple[S_co, T]]), (T, S_co)) - - def test_args_evaluated(self): - T = TypeVar('T') - self.assertEqual(get_args(Union[int, Tuple[T, int]][str], evaluate=True), - (int, Tuple[str, int])) - self.assertEqual(get_args(Dict[int, Tuple[T, T]][Optional[int]], evaluate=True), - (int, Tuple[Optional[int], Optional[int]])) - self.assertEqual(get_args(Callable[[], T][int], evaluate=True), ([], int,)) - - def test_generic_type(self): - T = TypeVar('T') - class Node(Generic[T]): pass - self.assertIs(get_generic_type(Node()), Node) - self.assertIs(get_generic_type(Node[int]()), Node[int]) - self.assertIs(get_generic_type(Node[T]()), Node[T],) - self.assertIs(get_generic_type(1), int) - - def test_generic_bases(self): - class MyClass(List[int], Mapping[str, List[int]]): pass - self.assertEqual(get_generic_bases(MyClass), - (List[int], Mapping[str, List[int]])) - self.assertEqual(get_generic_bases(int), ()) - - -if __name__ == '__main__': - main() diff --git a/tests/unittests/test_utilities.py b/tests/unittests/test_utilities.py deleted file mode 100644 index 99b014e09..000000000 --- a/tests/unittests/test_utilities.py +++ /dev/null @@ -1,390 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import os -import pathlib -import sys -import typing -import unittest -from unittest.mock import patch - -from azure_functions_worker.constants import PYTHON_EXTENSIONS_RELOAD_FUNCTIONS -from azure_functions_worker.utils import common, wrappers - -TEST_APP_SETTING_NAME = "TEST_APP_SETTING_NAME" -TEST_FEATURE_FLAG = "APP_SETTING_FEATURE_FLAG" -FEATURE_DEFAULT = 42 - - -class MockFeature: - @wrappers.enable_feature_by(TEST_FEATURE_FLAG) - def mock_feature_enabled(self, output: typing.List[str]) -> str: - result = 'mock_feature_enabled' - output.append(result) - return result - - @wrappers.enable_feature_by(TEST_FEATURE_FLAG, flag_default=True) - def mock_enabled_default_true(self, output: typing.List[str]) -> str: - result = 'mock_enabled_default_true' - output.append(result) - return result - - @wrappers.disable_feature_by(TEST_FEATURE_FLAG) - def mock_feature_disabled(self, output: typing.List[str]) -> str: - result = 'mock_feature_disabled' - output.append(result) - return result - - @wrappers.disable_feature_by(TEST_FEATURE_FLAG, flag_default=True) - def mock_disabled_default_true(self, output: typing.List[str]) -> str: - result = 'mock_disabled_default_true' - output.append(result) - return result - - @wrappers.enable_feature_by(TEST_FEATURE_FLAG, FEATURE_DEFAULT) - def mock_feature_default(self, output: typing.List[str]) -> str: - result = 'mock_feature_default' - output.append(result) - return result - - -class MockMethod: - @wrappers.attach_message_to_exception(ImportError, 'success') - def mock_load_function_success(self): - return True - - @wrappers.attach_message_to_exception(ImportError, 'module_not_found') - def mock_load_function_module_not_found(self): - raise ModuleNotFoundError('MODULE_NOT_FOUND') - - @wrappers.attach_message_to_exception(ImportError, 'import_error') - def mock_load_function_import_error(self): - # ImportError is a subclass of ModuleNotFoundError - raise ImportError('IMPORT_ERROR') - - @wrappers.attach_message_to_exception(ImportError, 'value_error') - def mock_load_function_value_error(self): - # ValueError is not a subclass of ImportError - raise ValueError('VALUE_ERROR') - - -class TestUtilities(unittest.TestCase): - - def setUp(self): - self._dummy_sdk_sys_path = os.path.join( - os.path.dirname(__file__), - 'resources', - 'mock_azure_functions' - ) - - self.mock_environ = patch.dict('os.environ', os.environ.copy()) - self.mock_sys_module = patch.dict('sys.modules', sys.modules.copy()) - self.mock_sys_path = patch('sys.path', sys.path.copy()) - self.mock_environ.start() - self.mock_sys_module.start() - self.mock_sys_path.start() - - def tearDown(self): - self.mock_sys_path.stop() - self.mock_sys_module.stop() - self.mock_environ.stop() - - def test_is_true_like_accepted(self): - self.assertTrue(common.is_true_like('1')) - self.assertTrue(common.is_true_like('true')) - self.assertTrue(common.is_true_like('T')) - self.assertTrue(common.is_true_like('YES')) - self.assertTrue(common.is_true_like('y')) - - def test_is_true_like_rejected(self): - self.assertFalse(common.is_true_like(None)) - self.assertFalse(common.is_true_like('')) - self.assertFalse(common.is_true_like('secret')) - - def test_is_false_like_accepted(self): - self.assertTrue(common.is_false_like('0')) - self.assertTrue(common.is_false_like('false')) - self.assertTrue(common.is_false_like('F')) - self.assertTrue(common.is_false_like('NO')) - self.assertTrue(common.is_false_like('n')) - - def test_is_false_like_rejected(self): - self.assertFalse(common.is_false_like(None)) - self.assertFalse(common.is_false_like('')) - self.assertFalse(common.is_false_like('secret')) - - def test_is_envvar_true(self): - os.environ[TEST_FEATURE_FLAG] = 'true' - self.assertTrue(common.is_envvar_true(TEST_FEATURE_FLAG)) - - def test_is_envvar_not_true_on_unset(self): - self._unset_feature_flag() - self.assertFalse(common.is_envvar_true(TEST_FEATURE_FLAG)) - - def test_is_envvar_false(self): - os.environ[TEST_FEATURE_FLAG] = 'false' - self.assertTrue(common.is_envvar_false(TEST_FEATURE_FLAG)) - - def test_is_envvar_not_false_on_unset(self): - self._unset_feature_flag() - self.assertFalse(common.is_envvar_true(TEST_FEATURE_FLAG)) - - def test_disable_feature_with_no_feature_flag(self): - mock_feature = MockFeature() - output = [] - result = mock_feature.mock_feature_enabled(output) - self.assertIsNone(result) - self.assertListEqual(output, []) - - def test_disable_feature_with_default_value(self): - mock_feature = MockFeature() - output = [] - result = mock_feature.mock_disabled_default_true(output) - self.assertIsNone(result) - self.assertListEqual(output, []) - - def test_enable_feature_with_feature_flag(self): - feature_flag = TEST_FEATURE_FLAG - os.environ[feature_flag] = '1' - mock_feature = MockFeature() - output = [] - result = mock_feature.mock_feature_enabled(output) - self.assertEqual(result, 'mock_feature_enabled') - self.assertListEqual(output, ['mock_feature_enabled']) - - def test_enable_feature_with_default_value(self): - mock_feature = MockFeature() - output = [] - result = mock_feature.mock_enabled_default_true(output) - self.assertEqual(result, 'mock_enabled_default_true') - self.assertListEqual(output, ['mock_enabled_default_true']) - - def test_enable_feature_with_no_rollback_flag(self): - mock_feature = MockFeature() - output = [] - result = mock_feature.mock_feature_disabled(output) - self.assertEqual(result, 'mock_feature_disabled') - self.assertListEqual(output, ['mock_feature_disabled']) - - def test_ignore_disable_default_value_when_set_explicitly(self): - feature_flag = TEST_FEATURE_FLAG - os.environ[feature_flag] = '0' - mock_feature = MockFeature() - output = [] - result = mock_feature.mock_disabled_default_true(output) - self.assertEqual(result, 'mock_disabled_default_true') - self.assertListEqual(output, ['mock_disabled_default_true']) - - def test_disable_feature_with_rollback_flag(self): - rollback_flag = TEST_FEATURE_FLAG - os.environ[rollback_flag] = '1' - mock_feature = MockFeature() - output = [] - result = mock_feature.mock_feature_disabled(output) - self.assertIsNone(result) - self.assertListEqual(output, []) - - def test_enable_feature_with_rollback_flag_is_false(self): - rollback_flag = TEST_FEATURE_FLAG - os.environ[rollback_flag] = 'false' - mock_feature = MockFeature() - output = [] - result = mock_feature.mock_feature_disabled(output) - self.assertEqual(result, 'mock_feature_disabled') - self.assertListEqual(output, ['mock_feature_disabled']) - - def test_ignore_enable_default_value_when_set_explicitly(self): - feature_flag = TEST_FEATURE_FLAG - os.environ[feature_flag] = '0' - mock_feature = MockFeature() - output = [] - result = mock_feature.mock_enabled_default_true(output) - self.assertIsNone(result) - self.assertListEqual(output, []) - - def test_fail_to_enable_feature_return_default_value(self): - mock_feature = MockFeature() - output = [] - result = mock_feature.mock_feature_default(output) - self.assertEqual(result, FEATURE_DEFAULT) - self.assertListEqual(output, []) - - def test_disable_feature_with_false_flag_return_default_value(self): - feature_flag = TEST_FEATURE_FLAG - os.environ[feature_flag] = 'false' - mock_feature = MockFeature() - output = [] - result = mock_feature.mock_feature_default(output) - self.assertEqual(result, FEATURE_DEFAULT) - self.assertListEqual(output, []) - - def test_exception_message_should_not_be_extended_on_success(self): - mock_method = MockMethod() - result = mock_method.mock_load_function_success() - self.assertTrue(result) - - def test_exception_message_should_be_extended_on_subexception(self): - mock_method = MockMethod() - with self.assertRaises(Exception) as e: - mock_method.mock_load_function_module_not_found() - self.assertIn('module_not_found', e.msg) - self.assertEqual(type(e), ModuleNotFoundError) - - def test_exception_message_should_be_extended_on_exact_exception(self): - mock_method = MockMethod() - with self.assertRaises(Exception) as e: - mock_method.mock_load_function_module_not_found() - self.assertIn('import_error', e.msg) - self.assertEqual(type(e), ImportError) - - def test_exception_message_should_not_be_extended_on_other_exception(self): - mock_method = MockMethod() - with self.assertRaises(Exception) as e: - mock_method.mock_load_function_value_error() - self.assertNotIn('import_error', e.msg) - self.assertEqual(type(e), ValueError) - - def test_app_settings_not_set_should_return_none(self): - app_setting = common.get_app_setting(TEST_APP_SETTING_NAME) - self.assertIsNone(app_setting) - - def test_app_settings_should_return_value(self): - # Set application setting by os.setenv - os.environ.update({TEST_APP_SETTING_NAME: '42'}) - - # Try using utility to acquire application setting - app_setting = common.get_app_setting(TEST_APP_SETTING_NAME) - self.assertEqual(app_setting, '42') - - def test_app_settings_not_set_should_return_default_value(self): - app_setting = common.get_app_setting(TEST_APP_SETTING_NAME, 'default') - self.assertEqual(app_setting, 'default') - - def test_app_settings_should_ignore_default_value(self): - # Set application setting by os.setenv - os.environ.update({TEST_APP_SETTING_NAME: '42'}) - - # Try using utility to acquire application setting - app_setting = common.get_app_setting(TEST_APP_SETTING_NAME, 'default') - self.assertEqual(app_setting, '42') - - def test_app_settings_should_not_trigger_validator_when_not_set(self): - def raise_excpt(value: str): - raise Exception('Should not raise on app setting not found') - - common.get_app_setting(TEST_APP_SETTING_NAME, validator=raise_excpt) - - def test_app_settings_return_default_value_when_validation_fail(self): - def parse_int_no_raise(value: str): - try: - int(value) - return True - except ValueError: - return False - - # Set application setting to an invalid value - os.environ.update({TEST_APP_SETTING_NAME: 'invalid'}) - - app_setting = common.get_app_setting( - TEST_APP_SETTING_NAME, - default_value='1', - validator=parse_int_no_raise - ) - - # Because 'invalid' is not an interger, falls back to default value - self.assertEqual(app_setting, '1') - - def test_app_settings_return_setting_value_when_validation_succeed(self): - def parse_int_no_raise(value: str): - try: - int(value) - return True - except ValueError: - return False - - # Set application setting to an invalid value - os.environ.update({TEST_APP_SETTING_NAME: '42'}) - - app_setting = common.get_app_setting( - TEST_APP_SETTING_NAME, - default_value='1', - validator=parse_int_no_raise - ) - - # Because 'invalid' is not an interger, falls back to default value - self.assertEqual(app_setting, '42') - - def test_is_python_version(self): - # Should pass at least 1 test - is_python_version_37 = common.is_python_version('3.7') - is_python_version_38 = common.is_python_version('3.8') - is_python_version_39 = common.is_python_version('3.9') - is_python_version_310 = common.is_python_version('3.10') - is_python_version_311 = common.is_python_version('3.11') - is_python_version_312 = common.is_python_version('3.12') - - self.assertTrue(any([ - is_python_version_37, - is_python_version_38, - is_python_version_39, - is_python_version_310, - is_python_version_311, - is_python_version_312 - ])) - - def test_get_sdk_from_sys_path(self): - """Test if the extension manager can find azure.functions module - """ - module = common.get_sdk_from_sys_path() - self.assertIsNotNone(module.__file__) - - def test_get_sdk_from_sys_path_after_updating_sys_path(self): - """Test if the get_sdk_from_sys_path can find the newer azure.functions - module after updating the sys.path. This is specifically for a scenario - after the dependency manager is switched to customer's path - """ - sys.path.insert(0, self._dummy_sdk_sys_path) - module = common.get_sdk_from_sys_path() - self.assertNotEqual( - os.path.dirname(module.__file__), - os.path.join(pathlib.Path.home(), 'azure', 'functions') - ) - - def test_get_sdk_version(self): - """Test if sdk version can be retrieved correctly - """ - module = common.get_sdk_from_sys_path() - sdk_version = common.get_sdk_version(module) - # e.g. 1.6.0, 1.7.0b, 1.8.1dev - self.assertRegex(sdk_version, r'\d+\.\d+\.\w+') - - def test_get_sdk_dummy_version(self): - """Test if sdk version can get dummy sdk version - """ - sys.path.insert(0, self._dummy_sdk_sys_path) - module = common.get_sdk_from_sys_path() - sdk_version = common.get_sdk_version(module) - self.assertNotEqual(sdk_version, 'dummy') - - def test_get_sdk_dummy_version_with_flag_enabled(self): - """Test if sdk version can get dummy sdk version - """ - os.environ[PYTHON_EXTENSIONS_RELOAD_FUNCTIONS] = '1' - sys.path.insert(0, self._dummy_sdk_sys_path) - module = common.get_sdk_from_sys_path() - sdk_version = common.get_sdk_version(module) - self.assertEqual(sdk_version, 'dummy') - - def test_valid_script_file_name(self): - file_name = 'test.py' - common.validate_script_file_name(file_name) - - def test_invalid_script_file_name(self): - file_name = 'test' - with self.assertRaises(common.InvalidFileNameError): - common.validate_script_file_name(file_name) - - def _unset_feature_flag(self): - try: - os.environ.pop(TEST_FEATURE_FLAG) - except KeyError: - pass diff --git a/tests/unittests/test_utilities_dependency.py b/tests/unittests/test_utilities_dependency.py deleted file mode 100644 index 432aee750..000000000 --- a/tests/unittests/test_utilities_dependency.py +++ /dev/null @@ -1,784 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import importlib.util -import os -import sys -import unittest -from unittest.mock import patch - -from tests.utils import testutils - -from azure_functions_worker.utils.dependency import DependencyManager - - -class TestDependencyManager(unittest.TestCase): - - def setUp(self): - self._patch_environ = patch.dict('os.environ', os.environ.copy()) - self._patch_sys_path = patch('sys.path', []) - self._patch_importer_cache = patch.dict('sys.path_importer_cache', {}) - self._patch_modules = patch.dict('sys.modules', {}) - self._customer_func_path = os.path.abspath( - os.path.join( - testutils.UNIT_TESTS_ROOT, 'resources', 'customer_func_path' - ) - ) - self._worker_deps_path = os.path.abspath( - os.path.join( - testutils.UNIT_TESTS_ROOT, 'resources', 'worker_deps_path' - ) - ) - self._customer_deps_path = os.path.abspath( - os.path.join( - testutils.UNIT_TESTS_ROOT, 'resources', 'customer_deps_path' - ) - ) - - self._patch_environ.start() - self._patch_sys_path.start() - self._patch_importer_cache.start() - self._patch_modules.start() - - def tearDown(self): - self._patch_environ.stop() - self._patch_sys_path.stop() - self._patch_importer_cache.stop() - self._patch_modules.stop() - DependencyManager.cx_deps_path = '' - DependencyManager.cx_working_dir = '' - DependencyManager.worker_deps_path = '' - - def test_should_not_have_any_paths_initially(self): - self.assertEqual(DependencyManager.cx_deps_path, '') - self.assertEqual(DependencyManager.cx_working_dir, '') - self.assertEqual(DependencyManager.worker_deps_path, '') - - def test_initialize_in_linux_consumption(self): - os.environ['AzureWebJobsScriptRoot'] = '/home/site/wwwroot' - sys.path.extend([ - '/tmp/functions\\standby\\wwwroot', - '/home/site/wwwroot/.python_packages/lib/site-packages', - '/azure-functions-host/workers/python/3.11/LINUX/X64', - '/home/site/wwwroot' - ]) - DependencyManager.initialize() - self.assertEqual( - DependencyManager.cx_deps_path, - '/home/site/wwwroot/.python_packages/lib/site-packages' - ) - self.assertEqual( - DependencyManager.cx_working_dir, - '/home/site/wwwroot', - ) - self.assertEqual( - DependencyManager.worker_deps_path, - '/azure-functions-host/workers/python/3.11/LINUX/X64' - ) - - def test_initialize_in_linux_dedicated(self): - os.environ['AzureWebJobsScriptRoot'] = '/home/site/wwwroot' - sys.path.extend([ - '/home/site/wwwroot', - '/home/site/wwwroot/.python_packages/lib/site-packages', - '/azure-functions-host/workers/python/3.11/LINUX/X64' - ]) - DependencyManager.initialize() - self.assertEqual( - DependencyManager.cx_deps_path, - '/home/site/wwwroot/.python_packages/lib/site-packages' - ) - self.assertEqual( - DependencyManager.cx_working_dir, - '/home/site/wwwroot', - ) - self.assertEqual( - DependencyManager.worker_deps_path, - '/azure-functions-host/workers/python/3.11/LINUX/X64' - ) - - def test_initialize_in_windows_core_tools(self): - os.environ['AzureWebJobsScriptRoot'] = 'C:\\FunctionApp' - sys.path.extend([ - 'C:\\Users\\user\\AppData\\Roaming\\npm\\' - 'node_modules\\azure-functions-core-tools\\bin\\' - 'workers\\python\\3.11\\WINDOWS\\X64', - 'C:\\FunctionApp\\.venv38\\lib\\site-packages', - 'C:\\FunctionApp' - ]) - DependencyManager.initialize() - self.assertEqual( - DependencyManager.cx_deps_path, - 'C:\\FunctionApp\\.venv38\\lib\\site-packages' - ) - self.assertEqual( - DependencyManager.cx_working_dir, - 'C:\\FunctionApp', - ) - self.assertEqual( - DependencyManager.worker_deps_path, - 'C:\\Users\\user\\AppData\\Roaming\\npm\\node_modules\\' - 'azure-functions-core-tools\\bin\\workers\\python\\3.11\\WINDOWS' - '\\X64' - ) - - def test_get_cx_deps_path_in_no_script_root(self): - result = DependencyManager._get_cx_deps_path() - self.assertEqual(result, '') - - def test_get_cx_deps_path_in_script_root_no_sys_path(self): - os.environ['AzureWebJobsScriptRoot'] = '/home/site/wwwroot' - result = DependencyManager._get_cx_deps_path() - self.assertEqual(result, '') - - def test_get_cx_deps_path_in_script_root_with_sys_path_linux(self): - # Test for Python 3.7+ Azure Environment - sys.path.append('/home/site/wwwroot/.python_packages/sites/lib/' - 'site-packages/') - os.environ['AzureWebJobsScriptRoot'] = '/home/site/wwwroot' - result = DependencyManager._get_cx_deps_path() - self.assertEqual(result, '/home/site/wwwroot/.python_packages/sites/' - 'lib/site-packages/') - - def test_get_cx_deps_path_in_script_root_with_sys_path_windows(self): - # Test for Windows Core Tools Environment - sys.path.append('C:\\FunctionApp\\sites\\lib\\site-packages') - os.environ['AzureWebJobsScriptRoot'] = 'C:\\FunctionApp' - result = DependencyManager._get_cx_deps_path() - self.assertEqual(result, - 'C:\\FunctionApp\\sites\\lib\\site-packages') - - def test_get_cx_working_dir_no_script_root(self): - result = DependencyManager._get_cx_working_dir() - self.assertEqual(result, '') - - def test_get_cx_working_dir_with_script_root_linux(self): - # Test for Azure Environment - os.environ['AzureWebJobsScriptRoot'] = '/home/site/wwwroot' - result = DependencyManager._get_cx_working_dir() - self.assertEqual(result, '/home/site/wwwroot') - - def test_get_cx_working_dir_with_script_root_windows(self): - # Test for Windows Core Tools Environment - os.environ['AzureWebJobsScriptRoot'] = 'C:\\FunctionApp' - result = DependencyManager._get_cx_working_dir() - self.assertEqual(result, 'C:\\FunctionApp') - - @unittest.skipIf(os.environ.get('VIRTUAL_ENV'), - 'Test is not capable to run in a virtual environment') - def test_get_worker_deps_path_with_no_worker_sys_path(self): - result = DependencyManager._get_worker_deps_path() - azf_spec = importlib.util.find_spec('azure.functions') - worker_parent = os.path.abspath( - os.path.join(os.path.dirname(azf_spec.origin), '..', '..') - ) - self.assertEqual(result.lower(), worker_parent.lower()) - - def test_get_worker_deps_path_from_windows_core_tools(self): - # Test for Windows Core Tools Environment - sys.path.append('C:\\Users\\user\\AppData\\Roaming\\npm\\' - 'node_modules\\azure-functions-core-tools\\bin\\' - 'workers\\python\\3.11\\WINDOWS\\X64') - result = DependencyManager._get_worker_deps_path() - self.assertEqual(result, - 'C:\\Users\\user\\AppData\\Roaming\\npm\\' - 'node_modules\\azure-functions-core-tools\\bin\\' - 'workers\\python\\3.11\\WINDOWS\\X64') - - def test_get_worker_deps_path_from_linux_azure_environment(self): - # Test for Azure Environment - sys.path.append('/azure-functions-host/workers/python/3.11/LINUX/X64') - result = DependencyManager._get_worker_deps_path() - self.assertEqual(result, - '/azure-functions-host/workers/python/3.11/LINUX/X64') - - @patch('azure_functions_worker.utils.dependency.importlib.util') - def test_get_worker_deps_path_without_worker_path(self, mock): - # Test when worker path is not provided - mock.find_spec.return_value = None - sys.path.append('/home/site/wwwroot') - result = DependencyManager._get_worker_deps_path() - worker_parent = os.path.abspath( - os.path.join(os.path.dirname(__file__), '..', '..') - ) - self.assertEqual(result.lower(), worker_parent.lower()) - - def test_add_to_sys_path_add_to_first(self): - DependencyManager._add_to_sys_path(self._customer_deps_path, True) - self.assertEqual(sys.path[0], self._customer_deps_path) - - def test_add_to_sys_path_add_to_last(self): - DependencyManager._add_to_sys_path(self._customer_deps_path, False) - self.assertEqual(sys.path[-1], self._customer_deps_path) - - def test_add_to_sys_path_no_duplication(self): - DependencyManager._add_to_sys_path(self._customer_deps_path, True) - DependencyManager._add_to_sys_path(self._customer_deps_path, True) - path_count = len(list(filter( - lambda x: x == self._customer_deps_path, sys.path - ))) - self.assertEqual(path_count, 1) - - def test_add_to_sys_path_import_module(self): - DependencyManager._add_to_sys_path(self._customer_deps_path, True) - import common_module # NoQA - self.assertEqual( - common_module.package_location, - os.path.join(self._customer_deps_path, 'common_module') - ) - - def test_add_to_sys_path_import_namespace_path(self): - """Check if a common_namespace can be loaded after adding its path - into sys.path - """ - DependencyManager._add_to_sys_path(self._customer_deps_path, True) - import common_namespace # NoQA - self.assertEqual(len(common_namespace.__path__), 1) - self.assertEqual( - common_namespace.__path__[0], - os.path.join(self._customer_deps_path, 'common_namespace') - ) - - def test_add_to_sys_path_import_nested_module_in_namespace(self): - """Check if a nested module in a namespace can be imported correctly - """ - DependencyManager._add_to_sys_path(self._customer_deps_path, True) - import common_namespace.nested_module # NoQA - self.assertEqual(common_namespace.nested_module.__version__, 'customer') - - def test_add_to_sys_path_disallow_module_resolution_from_namespace(self): - """The standard Python import mechanism does not allow deriving a - specific module from a namespace without the import statement, e.g. - - import azure - azure.functions # Error: module 'azure' has not attribute 'functions' - """ - DependencyManager._add_to_sys_path(self._customer_deps_path, True) - import common_namespace # NoQA - with self.assertRaises(AttributeError): - common_namespace.nested_module - - def test_add_to_sys_path_allow_resolution_from_import_statement(self): - """The standard Python import mechanism allows deriving a specific - module in an import statement, e.g. - - from azure import functions # OK - """ - DependencyManager._add_to_sys_path(self._customer_deps_path, True) - from common_namespace import nested_module # NoQA - self.assertEqual(nested_module.__version__, 'customer') - - def test_add_to_sys_path_picks_latest_module_in_same_namespace(self): - """If a Linux Consumption function app is switching from placeholder to - specialized customer's app, the latest call of a nested module should - be picked from the most recently import namespace. - """ - DependencyManager._add_to_sys_path(self._worker_deps_path, True) - from common_namespace import nested_module # NoQA - self.assertEqual(nested_module.__version__, 'worker') - - # Now switch to customer's function app - DependencyManager._remove_from_sys_path(self._worker_deps_path) - DependencyManager._add_to_sys_path(self._customer_deps_path, True) - from common_namespace import nested_module # NoQA - self.assertEqual(nested_module.__version__, 'customer') - - def test_add_to_sys_path_importer_cache(self): - DependencyManager._add_to_sys_path(self._customer_deps_path, True) - import common_module # NoQA - self.assertIn(self._customer_deps_path, sys.path_importer_cache) - - def test_add_to_sys_path_importer_cache_reloaded(self): - # First import the common module from worker_deps_path - DependencyManager._add_to_sys_path(self._worker_deps_path, True) - import common_module # NoQA - self.assertIn(self._worker_deps_path, sys.path_importer_cache) - self.assertEqual( - common_module.package_location, - os.path.join(self._worker_deps_path, 'common_module') - ) - - # Mock that the customer's script are running in a different module - # (e.g. HttpTrigger/__init__.py) - del sys.modules['common_module'] - del common_module - - # Import the common module from customer_deps_path - # Customer should only see their own module - DependencyManager._add_to_sys_path(self._customer_deps_path, True) - import common_module # NoQA - self.assertIn(self._customer_deps_path, sys.path_importer_cache) - self.assertEqual( - common_module.package_location, - os.path.join(self._customer_deps_path, 'common_module') - ) - - def test_reload_all_modules_from_customer_deps(self): - """The test simulates a linux consumption environment where the worker - transits from placeholder mode to specialized worker with customer's - dependencies. First the worker will use worker's dependencies for its - own modules. After worker init request, it starts adding customer's - library path into sys.path (e.g. .python_packages/). The final step - is in environment reload where the worker is fully specialized, - reloading all libraries from customer's package. - """ - self._initialize_scenario() - - # Ensure the common_module is imported from _worker_deps_path - DependencyManager.use_worker_dependencies() - import common_module # NoQA - self.assertEqual( - common_module.package_location, - os.path.join(self._worker_deps_path, 'common_module') - ) - - # At placeholder specialization from function_environment_reload - DependencyManager.prioritize_customer_dependencies( - self._customer_func_path - ) - - # Now the module should be imported from customer dependency - import common_module # NoQA - self.assertIn(self._customer_deps_path, sys.path_importer_cache) - self.assertEqual( - common_module.package_location, - os.path.join(self._customer_deps_path, 'common_module') - ) - - # Check if the order matches expectation - self._assert_path_order(sys.path, [ - self._customer_deps_path, - self._worker_deps_path, - self._customer_func_path, - ]) - - def test_reload_all_namespaces_from_customer_deps(self): - """The test simulates a linux consumption environment where the worker - transits from placeholder mode to specialized mode. In a very typical - scenario, the nested azure.functions library (with common azure) - namespace needs to be switched from worker_deps to customer_Deps. - """ - self._initialize_scenario() - - # Ensure the nested_module is imported from _worker_deps_path - DependencyManager.use_worker_dependencies() - import common_namespace.nested_module # NoQA - self.assertEqual(common_namespace.nested_module.__version__, 'worker') - - # At placeholder specialization from function_environment_reload - DependencyManager.prioritize_customer_dependencies( - self._customer_func_path - ) - - # Now the nested_module should be imported from customer dependency - import common_namespace.nested_module # NoQA - self.assertIn(self._customer_deps_path, sys.path_importer_cache) - self.assertEqual( - common_namespace.__path__[0], - os.path.join(self._customer_deps_path, 'common_namespace') - ) - self.assertEqual(common_namespace.nested_module.__version__, 'customer') - - # Check if the order matches expectation - self._assert_path_order(sys.path, [ - self._customer_deps_path, - self._worker_deps_path, - self._customer_func_path, - ]) - - def test_remove_from_sys_path(self): - sys.path.append(self._customer_deps_path) - DependencyManager._remove_from_sys_path(self._customer_deps_path) - self.assertNotIn(self._customer_deps_path, sys.path) - - def test_remove_from_sys_path_should_remove_all_duplications(self): - sys.path.insert(0, self._customer_deps_path) - sys.path.append(self._customer_deps_path) - DependencyManager._remove_from_sys_path(self._customer_deps_path) - self.assertNotIn(self._customer_deps_path, sys.path) - - def test_remove_from_sys_path_should_remove_path_importer_cache(self): - # Import a common_module from customer deps will create a path finter - # cache in sys.path_importer_cache - sys.path.insert(0, self._customer_deps_path) - import common_module # NoQA - self.assertIn(self._customer_deps_path, sys.path_importer_cache) - - # Remove sys.path_importer_cache - DependencyManager._remove_from_sys_path(self._customer_deps_path) - self.assertNotIn(self._customer_deps_path, sys.path_importer_cache) - - def test_remove_from_sys_path_should_remove_related_module(self): - # Import a common_module from customer deps will create a module import - # cache in sys.module - sys.path.insert(0, self._customer_deps_path) - import common_module # NoQA - self.assertIn('common_module', sys.modules) - - # Remove sys.path_importer_cache - DependencyManager._remove_from_sys_path(self._customer_deps_path) - self.assertNotIn('common_module', sys.modules) - - def test_remove_from_sys_path_should_remove_related_namespace(self): - """When a namespace is imported, the sys.modules should cache it. - After calling the remove_from_sys_path, the namespace in sys.modules - cache should be removed. - """ - sys.path.insert(0, self._customer_deps_path) - import common_namespace # NoQA - self.assertIn('common_namespace', sys.modules) - - # Remove from sys.modules via _remove_from_sys_path - DependencyManager._remove_from_sys_path(self._customer_deps_path) - self.assertNotIn('common_namespace', sys.modules) - - def test_remove_from_sys_path_should_remove_nested_module(self): - """When a nested module is imported into a namespace, the sys.modules - should cache it. After calling the remove_from_sys_path, the nested - module should be removed from sys.modules - """ - sys.path.insert(0, self._customer_deps_path) - import common_namespace.nested_module # NoQA - self.assertIn('common_namespace.nested_module', sys.modules) - - # Remove from sys.modules via _remove_from_sys_path - DependencyManager._remove_from_sys_path(self._customer_deps_path) - self.assertNotIn('common_namespace.nested_module', sys.modules) - - def test_clear_path_importer_cache_and_modules(self): - # Ensure sys.path_importer_cache and sys.modules cache is cleared - sys.path.insert(0, self._customer_deps_path) - import common_module # NoQA - self.assertIn('common_module', sys.modules) - - # Clear out cache - DependencyManager._clear_path_importer_cache_and_modules( - self._customer_deps_path - ) - - # Ensure cache is cleared - self.assertNotIn('common_module', sys.modules) - - def test_clear_path_importer_cache_and_modules_reimport(self): - # First import common_module from _customer_deps_path - sys.path.insert(0, self._customer_deps_path) - import common_module # NoQA - self.assertIn('common_module', sys.modules) - self.assertEqual( - common_module.package_location, - os.path.join(self._customer_deps_path, 'common_module') - ) - - # Clean up cache - DependencyManager._clear_path_importer_cache_and_modules( - self._customer_deps_path - ) - self.assertNotIn('common_module', sys.modules) - - # Clean up namespace - del common_module - - # Try import common_module from _worker_deps_path - sys.path.insert(0, self._worker_deps_path) - - # Ensure new import is from _worker_deps_path - import common_module # NoQA - self.assertIn('common_module', sys.modules) - self.assertEqual( - common_module.package_location, - os.path.join(self._worker_deps_path, 'common_module') - ) - - def test_clear_path_importer_cache_and_modules_retain_namespace(self): - # First import common_module from _customer_deps_path as customer_mod - sys.path.insert(0, self._customer_deps_path) - import common_module as customer_mod # NoQA - self.assertIn('common_module', sys.modules) - self.assertEqual( - customer_mod.package_location, - os.path.join(self._customer_deps_path, 'common_module') - ) - - # Clean up cache - DependencyManager._clear_path_importer_cache_and_modules( - self._customer_deps_path - ) - self.assertNotIn('common_module', sys.modules) - - # Try import common_module from _worker_deps_path as worker_mod - sys.path.insert(0, self._worker_deps_path) - - # Ensure new import is from _worker_deps_path - import common_module as worker_mod # NoQA - self.assertIn('common_module', sys.modules) - self.assertEqual( - worker_mod.package_location, - os.path.join(self._worker_deps_path, 'common_module') - ) - - def test_use_worker_dependencies(self): - # Setup app settings - os.environ['PYTHON_ISOLATE_WORKER_DEPENDENCIES'] = 'true' - - # Setup paths - DependencyManager.worker_deps_path = self._worker_deps_path - DependencyManager.cx_deps_path = self._customer_deps_path - DependencyManager.cx_working_dir = self._customer_func_path - - # Ensure the common_module is imported from _worker_deps_path - DependencyManager.use_worker_dependencies() - import common_module # NoQA - self.assertEqual( - common_module.package_location, - os.path.join(self._worker_deps_path, 'common_module') - ) - - def test_use_worker_dependencies_disable(self): - # Setup app settings - os.environ['PYTHON_ISOLATE_WORKER_DEPENDENCIES'] = 'false' - - # Setup paths - DependencyManager.worker_deps_path = self._worker_deps_path - DependencyManager.cx_deps_path = self._customer_deps_path - DependencyManager.cx_working_dir = self._customer_func_path - - # The common_module cannot be imported since feature is disabled - DependencyManager.use_worker_dependencies() - with self.assertRaises(ImportError): - import common_module # NoQA - - def test_use_worker_dependencies_default_python_all_versions(self): - # Feature should be disabled for all python versions - # Setup paths - DependencyManager.worker_deps_path = self._worker_deps_path - DependencyManager.cx_deps_path = self._customer_deps_path - DependencyManager.cx_working_dir = self._customer_func_path - - # The common_module cannot be imported since feature is disabled - DependencyManager.use_worker_dependencies() - with self.assertRaises(ImportError): - import common_module # NoQA - - def test_prioritize_customer_dependencies(self): - # Setup app settings - os.environ['PYTHON_ISOLATE_WORKER_DEPENDENCIES'] = 'true' - - # Setup paths - DependencyManager.worker_deps_path = self._worker_deps_path - DependencyManager.cx_deps_path = self._customer_deps_path - DependencyManager.cx_working_dir = self._customer_func_path - - # Ensure the common_module is imported from _customer_deps_path - DependencyManager.prioritize_customer_dependencies() - import common_module # NoQA - self.assertEqual( - common_module.package_location, - os.path.join(self._customer_deps_path, 'common_module') - ) - - # Check if the sys.path order matches the expected order - self._assert_path_order(sys.path, [ - self._customer_deps_path, - self._worker_deps_path, - self._customer_func_path, - ]) - - def test_prioritize_customer_dependencies_disable(self): - # Setup app settings - os.environ['PYTHON_ISOLATE_WORKER_DEPENDENCIES'] = 'false' - - # Setup paths - DependencyManager.worker_deps_path = self._worker_deps_path - DependencyManager.cx_deps_path = self._customer_deps_path - DependencyManager.cx_working_dir = self._customer_func_path - - # Ensure the common_module is imported from _customer_deps_path - DependencyManager.prioritize_customer_dependencies() - with self.assertRaises(ImportError): - import common_module # NoQA - - def test_prioritize_customer_dependencies_default_all_versions(self): - # Feature should be disabled in Python for all versions - # Setup paths - DependencyManager.worker_deps_path = self._worker_deps_path - DependencyManager.cx_deps_path = self._customer_deps_path - DependencyManager.cx_working_dir = self._customer_func_path - - # Ensure the common_module is imported from _customer_deps_path - DependencyManager.prioritize_customer_dependencies() - with self.assertRaises(ImportError): - import common_module # NoQA - - def test_prioritize_customer_dependencies_from_working_directory(self): - self._initialize_scenario() - - # Setup paths - DependencyManager.worker_deps_path = self._worker_deps_path - DependencyManager.cx_deps_path = self._customer_deps_path - DependencyManager.cx_working_dir = self._customer_func_path - - # Ensure the func_specific_module is imported from _customer_func_path - DependencyManager.prioritize_customer_dependencies() - import func_specific_module # NoQA - self.assertEqual( - func_specific_module.package_location, - os.path.join(self._customer_func_path, 'func_specific_module') - ) - - def test_remove_module_cache(self): - # First import the common_module and create a sys.modules cache - sys.path.append(self._customer_deps_path) - import common_module # NoQA - self.assertIn('common_module', sys.modules) - - # Ensure the module cache will be remove - DependencyManager._remove_module_cache(self._customer_deps_path) - self.assertNotIn('common_module', sys.modules) - - def test_remove_module_cache_with_namespace_remain(self): - # Create common_module namespace - sys.path.append(self._customer_deps_path) - import common_module # NoQA - - # Ensure namespace remains after module cache is removed - DependencyManager._remove_module_cache(self._customer_deps_path) - self.assertIsNotNone(common_module) - - @unittest.skipIf(sys.version_info.minor > 7, - "The worker brings different protobuf versions" - "between 3.7 and 3.8+.") - def test_newrelic_protobuf_import_scenario_worker_deps_37(self): - # https://github.com/Azure/azure-functions-python-worker/issues/1339 - # newrelic checks if protobuf has been imported and based on the - # version it finds, imports a specific pb2 file. - - # PIWD = 0. protobuf is brought through the worker's deps. - os.environ['PYTHON_ISOLATE_WORKER_DEPENDENCIES'] = 'false' - - # Setup paths - DependencyManager.worker_deps_path = self._worker_deps_path - DependencyManager.cx_deps_path = self._customer_deps_path - DependencyManager.cx_working_dir = self._customer_func_path - - DependencyManager.prioritize_customer_dependencies() - - # protobuf v3 is found - from google.protobuf import __version__ - - protobuf_version = tuple(int(v) for v in __version__.split(".")) - self.assertIsNotNone(protobuf_version) - self.assertEqual(protobuf_version[0], 3) - - @unittest.skipIf(sys.version_info.minor <= 7, - "The worker brings different protobuf versions" - "between 3.7 and 3.8+.") - def test_newrelic_protobuf_import_scenario_worker_deps(self): - # https://github.com/Azure/azure-functions-python-worker/issues/1339 - # newrelic checks if protobuf has been imported and based on the - # version it finds, imports a specific pb2 file. - - # PIWD = 0. protobuf is brought through the worker's deps. - os.environ['PYTHON_ISOLATE_WORKER_DEPENDENCIES'] = 'false' - - # Setup paths - DependencyManager.worker_deps_path = self._worker_deps_path - DependencyManager.cx_deps_path = self._customer_deps_path - DependencyManager.cx_working_dir = self._customer_func_path - - DependencyManager.prioritize_customer_dependencies() - - # protobuf v4 is found - from google.protobuf import __version__ - - protobuf_version = tuple(int(v) for v in __version__.split(".")) - self.assertIsNotNone(protobuf_version) - self.assertEqual(protobuf_version[0], 4) - - @unittest.skipIf(sys.version_info.minor > 7, - "The worker brings different protobuf versions" - "between 3.7 and 3.8+.") - def test_newrelic_protobuf_import_scenario_user_deps_37(self): - # https://github.com/Azure/azure-functions-python-worker/issues/1339 - # newrelic checks if protobuf has been imported and based on the - # version it finds, imports a specific pb2 file. - - # PIWD = 1. protobuf is brought through the user's deps. - os.environ['PYTHON_ISOLATE_WORKER_DEPENDENCIES'] = 'true' - - # Setup paths - DependencyManager.worker_deps_path = self._worker_deps_path - DependencyManager.cx_deps_path = self._customer_deps_path - DependencyManager.cx_working_dir = self._customer_func_path - - DependencyManager.prioritize_customer_dependencies() - - # protobuf is found from worker deps, but newrelic won't find it - from google.protobuf import __version__ - - protobuf_version = tuple(int(v) for v in __version__.split(".")) - self.assertIsNotNone(protobuf_version) - - # newrelic tries to import protobuf v3 - self.assertEqual(protobuf_version[0], 3) - - # newrelic tries to import protobuf v4 - self.assertNotEqual(protobuf_version[0], 4) - - @unittest.skipIf(sys.version_info.minor <= 7, - "The worker brings different protobuf versions" - "between 3.7 and 3.8+.") - def test_newrelic_protobuf_import_scenario_user_deps(self): - # https://github.com/Azure/azure-functions-python-worker/issues/1339 - # newrelic checks if protobuf has been imported and based on the - # version it finds, imports a specific pb2 file. - - # PIWD = 1. protobuf is brought through the user's deps. - os.environ['PYTHON_ISOLATE_WORKER_DEPENDENCIES'] = 'true' - - # Setup paths - DependencyManager.worker_deps_path = self._worker_deps_path - DependencyManager.cx_deps_path = self._customer_deps_path - DependencyManager.cx_working_dir = self._customer_func_path - - DependencyManager.prioritize_customer_dependencies() - - # protobuf is found from worker deps, but newrelic won't find it - from google.protobuf import __version__ - - protobuf_version = tuple(int(v) for v in __version__.split(".")) - self.assertIsNotNone(protobuf_version) - - # newrelic tries to import protobuf v4 - self.assertEqual(protobuf_version[0], 4) - - # newrelic tries to import protobuf v3 - self.assertNotEqual(protobuf_version[0], 3) - - def _initialize_scenario(self): - # Setup app settings - os.environ['PYTHON_ISOLATE_WORKER_DEPENDENCIES'] = 'true' - os.environ['AzureWebJobsScriptRoot'] = '/home/site/wwwroot' - - # Setup paths - DependencyManager.worker_deps_path = self._worker_deps_path - DependencyManager.cx_deps_path = self._customer_deps_path - DependencyManager.cx_working_dir = self._customer_func_path - - def _assert_path_order(self, sys_paths, expected_order): - """Check if the path exist in sys_paths meets the path ordering in - expected_order. - """ - if not expected_order: - return - - next_check = 0 - for path in sys_paths: - if path == expected_order[next_check]: - next_check += 1 - - if next_check == len(expected_order): - break - - self.assertEqual( - next_check, len(expected_order), - 'The order in sys_paths does not match the expected_order paths' - ) diff --git a/tests/unittests/third_party_http_functions/stein/asgi_function/function_app.py b/tests/unittests/third_party_http_functions/stein/asgi_function/function_app.py deleted file mode 100644 index da76f0714..000000000 --- a/tests/unittests/third_party_http_functions/stein/asgi_function/function_app.py +++ /dev/null @@ -1,173 +0,0 @@ -import asyncio -import logging -import sys -from urllib.request import urlopen -import base64 - -import azure.functions as func -from fastapi import FastAPI, Request, Response - -fast_app = FastAPI() -logger = logging.getLogger("my-function") -# 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') - - -@fast_app.get("/debug_logging") -async def debug_logging(): - 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 Response(content='OK-debug', media_type="text/plain") - - -@fast_app.get("/debug_user_logging") -async def debug_user_logging(): - logger.setLevel(logging.DEBUG) - - logger.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 Response(content='OK-user-debug', media_type="text/plain") - - -@fast_app.get("/hijack_current_event_loop") -async def hijack_current_event_loop(): - 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 Response(content='OK-hijack-current-event-loop', - media_type="text/plain") - - -@fast_app.get("/print_logging") -async def print_logging(message: str = "", flush: str = 'false', - console: str = 'false', is_stderr: str = 'false'): - flush_required = False - is_console_log = False - is_stderr = False - - if flush == 'true': - flush_required = True - if console == 'true': - is_console_log = True - if 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 Response(content='OK-print-logging', media_type="text/plain") - - -@fast_app.post("/raw_body_bytes") -async def raw_body_bytes(request: Request): - body = await request.body() - - base64_encoded = base64.b64encode(body).decode('utf-8') - html_img_tag = \ - f'PNG Image' - - return Response(html_img_tag, headers={'body-len': str(len(html_img_tag))}) - - -@fast_app.get("/return_http_no_body") -async def return_http_no_body(): - return Response(content='', media_type="text/plain") - - -@fast_app.get("/return_http") -async def return_http(request: Request): - return Response('

    Hello Worldâ„¢

    ', media_type='text/html') - - -@fast_app.get("/unhandled_error") -async def unhandled_error(): - 1 / 0 - - -@fast_app.get("/unhandled_urllib_error") -async def unhandled_urllib_error(img: str = ''): - urlopen(img).read() - - -class UnserializableException(Exception): - def __str__(self): - raise RuntimeError('cannot serialize me') - - -@fast_app.get("/unhandled_unserializable_error") -async def unhandled_unserializable_error(): - raise UnserializableException('foo') - - -app = func.AsgiFunctionApp(app=fast_app, - http_auth_level=func.AuthLevel.ANONYMOUS) diff --git a/tests/unittests/third_party_http_functions/stein/wsgi_function/function_app.py b/tests/unittests/third_party_http_functions/stein/wsgi_function/function_app.py deleted file mode 100644 index 3d2f63d91..000000000 --- a/tests/unittests/third_party_http_functions/stein/wsgi_function/function_app.py +++ /dev/null @@ -1,96 +0,0 @@ -import logging -import sys -from urllib.request import urlopen - -import azure.functions as func -from flask import Flask, Response, redirect, request, url_for - -flask_app = Flask(__name__) -logger = logging.getLogger("my-function") - - -@flask_app.get("/debug_logging") -def debug_logging(): - 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' - - -@flask_app.get("/debug_user_logging") -def debug_user_logging(): - logger.setLevel(logging.DEBUG) - - logger.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' - - -@flask_app.get("/print_logging") -def print_logging(): - flush_required = False - is_console_log = False - is_stderr = False - - message = request.args.get("message", '') - - if request.args.get("flush") == 'true': - flush_required = True - if request.args.get("console") == 'true': - is_console_log = True - if request.args.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' - - -@flask_app.get("/return_http_no_body") -def return_http_no_body(): - return '' - - -@flask_app.get("/return_http") -def return_http(): - return Response('

    Hello Worldâ„¢

    ', mimetype='text/html') - - -@flask_app.get("/return_http_redirect") -def return_http_redirect(code: str = ''): - return redirect(url_for('return_http')) - - -@flask_app.get("/unhandled_error") -def unhandled_error(): - 1 / 0 - - -@flask_app.get("/unhandled_urllib_error") -def unhandled_urllib_error(img: str = ''): - urlopen(img).read() - - -class UnserializableException(Exception): - def __str__(self): - raise RuntimeError('cannot serialize me') - - -@flask_app.get("/unhandled_unserializable_error") -def unhandled_unserializable_error(): - raise UnserializableException('foo') - - -app = func.WsgiFunctionApp(app=flask_app.wsgi_app, - http_auth_level=func.AuthLevel.ANONYMOUS) diff --git a/tests/unittests/timer_functions/return_pastdue/function.json b/tests/unittests/timer_functions/return_pastdue/function.json deleted file mode 100644 index 95c9914ff..000000000 --- a/tests/unittests/timer_functions/return_pastdue/function.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "timerTrigger", - "direction": "in", - "name": "timer", - "schedule": "*/5 * * * * *" - }, - { - "direction": "out", - "name": "pastdue", - "type": "http" - } - ] -} diff --git a/tests/unittests/timer_functions/return_pastdue/main.py b/tests/unittests/timer_functions/return_pastdue/main.py deleted file mode 100644 index d272f4982..000000000 --- a/tests/unittests/timer_functions/return_pastdue/main.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import azure.functions as azf - - -def main(timer: azf.TimerRequest, pastdue: azf.Out[str]): - pastdue.set(str(timer.past_due)) diff --git a/tests/unittests/timer_functions/user_event_loop_timer/function.json b/tests/unittests/timer_functions/user_event_loop_timer/function.json deleted file mode 100644 index 27de92aab..000000000 --- a/tests/unittests/timer_functions/user_event_loop_timer/function.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "scriptFile": "main.py", - "bindings": [ - { - "type": "timerTrigger", - "direction": "in", - "name": "timer", - "schedule": "*/5 * * * * *" - } - ] -} diff --git a/tests/unittests/timer_functions/user_event_loop_timer/main.py b/tests/unittests/timer_functions/user_event_loop_timer/main.py deleted file mode 100644 index 0bb039078..000000000 --- a/tests/unittests/timer_functions/user_event_loop_timer/main.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import asyncio -import logging - -import azure.functions as func - -logger = logging.getLogger('my function') - - -async def try_log(): - logger.info("try_log") - - -def main(timer: func.TimerRequest): - loop = asyncio.SelectorEventLoop() - asyncio.set_event_loop(loop) - loop.run_until_complete(try_log()) - loop.close() diff --git a/tests/utils/constants.py b/tests/utils/constants.py deleted file mode 100644 index 34c262f20..000000000 --- a/tests/utils/constants.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import pathlib - -# Extensions necessary for non-core bindings. -EXTENSIONS_CSPROJ_TEMPLATE = """\ - - - - net8.0 - - ** - - - - - - - - - - - - - - - - - - -""" - -NUGET_CONFIG = """\ - - - - - - - - - - -""" - -# PROJECT_ROOT refers to the path to azure-functions-python-worker -# TODO: Find root folder without .parent -PROJECT_ROOT = pathlib.Path(__file__).parent.parent.parent -TESTS_ROOT = PROJECT_ROOT / 'tests' -WORKER_CONFIG = PROJECT_ROOT / '.testconfig' - -# E2E Integration Flags and Configurations -PYAZURE_INTEGRATION_TEST = "PYAZURE_INTEGRATION_TEST" -PYAZURE_WORKER_DIR = "PYAZURE_WORKER_DIR" - -# Debug Flags -PYAZURE_WEBHOST_DEBUG = "PYAZURE_WEBHOST_DEBUG" -ARCHIVE_WEBHOST_LOGS = "ARCHIVE_WEBHOST_LOGS" - -# CI test constants -CONSUMPTION_DOCKER_TEST = "CONSUMPTION_DOCKER_TEST" -DEDICATED_DOCKER_TEST = "DEDICATED_DOCKER_TEST" diff --git a/tests/utils/testutils.py b/tests/utils/testutils.py index c04b134c5..de229fc3e 100644 --- a/tests/utils/testutils.py +++ b/tests/utils/testutils.py @@ -6,118 +6,13 @@ and can be changed without a notice. """ -import argparse import asyncio -import concurrent.futures -import configparser import functools import inspect -import json -import logging -import os -import pathlib -import platform -import queue -import random -import re -import shutil -import socket -import string -import subprocess -import sys -import tempfile -import time -import typing import unittest -import uuid - -import grpc -import requests -from tests.utils.constants import ( - ARCHIVE_WEBHOST_LOGS, - CONSUMPTION_DOCKER_TEST, - DEDICATED_DOCKER_TEST, - EXTENSIONS_CSPROJ_TEMPLATE, - PROJECT_ROOT, - PYAZURE_INTEGRATION_TEST, - PYAZURE_WEBHOST_DEBUG, - PYAZURE_WORKER_DIR, - WORKER_CONFIG, -) -from tests.utils.testutils_docker import ( - DockerConfigs, - WebHostConsumption, - WebHostDedicated, -) - -from azure_functions_worker import dispatcher, protos -from azure_functions_worker.bindings.shared_memory_data_transfer import ( - FileAccessorFactory, -) -from azure_functions_worker.bindings.shared_memory_data_transfer import ( - SharedMemoryConstants as consts, -) -from azure_functions_worker.constants import ( - FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED, - UNIX_SHARED_MEMORY_DIRECTORIES, -) -from azure_functions_worker.utils.common import get_app_setting, is_envvar_true - -TESTS_ROOT = PROJECT_ROOT / 'tests' -E2E_TESTS_FOLDER = pathlib.Path('endtoend') -E2E_TESTS_ROOT = TESTS_ROOT / E2E_TESTS_FOLDER -UNIT_TESTS_FOLDER = pathlib.Path('unittests') -UNIT_TESTS_ROOT = TESTS_ROOT / UNIT_TESTS_FOLDER -EMULATOR_TESTS_FOLDER = pathlib.Path('emulator_tests') -EXTENSION_TESTS_FOLDER = pathlib.Path('extension_tests') -WEBHOST_DLL = "Microsoft.Azure.WebJobs.Script.WebHost.dll" -DEFAULT_WEBHOST_DLL_PATH = ( - PROJECT_ROOT / 'build' / 'webhost' / 'bin' / WEBHOST_DLL -) -EXTENSIONS_PATH = PROJECT_ROOT / 'build' / 'extensions' / 'bin' -FUNCS_PATH = TESTS_ROOT / UNIT_TESTS_FOLDER / 'http_functions' -WORKER_PATH = PROJECT_ROOT / 'python' / 'test' -ON_WINDOWS = platform.system() == 'Windows' -LOCALHOST = "127.0.0.1" - -# The template of host.json that will be applied to each test functions -HOST_JSON_TEMPLATE = """\ -{ - "version": "2.0", - "logging": {"logLevel": {"default": "Trace"}} -} -""" - -SECRETS_TEMPLATE = """\ -{ - "masterKey": { - "name": "master", - "value": "testMasterKey", - "encrypted": false - }, - "functionKeys": [ - { - "name": "default", - "value": "testFunctionKey", - "encrypted": false - } - ], - "systemKeys": [ - { - "name": "eventgridextensionconfig_extension", - "value": "testSystemKey", - "encrypted": false - } - ], - "hostName": null, - "instanceId": "0000000000000000000000001C69C103", - "source": "runtime" -} -""" class AsyncTestCaseMeta(type(unittest.TestCase)): - def __new__(mcls, name, bases, ns): for attrname, attr in ns.items(): if (attrname.startswith('test_') @@ -137,949 +32,3 @@ def wrapper(*args, **kwargs): class AsyncTestCase(unittest.TestCase, metaclass=AsyncTestCaseMeta): pass - - -class WebHostTestCaseMeta(type(unittest.TestCase)): - - def __new__(mcls, name, bases, dct): - if is_envvar_true(DEDICATED_DOCKER_TEST) \ - or is_envvar_true(CONSUMPTION_DOCKER_TEST): - return super().__new__(mcls, name, bases, dct) - - for attrname, attr in dct.items(): - if attrname.startswith('test_') and callable(attr): - test_case = attr - check_log_name = attrname.replace('test_', 'check_log_', 1) - check_log_case = dct.get(check_log_name) - - @functools.wraps(test_case) - def wrapper(self, *args, __meth__=test_case, - __check_log__=check_log_case, **kwargs): - if (__check_log__ is not None - and callable(__check_log__) - and not is_envvar_true(PYAZURE_WEBHOST_DEBUG)): - - # Check logging output for unit test scenarios - result = self._run_test(__meth__, *args, **kwargs) - - # Trim off host output timestamps - host_output = getattr(self, 'host_out', '') - output_lines = host_output.splitlines() - ts_re = r"^\[\d+(\/|-)\d+(\/|-)\d+T*\d+\:\d+\:\d+.*(" \ - r"A|P)*M*\]" - output = list(map(lambda s: - re.sub(ts_re, '', s).strip(), - output_lines)) - - # Execute check_log_ test cases - self._run_test(__check_log__, host_out=output) - return result - else: - # Check normal unit test - return self._run_test(__meth__, *args, **kwargs) - - dct[attrname] = wrapper - - return super().__new__(mcls, name, bases, dct) - - -class WebHostTestCase(unittest.TestCase, metaclass=WebHostTestCaseMeta): - """Base class for integration tests that need a WebHost. - - In addition to automatically starting up a WebHost instance, - this test case class logs WebHost stdout/stderr in case - a unit test fails. - - You can write two sets of test - test_* and check_log_* tests. - - test_ABC - Unittest - check_log_ABC - Check logs generated during the execution of test_ABC. - """ - host_stdout_logger = logging.getLogger('webhosttests') - env_variables = {} - - @classmethod - def get_script_dir(cls): - raise NotImplementedError - - @classmethod - def get_libraries_to_install(cls) -> typing.List: - pass - - @classmethod - def get_environment_variables(cls): - pass - - @classmethod - def docker_tests_enabled(self) -> (bool, str): - """ - Returns True if the environment variables - CONSUMPTION_DOCKER_TEST or DEDICATED_DOCKER_TEST - is enabled else returns False - """ - if is_envvar_true(CONSUMPTION_DOCKER_TEST): - return True, CONSUMPTION_DOCKER_TEST - elif is_envvar_true(DEDICATED_DOCKER_TEST): - return True, DEDICATED_DOCKER_TEST - else: - return False, None - - @classmethod - def setUpClass(cls): - script_dir = pathlib.Path(cls.get_script_dir()) - is_unit_test = True if 'unittests' in script_dir.parts else False - - docker_tests_enabled, sku = cls.docker_tests_enabled() - - cls.host_stdout = None if is_envvar_true(PYAZURE_WEBHOST_DEBUG) \ - else tempfile.NamedTemporaryFile('w+t') - - try: - if docker_tests_enabled: - docker_configs = DockerConfigs( - script_path=script_dir, - libraries=cls.get_libraries_to_install(), - env=cls.get_environment_variables() or {}) - if sku == CONSUMPTION_DOCKER_TEST: - cls.webhost = \ - WebHostConsumption(docker_configs).spawn_container() - elif sku == DEDICATED_DOCKER_TEST: - cls.webhost = \ - WebHostDedicated(docker_configs).spawn_container() - else: - _setup_func_app(TESTS_ROOT / script_dir, is_unit_test) - try: - cls.webhost = start_webhost(script_dir=script_dir, - stdout=cls.host_stdout) - except Exception: - raise - - if not cls.webhost.is_healthy() and cls.host_stdout is not None: - cls.host_out = cls.host_stdout.read() - if cls.host_out is not None and len(cls.host_out) > 0: - error_message = 'WebHost is not started correctly.' - f'{cls.host_stdout.name}: {cls.host_out}' - 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.tearDownClass() - raise - - @classmethod - def tearDownClass(cls): - cls.webhost.close() - cls.webhost = None - - if cls.host_stdout is not None: - if is_envvar_true(ARCHIVE_WEBHOST_LOGS): - cls.host_stdout.seek(0) - content = cls.host_stdout.read() - if content is not None and len(content) > 0: - version_info = sys.version_info - log_file = ( - "logs/" - f"{cls.__module__}_{cls.__name__}" - f"{version_info.minor}_webhost.log" - ) - with open(log_file, 'w+') as file: - file.write(content) - cls.host_stdout_logger.info("WebHost log is archived to" - f"{log_file} in the artifact") - - cls.host_stdout.close() - cls.host_stdout = None - - script_dir = pathlib.Path(cls.get_script_dir()) - _teardown_func_app(TESTS_ROOT / script_dir) - - def _run_test(self, test, *args, **kwargs): - if self.host_stdout is None: - test(self, *args, **kwargs) - else: - # Discard any host stdout left from the previous test or - # from the setup. - self.host_stdout.read() - last_pos = self.host_stdout.tell() - - test_exception = None - try: - test(self, *args, **kwargs) - except Exception as e: - test_exception = e - finally: - try: - self.host_stdout.seek(last_pos) - self.host_out = self.host_stdout.read() - if self.host_out is not None and len(self.host_out) > 0: - self.host_stdout_logger.error( - 'Captured WebHost log generated during test ' - '%s from %s :\n%s', test.__name__, - self.host_stdout.name, self.host_out) - finally: - if test_exception is not None: - raise test_exception - - -class SharedMemoryTestCase(unittest.TestCase): - """ - For tests involving shared memory data transfer usage. - """ - - def setUp(self): - self.was_shmem_env_true = is_envvar_true( - FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED) - os.environ.update( - {FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED: '1'}) - - os_name = platform.system() - if os_name == 'Darwin': - # If an existing AppSetting is specified, save it so it can be - # restored later - self.was_shmem_dirs = get_app_setting( - UNIX_SHARED_MEMORY_DIRECTORIES - ) - self._setUpDarwin() - elif os_name == 'Linux': - self._setUpLinux() - self.file_accessor = FileAccessorFactory.create_file_accessor() - - def tearDown(self): - os_name = platform.system() - if os_name == 'Darwin': - self._tearDownDarwin() - if self.was_shmem_dirs is not None: - # If an AppSetting was set before the tests ran, restore it back - os.environ.update( - {UNIX_SHARED_MEMORY_DIRECTORIES: self.was_shmem_dirs}) - elif os_name == 'Linux': - self._tearDownLinux() - - if not self.was_shmem_env_true: - os.environ.update( - {FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED: '0'}) - - def get_new_mem_map_name(self): - return str(uuid.uuid4()) - - def get_random_bytes(self, num_bytes): - return bytearray(random.getrandbits(8) for _ in range(num_bytes)) - - def get_random_string(self, num_chars): - return ''.join(random.choices(string.ascii_uppercase + string.digits, - k=num_chars)) - - def is_valid_uuid(self, uuid_to_test: str, version: int = 4) -> bool: - """ - Check if uuid_to_test is a valid UUID. - Reference: https://stackoverflow.com/a/33245493/3132415 - """ - try: - uuid_obj = uuid.UUID(uuid_to_test, version=version) - except ValueError: - return False - return str(uuid_obj) == uuid_to_test - - def _createSharedMemoryDirectories(self, directories): - for temp_dir in directories: - temp_dir_path = os.path.join(temp_dir, consts.UNIX_TEMP_DIR_SUFFIX) - if not os.path.exists(temp_dir_path): - os.makedirs(temp_dir_path) - - def _deleteSharedMemoryDirectories(self, directories): - for temp_dir in directories: - temp_dir_path = os.path.join(temp_dir, consts.UNIX_TEMP_DIR_SUFFIX) - shutil.rmtree(temp_dir_path) - - def _setUpLinux(self): - self._createSharedMemoryDirectories(consts.UNIX_TEMP_DIRS) - - def _tearDownLinux(self): - self._deleteSharedMemoryDirectories(consts.UNIX_TEMP_DIRS) - - def _setUpDarwin(self): - """ - Create a RAM disk on macOS. - Ref: https://stackoverflow.com/a/2033417/3132415 - """ - size_in_mb = consts.MAX_BYTES_FOR_SHARED_MEM_TRANSFER / (1024 * 1024) - size = 2048 * size_in_mb - # The following command returns the name of the created disk - cmd = ['hdiutil', 'attach', '-nomount', f'ram://{size}'] - result = subprocess.run(cmd, stdout=subprocess.PIPE) - if result.returncode != 0: - raise IOError(f'Cannot create ram disk with command: {cmd} - ' - f'{result.stdout} - {result.stderr}') - disk_name = result.stdout.strip().decode() - # We create a volume on the disk created above and mount it - volume_name = 'shm' - cmd = ['diskutil', 'eraseVolume', 'HFS+', volume_name, disk_name] - result = subprocess.run(cmd, stdout=subprocess.PIPE) - if result.returncode != 0: - raise IOError(f'Cannot create volume with command: {cmd} - ' - f'{result.stdout} - {result.stderr}') - directory = f'/Volumes/{volume_name}' - self.created_directories = [directory] - # Create directories in the volume for shared memory maps - self._createSharedMemoryDirectories(self.created_directories) - # Override the AppSetting for the duration of this test so the - # FileAccessorUnix can use these directories for creating memory maps - os.environ.update( - {UNIX_SHARED_MEMORY_DIRECTORIES: ','.join(self.created_directories)} - ) - - def _tearDownDarwin(self): - # Delete the directories containing shared memory maps - self._deleteSharedMemoryDirectories(self.created_directories) - # Unmount the volume used for shared memory maps - volume_name = 'shm' - cmd = f"find /Volumes -type d -name '{volume_name}*' -print0 " \ - "| xargs -0 umount -f" - result = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True) - if result.returncode != 0: - raise IOError(f'Cannot delete volume with command: {cmd} - ' - f'{result.stdout} - {result.stderr}') - - -class _MockWebHostServicer(protos.FunctionRpcServicer): - _STOP = object() - - def __init__(self, host): - self._host = host - - def EventStream(self, client_response_iterator, context): - client_response = next(client_response_iterator) - rtype = client_response.WhichOneof('content') - try: - if rtype != 'start_stream': - raise AssertionError( - f'unexpected {rtype!r} initial message from the worker') - - if client_response.start_stream.worker_id != self._host.worker_id: - raise AssertionError('worker_id mismatch') - - except Exception as ex: - self._host._loop.call_soon_threadsafe( - self._host._connected_fut.set_exception, ex) - return - else: - self._host._loop.call_soon_threadsafe( - self._host._connected_fut.set_result, True) - - while True: - message, wait_for = self._host._in_queue.get() - if message is self._STOP: - return - - yield message - - if wait_for is None: - continue - - response = None - logs = [] - - for client_response in client_response_iterator: - rtype = client_response.WhichOneof('content') - unpacked = getattr(client_response, rtype) - - if rtype == wait_for: - response = unpacked - break - elif rtype == 'rpc_log': - logs.append(unpacked) - else: - raise RuntimeError( - f'unexpected response from worker: ' - f'expected to receive {wait_for!r}, got {rtype!r}') - - self._host._loop.call_soon_threadsafe( - self._host._out_aqueue.put_nowait, - _WorkerResponseMessages(response, logs)) - - -class _WebHostFunction(typing.NamedTuple): - id: str - name: str - desc: dict - script: pathlib.Path - - -class _WorkerResponseMessages(typing.NamedTuple): - response: object - logs: list - - -class _MockWebHost: - - def __init__(self, loop, scripts_dir): - self._loop = loop - self._scripts_dir = scripts_dir - - self._available_functions = {} - self._read_available_functions() - - self._connected_fut = loop.create_future() - self._in_queue = queue.Queue() - self._out_aqueue = asyncio.Queue() - self._threadpool = concurrent.futures.ThreadPoolExecutor(max_workers=1) - self._server = grpc.server(self._threadpool) - self._servicer = _MockWebHostServicer(self) - - protos.add_FunctionRpcServicer_to_server(self._servicer, self._server) - self._port = self._server.add_insecure_port(f'{LOCALHOST}:0') - self._worker_id = self.make_id() - self._request_id = self.make_id() - - def make_id(self): - return str(uuid.uuid4()) - - @property - def worker_id(self): - return self._worker_id - - @property - def request_id(self): - return self._request_id - - 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 - ) - ), - wait_for='worker_init_response' - ) - - return r - - async def get_functions_metadata(self): - r = await self.communicate( - protos.StreamingMessage( - functions_metadata_request=protos.FunctionsMetadataRequest( - function_app_directory=str(self._scripts_dir) - ) - ), - wait_for='function_metadata_response' - ) - - return r - - async def load_function(self, name): - if name not in self._available_functions: - raise RuntimeError(f'cannot load function {name}') - - func = self._available_functions[name] - - bindings = {} - for b in func.desc['bindings']: - direction = getattr(protos.BindingInfo, b['direction']) - - data_type_v = b.get('dataType') - if not data_type_v: - data_type = protos.BindingInfo.undefined - elif data_type_v == 'binary': - data_type = protos.BindingInfo.binary - elif data_type_v == 'string': - data_type = protos.BindingInfo.string - elif data_type_v == 'stream': - data_type = protos.BindingInfo.stream - else: - raise RuntimeError(f'invalid dataType: {data_type_v!r}') - - bindings[b['name']] = protos.BindingInfo( - type=b['type'], - data_type=data_type, - direction=direction) - - r = await self.communicate( - protos.StreamingMessage( - function_load_request=protos.FunctionLoadRequest( - function_id=func.id, - metadata=protos.RpcFunctionMetadata( - name=func.name, - directory=os.path.dirname(func.script), - script_file=func.script, - bindings=bindings))), - wait_for='function_load_response') - - return func.id, r - - async def invoke_function( - self, - name, - input_data: typing.List[protos.ParameterBinding], - metadata: typing.Optional[ - typing.Mapping[str, protos.TypedData]] = None): - - if metadata is None: - metadata = {} - - if name not in self._available_functions: - raise RuntimeError(f'cannot load function {name}') - - func = self._available_functions[name] - invocation_id = self.make_id() - - r = await self.communicate( - protos.StreamingMessage( - invocation_request=protos.InvocationRequest( - invocation_id=invocation_id, - function_id=func.id, - input_data=input_data, - trigger_metadata=metadata, - ) - ), - wait_for='invocation_response') - - return invocation_id, r - - async def close_shared_memory_resources( - self, - map_names: typing.List[str]): - - request = protos.CloseSharedMemoryResourcesRequest( - map_names=map_names) - - r = await self.communicate( - protos.StreamingMessage( - close_shared_memory_resources_request=request - ), - wait_for='close_shared_memory_resources_response') - - return r - - async def reload_environment( - self, - environment: typing.Dict[str, str], - function_project_path: str = '/home/site/wwwroot' - ) -> protos.FunctionEnvironmentReloadResponse: - - request_content = protos.FunctionEnvironmentReloadRequest( - function_app_directory=function_project_path, - environment_variables={ - k.encode(): v.encode() for k, v in environment.items() - } - ) - - r = await self.communicate( - protos.StreamingMessage( - function_environment_reload_request=request_content - ), - wait_for='function_environment_reload_response' - ) - - return r - - async def get_worker_status(self): - r = await self.communicate( - protos.StreamingMessage( - worker_status_request=protos.WorkerStatusRequest() - ), - wait_for='worker_status_response' - ) - - return r - - async def send(self, message): - self._in_queue.put_nowait((message, None)) - - async def communicate(self, message, *, wait_for): - self._in_queue.put_nowait((message, wait_for)) - return await self._out_aqueue.get() - - async def start(self): - self._server.start() - - async def close(self): - self._in_queue.put_nowait((_MockWebHostServicer._STOP, None)) - self._server.stop(1) - - def _read_available_functions(self): - for fd in self._scripts_dir.iterdir(): - if not fd.is_dir(): - continue - - fjson_fn = fd / 'function.json' - if not fjson_fn.exists(): - continue - - try: - with open(fjson_fn, 'rt') as f: - fjson = json.loads(f.read()) - - fscript = fjson['scriptFile'] - fscript_fn = fd / fscript - if not fscript_fn.exists(): - raise RuntimeError(f'{fscript_fn} path does not exist') - - except Exception as ex: - raise RuntimeError( - f'could not load function {fd.name}') from ex - - fn = _WebHostFunction( - name=fd.name, desc=fjson, script=str(fscript_fn), - id=self.make_id()) - - self._available_functions[fn.name] = fn - - -class _MockWebHostController: - - def __init__(self, scripts_dir: pathlib.PurePath): - self._host: typing.Optional[_MockWebHost] = None - self._scripts_dir: pathlib.PurePath = scripts_dir - self._worker: typing.Optional[dispatcher.Dispatcher] = None - - async def __aenter__(self) -> _MockWebHost: - loop = asyncio.get_running_loop() - self._host = _MockWebHost(loop, self._scripts_dir) - - await self._host.start() - - self._worker = await dispatcher. \ - Dispatcher.connect(LOCALHOST, self._host._port, - self._host.worker_id, self._host.request_id, - connect_timeout=5.0) - - self._worker_task = loop.create_task(self._worker.dispatch_forever()) - - done, pending = await asyncio. \ - wait([self._host._connected_fut, self._worker_task], - return_when=asyncio.FIRST_COMPLETED) - - # noinspection PyBroadException - try: - if self._worker_task in done: - self._worker_task.result() - - if self._host._connected_fut not in done: - raise RuntimeError('could not start a worker thread') - except Exception: - try: - await self._host.close() - self._worker.stop() - finally: - raise - - return self._host - - async def __aexit__(self, *exc): - if not self._worker_task.done(): - self._worker_task.cancel() - try: - await self._worker_task - except asyncio.CancelledError: - pass - - self._worker_task = None - self._worker = None - - await self._host.close() - self._host = None - - -def start_mockhost(*, script_root=FUNCS_PATH): - scripts_dir = TESTS_ROOT / script_root - if not (scripts_dir.exists() and scripts_dir.is_dir()): - raise RuntimeError( - f'invalid script_root argument: ' - f'{scripts_dir} directory does not exist') - - sys.path.append(str(scripts_dir)) - - return _MockWebHostController(scripts_dir) - - -class _WebHostProxy: - - def __init__(self, proc, addr): - self._proc = proc - self._addr = addr - - def is_healthy(self): - r = self.request('GET', '', no_prefix=True) - return 200 <= r.status_code < 300 - - def request(self, meth, funcname, *args, **kwargs): - request_method = getattr(requests, meth.lower()) - params = dict(kwargs.pop('params', {})) - no_prefix = kwargs.pop('no_prefix', False) - if 'code' not in params: - params['code'] = 'testFunctionKey' - - return request_method( - self._addr + ('/' if no_prefix else '/api/') + funcname, - *args, params=params, **kwargs) - - def close(self): - if self._proc.stdout: - self._proc.stdout.close() - if self._proc.stderr: - self._proc.stderr.close() - - self._proc.terminate() - try: - self._proc.wait(20) - except subprocess.TimeoutExpired: - self._proc.kill() - - -def _find_open_port(): - with socket.socket() as s: - s.bind((LOCALHOST, 0)) - s.listen(1) - return s.getsockname()[1] - - -def popen_webhost(*, stdout, stderr, script_root=FUNCS_PATH, port=None): - testconfig = None - if WORKER_CONFIG.exists(): - testconfig = configparser.ConfigParser() - testconfig.read(WORKER_CONFIG) - - hostexe_args = [] - - # If we want to use core-tools - coretools_exe = os.environ.get('CORE_TOOLS_EXE_PATH') - if coretools_exe: - coretools_exe = coretools_exe.strip() - if pathlib.Path(coretools_exe).exists(): - hostexe_args = [str(coretools_exe), 'host', 'start', '--verbose'] - if port is not None: - hostexe_args.extend(['--port', str(port)]) - - # If we need to use Functions host directly - if not hostexe_args: - dll = os.environ.get('PYAZURE_WEBHOST_DLL') - if not dll and testconfig and testconfig.has_section('webhost'): - dll = testconfig['webhost'].get('dll') - - if dll: - # Paths from environment might contain trailing - # or leading whitespace. - dll = dll.strip() - - if not dll: - dll = DEFAULT_WEBHOST_DLL_PATH - - os.makedirs(dll.parent / 'Secrets', exist_ok=True) - with open(dll.parent / 'Secrets' / 'host.json', 'w') as f: - secrets = SECRETS_TEMPLATE - - f.write(secrets) - - if dll and pathlib.Path(dll).exists(): - hostexe_args = ['dotnet', str(dll)] - - if not hostexe_args: - raise RuntimeError('\n'.join([ - 'Unable to locate Azure Functions Host binary.', - 'Please do one of the following:', - ' * run the following command from the root folder of', - ' the project:', - '', - f'cd tests && $ {sys.executable} -m invoke -c test_setup webhost', - '', - ' * or download or build the Azure Functions Host and' - ' then write the full path to WebHost.dll' - ' into the `PYAZURE_WEBHOST_DLL` environment variable.', - ' Alternatively, you can create the', - f' {WORKER_CONFIG.name} file in the root folder', - ' of the project with the following structure:', - '', - ' [webhost]', - ' dll = /path/Microsoft.Azure.WebJobs.Script.WebHost.dll', - ' * or download Azure Functions Core Tools binaries and', - ' then write the full path to func.exe into the ', - ' `CORE_TOOLS_EXE_PATH` environment variable.', - '', - 'Setting "export PYAZURE_WEBHOST_DEBUG=true" to get the full', - 'stdout and stderr from function host.' - ])) - - worker_path = os.environ.get(PYAZURE_WORKER_DIR) - worker_path = WORKER_PATH if not worker_path else pathlib.Path(worker_path) - if not worker_path.exists(): - raise RuntimeError(f'Worker path {worker_path} does not exist') - - # Casting to strings is necessary because Popen doesn't like - # path objects there on Windows. - extra_env = { - 'AzureWebJobsScriptRoot': str(script_root), - 'languageWorkers:python:workerDirectory': str(worker_path), - 'host:logger:consoleLoggingMode': 'always', - 'AZURE_FUNCTIONS_ENVIRONMENT': 'development', - 'AzureWebJobsSecretStorageType': 'files', - 'FUNCTIONS_WORKER_RUNTIME': 'python' - } - - # In E2E Integration mode, we should use the core tools worker - # from the latest artifact instead of the azure_functions_worker module - if is_envvar_true(PYAZURE_INTEGRATION_TEST): - extra_env.pop('languageWorkers:python:workerDirectory') - - if testconfig and 'azure' in testconfig: - st = testconfig['azure'].get('storage_key') - if st: - extra_env['AzureWebJobsStorage'] = st - - cosmos = testconfig['azure'].get('cosmosdb_key') - if cosmos: - extra_env['AzureWebJobsCosmosDBConnectionString'] = cosmos - - eventhub = testconfig['azure'].get('eventhub_key') - if eventhub: - extra_env['AzureWebJobsEventHubConnectionString'] = eventhub - - servicebus = testconfig['azure'].get('servicebus_key') - if servicebus: - extra_env['AzureWebJobsServiceBusConnectionString'] = servicebus - - sql = testconfig['azure'].get('sql_key') - if sql: - extra_env['AzureWebJobsSqlConnectionString'] = sql - - eventgrid_topic_uri = testconfig['azure'].get('eventgrid_topic_uri') - if eventgrid_topic_uri: - extra_env['AzureWebJobsEventGridTopicUri'] = eventgrid_topic_uri - - eventgrid_topic_key = testconfig['azure'].get('eventgrid_topic_key') - if eventgrid_topic_key: - extra_env['AzureWebJobsEventGridConnectionKey'] = \ - eventgrid_topic_key - - if port is not None: - extra_env['ASPNETCORE_URLS'] = f'http://*:{port}' - - return subprocess.Popen( - hostexe_args, - cwd=script_root, - env={ - **os.environ, - **extra_env, - }, - stdout=stdout, - stderr=stderr) - - -def start_webhost(*, script_dir=None, stdout=None): - script_root = TESTS_ROOT / script_dir if script_dir else FUNCS_PATH - if stdout is None: - if is_envvar_true(PYAZURE_WEBHOST_DEBUG): - stdout = sys.stdout - else: - stdout = subprocess.DEVNULL - - port = _find_open_port() - - proc = popen_webhost(stdout=stdout, stderr=subprocess.STDOUT, - script_root=script_root, port=port) - time.sleep(10) # Giving host some time to start fully. - - addr = f'http://{LOCALHOST}:{port}' - return _WebHostProxy(proc, addr) - - -def create_dummy_dispatcher(): - dummy_event_loop = asyncio.new_event_loop() - disp = dispatcher.Dispatcher( - dummy_event_loop, LOCALHOST, 0, - 'test_worker_id', 'test_request_id', - 1.0, 1000) - dummy_event_loop.close() - return disp - - -def retryable_test( - number_of_retries: int, - interval_sec: int, - expected_exception: type = Exception -): - def decorate(func): - def call(*args, **kwargs): - retries = number_of_retries - while True: - try: - return func(*args, **kwargs) - except expected_exception as e: - retries -= 1 - if retries <= 0: - raise e - - time.sleep(interval_sec) - - return call - - return decorate - - -def remove_path(path): - if path.is_symlink(): - path.unlink() - elif path.is_dir(): - shutil.rmtree(str(path)) - elif path.exists(): - path.unlink() - - -def _symlink_dir(src, dst): - remove_path(dst) - - if ON_WINDOWS: - shutil.copytree(str(src), str(dst)) - else: - dst.symlink_to(src, target_is_directory=True) - - -def _setup_func_app(app_root, is_unit_test=False): - extensions = app_root / 'bin' - host_json = app_root / 'host.json' - extensions_csproj_file = app_root / 'extensions.csproj' - - if not os.path.isfile(host_json): - with open(host_json, 'w') as f: - f.write(HOST_JSON_TEMPLATE) - - if not os.path.isfile(extensions_csproj_file) and not is_unit_test: - with open(extensions_csproj_file, 'w') as f: - f.write(EXTENSIONS_CSPROJ_TEMPLATE) - - _symlink_dir(EXTENSIONS_PATH, extensions) - - -def _teardown_func_app(app_root): - extensions = app_root / 'bin' - host_json = app_root / 'host.json' - extensions_csproj_file = app_root / 'extensions.csproj' - extensions_obj_file = app_root / 'obj' - libraries_path = app_root / '.python_packages' - - for path in (extensions, host_json, extensions_csproj_file, - extensions_obj_file, libraries_path): - remove_path(path) - - -def _main(): - parser = argparse.ArgumentParser(description='Run a Python worker.') - parser.add_argument('scriptroot', - help='directory with functions to load') - - args = parser.parse_args() - - app_root = pathlib.Path(args.scriptroot) - _setup_func_app(app_root) - - host = popen_webhost( - stdout=sys.stdout, stderr=sys.stderr, - script_root=os.path.abspath(args.scriptroot)) - try: - host.wait() - finally: - host.terminate() - _teardown_func_app() - - -if __name__ == '__main__': - _main() diff --git a/tests/utils/testutils_docker.py b/tests/utils/testutils_docker.py deleted file mode 100644 index 964d8a83e..000000000 --- a/tests/utils/testutils_docker.py +++ /dev/null @@ -1,214 +0,0 @@ -import os -import re -import subprocess -import sys -import typing -import unittest -import uuid -from dataclasses import dataclass -from pathlib import Path -from time import sleep - -import requests -from tests.utils.constants import PROJECT_ROOT, TESTS_ROOT - -_DOCKER_PATH = "DOCKER_PATH" -_DOCKER_DEFAULT_PATH = "docker" -_HOST_VERSION = "4" -_docker_cmd = os.getenv(_DOCKER_PATH, _DOCKER_DEFAULT_PATH) -_addr = "" -_python_version = f'{sys.version_info.major}.{sys.version_info.minor}' -_libraries_path = '.python_packages/lib/site-packages' -_uuid = str(uuid.uuid4()) -_MESH_IMAGE_URL = "https://mcr.microsoft.com/v2/azure-functions/mesh/tags/list" -_MESH_IMAGE_REPO = "mcr.microsoft.com/azure-functions/mesh" -_IMAGE_URL = "https://mcr.microsoft.com/v2/azure-functions/python/tags/list" -_IMAGE_REPO = "mcr.microsoft.com/azure-functions/python" -_CUSTOM_IMAGE = os.getenv("IMAGE_NAME") - - -@dataclass -class DockerConfigs: - script_path: Path - libraries: typing.List = None - env: typing.Dict = None - - -class WebHostProxy: - - def __init__(self, proc, addr): - self._proc = proc - self._addr = addr - - def request(self, meth, funcname, *args, **kwargs): - request_method = getattr(requests, meth.lower()) - params = dict(kwargs.pop('params', {})) - no_prefix = kwargs.pop('no_prefix', False) - - return request_method( - self._addr + ('/' if no_prefix else '/api/') + funcname, - *args, params=params, **kwargs) - - def close(self) -> bool: - """Kill a container by its name. Returns True on success. - """ - kill_cmd = [_docker_cmd, "rm", "-f", _uuid] - kill_process = subprocess.run(args=kill_cmd, stdout=subprocess.DEVNULL) - exit_code = kill_process.returncode - - return exit_code == 0 - - def is_healthy(self) -> bool: - pass - - -class WebHostDockerContainerBase(unittest.TestCase): - - @staticmethod - def find_latest_image(image_repo: str, - image_url: str) -> str: - - regex = re.compile(_HOST_VERSION + r'.\d+.\d+-python' + _python_version) - - response = requests.get(image_url, allow_redirects=True) - if not response.ok: - raise RuntimeError(f'Failed to query latest image for v4' - f' Python {_python_version}.' - f' Status {response.status_code}') - - tag_list = response.json().get('tags', []) - # Removing images with a -upgrade and -slim. Upgrade images were - # temporary images used to onboard customers from a previous version. - # These images are no longer used. - tag_list = [x.strip("-upgrade") for x in tag_list] - tag_list = [x.strip("-slim") for x in tag_list] - - # Listing all the versions from the tags with suffix -python - python_versions = list(filter(regex.match, tag_list)) - - # sorting all the python versions based on the runtime version and - # getting the latest released runtime version for python. - latest_version = sorted(python_versions, key=lambda x: float( - x.split(_HOST_VERSION + '.')[-1].split("-python")[0]))[-1] - - image_tag = f'{image_repo}:{latest_version}' - return image_tag - - def create_container(self, image_repo: str, image_url: str, - configs: DockerConfigs): - """Create a docker container and record its port. Create a docker - container according to the image name. Return the port of container. - """ - - worker_path = os.path.join(PROJECT_ROOT, 'azure_functions_worker') - script_path = os.path.join(TESTS_ROOT, configs.script_path) - env = {"AzureWebJobsFeatureFlags": "EnableWorkerIndexing", - "AzureWebJobsStorage": f"{os.getenv('AzureWebJobsStorage')}", - "AzureWebJobsEventHubConnectionString": - f"{os.getenv('AzureWebJobsEventHubConnectionString')}", - "AzureWebJobsCosmosDBConnectionString": - f"{os.getenv('AzureWebJobsCosmosDBConnectionString')}", - "AzureWebJobsServiceBusConnectionString": - f"{os.getenv('AzureWebJobsServiceBusConnectionString')}", - "AzureWebJobsSqlConnectionString": - f"{os.getenv('AzureWebJobsSqlConnectionString')}", - "AzureWebJobsEventGridTopicUri": - f"{os.getenv('AzureWebJobsEventGridTopicUri')}", - "AzureWebJobsEventGridConnectionKey": - f"{os.getenv('AzureWebJobsEventGridConnectionKey')}" - } - - configs.env.update(env) - - if _CUSTOM_IMAGE: - image = _CUSTOM_IMAGE - else: - image = self.find_latest_image(image_repo, image_url) - - container_worker_path = ( - f"/azure-functions-host/workers/python/{_python_version}/" - "LINUX/X64/azure_functions_worker" - ) - - function_path = "/home/site/wwwroot" - configs.libraries = ((configs.libraries or []) - + ['azurefunctions-extensions-base']) - install_libraries_cmd = [] - install_libraries_cmd.extend(['pip', 'install']) - install_libraries_cmd.extend(['--platform=manylinux2014_x86_64']) - install_libraries_cmd.extend(configs.libraries) - install_libraries_cmd.extend(['-t', - f'{script_path}/{_libraries_path}']) - install_libraries_cmd.extend(['--only-binary=:all:']) - - install_libraries_process = \ - subprocess.run(args=install_libraries_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - - if install_libraries_process.returncode != 0: - raise RuntimeError('Failed to install libraries') - - run_cmd = [] - run_cmd.extend([_docker_cmd, "run", "-p", "0:80", "-d"]) - run_cmd.extend(["--name", _uuid, "--privileged"]) - run_cmd.extend(["--cap-add", "SYS_ADMIN"]) - run_cmd.extend(["--device", "/dev/fuse"]) - run_cmd.extend(["-e", f"CONTAINER_NAME={_uuid}"]) - run_cmd.extend(["-e", f"AzureFunctionsWebHost__hostid={_uuid}"]) - run_cmd.extend(["-v", f"{worker_path}:{container_worker_path}"]) - run_cmd.extend(["-v", f"{script_path}:{function_path}"]) - - if configs.env: - for key, value in configs.env.items(): - run_cmd.extend(["-e", f"{key}={value}"]) - - run_cmd.append(image) - run_process = subprocess.run(args=run_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - - if run_process.returncode != 0: - raise RuntimeError('Failed to create docker container for' - f' {image} with uuid {_uuid}.' - f' stderr: {run_process.stderr}') - - # Wait for six seconds for the port to expose - sleep(6) - - # Acquire the port number of the container - port_cmd = [_docker_cmd, "port", _uuid] - port_process = subprocess.run(args=port_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - if port_process.returncode != 0: - raise RuntimeError(f'Failed to acquire port for {_uuid}.' - f' stderr: {port_process.stderr}') - port_number = port_process.stdout.decode().strip('\n').split(':')[-1] - - # Wait for six seconds for the container to be in ready state - sleep(6) - self._addr = f'http://localhost:{port_number}' - - return WebHostProxy(run_process, self._addr) - - -class WebHostConsumption(WebHostDockerContainerBase): - - def __init__(self, configs: DockerConfigs): - self.configs = configs - - def spawn_container(self): - return self.create_container(_MESH_IMAGE_REPO, - _MESH_IMAGE_URL, - self.configs) - - -class WebHostDedicated(WebHostDockerContainerBase): - - def __init__(self, configs: DockerConfigs): - self.configs = configs - - def spawn_container(self): - return self.create_container(_IMAGE_REPO, _IMAGE_URL, - self.configs) diff --git a/tests/utils/testutils_lc.py b/tests/utils/testutils_lc.py deleted file mode 100644 index 43665a65a..000000000 --- a/tests/utils/testutils_lc.py +++ /dev/null @@ -1,339 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import base64 -import json -import os -import re -import shutil -import subprocess -import sys -import tempfile -import time -import uuid -from io import BytesIO -from typing import Dict -from urllib.request import urlopen -from zipfile import ZipFile - -import requests -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives import padding - -from tests.utils.constants import PROJECT_ROOT - -# Linux Consumption Testing Constants -_DOCKER_PATH = "DOCKER_PATH" -_DOCKER_DEFAULT_PATH = "docker" -_MESH_IMAGE_URL = "https://mcr.microsoft.com/v2/azure-functions/mesh/tags/list" -_MESH_IMAGE_REPO = "mcr.microsoft.com/azure-functions/mesh" -_FUNC_GITHUB_ZIP = "https://github.com/Azure/azure-functions-python-library" \ - "/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 - test cases on it. - """ - - _docker_cmd = os.getenv(_DOCKER_PATH, _DOCKER_DEFAULT_PATH) - _ports: Dict[str, str] = {} # { uuid: port } - _mesh_images: Dict[str, str] = {} # { host version: image tag } - - def __init__(self, host_version: str, python_version: str): - """Initialize a new container for - """ - self._uuid = str(uuid.uuid4()) - self._host_version = host_version # "3" - self._py_version = python_version # "3.9" - - @property - def url(self) -> str: - if self._uuid not in self._ports: - raise RuntimeError(f'Failed to assign container {self._name} since' - ' it is not spawned') - - return f'http://localhost:{self._ports[self._uuid]}' - - def assign_container(self, env: Dict[str, str] = {}): - """Make a POST request to /admin/instance/assign to specialize the - container - """ - url = f'http://localhost:{self._ports[self._uuid]}' - - # Add compulsory fields in specialization context - env["FUNCTIONS_EXTENSION_VERSION"] = f"~{self._host_version}" - env["FUNCTIONS_WORKER_RUNTIME"] = "python" - env["FUNCTIONS_WORKER_RUNTIME_VERSION"] = self._py_version - env["WEBSITE_SITE_NAME"] = self._uuid - env["WEBSITE_HOSTNAME"] = f"{self._uuid}.azurewebsites.com" - - # Send the specialization context via a POST request - req = requests.Request( - method="POST", - url=f"{url}/admin/instance/assign", - data=json.dumps({ - "encryptedContext": self._get_site_encrypted_context( - self._uuid, env - ) - }) - ) - response = self.send_request(req) - if not response.ok: - stdout = self.get_container_logs() - raise RuntimeError(f'Failed to specialize container {self._uuid}' - f' at {url} (status {response.status_code}).' - f' stdout: {stdout}') - - def send_request( - self, - req: requests.Request, - ses: requests.Session = None - ) -> requests.Response: - """Send a request with authorization token. Return a Response object""" - session = ses - if session is None: - session = requests.Session() - - prepped = session.prepare_request(req) - prepped.headers['Content-Type'] = 'application/json' - prepped.headers['x-ms-site-restricted-token'] = ( - self._get_site_restricted_token() - ) - prepped.headers['x-site-deployment-id'] = self._uuid - - resp = session.send(prepped) - return resp - - @classmethod - def _find_latest_mesh_image(cls, - host_major: str, - python_version: str) -> str: - """Find the latest image in https://mcr.microsoft.com/v2/ - azure-functions/mesh/tags/list. Match either (3.1.3, or 3.1.3-python3.x) - """ - if host_major in cls._mesh_images: - return cls._mesh_images[host_major] - - # match 3.1.3 - regex = re.compile(host_major + r'.\d+.\d+-python' + python_version) - - response = requests.get(_MESH_IMAGE_URL, allow_redirects=True) - if not response.ok: - raise RuntimeError(f'Failed to query latest image for v{host_major}' - f' Python {python_version}.' - f' Status {response.status_code}') - - tag_list = response.json().get('tags', []) - # Removing images with a -upgrade. Upgrade images were temporary - # images used to onboard customers from a previous version. These - # images are no longer used. - tag_list = [x.strip("-upgrade") for x in tag_list] - - # Listing all the versions from the tags with suffix -python - python_versions = list(filter(regex.match, tag_list)) - - # sorting all the python versions based on the runtime version and - # getting the latest released runtime version for python. - latest_version = sorted(python_versions, key=lambda x: float( - x.split(host_major + '.')[-1].split("-python")[0]))[-1] - - image_tag = f'{_MESH_IMAGE_REPO}:{latest_version}' - cls._mesh_images[host_major] = image_tag - return image_tag - - @staticmethod - def _download_azure_functions() -> str: - with urlopen(_FUNC_GITHUB_ZIP) as zipresp: - 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(tempfile.gettempdir()) - - return folder - - def spawn_container(self, - image: str, - env: Dict[str, str] = {}) -> int: - """Create a docker container and record its port. Create a docker - container according to the image name. Return the port of container. - """ - # Construct environment variables and start the docker container - worker_path = os.path.join(PROJECT_ROOT, 'azure_functions_worker') - - # 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/azurefunctions/extensions/base" - ) - - base_ext_local_path = ( - f'{ext_folder}/azure-functions-python' - '-extensions-dev/azurefunctions-extensions-base' - '/azurefunctions/extensions/base' - ) - run_cmd = [] - run_cmd.extend([self._docker_cmd, "run", "-p", "0:80", "-d"]) - run_cmd.extend(["--name", self._uuid, "--privileged"]) - run_cmd.extend(["--cap-add", "SYS_ADMIN"]) - run_cmd.extend(["--device", "/dev/fuse"]) - run_cmd.extend(["-e", f"CONTAINER_NAME={self._uuid}"]) - run_cmd.extend(["-e", - f"CONTAINER_ENCRYPTION_KEY={os.getenv('_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}"]) - run_cmd.append(image) - - run_process = subprocess.run(args=run_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - - if run_process.returncode != 0: - raise RuntimeError('Failed to spawn docker container for' - f' {image} with uuid {self._uuid}.' - f' stderr: {run_process.stderr}') - - # Wait for three seconds for the port to expose - time.sleep(3) - - # Acquire the port number of the container - port_cmd = [self._docker_cmd, "port", self._uuid] - port_process = subprocess.run(args=port_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - if port_process.returncode != 0: - raise RuntimeError(f'Failed to acquire port for {self._uuid}.' - f' stderr: {port_process.stderr}') - port_number = port_process.stdout.decode().strip('\n').split(':')[-1] - - # Register port number onto the table - self._ports[self._uuid] = port_number - - # Wait for three seconds for the container to be in ready state - time.sleep(6) - return port_number - - def get_container_logs(self) -> str: - """Get container logs, the first element in tuple is stdout and the - second element is stderr - """ - get_logs_cmd = [self._docker_cmd, "logs", self._uuid] - get_logs_process = subprocess.run(args=get_logs_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - - # The `docker logs` command will merge stdout and stderr into stdout - return get_logs_process.stdout.decode('utf-8') - - def safe_kill_container(self) -> bool: - """Kill a container by its name. Returns True on success. - """ - kill_cmd = [self._docker_cmd, "rm", "-f", self._uuid] - kill_process = subprocess.run(args=kill_cmd, stdout=subprocess.DEVNULL) - exit_code = kill_process.returncode - - if self._uuid in self._ports: - del self._ports[self._uuid] - return exit_code == 0 - - @classmethod - def _get_site_restricted_token(cls) -> str: - """Get the header value which can be used by x-ms-site-restricted-token - which expires in one day. - """ - exp_ns = int(time.time() + 24 * 60 * 60) * 1000000000 - return cls._encrypt_context(os.getenv('_DUMMY_CONT_KEY'), f'exp={exp_ns}') - - @classmethod - def _get_site_encrypted_context(cls, - site_name: str, - env: Dict[str, str]) -> str: - """Get the encrypted context for placeholder mode specialization""" - ctx = { - "SiteId": 1, - "SiteName": site_name, - "Environment": env - } - - # Ensure WEBSITE_SITE_NAME is set to simulate production mode - ctx["Environment"]["WEBSITE_SITE_NAME"] = site_name - return cls._encrypt_context(os.getenv('_DUMMY_CONT_KEY'), json.dumps(ctx)) - - @classmethod - def _encrypt_context(cls, encryption_key: str, plain_text: str) -> str: - """Encrypt plain text context into a encrypted message which can - be accepted by the host - """ - # Decode the encryption key - encryption_key_bytes = base64.b64decode(encryption_key.encode()) - - # Pad the plaintext to be a multiple of the AES block size - padder = padding.PKCS7(algorithms.AES.block_size).padder() - plain_text_bytes = padder.update(plain_text.encode()) + padder.finalize() - - # Initialization vector (IV) (fixed value for simplicity) - iv_bytes = '0123456789abcedf'.encode() - - # Create AES cipher with CBC mode - cipher = Cipher(algorithms.AES(encryption_key_bytes), - modes.CBC(iv_bytes), backend=default_backend()) - - # Perform encryption - encryptor = cipher.encryptor() - encrypted_bytes = encryptor.update(plain_text_bytes) + encryptor.finalize() - - # Compute SHA256 hash of the encryption key - digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) - digest.update(encryption_key_bytes) - key_sha256 = digest.finalize() - - # Encode IV, encrypted message, and SHA256 hash in base64 - iv_base64 = base64.b64encode(iv_bytes).decode() - encrypted_base64 = base64.b64encode(encrypted_bytes).decode() - key_sha256_base64 = base64.b64encode(key_sha256).decode() - - # Return the final result - return f'{iv_base64}.{encrypted_base64}.{key_sha256_base64}' - - def __enter__(self): - mesh_image = (os.environ.get(_CUSTOM_IMAGE) - or self._find_latest_mesh_image(self._host_version, - self._py_version)) - self.spawn_container(image=mesh_image) - return self - - def __exit__(self, exc_type, exc_value, traceback): - logs = self.get_container_logs() - self.safe_kill_container() - shutil.rmtree(os.path.join(tempfile.gettempdir(), _FUNC_FILE_NAME), - ignore_errors=True) - - if traceback: - print(f'Test failed with container logs: {logs}', - file=sys.stderr, - flush=True) From cf747d640caf58769bd4790794b9aa05a1f54a44 Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Mon, 27 Jan 2025 10:25:25 -0600 Subject: [PATCH 02/45] lint + public build --- .flake8 | 2 +- CODEOWNERS | 2 +- azure_functions_worker/__init__.py | 12 +++- azure_functions_worker/bindings/context.py | 2 +- azure_functions_worker/bindings/datumdef.py | 9 ++- azure_functions_worker/functions.py | 3 +- azure_functions_worker/handle_event.py | 61 +++++++++---------- azure_functions_worker/loader.py | 2 +- azure_functions_worker/utils/__init__.py | 2 +- azure_functions_worker/utils/current.py | 1 - azure_functions_worker/utils/env_state.py | 1 - azure_functions_worker/utils/helpers.py | 2 +- azure_functions_worker/utils/tracing.py | 2 +- azure_functions_worker/utils/validators.py | 2 - eng/ci/public-build.yml | 26 ++++---- eng/templates/jobs/build.yml | 17 +++++- .../{unit_tests => unittests}/function_app.py | 0 tests/unittests/test_code_quality.py | 54 ++++++++++++++++ .../test_handle_event.py | 10 ++- 19 files changed, 140 insertions(+), 70 deletions(-) rename tests/{unit_tests => unittests}/function_app.py (100%) create mode 100644 tests/unittests/test_code_quality.py rename tests/{unit_tests => unittests}/test_handle_event.py (89%) diff --git a/.flake8 b/.flake8 index 8e3282da0..38d73d9c9 100644 --- a/.flake8 +++ b/.flake8 @@ -7,7 +7,7 @@ ignore = W503,E402,E731 exclude = .git, __pycache__, build, dist, .eggs, .github, .local, docs/, Samples, azure_functions_worker/protos/, - azure_functions_worker/_thirdparty/typing_inspect.py, + azure_functions_worker/utils/typing_inspect.py, tests/unittests/test_typing_inspect.py, tests/unittests/broken_functions/syntax_error/main.py, tests/protos/, diff --git a/CODEOWNERS b/CODEOWNERS index f93501102..e5f28aee3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -10,4 +10,4 @@ # For all file changes, github would automatically # include the following people in the PRs. -* @vrdmr @gavin-aguiar @YunchuWang @pdthummar @hallvictoria +* @vrdmr @gavin-aguiar @hallvictoria diff --git a/azure_functions_worker/__init__.py b/azure_functions_worker/__init__.py index b567df2db..0c57acd05 100644 --- a/azure_functions_worker/__init__.py +++ b/azure_functions_worker/__init__.py @@ -1,6 +1,14 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from .handle_event import worker_init_request, functions_metadata_request, function_environment_reload_request, invocation_request, functions_load_request +from .handle_event import (worker_init_request, + functions_metadata_request, + function_environment_reload_request, + invocation_request, + functions_load_request) -__all__ = ('worker_init_request', 'functions_metadata_request', 'function_environment_reload_request', 'invocation_request', 'functions_load_request') +__all__ = ('worker_init_request', + 'functions_metadata_request', + 'function_environment_reload_request', + 'invocation_request', + 'functions_load_request') diff --git a/azure_functions_worker/bindings/context.py b/azure_functions_worker/bindings/context.py index b74a53173..6181e630f 100644 --- a/azure_functions_worker/bindings/context.py +++ b/azure_functions_worker/bindings/context.py @@ -50,7 +50,7 @@ def retry_context(self) -> RetryContext: def get_context(invoc_request, name: str, - directory: str) -> Context: + directory: str) -> Context: """ For more information refer: https://aka.ms/azfunc-invocation-context """ diff --git a/azure_functions_worker/bindings/datumdef.py b/azure_functions_worker/bindings/datumdef.py index da1321f03..c67ccb8e8 100644 --- a/azure_functions_worker/bindings/datumdef.py +++ b/azure_functions_worker/bindings/datumdef.py @@ -14,8 +14,6 @@ to_nullable_timestamp, ) -from ..logging import logger - try: from http.cookies import SimpleCookie except ImportError: @@ -69,8 +67,9 @@ def __repr__(self): def from_typed_data(cls, protos): try: td = protos.TypedData - except Exception as ex: - # Todo: better catch for Datum.from_typed_data(http.body) -- if the data being sent in is already protos.TypedData + except Exception: + # Todo: better catch for Datum.from_typed_data(http.body) + # if the data being sent in is already protos.TypedData td = protos tt = td.WhichOneof('data') if tt == 'http': @@ -116,7 +115,7 @@ def from_typed_data(cls, protos): return cls(val, tt) -def datum_as_proto(datum: Datum, protos): +def datum_as_proto(datum: Datum, protos): if datum.type == 'string': return protos.TypedData(string=datum.value) elif datum.type == 'bytes': diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py index 69f2a5a4f..f4b743a1d 100644 --- a/azure_functions_worker/functions.py +++ b/azure_functions_worker/functions.py @@ -136,7 +136,8 @@ def is_context_required(params, bound_params: dict, def validate_function_params(params: dict, bound_params: dict, annotations: dict, func_name: str, protos): - logger.info("Params: %s, BoundParams: %s, Annotations: %s, FuncName: %s", params, bound_params, annotations, func_name) + logger.info("Params: %s, BoundParams: %s, Annotations: %s, FuncName: %s", + params, bound_params, annotations, func_name) if set(params) - set(bound_params): raise FunctionLoadError( func_name, diff --git a/azure_functions_worker/handle_event.py b/azure_functions_worker/handle_event.py index 450539ff7..7edf06bfe 100644 --- a/azure_functions_worker/handle_event.py +++ b/azure_functions_worker/handle_event.py @@ -6,7 +6,6 @@ import os import sys -from datetime import datetime from typing import List, Optional from .functions import FunctionInfo, Registry @@ -22,7 +21,9 @@ from .otel import otel_manager, initialize_azure_monitor, configure_opentelemetry from .bindings.context import get_context -from .bindings.meta import load_binding_registry, is_trigger_binding, from_incoming_proto, to_outgoing_param_binding, to_outgoing_proto +from .bindings.meta import (load_binding_registry, is_trigger_binding, + from_incoming_proto, to_outgoing_param_binding, + to_outgoing_proto) from .bindings.out import Out from .utils.constants import (FUNCTION_DATA_CACHE, RAW_HTTP_BODY_BYTES, @@ -56,7 +57,6 @@ protos = None -# Protos will be the retry / binding / metadata protos object that we populate and return async def worker_init_request(request): logger.info("Library Worker: received worker_init_request") global result, _host, protos, _function_data_cache_enabled @@ -83,14 +83,14 @@ async def worker_init_request(request): if otel_manager.get_azure_monitor_available(): capabilities[WORKER_OPEN_TELEMETRY_ENABLED] = TRUE - # loading bindings registry and saving results to a static # dictionary which will be later used in the invocation request load_binding_registry() try: - result = asyncio.create_task(load_function_metadata(init_request.function_app_directory, - caller_info="worker_init_request")) + result = asyncio.create_task(load_function_metadata( + init_request.function_app_directory, + caller_info="worker_init_request")) if get_app_setting(setting=PYTHON_ENABLE_INIT_INDEXING): capabilities[HTTP_URI] = \ initialize_http_server(_host) @@ -130,11 +130,10 @@ async def functions_metadata_request(request): else: return protos.FunctionMetadataResponse( - use_default_metadata_indexing=False, - function_metadata_results=metadata_result, - result=protos.StatusResult( - status=protos.StatusResult.Success)) - + use_default_metadata_indexing=False, + function_metadata_results=metadata_result, + result=protos.StatusResult( + status=protos.StatusResult.Success)) async def functions_load_request(request): @@ -152,7 +151,6 @@ async def functions_load_request(request): async def invocation_request(request): logger.info("Library Worker: received worker_invocation_request") global protos - invocation_time = datetime.now() invoc_request = request.request.invocation_request invocation_id = invoc_request.invocation_id function_id = invoc_request.function_id @@ -256,11 +254,11 @@ async def invocation_request(request): # Actively flush customer print() function to console sys.stdout.flush() return protos.InvocationResponse( - invocation_id=invocation_id, - return_value=return_value, - result=protos.StatusResult( - status=protos.StatusResult.Success), - output_data=output_data) + invocation_id=invocation_id, + return_value=return_value, + result=protos.StatusResult( + status=protos.StatusResult.Success), + output_data=output_data) except Exception as ex: if http_v2_enabled: @@ -268,10 +266,10 @@ async def invocation_request(request): global metadata_result metadata_result = ex return protos.InvocationResponse( - invocation_id=invocation_id, - result=protos.StatusResult( - status=protos.StatusResult.Failure, - exception=serialize_exception(ex))) + invocation_id=invocation_id, + result=protos.StatusResult( + status=protos.StatusResult.Failure, + exception=serialize_exception(ex))) async def function_environment_reload_request(request): @@ -308,9 +306,10 @@ async def function_environment_reload_request(request): global _host, result, protos _host = request.properties.get("host") protos = request.properties.get("protos") - result = asyncio.create_task(load_function_metadata(directory, - caller_info="environment_reload_request")) - if get_app_setting(setting=PYTHON_ENABLE_INIT_INDEXING): # PYTHON_ENABLE_HTTP_STREAMING + result = asyncio.create_task(load_function_metadata( + directory, + caller_info="environment_reload_request")) + if get_app_setting(setting=PYTHON_ENABLE_INIT_INDEXING): capabilities[HTTP_URI] = \ initialize_http_server(_host) capabilities[REQUIRES_ROUTE_PARAMETERS] = TRUE @@ -324,18 +323,18 @@ async def function_environment_reload_request(request): func_env_reload_request.function_app_directory) return protos.FunctionEnvironmentReloadResponse( - capabilities=capabilities, - worker_metadata=get_worker_metadata(protos), - result=protos.StatusResult( - status=protos.StatusResult.Success)) + capabilities=capabilities, + worker_metadata=get_worker_metadata(protos), + result=protos.StatusResult( + status=protos.StatusResult.Success)) except Exception as ex: global metadata_exception metadata_exception = ex return protos.FunctionEnvironmentReloadResponse( - result=protos.StatusResult( - status=protos.StatusResult.Failure, - exception=serialize_exception(ex))) + result=protos.StatusResult( + status=protos.StatusResult.Failure, + exception=serialize_exception(ex))) async def load_function_metadata(function_app_directory, caller_info): diff --git a/azure_functions_worker/loader.py b/azure_functions_worker/loader.py index 4d1acc6af..ae4d9c656 100644 --- a/azure_functions_worker/loader.py +++ b/azure_functions_worker/loader.py @@ -9,7 +9,7 @@ import time from datetime import timedelta -from typing import Dict, Optional, Union +from typing import Dict, Optional from .functions import Registry diff --git a/azure_functions_worker/utils/__init__.py b/azure_functions_worker/utils/__init__.py index 6fcf0de49..5b7f7a925 100644 --- a/azure_functions_worker/utils/__init__.py +++ b/azure_functions_worker/utils/__init__.py @@ -1,2 +1,2 @@ # Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. \ No newline at end of file +# Licensed under the MIT License. diff --git a/azure_functions_worker/utils/current.py b/azure_functions_worker/utils/current.py index b9f304298..c2f153a74 100644 --- a/azure_functions_worker/utils/current.py +++ b/azure_functions_worker/utils/current.py @@ -28,4 +28,3 @@ def run_sync_func(invocation_id, context, func, params): return result(params) finally: context.thread_local_storage.invocation_id = None - diff --git a/azure_functions_worker/utils/env_state.py b/azure_functions_worker/utils/env_state.py index 3bc8d8f4f..431bc4051 100644 --- a/azure_functions_worker/utils/env_state.py +++ b/azure_functions_worker/utils/env_state.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. import os -import sys from typing import Callable, Optional diff --git a/azure_functions_worker/utils/helpers.py b/azure_functions_worker/utils/helpers.py index 4ac1c1468..170d136e4 100644 --- a/azure_functions_worker/utils/helpers.py +++ b/azure_functions_worker/utils/helpers.py @@ -26,4 +26,4 @@ def get_worker_metadata(protos): f"{sys.version_info.minor}", worker_version=VERSION, worker_bitness=platform.machine(), - custom_properties={}) \ No newline at end of file + custom_properties={}) diff --git a/azure_functions_worker/utils/tracing.py b/azure_functions_worker/utils/tracing.py index 62385ac8e..ba35be83a 100644 --- a/azure_functions_worker/utils/tracing.py +++ b/azure_functions_worker/utils/tracing.py @@ -53,4 +53,4 @@ def serialize_exception(exc: Exception, protos): except Exception: stack_trace = '' - return protos.RpcException(message=message, stack_trace=stack_trace) \ No newline at end of file + return protos.RpcException(message=message, stack_trace=stack_trace) diff --git a/azure_functions_worker/utils/validators.py b/azure_functions_worker/utils/validators.py index e5997dcf0..6d393c2cd 100644 --- a/azure_functions_worker/utils/validators.py +++ b/azure_functions_worker/utils/validators.py @@ -3,8 +3,6 @@ import re -from .constants import PYTHON_THREADPOOL_THREAD_COUNT, PYTHON_THREADPOOL_THREAD_COUNT_MIN - class InvalidFileNameError(Exception): diff --git a/eng/ci/public-build.yml b/eng/ci/public-build.yml index 26f1b6625..e86bb8993 100644 --- a/eng/ci/public-build.yml +++ b/eng/ci/public-build.yml @@ -28,14 +28,6 @@ resources: variables: - template: /eng/templates/utils/variables.yml@self - - name: codeql.excludePathPatterns - value: deps/,build/ - - name: codeql.compiled.enabled - value: true - - name: codeql.runSourceLanguagesInSourceAnalysis - value: true - - name: codeql.sourceLanguages - value: python, powershell extends: template: v1/1ES.Unofficial.PipelineTemplate.yml@1es @@ -44,8 +36,18 @@ extends: name: 1es-pool-azfunc-public image: 1es-windows-2022 os: windows + + sdl: + codeql: + compiled: + enabled: true # still only runs for default branch + sourceLanguages: python, powershell + excludePathPatterns: deps/,build/ + runSourceLanguagesInSourceAnalysis: true + settings: skipBuildTagsForGitHubPullRequests: ${{ variables['System.PullRequest.IsFork'] }} + stages: - stage: Build jobs: @@ -53,10 +55,4 @@ extends: - stage: RunUnitTests dependsOn: Build jobs: - - template: /eng/templates/jobs/ci-unit-tests.yml@self - - stage: RunEmulatorTests - dependsOn: Build - jobs: - - template: /eng/templates/jobs/ci-emulator-tests.yml@self - parameters: - PoolName: 1es-pool-azfunc-public \ No newline at end of file + - template: /eng/templates/jobs/ci-unit-tests.yml@self \ No newline at end of file diff --git a/eng/templates/jobs/build.yml b/eng/templates/jobs/build.yml index 0efdc13b1..8b28f114e 100644 --- a/eng/templates/jobs/build.yml +++ b/eng/templates/jobs/build.yml @@ -7,16 +7,29 @@ jobs: image: 1es-ubuntu-22.04 os: linux + variables: + # Extract the version number from the branch name + pythonVersion: $[substring(variables['Build.SourceBranchName'], 4, 2)] # Extract last two digits from dev-xxx + steps: + - script: | + echo "Branch name: $(Build.SourceBranchName)" + # Extract the last two digits (minor version) from the branch name + version=$(echo $(Build.SourceBranchName) | sed 's/dev-\([0-9]*\)/\1/') + minor_version=${version: -2} # Get last two digits + echo "Extracted minor version: $minor_version" + echo "##vso[task.setvariable variable=pythonVersion]$minor_version" + displayName: 'Extract Python version from branch name' - task: UsePythonVersion@0 inputs: - versionSpec: "3.13" + versionSpec: '3.$(pythonVersion)' - bash: | python --version displayName: 'Check python version' - bash: | python -m pip install --upgrade pip - python -m pip install . + python -m pip install build + python -m build displayName: 'Build python worker' - bash: | pip install pip-audit diff --git a/tests/unit_tests/function_app.py b/tests/unittests/function_app.py similarity index 100% rename from tests/unit_tests/function_app.py rename to tests/unittests/function_app.py diff --git a/tests/unittests/test_code_quality.py b/tests/unittests/test_code_quality.py new file mode 100644 index 000000000..54d1cc725 --- /dev/null +++ b/tests/unittests/test_code_quality.py @@ -0,0 +1,54 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import pathlib +import subprocess +import sys +import unittest + +ROOT_PATH = pathlib.Path(__file__).parent.parent.parent + + +class TestCodeQuality(unittest.TestCase): + def test_mypy(self): + try: + import mypy # NoQA + except ImportError as e: + raise unittest.SkipTest('mypy module is missing') from e + + try: + subprocess.run( + [sys.executable, '-m', 'mypy', '-m', 'azure_functions_worker'], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=str(ROOT_PATH)) + except subprocess.CalledProcessError as ex: + if (sys.version_info[1] == 7 + and sys.version_info[2] == 3): + raise unittest.SkipTest('Subprocess start failing for 3.7.3') \ + from ex + output = ex.output.decode() + raise AssertionError( + f'mypy validation failed:\n{output}') from None + + def test_flake8(self): + try: + import flake8 # NoQA + except ImportError as e: + raise unittest.SkipTest('flake8 module is missing') from e + + config_path = ROOT_PATH / '.flake8' + if not config_path.exists(): + raise unittest.SkipTest('could not locate the .flake8 file') + + try: + subprocess.run( + [sys.executable, '-m', 'flake8', '--config', str(config_path)], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=str(ROOT_PATH)) + except subprocess.CalledProcessError as ex: + output = ex.output.decode() + raise AssertionError( + f'flake8 validation failed:\n{output}') from None diff --git a/tests/unit_tests/test_handle_event.py b/tests/unittests/test_handle_event.py similarity index 89% rename from tests/unit_tests/test_handle_event.py rename to tests/unittests/test_handle_event.py index ed5c51b76..500d2652f 100644 --- a/tests/unit_tests/test_handle_event.py +++ b/tests/unittests/test_handle_event.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import asyncio import unittest from typing import Any @@ -30,7 +29,7 @@ def __init__(self, name: Any): class InnerInnerRequest: def __init__(self, name: Any): self.capabilities = name - self.function_app_directory = "tests\\unit_tests" + self.function_app_directory = "tests\\unittests" class TestObjects(unittest.TestCase): @@ -51,7 +50,12 @@ async def test_worker_init_request(self): properties={'host': '123', 'protos': protos}) result = await worker_init_request(worker_request) - self.assertEqual(result.capabilities, {'WorkerStatus': 'true', 'RpcHttpBodyOnly': 'true', 'SharedMemoryDataTransfer': 'true', 'RpcHttpTriggerMetadataRemoved': 'true', 'RawHttpBodyBytes': 'true', 'TypedDataCollection': 'true'}) + self.assertEqual(result.capabilities, {'WorkerStatus': 'true', + 'RpcHttpBodyOnly': 'true', + 'SharedMemoryDataTransfer': 'true', + 'RpcHttpTriggerMetadataRemoved': 'true', + 'RawHttpBodyBytes': 'true', + 'TypedDataCollection': 'true'}) self.assertEqual(result.worker_metadata.runtime_name, "python") self.assertIsNotNone(result.worker_metadata.runtime_version) self.assertIsNotNone(result.worker_metadata.worker_version) From e66218aa0a721f5bfedc8226b0e0488f7e1eb9d9 Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Mon, 27 Jan 2025 10:33:01 -0600 Subject: [PATCH 03/45] removed variables --- azure_functions_worker/utils/typing_inspect.py | 4 ++-- eng/ci/official-build.yml | 2 -- eng/ci/public-build.yml | 3 --- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/azure_functions_worker/utils/typing_inspect.py b/azure_functions_worker/utils/typing_inspect.py index 27ba19da8..41a544c54 100644 --- a/azure_functions_worker/utils/typing_inspect.py +++ b/azure_functions_worker/utils/typing_inspect.py @@ -12,8 +12,8 @@ # NOTE: This module must support Python 2.7 in addition to Python 3.x import collections.abc -import sys -from typing import Callable, ClassVar, Generic, Tuple, TypeVar, Union, _GenericAlias, _SpecialGenericAlias +from typing import (Callable, ClassVar, Generic, Tuple, + TypeVar, Union, _GenericAlias, _SpecialGenericAlias) # from mypy_extensions import _TypedDictMeta diff --git a/eng/ci/official-build.yml b/eng/ci/official-build.yml index 2621689d1..3bbfa50e1 100644 --- a/eng/ci/official-build.yml +++ b/eng/ci/official-build.yml @@ -28,8 +28,6 @@ resources: ref: refs/tags/release variables: - - template: /eng/templates/utils/variables.yml@self - - template: /eng/templates/utils/official-variables.yml@self - name: codeql.excludePathPatterns value: deps/,build/ diff --git a/eng/ci/public-build.yml b/eng/ci/public-build.yml index e86bb8993..868b35504 100644 --- a/eng/ci/public-build.yml +++ b/eng/ci/public-build.yml @@ -26,9 +26,6 @@ resources: name: 1ESPipelineTemplates/1ESPipelineTemplates ref: refs/tags/release -variables: - - template: /eng/templates/utils/variables.yml@self - extends: template: v1/1ES.Unofficial.PipelineTemplate.yml@1es parameters: From 68427701ae7f1eb8ec1fe28db145a19f84fdc3d0 Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Mon, 27 Jan 2025 10:48:00 -0600 Subject: [PATCH 04/45] default minor version --- eng/templates/jobs/build.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/eng/templates/jobs/build.yml b/eng/templates/jobs/build.yml index 8b28f114e..97e032190 100644 --- a/eng/templates/jobs/build.yml +++ b/eng/templates/jobs/build.yml @@ -17,6 +17,12 @@ jobs: # Extract the last two digits (minor version) from the branch name version=$(echo $(Build.SourceBranchName) | sed 's/dev-\([0-9]*\)/\1/') minor_version=${version: -2} # Get last two digits + + # Check if minor_version is a number; if not, set default to 13 + if ! [[ "$minor_version" =~ ^[0-9]+$ ]]; then + minor_version=13 + fi + echo "Extracted minor version: $minor_version" echo "##vso[task.setvariable variable=pythonVersion]$minor_version" displayName: 'Extract Python version from branch name' From 6b242669aa65bb9f26d74bd85656b38bc5cab239 Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Mon, 27 Jan 2025 11:07:36 -0600 Subject: [PATCH 05/45] fix pip audit --- eng/templates/jobs/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/templates/jobs/build.yml b/eng/templates/jobs/build.yml index 97e032190..baa24a9fd 100644 --- a/eng/templates/jobs/build.yml +++ b/eng/templates/jobs/build.yml @@ -39,5 +39,5 @@ jobs: displayName: 'Build python worker' - bash: | pip install pip-audit - pip-audit -r requirements.txt + pip-audit . displayName: 'Run vulnerability scan' \ No newline at end of file From 3f1a774ccecbdf1aa338ceab43cd23ebd5cca3fb Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Mon, 27 Jan 2025 15:12:38 -0600 Subject: [PATCH 06/45] working official build --- .artifactignore | 1 + eng/ci/official-build.yml | 3 ++ eng/scripts/install-dependencies.sh | 12 ++++++ eng/templates/jobs/ci-unit-tests.yml | 4 ++ .../official/jobs/build-artifacts.yml | 41 ++++++++++++++++--- pyproject.toml | 4 +- 6 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 .artifactignore create mode 100644 eng/scripts/install-dependencies.sh diff --git a/.artifactignore b/.artifactignore new file mode 100644 index 000000000..850e600d6 --- /dev/null +++ b/.artifactignore @@ -0,0 +1 @@ +_manifest\** \ No newline at end of file diff --git a/eng/ci/official-build.yml b/eng/ci/official-build.yml index 3bbfa50e1..e55e67bd2 100644 --- a/eng/ci/official-build.yml +++ b/eng/ci/official-build.yml @@ -47,6 +47,9 @@ extends: - stage: Build jobs: - template: /eng/templates/official/jobs/build-artifacts.yml@self + - stage: Format + jobs: + - template: /eng/templates/official/jobs/format-artifacts.yml@self - stage: RunUnitTests dependsOn: Build jobs: diff --git a/eng/scripts/install-dependencies.sh b/eng/scripts/install-dependencies.sh new file mode 100644 index 000000000..d1b953642 --- /dev/null +++ b/eng/scripts/install-dependencies.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +python -m pip install --upgrade pip +python -m pip install -U azure-functions --pre +python -m pip install -U -e .[dev] + +if [[ $1 != "3.7" ]]; then + python -m pip install --pre -U -e .[test-http-v2] +fi +if [[ $1 != "3.7" && $1 != "3.8" ]]; then + python -m pip install --pre -U -e .[test-deferred-bindings] +fi \ No newline at end of file diff --git a/eng/templates/jobs/ci-unit-tests.yml b/eng/templates/jobs/ci-unit-tests.yml index 00ea36731..c5785c47c 100644 --- a/eng/templates/jobs/ci-unit-tests.yml +++ b/eng/templates/jobs/ci-unit-tests.yml @@ -15,6 +15,10 @@ jobs: displayName: 'Install .NET 8' inputs: version: 8.0.x + - bash: | + chmod +x eng/scripts/install-dependencies.sh + eng/scripts/install-dependencies.sh + displayName: 'Install dependencies' - bash: | python -m pytest -q -n auto --dist loadfile --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch tests/unittests displayName: "Running $(PYTHON_VERSION) Unit Tests" diff --git a/eng/templates/official/jobs/build-artifacts.yml b/eng/templates/official/jobs/build-artifacts.yml index 97c70daa1..6d829a199 100644 --- a/eng/templates/official/jobs/build-artifacts.yml +++ b/eng/templates/official/jobs/build-artifacts.yml @@ -12,10 +12,10 @@ jobs: pythonVersion: $[substring(variables['Build.SourceBranchName'], 4, 2)] # Extract last two digits from dev-xxx templateContext: - outputParentDirectory: $(Build.ArtifactStagingDirectory) + outputParentDirectory: $(Build.c) outputs: - output: pipelineArtifact - targetPath: $(Build.SourcesDirectory) + targetPath: $(Build.SourcesDirectory)/dist artifactName: "azure-functions-worker" steps: @@ -24,6 +24,12 @@ jobs: # Extract the last two digits (minor version) from the branch name version=$(echo $(Build.SourceBranchName) | sed 's/dev-\([0-9]*\)/\1/') minor_version=${version: -2} # Get last two digits + + # Check if minor_version is a number; if not, set default to 13 + if ! [[ "$minor_version" =~ ^[0-9]+$ ]]; then + minor_version=13 + fi + echo "Extracted minor version: $minor_version" echo "##vso[task.setvariable variable=pythonVersion]$minor_version" displayName: 'Extract Python version from branch name' @@ -38,11 +44,34 @@ jobs: python -m pip install build python -m build displayName: 'Build Python Library Worker' + - script: | + echo "Contents of dist folder:" + ls -al $(Build.SourcesDirectory)/dist + displayName: 'Verify dist folder contents' + - script: | + echo "Branch name: $(Build.SourceBranchName)" + # Extract the part after the slash (release/-) + branch_name=$(echo $(Build.SourceBranchName) | sed 's/refs\/heads\///') + + # Extract the package version (everything before the first dash) + package_version=$(echo $branch_name | sed 's|^release/\([^/]*\)-.*|\1|') + + if [[ ! "$package_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([a-zA-Z]+[0-9]*)?$ ]]; then + echo "Invalid package version detected. Setting to default: 1.0.0a13" + package_version="1.0.0" + fi + + # Print the extracted package version + echo "Extracted package version: $package_version" + + # Set the package version as a pipeline variable + echo "##vso[task.setvariable variable=packageVersion]$package_version" + displayName: 'Extract Package Version from Branch Name' + - script: | + wheel_file=$(ls $(Build.SourcesDirectory)/dist/*.whl) + new_wheel_name="azure-functions-worker-$(packageVersion)-py3$(pythonVersion)-none-any.whl" + mv "$wheel_file" "$(Build.SourcesDirectory)/dist/$new_wheel_name" - bash: | pip install pip-audit pip-audit . displayName: 'Run vulnerability scan' - - publish: $(Build.ArtifactStagingDirectory) - artifact: build-artifact - displayName: 'Publish Build Artifact' - diff --git a/pyproject.toml b/pyproject.toml index ef11c7f5f..10b700a4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,9 +45,11 @@ dev = [ "flake8==5.*; python_version == '3.7'", "flake8==6.*; python_version >= '3.8'", "mypy", - "pytest~=7.4.4", + "pytest", "requests==2.*", "coverage", + "grpcio~=1.59.0", + "grpcio-tools~=1.59.0", "pytest-sugar", "pytest-cov", "pytest-xdist", From ecf4d99487effdf06e3f54ee733f22386f829b7c Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 29 Jan 2025 15:49:12 -0600 Subject: [PATCH 07/45] working package compilation --- eng/ci/official-build.yml | 3 -- eng/scripts/install-dependencies.sh | 8 +--- eng/templates/jobs/build.yml | 4 +- .../official/jobs/aggregate-artifacts.yml | 42 +++++++++++-------- .../official/jobs/build-artifacts.yml | 8 ++-- 5 files changed, 32 insertions(+), 33 deletions(-) diff --git a/eng/ci/official-build.yml b/eng/ci/official-build.yml index e55e67bd2..3bbfa50e1 100644 --- a/eng/ci/official-build.yml +++ b/eng/ci/official-build.yml @@ -47,9 +47,6 @@ extends: - stage: Build jobs: - template: /eng/templates/official/jobs/build-artifacts.yml@self - - stage: Format - jobs: - - template: /eng/templates/official/jobs/format-artifacts.yml@self - stage: RunUnitTests dependsOn: Build jobs: diff --git a/eng/scripts/install-dependencies.sh b/eng/scripts/install-dependencies.sh index d1b953642..913cbb04f 100644 --- a/eng/scripts/install-dependencies.sh +++ b/eng/scripts/install-dependencies.sh @@ -4,9 +4,5 @@ python -m pip install --upgrade pip python -m pip install -U azure-functions --pre python -m pip install -U -e .[dev] -if [[ $1 != "3.7" ]]; then - python -m pip install --pre -U -e .[test-http-v2] -fi -if [[ $1 != "3.7" && $1 != "3.8" ]]; then - python -m pip install --pre -U -e .[test-deferred-bindings] -fi \ No newline at end of file +python -m pip install --pre -U -e .[test-http-v2] +python -m pip install --pre -U -e .[test-deferred-bindings] \ No newline at end of file diff --git a/eng/templates/jobs/build.yml b/eng/templates/jobs/build.yml index baa24a9fd..290d6dfd9 100644 --- a/eng/templates/jobs/build.yml +++ b/eng/templates/jobs/build.yml @@ -8,8 +8,8 @@ jobs: os: linux variables: - # Extract the version number from the branch name - pythonVersion: $[substring(variables['Build.SourceBranchName'], 4, 2)] # Extract last two digits from dev-xxx + # Extract the last two digits from the branch name + pythonVersion: $[substring(variables['Build.SourceBranchName'], 4, 2)] steps: - script: | diff --git a/eng/templates/official/jobs/aggregate-artifacts.yml b/eng/templates/official/jobs/aggregate-artifacts.yml index ff665e899..7f4ad1bee 100644 --- a/eng/templates/official/jobs/aggregate-artifacts.yml +++ b/eng/templates/official/jobs/aggregate-artifacts.yml @@ -6,26 +6,32 @@ jobs: name: 1es-pool-azfunc image: 1es-ubuntu-22.04 os: linux + + templateContext: + outputParentDirectory: $(Build.ArtifactStagingDirectory) + outputs: + - output: pipelineArtifact + targetPath: $(Build.SourcesDirectory)/final_package + artifactName: "azure-functions-worker" steps: - - task: DownloadBuildArtifacts@0 + # Add download task for each artifact + - task: DownloadPipelineArtifact@2 + displayName: 'Download Python Worker Artifact' inputs: - buildType: 'specific' - project: $(System.TeamProject) - pipeline: 652 # official build pipeline ID - buildVersionToDownload: 'specific' - specificBuildId: $(BuildId1) # exact build ID - downloadPath: $(Build.ArtifactStagingDirectory) - displayName: 'Download Artifacts from Specific Build' - + buildType: specific + project: 'internal' + definition: 652 + buildVersionToDownload: specific + pipelineId: $(BuildId1) + artifactName: 'azure-functions-worker-py313' + targetPath: "azure-functions-worker-py313" + # Compiles the python packages - script: | - # Assuming each branch build produced a wheel or tar.gz, merge them. - # This can depend on your packaging logic. - mkdir -p $(Build.ArtifactStagingDirectory)/final_package - cp $(Build.ArtifactStagingDirectory)/dev-*/dist/* $(Build.ArtifactStagingDirectory)/dist/ + mkdir -p $(Build.SourcesDirectory)/final_package + cp $(Build.SourcesDirectory)/azure-functions-worker-py3*/*.whl $(Build.SourcesDirectory)/final_package/ displayName: 'Merge and Compile Python Packages' - - - publish: $(Build.ArtifactStagingDirectory)/dist - artifact: azure-functions-worker - displayName: 'Publish Final Python Package' - + - script: | + echo "Contents of final_package folder:" + ls -al $(Build.SourcesDirectory)/final_package + displayName: 'Verify folder contents post compilation' \ No newline at end of file diff --git a/eng/templates/official/jobs/build-artifacts.yml b/eng/templates/official/jobs/build-artifacts.yml index 6d829a199..6f1423311 100644 --- a/eng/templates/official/jobs/build-artifacts.yml +++ b/eng/templates/official/jobs/build-artifacts.yml @@ -9,14 +9,14 @@ jobs: variables: # Extract the version number from the branch name - pythonVersion: $[substring(variables['Build.SourceBranchName'], 4, 2)] # Extract last two digits from dev-xxx + pythonVersion: $[substring(variables['Build.SourceBranchName'], 4, 2)] templateContext: - outputParentDirectory: $(Build.c) + outputParentDirectory: $(Build.ArtifactStagingDirectory) outputs: - output: pipelineArtifact targetPath: $(Build.SourcesDirectory)/dist - artifactName: "azure-functions-worker" + artifactName: "azure-functions-worker-py3$(pythonVersion)" steps: - script: | @@ -57,7 +57,7 @@ jobs: package_version=$(echo $branch_name | sed 's|^release/\([^/]*\)-.*|\1|') if [[ ! "$package_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([a-zA-Z]+[0-9]*)?$ ]]; then - echo "Invalid package version detected. Setting to default: 1.0.0a13" + echo "Invalid package version detected. Setting to default: 1.0.0" package_version="1.0.0" fi From 67f10866de7217f3d1be4935b16178e623f0cfa9 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 29 Jan 2025 15:52:35 -0600 Subject: [PATCH 08/45] run pipelines for dev-3* branches --- eng/ci/code-mirror.yml | 2 +- eng/ci/official-build.yml | 4 ++-- eng/ci/public-build.yml | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/eng/ci/code-mirror.yml b/eng/ci/code-mirror.yml index 46edc6bf1..c9e965de7 100644 --- a/eng/ci/code-mirror.yml +++ b/eng/ci/code-mirror.yml @@ -1,7 +1,7 @@ trigger: branches: include: - - dev-* + - dev-3* - library-release/* resources: diff --git a/eng/ci/official-build.yml b/eng/ci/official-build.yml index 3bbfa50e1..2a5bd84ad 100644 --- a/eng/ci/official-build.yml +++ b/eng/ci/official-build.yml @@ -2,7 +2,7 @@ trigger: batch: true branches: include: - - dev-* + - dev-3* - library-release/* # CI only, does not trigger on PRs. @@ -13,7 +13,7 @@ schedules: displayName: At 12:00 AM, only on Monday branches: include: - - dev + - dev-3* always: true resources: diff --git a/eng/ci/public-build.yml b/eng/ci/public-build.yml index 868b35504..22fae9b39 100644 --- a/eng/ci/public-build.yml +++ b/eng/ci/public-build.yml @@ -2,21 +2,21 @@ trigger: batch: true branches: include: - - dev + - dev-3* - sdk/* - extensions/* pr: branches: include: - - dev + - dev-3* schedules: - cron: '0 0 * * MON' displayName: At 12:00 AM, only on Monday branches: include: - - dev + - dev-3* always: true resources: From 02a2504bf4a73c3694ecc6b3450f881e4ffa5e3c Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Fri, 31 Jan 2025 09:58:49 -0600 Subject: [PATCH 09/45] updatge grpc versions --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 10b700a4c..132c4ce68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,8 +48,8 @@ dev = [ "pytest", "requests==2.*", "coverage", - "grpcio~=1.59.0", - "grpcio-tools~=1.59.0", + "grpcio~=1.70.0", + "grpcio-tools~=1.70.0", "pytest-sugar", "pytest-cov", "pytest-xdist", From a41ce2d1a1f8775d33d8ec0d04de7b4dfdf309eb Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Fri, 31 Jan 2025 10:06:05 -0600 Subject: [PATCH 10/45] default pythonVersion --- eng/templates/jobs/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eng/templates/jobs/build.yml b/eng/templates/jobs/build.yml index 290d6dfd9..285680642 100644 --- a/eng/templates/jobs/build.yml +++ b/eng/templates/jobs/build.yml @@ -8,8 +8,8 @@ jobs: os: linux variables: - # Extract the last two digits from the branch name - pythonVersion: $[substring(variables['Build.SourceBranchName'], 4, 2)] + # Default Variable + pythonVersion: '3.13' steps: - script: | From 9e104c78f17f497b0544313a9704f687ea7948f1 Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Fri, 31 Jan 2025 10:37:18 -0600 Subject: [PATCH 11/45] install azure-functions, mypy fix --- eng/templates/official/jobs/build-artifacts.yml | 4 ++-- mypy.ini | 2 ++ pyproject.toml | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 mypy.ini diff --git a/eng/templates/official/jobs/build-artifacts.yml b/eng/templates/official/jobs/build-artifacts.yml index 6f1423311..cce8fce6a 100644 --- a/eng/templates/official/jobs/build-artifacts.yml +++ b/eng/templates/official/jobs/build-artifacts.yml @@ -8,8 +8,8 @@ jobs: os: linux variables: - # Extract the version number from the branch name - pythonVersion: $[substring(variables['Build.SourceBranchName'], 4, 2)] + # Default version + pythonVersion: '3.13' templateContext: outputParentDirectory: $(Build.ArtifactStagingDirectory) diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 000000000..86f91047d --- /dev/null +++ b/mypy.ini @@ -0,0 +1,2 @@ +[azure_functions_worker/utils/typing_inspect.py] +ignore_errors = True diff --git a/pyproject.toml b/pyproject.toml index 132c4ce68..86bf803a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dynamic = ["version"] #] dependencies = [ "azurefunctions-extensions-base; python_version >= '3.8'", - "vtest-sdk" + "azure-functions" ] # #[project.urls] From 327e0b6e3da2d385449a27adf1b7b2eb76525b45 Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Fri, 31 Jan 2025 14:58:11 -0600 Subject: [PATCH 12/45] mypy fixes --- azure_functions_worker/bindings/context.py | 6 ++-- azure_functions_worker/bindings/datumdef.py | 2 +- azure_functions_worker/bindings/meta.py | 36 +++++++++---------- azure_functions_worker/bindings/out.py | 5 ++- azure_functions_worker/functions.py | 6 ++-- azure_functions_worker/handle_event.py | 2 +- azure_functions_worker/http_v2.py | 6 ++-- azure_functions_worker/loader.py | 6 ++-- azure_functions_worker/logging.py | 11 +----- .../utils/typing_inspect.py | 1 + azure_functions_worker/utils/wrappers.py | 2 +- mypy.ini | 3 ++ 12 files changed, 41 insertions(+), 45 deletions(-) diff --git a/azure_functions_worker/bindings/context.py b/azure_functions_worker/bindings/context.py index 6181e630f..18149abbc 100644 --- a/azure_functions_worker/bindings/context.py +++ b/azure_functions_worker/bindings/context.py @@ -3,8 +3,6 @@ import threading -from typing import Type - from .retrycontext import RetryContext from .tracecontext import TraceContext @@ -14,7 +12,7 @@ def __init__(self, func_name: str, func_dir: str, invocation_id: str, - thread_local_storage: Type[threading.local], + thread_local_storage: threading.local, trace_context: TraceContext, retry_context: RetryContext) -> None: self.__func_name = func_name @@ -29,7 +27,7 @@ def invocation_id(self) -> str: return self.__invocation_id @property - def thread_local_storage(self) -> Type[threading.local]: + def thread_local_storage(self) -> threading.local: return self.__thread_local_storage @property diff --git a/azure_functions_worker/bindings/datumdef.py b/azure_functions_worker/bindings/datumdef.py index c67ccb8e8..7e3c4639e 100644 --- a/azure_functions_worker/bindings/datumdef.py +++ b/azure_functions_worker/bindings/datumdef.py @@ -17,7 +17,7 @@ try: from http.cookies import SimpleCookie except ImportError: - from Cookie import SimpleCookie + from Cookie import SimpleCookie # type: ignore class Datum: diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py index f82eed2eb..82ec0d397 100644 --- a/azure_functions_worker/bindings/meta.py +++ b/azure_functions_worker/bindings/meta.py @@ -1,9 +1,10 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +# mypy: disable-error-code="attr-defined" import os import sys -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Union from .datumdef import Datum, datum_as_proto from .generic import GenericBinding @@ -24,7 +25,7 @@ BINDING_REGISTRY = None DEFERRED_BINDING_REGISTRY = None -deferred_bindings_cache = {} +deferred_bindings_cache: Dict[Any, Any] = {} def _check_http_input_type_annotation(bind_name: str, pytype: type, @@ -69,7 +70,7 @@ def load_binding_registry() -> None: import azure.functions as func global BINDING_REGISTRY - BINDING_REGISTRY = func.get_binding_registry() + BINDING_REGISTRY = func.get_binding_registry() # type: ignore if BINDING_REGISTRY is None: raise AttributeError('BINDING_REGISTRY is None. azure-functions ' @@ -103,9 +104,9 @@ def get_binding(bind_name: str, """ binding = None if binding is None and not is_deferred_binding: - binding = BINDING_REGISTRY.get(bind_name) + binding = BINDING_REGISTRY.get(bind_name) # type: ignore if binding is None and is_deferred_binding: - binding = DEFERRED_BINDING_REGISTRY.get(bind_name) + binding = DEFERRED_BINDING_REGISTRY.get(bind_name) # type: ignore if binding is None: binding = GenericBinding return binding @@ -144,7 +145,7 @@ def has_implicit_output(bind_name: str) -> bool: # Need to pass in bind_name to exempt Durable Functions if binding is GenericBinding: return (getattr(binding, 'has_implicit_output', lambda: False) - (bind_name)) + (bind_name)) # type: ignore else: # If the binding does not have metaclass of meta.InConverter @@ -159,7 +160,7 @@ def from_incoming_proto( trigger_metadata: Optional[Dict[str, Any]], function_name: str, is_deferred_binding: Optional[bool] = False) -> Any: - binding = get_binding(binding, is_deferred_binding) + binding_obj = get_binding(binding, is_deferred_binding) if trigger_metadata: metadata = { k: Datum.from_typed_data(v) @@ -178,30 +179,30 @@ def from_incoming_proto( try: # if the binding is an sdk type binding if is_deferred_binding: - return deferred_bindings_decode(binding=binding, + return deferred_bindings_decode(binding=binding_obj, pb=pb, pytype=pytype, datum=datum, metadata=metadata, function_name=function_name) - return binding.decode(datum, trigger_metadata=metadata) + return binding_obj.decode(datum, trigger_metadata=metadata) except NotImplementedError: # Binding does not support the data. dt = val.WhichOneof('data') raise TypeError( f'unable to decode incoming TypedData: ' f'unsupported combination of TypedData field {dt!r} ' - f'and expected binding type {binding}') + f'and expected binding type {binding_obj}') def get_datum(binding: str, obj: Any, - pytype: Optional[type]) -> Datum: + pytype: Optional[type]) -> Union[Datum, None]: """ Convert an object to a datum with the specified type. """ - binding = get_binding(binding) + binding_obj = get_binding(binding) try: - datum = binding.encode(obj, expected_type=pytype) + datum = binding_obj.encode(obj, expected_type=pytype) except NotImplementedError: # Binding does not support the data. raise TypeError( @@ -220,7 +221,7 @@ def to_outgoing_proto(binding: str, obj: Any, *, pytype: Optional[type], protos): datum = get_datum(binding, obj, pytype) - return datum_as_proto(datum, protos) + return datum_as_proto(datum, protos) # type: ignore def to_outgoing_param_binding(binding: str, obj: Any, *, @@ -230,7 +231,7 @@ def to_outgoing_param_binding(binding: str, obj: Any, *, datum = get_datum(binding, obj, pytype) # If not, send it as part of the response message over RPC # rpc_val can be None here as we now support a None return type - rpc_val = datum_as_proto(datum, protos) + rpc_val = datum_as_proto(datum, protos) # type: ignore return protos.ParameterBinding( name=out_name, data=rpc_val) @@ -277,9 +278,8 @@ def deferred_bindings_decode(binding: Any, return deferred_binding_type -def check_deferred_bindings_enabled(param_anno: type, - deferred_bindings_enabled: bool) -> (bool, - bool): +def check_deferred_bindings_enabled(param_anno: Union[type, None], + deferred_bindings_enabled: bool) -> Any: """ Checks if deferred bindings is enabled at fx and single binding level diff --git a/azure_functions_worker/bindings/out.py b/azure_functions_worker/bindings/out.py index 6e9d0a999..3e2dc0d4b 100644 --- a/azure_functions_worker/bindings/out.py +++ b/azure_functions_worker/bindings/out.py @@ -1,6 +1,9 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +from typing import Optional + + class Out: def __init__(self) -> None: @@ -9,5 +12,5 @@ def __init__(self) -> None: def set(self, val): self.__value = val - def get(self) -> str: + def get(self) -> Optional[str]: return self.__value diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py index f4b743a1d..290bef0e9 100644 --- a/azure_functions_worker/functions.py +++ b/azure_functions_worker/functions.py @@ -12,7 +12,7 @@ check_output_type_annotation, check_input_type_annotation) from .utils.constants import HTTP_TRIGGER -from .utils.typing_inspect import is_generic_type, get_origin, get_args +from .utils.typing_inspect import is_generic_type, get_origin, get_args # type: ignore class ParamTypeInfo(typing.NamedTuple): @@ -35,7 +35,7 @@ class FunctionInfo(typing.NamedTuple): input_types: typing.Mapping[str, ParamTypeInfo] output_types: typing.Mapping[str, ParamTypeInfo] - return_type: typing.Optional[ParamTypeInfo] + return_type: typing.Optional[typing.Union[str, ParamTypeInfo]] trigger_metadata: typing.Optional[typing.Dict[str, typing.Any]] @@ -54,7 +54,7 @@ class Registry: def __init__(self) -> None: self._functions = {} - def get_function(self, function_id: str) -> FunctionInfo: + def get_function(self, function_id: str) -> typing.Union[FunctionInfo, None]: if function_id in self._functions: return self._functions[function_id] diff --git a/azure_functions_worker/handle_event.py b/azure_functions_worker/handle_event.py index 7edf06bfe..1eedbaee9 100644 --- a/azure_functions_worker/handle_event.py +++ b/azure_functions_worker/handle_event.py @@ -53,7 +53,7 @@ result = None # Todo: type is coroutine? _functions = Registry() _function_data_cache_enabled: bool = False -_host: str = None +_host: str = "" protos = None diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py index 87546b5f2..d6847a53c 100644 --- a/azure_functions_worker/http_v2.py +++ b/azure_functions_worker/http_v2.py @@ -6,7 +6,7 @@ import importlib import socket import sys -from typing import Dict +from typing import Any, Dict from .utils.constants import ( BASE_EXT_SUPPORTED_PY_MINOR_VERSION, @@ -119,7 +119,7 @@ class SingletonMeta(type): """ Metaclass for implementing the singleton pattern. """ - _instances = {} + _instances: Dict[Any, Any] = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: @@ -214,7 +214,7 @@ def initialize_http_server(host_addr, **kwargs): @app.route async def catch_all(request: request_type): # type: ignore - invoc_id = request.headers.get(X_MS_INVOCATION_ID) + invoc_id = request.headers.get(X_MS_INVOCATION_ID) # type: ignore if invoc_id is None: raise MissingHeaderError("Header %s not found" % X_MS_INVOCATION_ID) diff --git a/azure_functions_worker/loader.py b/azure_functions_worker/loader.py index ae4d9c656..768611d94 100644 --- a/azure_functions_worker/loader.py +++ b/azure_functions_worker/loader.py @@ -9,7 +9,7 @@ import time from datetime import timedelta -from typing import Dict, Optional +from typing import Any, Dict, Optional, Union from .functions import Registry @@ -32,7 +32,7 @@ _AZURE_NAMESPACE = '__app__' _DEFAULT_SCRIPT_FILENAME = '__init__.py' _DEFAULT_ENTRY_POINT = 'main' -_submodule_dirs = [] +_submodule_dirsL: list[Any] = [] def convert_to_seconds(timestr: str): @@ -52,7 +52,7 @@ def build_binding_protos(protos, indexed_function) -> Dict: return binding_protos -def build_retry_protos(protos, indexed_function) -> Dict: +def build_retry_protos(protos, indexed_function) -> Union[Dict, None]: retry = get_retry_settings(indexed_function) if not retry: diff --git a/azure_functions_worker/logging.py b/azure_functions_worker/logging.py index 5591de7c8..49be533f6 100644 --- a/azure_functions_worker/logging.py +++ b/azure_functions_worker/logging.py @@ -2,7 +2,6 @@ # Licensed under the MIT License. import logging.handlers -import sys import traceback # Logging Prefixes @@ -13,13 +12,5 @@ def format_exception(exception: Exception) -> str: msg = str(exception) + "\n" - if (sys.version_info.major, sys.version_info.minor) < (3, 10): - msg += ''.join(traceback.format_exception( - etype=type(exception), - tb=exception.__traceback__, - value=exception)) - elif (sys.version_info.major, sys.version_info.minor) >= (3, 10): - msg += ''.join(traceback.format_exception(exception)) - else: - msg = str(exception) + msg += ''.join(traceback.format_exception(exception)) return msg diff --git a/azure_functions_worker/utils/typing_inspect.py b/azure_functions_worker/utils/typing_inspect.py index 41a544c54..67726ee3a 100644 --- a/azure_functions_worker/utils/typing_inspect.py +++ b/azure_functions_worker/utils/typing_inspect.py @@ -1,3 +1,4 @@ +# type: ignore # Imported from https://github.com/ilevkivskyi/typing_inspect/blob/168fa6f7c5c55f720ce6282727211cf4cf6368f6/typing_inspect.py # NoQA E501 # Author: Ivan Levkivskyi # License: MIT diff --git a/azure_functions_worker/utils/wrappers.py b/azure_functions_worker/utils/wrappers.py index bd1aeb1bb..1761da37e 100644 --- a/azure_functions_worker/utils/wrappers.py +++ b/azure_functions_worker/utils/wrappers.py @@ -36,7 +36,7 @@ def call(*args, **kwargs): return decorate -def attach_message_to_exception(expt_type: Exception, message: str, +def attach_message_to_exception(expt_type: type[Exception], message: str, debug_logs=None) -> Callable: def decorate(func): def call(*args, **kwargs): diff --git a/mypy.ini b/mypy.ini index 86f91047d..ac0b766bc 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,2 +1,5 @@ [azure_functions_worker/utils/typing_inspect.py] ignore_errors = True + +[mypy] +ignore_missing_imports = True From abbec6fca02355ffaf6d592e150756d17f9bf405 Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Thu, 6 Feb 2025 15:12:07 -0600 Subject: [PATCH 13/45] rename to azure-functions-runtime --- azure_functions_worker/version.py | 2 +- .../official/jobs/aggregate-artifacts.yml | 8 +-- .../official/jobs/build-artifacts.yml | 4 +- pyproject.toml | 54 +++++++++---------- 4 files changed, 32 insertions(+), 36 deletions(-) diff --git a/azure_functions_worker/version.py b/azure_functions_worker/version.py index 77c039c52..8013ff3bc 100644 --- a/azure_functions_worker/version.py +++ b/azure_functions_worker/version.py @@ -1,4 +1,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -VERSION = '1.0.0a13' +VERSION = '0.0.0a1' diff --git a/eng/templates/official/jobs/aggregate-artifacts.yml b/eng/templates/official/jobs/aggregate-artifacts.yml index 7f4ad1bee..fdacc8378 100644 --- a/eng/templates/official/jobs/aggregate-artifacts.yml +++ b/eng/templates/official/jobs/aggregate-artifacts.yml @@ -12,7 +12,7 @@ jobs: outputs: - output: pipelineArtifact targetPath: $(Build.SourcesDirectory)/final_package - artifactName: "azure-functions-worker" + artifactName: "dist" steps: # Add download task for each artifact @@ -24,12 +24,12 @@ jobs: definition: 652 buildVersionToDownload: specific pipelineId: $(BuildId1) - artifactName: 'azure-functions-worker-py313' - targetPath: "azure-functions-worker-py313" + artifactName: 'azure-functions-runtime-py313' + targetPath: "azure-functions-runtime-py313" # Compiles the python packages - script: | mkdir -p $(Build.SourcesDirectory)/final_package - cp $(Build.SourcesDirectory)/azure-functions-worker-py3*/*.whl $(Build.SourcesDirectory)/final_package/ + cp $(Build.SourcesDirectory)/azure-functions-runtime-py3*/*.whl $(Build.SourcesDirectory)/final_package/ displayName: 'Merge and Compile Python Packages' - script: | echo "Contents of final_package folder:" diff --git a/eng/templates/official/jobs/build-artifacts.yml b/eng/templates/official/jobs/build-artifacts.yml index cce8fce6a..85d63f1f9 100644 --- a/eng/templates/official/jobs/build-artifacts.yml +++ b/eng/templates/official/jobs/build-artifacts.yml @@ -16,7 +16,7 @@ jobs: outputs: - output: pipelineArtifact targetPath: $(Build.SourcesDirectory)/dist - artifactName: "azure-functions-worker-py3$(pythonVersion)" + artifactName: "azure-functions-runtime-py3$(pythonVersion)" steps: - script: | @@ -69,7 +69,7 @@ jobs: displayName: 'Extract Package Version from Branch Name' - script: | wheel_file=$(ls $(Build.SourcesDirectory)/dist/*.whl) - new_wheel_name="azure-functions-worker-$(packageVersion)-py3$(pythonVersion)-none-any.whl" + new_wheel_name="azure-functions-runtime-$(packageVersion)-py3$(pythonVersion)-none-any.whl" mv "$wheel_file" "$(Build.SourcesDirectory)/dist/$new_wheel_name" - bash: | pip install pip-audit diff --git a/pyproject.toml b/pyproject.toml index 86bf803a7..e8c98bc72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,38 +1,34 @@ [project] -name = "test-worker" +name = "azure-functions-runtime" dynamic = ["version"] -#description = "Python Language Worker for Azure Functions Runtime" -#authors = [ -# { name = "Azure Functions team at Microsoft Corp.", email = "azurefunctions@microsoft.com" } -#] -#keywords = ["azure", "functions", "azurefunctions", -# "python", "serverless"] -#license = { name = "MIT", file = "LICENSE" } -#readme = { file = "README.md", content-type = "text/markdown" } -#classifiers = [ -# "Development Status :: 5 - Production/Stable", -# "Programming Language :: Python", -# "Programming Language :: Python :: 3", -# "Programming Language :: Python :: 3.7", -# "Programming Language :: Python :: 3.8", -# "Programming Language :: Python :: 3.9", -# "Programming Language :: Python :: 3.10", -# "Programming Language :: Python :: 3.11", -# "Operating System :: Microsoft :: Windows", -# "Operating System :: POSIX", -# "Operating System :: MacOS :: MacOS X", -# "Environment :: Web Environment", -# "License :: OSI Approved :: MIT License", -# "Intended Audience :: Developers" -#] +description = "Python Language Worker for Azure Functions Runtime" +authors = [ + { name = "Azure Functions team at Microsoft Corp.", email = "azurefunctions@microsoft.com" } +] +keywords = ["azure", "functions", "azurefunctions", + "python", "serverless"] +license = { name = "MIT", file = "LICENSE" } +readme = { file = "README.md", content-type = "text/markdown" } +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.13", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Operating System :: MacOS :: MacOS X", + "Environment :: Web Environment", + "License :: OSI Approved :: MIT License", + "Intended Audience :: Developers" +] dependencies = [ "azurefunctions-extensions-base; python_version >= '3.8'", "azure-functions" ] -# -#[project.urls] -#Documentation = "https://github.com/Azure/azure-functions-python-worker?tab=readme-ov-file#-azure-functions-python-worker" -#Repository = "https://github.com/Azure/azure-functions-python-worker" + +[project.urls] +Documentation = "https://github.com/Azure/azure-functions-python-worker?tab=readme-ov-file#-azure-functions-python-worker" +Repository = "https://github.com/Azure/azure-functions-python-worker" [project.optional-dependencies] dev = [ From e292f4a788b3c092146c107b5d73b595a5bd0352 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 12 Feb 2025 14:43:52 -0600 Subject: [PATCH 14/45] adding tests --- .gitignore | 3 + azure_functions_worker/__init__.py | 4 +- azure_functions_worker/handle_event.py | 30 +- azure_functions_worker/http_v2.py | 3 +- .../unittests/basic_function/function_app.py | 44 ++ tests/unittests/function_app.py | 393 ------------------ .../function_app.py | 35 ++ .../streaming_function/function_app.py | 100 +++++ tests/unittests/test_handle_event.py | 93 +++-- 9 files changed, 266 insertions(+), 439 deletions(-) create mode 100644 tests/unittests/basic_function/function_app.py delete mode 100644 tests/unittests/function_app.py create mode 100644 tests/unittests/indexing_exception_function/function_app.py create mode 100644 tests/unittests/streaming_function/function_app.py diff --git a/.gitignore b/.gitignore index e8d9736a3..ca875958b 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,6 @@ prof/ tests/**/host.json tests/**/bin tests/**/extensions.csproj + +# Protos +*pb2* diff --git a/azure_functions_worker/__init__.py b/azure_functions_worker/__init__.py index 0c57acd05..ac3640937 100644 --- a/azure_functions_worker/__init__.py +++ b/azure_functions_worker/__init__.py @@ -5,10 +5,10 @@ functions_metadata_request, function_environment_reload_request, invocation_request, - functions_load_request) + function_load_request) __all__ = ('worker_init_request', 'functions_metadata_request', 'function_environment_reload_request', 'invocation_request', - 'functions_load_request') + 'function_load_request') diff --git a/azure_functions_worker/handle_event.py b/azure_functions_worker/handle_event.py index 1eedbaee9..14ccaefaa 100644 --- a/azure_functions_worker/handle_event.py +++ b/azure_functions_worker/handle_event.py @@ -19,6 +19,7 @@ from .loader import index_function_app, process_indexed_function from .logging import logger from .otel import otel_manager, initialize_azure_monitor, configure_opentelemetry +from .version import VERSION from .bindings.context import get_context from .bindings.meta import (load_binding_registry, is_trigger_binding, @@ -58,7 +59,8 @@ async def worker_init_request(request): - logger.info("Library Worker: received worker_init_request") + logger.info("V2 Library Worker: received WorkerInitRequest," + "Version %s", VERSION) global result, _host, protos, _function_data_cache_enabled init_request = request.request.worker_init_request host_capabilities = init_request.capabilities @@ -91,10 +93,11 @@ async def worker_init_request(request): result = asyncio.create_task(load_function_metadata( init_request.function_app_directory, caller_info="worker_init_request")) - if get_app_setting(setting=PYTHON_ENABLE_INIT_INDEXING): - capabilities[HTTP_URI] = \ - initialize_http_server(_host) - capabilities[REQUIRES_ROUTE_PARAMETERS] = TRUE + if is_envvar_true(PYTHON_ENABLE_INIT_INDEXING): + if HttpV2Registry.http_v2_enabled(): + capabilities[HTTP_URI] = \ + initialize_http_server(_host) + capabilities[REQUIRES_ROUTE_PARAMETERS] = TRUE except HttpServerInitError: raise except Exception as ex: @@ -114,10 +117,8 @@ async def worker_init_request(request): # worker_status_request can be done in the proxy worker async def functions_metadata_request(request): - logger.info("Library Worker: received worker_metadata_request") - global protos - # Todo: should there be a check on if result is None? - global result, metadata_result, metadata_exception + logger.info("V2 Library Worker: received WorkerMetadataRequest") + global protos, result, metadata_result, metadata_exception if result: await result @@ -136,8 +137,8 @@ async def functions_metadata_request(request): status=protos.StatusResult.Success)) -async def functions_load_request(request): - logger.info("Library Worker: received worker_load_request") +async def function_load_request(request): + logger.info("V2 Library Worker: received WorkerLoadRequest") global protos func_request = request.request.function_load_request function_id = func_request.function_id @@ -149,7 +150,7 @@ async def functions_load_request(request): async def invocation_request(request): - logger.info("Library Worker: received worker_invocation_request") + logger.info("V2 Library Worker: received WorkerInvocationRequest") global protos invoc_request = request.request.invocation_request invocation_id = invoc_request.invocation_id @@ -277,7 +278,8 @@ async def function_environment_reload_request(request): This is called only when placeholder mode is true. On worker restarts worker init request will be called directly. """ - logger.info("Library Worker: received worker_env_reload_request") + logger.info("V2 Library Worker: received WorkerInitRequest," + "Version %s", VERSION) try: func_env_reload_request = \ @@ -334,7 +336,7 @@ async def function_environment_reload_request(request): return protos.FunctionEnvironmentReloadResponse( result=protos.StatusResult( status=protos.StatusResult.Failure, - exception=serialize_exception(ex))) + exception=serialize_exception(ex, protos))) async def load_function_metadata(function_app_directory, caller_info): diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py index d6847a53c..733852a26 100644 --- a/azure_functions_worker/http_v2.py +++ b/azure_functions_worker/http_v2.py @@ -285,8 +285,9 @@ def _check_http_v2_enabled(cls): import azurefunctions.extensions.base as ext_base cls._ext_base = ext_base + return True - return cls._ext_base.HttpV2FeatureChecker.http_v2_enabled() + # return cls._ext_base.HttpV2FeatureChecker.http_v2_enabled() http_coordinator = HttpCoordinator() diff --git a/tests/unittests/basic_function/function_app.py b/tests/unittests/basic_function/function_app.py new file mode 100644 index 000000000..512cf83c2 --- /dev/null +++ b/tests/unittests/basic_function/function_app.py @@ -0,0 +1,44 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import logging +import time +from datetime import datetime + +import azure.functions as func + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + + +@app.route(route="default_template") +def default_template(req: func.HttpRequest) -> func.HttpResponse: + logging.info('Python HTTP trigger function processed a request.') + + name = req.params.get('name') + if not name: + try: + req_body = req.get_json() + except ValueError: + pass + else: + name = req_body.get('name') + + if name: + return func.HttpResponse( + f"Hello, {name}. This HTTP triggered function " + f"executed successfully.") + else: + return func.HttpResponse( + "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: func.HttpRequest) -> func.HttpResponse: + time.sleep(1) + + current_time = datetime.now().strftime("%H:%M:%S") + return func.HttpResponse(f"{current_time}") diff --git a/tests/unittests/function_app.py b/tests/unittests/function_app.py deleted file mode 100644 index 51349a631..000000000 --- a/tests/unittests/function_app.py +++ /dev/null @@ -1,393 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import hashlib -import io -import json -import random -import string - -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.function_name(name="blob_trigger") -@app.blob_trigger(arg_name="file", - path="python-worker-tests/test-blob-trigger.txt", - connection="AzureWebJobsStorage") -@app.blob_output(arg_name="$return", - path="python-worker-tests/test-blob-triggered.txt", - connection="AzureWebJobsStorage") -def blob_trigger(file: func.InputStream) -> str: - return json.dumps({ - 'name': file.name, - 'length': file.length, - 'content': file.read().decode('utf-8') - }) - - -@app.function_name(name="get_blob_as_bytes") -@app.route(route="get_blob_as_bytes") -@app.blob_input(arg_name="file", - path="python-worker-tests/test-bytes.txt", - data_type="BINARY", - connection="AzureWebJobsStorage") -def get_blob_as_bytes(req: func.HttpRequest, file: bytes) -> str: - assert isinstance(file, bytes) - return file.decode('utf-8') - - -@app.function_name(name="get_blob_as_bytes_return_http_response") -@app.route(route="get_blob_as_bytes_return_http_response") -@app.blob_input(arg_name="file", - path="python-worker-tests/shmem-test-bytes.txt", - data_type="BINARY", - connection="AzureWebJobsStorage") -def get_blob_as_bytes_return_http_response(req: func.HttpRequest, file: bytes) \ - -> func.HttpResponse: - """ - Read a blob (bytes) and respond back (in HTTP response) with the number of - bytes read and the MD5 digest of the content. - """ - assert isinstance(file, bytes) - - content_size = len(file) - content_md5 = hashlib.md5(file).hexdigest() - - response_dict = { - 'content_size': content_size, - 'content_md5': content_md5 - } - - response_body = json.dumps(response_dict, indent=2) - - return func.HttpResponse( - body=response_body, - mimetype="application/json", - status_code=200 - ) - - -@app.function_name(name="get_blob_as_bytes_stream_return_http_response") -@app.route(route="get_blob_as_bytes_stream_return_http_response") -@app.blob_input(arg_name="file", - path="python-worker-tests/shmem-test-bytes.txt", - data_type="BINARY", - connection="AzureWebJobsStorage") -def get_blob_as_bytes_stream_return_http_response(req: func.HttpRequest, - file: func.InputStream) \ - -> func.HttpResponse: - """ - Read a blob (as azf.InputStream) and respond back (in HTTP response) with - the number of bytes read and the MD5 digest of the content. - """ - file_bytes = file.read() - - content_size = len(file_bytes) - content_md5 = hashlib.md5(file_bytes).hexdigest() - - response_dict = { - 'content_size': content_size, - 'content_md5': content_md5 - } - - response_body = json.dumps(response_dict, indent=2) - - return func.HttpResponse( - body=response_body, - mimetype="application/json", - status_code=200 - ) - - -@app.function_name(name="get_blob_as_str") -@app.route(route="get_blob_as_str") -@app.blob_input(arg_name="file", - path="python-worker-tests/test-str.txt", - data_type="STRING", - connection="AzureWebJobsStorage") -def get_blob_as_str(req: func.HttpRequest, file: str) -> str: - assert isinstance(file, str) - return file - - -@app.function_name(name="get_blob_as_str_return_http_response") -@app.route(route="get_blob_as_str_return_http_response") -@app.blob_input(arg_name="file", - path="python-worker-tests/shmem-test-bytes.txt", - data_type="STRING", - connection="AzureWebJobsStorage") -def get_blob_as_str_return_http_response(req: func.HttpRequest, - file: str) -> func.HttpResponse: - """ - Read a blob (string) and respond back (in HTTP response) with the number of - characters read and the MD5 digest of the utf-8 encoded content. - """ - assert isinstance(file, str) - - num_chars = len(file) - content_bytes = file.encode('utf-8') - content_md5 = hashlib.md5(content_bytes).hexdigest() - - response_dict = { - 'num_chars': num_chars, - 'content_md5': content_md5 - } - - response_body = json.dumps(response_dict, indent=2) - - return func.HttpResponse( - body=response_body, - mimetype="application/json", - status_code=200 - ) - - -@app.function_name(name="get_blob_bytes") -@app.route(route="get_blob_bytes") -@app.blob_input(arg_name="file", - path="python-worker-tests/test-bytes.txt", - connection="AzureWebJobsStorage") -def get_blob_bytes(req: func.HttpRequest, file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -@app.function_name(name="get_blob_filelike") -@app.route(route="get_blob_filelike") -@app.blob_input(arg_name="file", - path="python-worker-tests/test-filelike.txt", - connection="AzureWebJobsStorage") -def get_blob_filelike(req: func.HttpRequest, file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -@app.function_name(name="get_blob_return") -@app.route(route="get_blob_return") -@app.blob_input(arg_name="file", - path="python-worker-tests/test-return.txt", - connection="AzureWebJobsStorage") -def get_blob_return(req: func.HttpRequest, file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -@app.function_name(name="get_blob_str") -@app.route(route="get_blob_str") -@app.blob_input(arg_name="file", - path="python-worker-tests/test-str.txt", - connection="AzureWebJobsStorage") -def get_blob_str(req: func.HttpRequest, file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -@app.function_name(name="get_blob_triggered") -@app.blob_input(arg_name="file", - path="python-worker-tests/test-blob-triggered.txt", - connection="AzureWebJobsStorage") -@app.route(route="get_blob_triggered") -def get_blob_triggered(req: func.HttpRequest, file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -@app.function_name(name="put_blob_as_bytes_return_http_response") -@app.blob_output(arg_name="file", - path="python-worker-tests/shmem-test-bytes-out.txt", - data_type="BINARY", - connection="AzureWebJobsStorage") -@app.route(route="put_blob_as_bytes_return_http_response") -def put_blob_as_bytes_return_http_response(req: func.HttpRequest, - file: func.Out[ - bytes]) -> func.HttpResponse: - """ - Write a blob (bytes) and respond back (in HTTP response) with the number of - bytes written and the MD5 digest of the content. - The number of bytes to write are specified in the input HTTP request. - """ - content_size = int(req.params['content_size']) - - # When this is set, then 0x01 byte is repeated content_size number of - # times to use as input. - # This is to avoid generating random input for large size which can be - # slow. - if 'no_random_input' in req.params: - content = b'\x01' * content_size - else: - content = bytearray(random.getrandbits(8) for _ in range(content_size)) - content_md5 = hashlib.md5(content).hexdigest() - - file.set(content) - - response_dict = { - 'content_size': content_size, - 'content_md5': content_md5 - } - - response_body = json.dumps(response_dict, indent=2) - - return func.HttpResponse( - body=response_body, - mimetype="application/json", - status_code=200 - ) - - -@app.function_name(name="put_blob_as_str_return_http_response") -@app.blob_output(arg_name="file", - path="python-worker-tests/shmem-test-str-out.txt", - data_type="STRING", - connection="AzureWebJobsStorage") -@app.route(route="put_blob_as_str_return_http_response") -def put_blob_as_str_return_http_response(req: func.HttpRequest, file: func.Out[ - str]) -> func.HttpResponse: - """ - Write a blob (string) and respond back (in HTTP response) with the number of - characters written and the MD5 digest of the utf-8 encoded content. - The number of characters to write are specified in the input HTTP request. - """ - num_chars = int(req.params['num_chars']) - - content = ''.join(random.choices(string.ascii_uppercase + string.digits, - k=num_chars)) - content_bytes = content.encode('utf-8') - content_size = len(content_bytes) - content_md5 = hashlib.md5(content_bytes).hexdigest() - - file.set(content) - - response_dict = { - 'num_chars': num_chars, - 'content_size': content_size, - 'content_md5': content_md5 - } - - response_body = json.dumps(response_dict, indent=2) - - return func.HttpResponse( - body=response_body, - mimetype="application/json", - status_code=200 - ) - - -@app.function_name(name="put_blob_bytes") -@app.blob_output(arg_name="file", - path="python-worker-tests/test-bytes.txt", - connection="AzureWebJobsStorage") -@app.route(route="put_blob_bytes") -def put_blob_bytes(req: func.HttpRequest, file: func.Out[bytes]) -> str: - file.set(req.get_body()) - return 'OK' - - -@app.function_name(name="put_blob_filelike") -@app.blob_output(arg_name="file", - path="python-worker-tests/test-filelike.txt", - connection="AzureWebJobsStorage") -@app.route(route="put_blob_filelike") -def put_blob_filelike(req: func.HttpRequest, - file: func.Out[io.StringIO]) -> str: - file.set(io.StringIO('filelike')) - return 'OK' - - -@app.function_name(name="put_blob_return") -@app.blob_output(arg_name="$return", - path="python-worker-tests/test-return.txt", - connection="AzureWebJobsStorage") -@app.route(route="put_blob_return", binding_arg_name="resp") -def put_blob_return(req: func.HttpRequest, - resp: func.Out[func.HttpResponse]) -> str: - return 'FROM RETURN' - - -@app.function_name(name="put_blob_str") -@app.blob_output(arg_name="file", - path="python-worker-tests/test-str.txt", - connection="AzureWebJobsStorage") -@app.route(route="put_blob_str") -def put_blob_str(req: func.HttpRequest, file: func.Out[str]) -> str: - file.set(req.get_body()) - return 'OK' - - -@app.function_name(name="put_blob_trigger") -@app.blob_output(arg_name="file", - path="python-worker-tests/test-blob-trigger.txt", - connection="AzureWebJobsStorage") -@app.route(route="put_blob_trigger") -def put_blob_trigger(req: func.HttpRequest, file: func.Out[str]) -> str: - file.set(req.get_body()) - return 'OK' - - -def _generate_content_and_digest(content_size): - content = bytearray(random.getrandbits(8) for _ in range(content_size)) - content_md5 = hashlib.md5(content).hexdigest() - return content, content_md5 - - -@app.function_name(name="put_get_multiple_blobs_as_bytes_return_http_response") -@app.blob_input(arg_name="inputfile1", - data_type="BINARY", - path="python-worker-tests/shmem-test-bytes-1.txt", - connection="AzureWebJobsStorage") -@app.blob_input(arg_name="inputfile2", - data_type="BINARY", - path="python-worker-tests/shmem-test-bytes-2.txt", - connection="AzureWebJobsStorage") -@app.blob_output(arg_name="outputfile1", - path="python-worker-tests/shmem-test-bytes-out-1.txt", - data_type="BINARY", - connection="AzureWebJobsStorage") -@app.blob_output(arg_name="outputfile2", - path="python-worker-tests/shmem-test-bytes-out-2.txt", - data_type="BINARY", - connection="AzureWebJobsStorage") -@app.route(route="put_get_multiple_blobs_as_bytes_return_http_response") -def put_get_multiple_blobs_as_bytes_return_http_response( - req: func.HttpRequest, - inputfile1: bytes, - inputfile2: bytes, - outputfile1: func.Out[bytes], - outputfile2: func.Out[bytes]) -> func.HttpResponse: - """ - Read two blobs (bytes) and respond back (in HTTP response) with the number - of bytes read from each blob and the MD5 digest of the content of each. - Write two blobs (bytes) and respond back (in HTTP response) with the number - bytes written in each blob and the MD5 digest of the content of each. - The number of bytes to write are specified in the input HTTP request. - """ - input_content_size_1 = len(inputfile1) - input_content_size_2 = len(inputfile2) - - input_content_md5_1 = hashlib.md5(inputfile1).hexdigest() - input_content_md5_2 = hashlib.md5(inputfile2).hexdigest() - - output_content_size_1 = int(req.params['output_content_size_1']) - output_content_size_2 = int(req.params['output_content_size_2']) - - output_content_1, output_content_md5_1 = \ - _generate_content_and_digest(output_content_size_1) - output_content_2, output_content_md5_2 = \ - _generate_content_and_digest(output_content_size_2) - - outputfile1.set(output_content_1) - outputfile2.set(output_content_2) - - response_dict = { - 'input_content_size_1': input_content_size_1, - 'input_content_size_2': input_content_size_2, - 'input_content_md5_1': input_content_md5_1, - 'input_content_md5_2': input_content_md5_2, - 'output_content_size_1': output_content_size_1, - 'output_content_size_2': output_content_size_2, - 'output_content_md5_1': output_content_md5_1, - 'output_content_md5_2': output_content_md5_2 - } - - response_body = json.dumps(response_dict, indent=2) - - return func.HttpResponse( - body=response_body, - mimetype="application/json", - status_code=200 - ) diff --git a/tests/unittests/indexing_exception_function/function_app.py b/tests/unittests/indexing_exception_function/function_app.py new file mode 100644 index 000000000..c4f22a38b --- /dev/null +++ b/tests/unittests/indexing_exception_function/function_app.py @@ -0,0 +1,35 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import logging +import nonexistent.package # noqa + +import azure.functions as func + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + + +@app.route(route="default_template") +def default_template(req: func.HttpRequest) -> func.HttpResponse: + logging.info('Python HTTP trigger function processed a request.') + + name = req.params.get('name') + if not name: + try: + req_body = req.get_json() + except ValueError: + pass + else: + name = req_body.get('name') + + if name: + return func.HttpResponse( + f"Hello, {name}. This HTTP triggered function " + f"executed successfully.") + else: + return func.HttpResponse( + "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 + ) diff --git a/tests/unittests/streaming_function/function_app.py b/tests/unittests/streaming_function/function_app.py new file mode 100644 index 000000000..b20f5440c --- /dev/null +++ b/tests/unittests/streaming_function/function_app.py @@ -0,0 +1,100 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import logging +import time +from datetime import datetime + +import azure.functions as func +from azurefunctions.extensions.http.fastapi import ( + FileResponse, + HTMLResponse, + ORJSONResponse, + Request, + Response, + StreamingResponse, + UJSONResponse, +) + +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") diff --git a/tests/unittests/test_handle_event.py b/tests/unittests/test_handle_event.py index 500d2652f..bc6f565c0 100644 --- a/tests/unittests/test_handle_event.py +++ b/tests/unittests/test_handle_event.py @@ -1,18 +1,23 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - -import unittest - +import os from typing import Any +from unittest.mock import patch -from tests.utils import testutils -import tests.protos as protos - +from azure_functions_worker.utils.constants import PYTHON_ENABLE_INIT_INDEXING from azure_functions_worker.handle_event import (worker_init_request, functions_metadata_request, function_environment_reload_request) +from tests.utils import testutils +import tests.protos as protos +BASIC_FUNCTION_DIRECTORY = "tests\\unittests\\basic_function" +STREAMING_FUNCTION_DIRECTORY = "tests\\unittests\\streaming_function" +INDEXING_EXCEPTION_FUNCTION_DIRECTORY = "tests\\unittests\\indexing_exception_function" + + +# This represents the top level protos request sent from the host class WorkerRequest: def __init__(self, name: str, request: Any, properties: dict): self.name = name @@ -20,33 +25,26 @@ def __init__(self, name: str, request: Any, properties: dict): self.properties = properties -class InnerRequest: +# This represents the inner request +class Request: def __init__(self, name: Any): self.worker_init_request = name self.function_environment_reload_request = name -class InnerInnerRequest: - def __init__(self, name: Any): - self.capabilities = name - self.function_app_directory = "tests\\unittests" - - -class TestObjects(unittest.TestCase): - def test_stringify_enum(self): - pass - - def test_status(self): - pass - - def test_worker_response(self): - pass +# This represents the Function Init/Metadata/Load/Invocation request +class FunctionRequest: + def __init__(self, capabilities: Any, function_app_directory: Any): + self.capabilities = capabilities + self.function_app_directory = function_app_directory class TestHandleEvent(testutils.AsyncTestCase): async def test_worker_init_request(self): worker_request = WorkerRequest(name='worker_init_request', - request=InnerRequest(InnerInnerRequest('hello')), + request=Request(FunctionRequest( + 'hello', + BASIC_FUNCTION_DIRECTORY)), properties={'host': '123', 'protos': protos}) result = await worker_init_request(worker_request) @@ -62,11 +60,48 @@ async def test_worker_init_request(self): self.assertIsNotNone(result.worker_metadata.worker_bitness) self.assertEqual(result.result.status, 1) - def test_worker_init_request_with_streaming(self): - pass + @patch.dict(os.environ, {PYTHON_ENABLE_INIT_INDEXING: '1'}) + async def test_worker_init_request_with_streaming(self): + worker_request = WorkerRequest(name='worker_init_request', + request=Request(FunctionRequest( + 'hello', + STREAMING_FUNCTION_DIRECTORY)), + properties={'host': '123', + 'protos': protos}) + result = await worker_init_request(worker_request) + self.assertEqual(result.capabilities, {'WorkerStatus': 'true', + 'RpcHttpBodyOnly': 'true', + 'SharedMemoryDataTransfer': 'true', + 'RpcHttpTriggerMetadataRemoved': 'true', + 'RawHttpBodyBytes': 'true', + 'TypedDataCollection': 'true'}) + self.assertEqual(result.worker_metadata.runtime_name, "python") + self.assertIsNotNone(result.worker_metadata.runtime_version) + self.assertIsNotNone(result.worker_metadata.worker_version) + self.assertIsNotNone(result.worker_metadata.worker_bitness) + self.assertEqual(result.result.status, 1) - def test_worker_init_request_with_exception(self): - pass + async def test_worker_init_request_with_exception(self): + # Even if an exception happens during indexing, + # we still return the WorkerInitResponse + worker_request = WorkerRequest(name='worker_init_request', + request=Request(FunctionRequest( + 'hello', + INDEXING_EXCEPTION_FUNCTION_DIRECTORY)), + properties={'host': '123', + 'protos': protos}) + result = await worker_init_request(worker_request) + self.assertEqual(result.capabilities, {'WorkerStatus': 'true', + 'RpcHttpBodyOnly': 'true', + 'SharedMemoryDataTransfer': 'true', + 'RpcHttpTriggerMetadataRemoved': 'true', + 'RawHttpBodyBytes': 'true', + 'TypedDataCollection': 'true'}) + self.assertEqual(result.worker_metadata.runtime_name, "python") + self.assertIsNotNone(result.worker_metadata.runtime_version) + self.assertIsNotNone(result.worker_metadata.worker_version) + self.assertIsNotNone(result.worker_metadata.worker_bitness) + self.assertEqual(result.result.status, 1) async def test_functions_metadata_request(self): result = await self.run_init_then_meta() @@ -76,7 +111,7 @@ async def test_functions_metadata_request(self): async def run_init_then_meta(self): worker_request = WorkerRequest(name='worker_init_request', - request=InnerRequest(InnerInnerRequest('hello')), + request=Request(FunctionRequest('hello')), properties={'host': '123', 'protos': protos}) _ = await worker_init_request(worker_request) @@ -97,7 +132,7 @@ def test_invocation_request_with_exception(self): async def test_function_environment_reload_request(self): worker_request = WorkerRequest(name='function_environment_reload_request', - request=InnerRequest(InnerInnerRequest('hello')), + request=Request(FunctionRequest('hello')), properties={'host': '123', 'protos': protos}) result = await function_environment_reload_request(worker_request) From 2038019ca90fb7db7f20ed9717ed6c5282288db9 Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Fri, 21 Feb 2025 11:24:34 -0600 Subject: [PATCH 15/45] a17: rename to azure_functions_worker_v2 --- .coveragerc | 2 +- .flake8 | 2 +- .github/linters/tox.ini | 4 ++-- .github/workflows/linter.yml | 2 +- .../__init__.py | 0 .../bindings/context.py | 0 .../bindings/datumdef.py | 0 .../bindings/generic.py | 0 .../bindings/meta.py | 0 .../bindings/nullable_converters.py | 0 .../bindings/out.py | 0 .../bindings/retrycontext.py | 0 .../bindings/tracecontext.py | 0 .../functions.py | 0 .../handle_event.py | 0 .../http_v2.py | 2 +- .../loader.py | 0 .../logging.py | 0 .../otel.py | 0 .../utils/__init__.py | 0 .../utils/constants.py | 0 .../utils/current.py | 0 .../utils/env_state.py | 0 .../utils/helpers.py | 0 .../utils/tracing.py | 0 .../utils/typing_inspect.py | 0 .../utils/validators.py | 0 .../utils/wrappers.py | 0 .../version.py | 2 +- eng/templates/jobs/ci-unit-tests.yml | 2 +- mypy.ini | 2 +- pyproject.toml | 8 ++++---- setup.cfg | 4 ++-- tests/unittests/test_code_quality.py | 2 +- tests/unittests/test_handle_event.py | 6 +++--- 35 files changed, 19 insertions(+), 19 deletions(-) rename {azure_functions_worker => azure_functions_worker_v2}/__init__.py (100%) rename {azure_functions_worker => azure_functions_worker_v2}/bindings/context.py (100%) rename {azure_functions_worker => azure_functions_worker_v2}/bindings/datumdef.py (100%) rename {azure_functions_worker => azure_functions_worker_v2}/bindings/generic.py (100%) rename {azure_functions_worker => azure_functions_worker_v2}/bindings/meta.py (100%) rename {azure_functions_worker => azure_functions_worker_v2}/bindings/nullable_converters.py (100%) rename {azure_functions_worker => azure_functions_worker_v2}/bindings/out.py (100%) rename {azure_functions_worker => azure_functions_worker_v2}/bindings/retrycontext.py (100%) rename {azure_functions_worker => azure_functions_worker_v2}/bindings/tracecontext.py (100%) rename {azure_functions_worker => azure_functions_worker_v2}/functions.py (100%) rename {azure_functions_worker => azure_functions_worker_v2}/handle_event.py (100%) rename {azure_functions_worker => azure_functions_worker_v2}/http_v2.py (99%) rename {azure_functions_worker => azure_functions_worker_v2}/loader.py (100%) rename {azure_functions_worker => azure_functions_worker_v2}/logging.py (100%) rename {azure_functions_worker => azure_functions_worker_v2}/otel.py (100%) rename {azure_functions_worker => azure_functions_worker_v2}/utils/__init__.py (100%) rename {azure_functions_worker => azure_functions_worker_v2}/utils/constants.py (100%) rename {azure_functions_worker => azure_functions_worker_v2}/utils/current.py (100%) rename {azure_functions_worker => azure_functions_worker_v2}/utils/env_state.py (100%) rename {azure_functions_worker => azure_functions_worker_v2}/utils/helpers.py (100%) rename {azure_functions_worker => azure_functions_worker_v2}/utils/tracing.py (100%) rename {azure_functions_worker => azure_functions_worker_v2}/utils/typing_inspect.py (100%) rename {azure_functions_worker => azure_functions_worker_v2}/utils/validators.py (100%) rename {azure_functions_worker => azure_functions_worker_v2}/utils/wrappers.py (100%) rename {azure_functions_worker => azure_functions_worker_v2}/version.py (81%) diff --git a/.coveragerc b/.coveragerc index a3dbea58b..04ff8b376 100644 --- a/.coveragerc +++ b/.coveragerc @@ -49,4 +49,4 @@ omit = # Removing the imported libraries that might show up in this. */azure/functions/* */azure/* - */azure_functions_worker/_thirdparty/* + */azure_functions_worker_v2/_thirdparty/* diff --git a/.flake8 b/.flake8 index 38d73d9c9..8fa776784 100644 --- a/.flake8 +++ b/.flake8 @@ -6,7 +6,7 @@ ignore = W503,E402,E731 exclude = .git, __pycache__, build, dist, .eggs, .github, .local, docs/, - Samples, azure_functions_worker/protos/, + Samples, azure_functions_worker_v2_v2/protos/, azure_functions_worker/utils/typing_inspect.py, tests/unittests/test_typing_inspect.py, tests/unittests/broken_functions/syntax_error/main.py, diff --git a/.github/linters/tox.ini b/.github/linters/tox.ini index e024de18f..43dd07423 100644 --- a/.github/linters/tox.ini +++ b/.github/linters/tox.ini @@ -23,8 +23,8 @@ exclude = docs, Samples, __pycache__, - azure_functions_worker/protos/, - azure_functions_worker/_thirdparty/typing_inspect.py, + azure_functions_worker_v2/protos/, + azure_functions_worker_v2/_thirdparty/typing_inspect.py, tests/unittests/test_typing_inspect.py, .venv*, .env*, diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 91c0ee6d5..d17ba9914 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -52,7 +52,7 @@ jobs: # VALIDATE_PYTHON_PYLINT: false # disable pylint, as we have not configure it # VALIDATE_PYTHON_BLACK: false # same as above PYTHON_FLAKE8_CONFIG_FILE: tox.ini - FILTER_REGEX_INCLUDE: azure_functions_worker/.* + FILTER_REGEX_INCLUDE: azure_functions_worker_v2/.* FILTER_REGEX_EXCLUDE: tests/.* DEFAULT_BRANCH: dev GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/azure_functions_worker/__init__.py b/azure_functions_worker_v2/__init__.py similarity index 100% rename from azure_functions_worker/__init__.py rename to azure_functions_worker_v2/__init__.py diff --git a/azure_functions_worker/bindings/context.py b/azure_functions_worker_v2/bindings/context.py similarity index 100% rename from azure_functions_worker/bindings/context.py rename to azure_functions_worker_v2/bindings/context.py diff --git a/azure_functions_worker/bindings/datumdef.py b/azure_functions_worker_v2/bindings/datumdef.py similarity index 100% rename from azure_functions_worker/bindings/datumdef.py rename to azure_functions_worker_v2/bindings/datumdef.py diff --git a/azure_functions_worker/bindings/generic.py b/azure_functions_worker_v2/bindings/generic.py similarity index 100% rename from azure_functions_worker/bindings/generic.py rename to azure_functions_worker_v2/bindings/generic.py diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker_v2/bindings/meta.py similarity index 100% rename from azure_functions_worker/bindings/meta.py rename to azure_functions_worker_v2/bindings/meta.py diff --git a/azure_functions_worker/bindings/nullable_converters.py b/azure_functions_worker_v2/bindings/nullable_converters.py similarity index 100% rename from azure_functions_worker/bindings/nullable_converters.py rename to azure_functions_worker_v2/bindings/nullable_converters.py diff --git a/azure_functions_worker/bindings/out.py b/azure_functions_worker_v2/bindings/out.py similarity index 100% rename from azure_functions_worker/bindings/out.py rename to azure_functions_worker_v2/bindings/out.py diff --git a/azure_functions_worker/bindings/retrycontext.py b/azure_functions_worker_v2/bindings/retrycontext.py similarity index 100% rename from azure_functions_worker/bindings/retrycontext.py rename to azure_functions_worker_v2/bindings/retrycontext.py diff --git a/azure_functions_worker/bindings/tracecontext.py b/azure_functions_worker_v2/bindings/tracecontext.py similarity index 100% rename from azure_functions_worker/bindings/tracecontext.py rename to azure_functions_worker_v2/bindings/tracecontext.py diff --git a/azure_functions_worker/functions.py b/azure_functions_worker_v2/functions.py similarity index 100% rename from azure_functions_worker/functions.py rename to azure_functions_worker_v2/functions.py diff --git a/azure_functions_worker/handle_event.py b/azure_functions_worker_v2/handle_event.py similarity index 100% rename from azure_functions_worker/handle_event.py rename to azure_functions_worker_v2/handle_event.py diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker_v2/http_v2.py similarity index 99% rename from azure_functions_worker/http_v2.py rename to azure_functions_worker_v2/http_v2.py index 733852a26..c997a2b24 100644 --- a/azure_functions_worker/http_v2.py +++ b/azure_functions_worker_v2/http_v2.py @@ -13,7 +13,7 @@ PYTHON_ENABLE_INIT_INDEXING, X_MS_INVOCATION_ID, ) -from azure_functions_worker.logging import logger +from azure_functions_worker_v2.logging import logger from .utils.env_state import is_envvar_false diff --git a/azure_functions_worker/loader.py b/azure_functions_worker_v2/loader.py similarity index 100% rename from azure_functions_worker/loader.py rename to azure_functions_worker_v2/loader.py diff --git a/azure_functions_worker/logging.py b/azure_functions_worker_v2/logging.py similarity index 100% rename from azure_functions_worker/logging.py rename to azure_functions_worker_v2/logging.py diff --git a/azure_functions_worker/otel.py b/azure_functions_worker_v2/otel.py similarity index 100% rename from azure_functions_worker/otel.py rename to azure_functions_worker_v2/otel.py diff --git a/azure_functions_worker/utils/__init__.py b/azure_functions_worker_v2/utils/__init__.py similarity index 100% rename from azure_functions_worker/utils/__init__.py rename to azure_functions_worker_v2/utils/__init__.py diff --git a/azure_functions_worker/utils/constants.py b/azure_functions_worker_v2/utils/constants.py similarity index 100% rename from azure_functions_worker/utils/constants.py rename to azure_functions_worker_v2/utils/constants.py diff --git a/azure_functions_worker/utils/current.py b/azure_functions_worker_v2/utils/current.py similarity index 100% rename from azure_functions_worker/utils/current.py rename to azure_functions_worker_v2/utils/current.py diff --git a/azure_functions_worker/utils/env_state.py b/azure_functions_worker_v2/utils/env_state.py similarity index 100% rename from azure_functions_worker/utils/env_state.py rename to azure_functions_worker_v2/utils/env_state.py diff --git a/azure_functions_worker/utils/helpers.py b/azure_functions_worker_v2/utils/helpers.py similarity index 100% rename from azure_functions_worker/utils/helpers.py rename to azure_functions_worker_v2/utils/helpers.py diff --git a/azure_functions_worker/utils/tracing.py b/azure_functions_worker_v2/utils/tracing.py similarity index 100% rename from azure_functions_worker/utils/tracing.py rename to azure_functions_worker_v2/utils/tracing.py diff --git a/azure_functions_worker/utils/typing_inspect.py b/azure_functions_worker_v2/utils/typing_inspect.py similarity index 100% rename from azure_functions_worker/utils/typing_inspect.py rename to azure_functions_worker_v2/utils/typing_inspect.py diff --git a/azure_functions_worker/utils/validators.py b/azure_functions_worker_v2/utils/validators.py similarity index 100% rename from azure_functions_worker/utils/validators.py rename to azure_functions_worker_v2/utils/validators.py diff --git a/azure_functions_worker/utils/wrappers.py b/azure_functions_worker_v2/utils/wrappers.py similarity index 100% rename from azure_functions_worker/utils/wrappers.py rename to azure_functions_worker_v2/utils/wrappers.py diff --git a/azure_functions_worker/version.py b/azure_functions_worker_v2/version.py similarity index 81% rename from azure_functions_worker/version.py rename to azure_functions_worker_v2/version.py index 8013ff3bc..a16c3e329 100644 --- a/azure_functions_worker/version.py +++ b/azure_functions_worker_v2/version.py @@ -1,4 +1,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -VERSION = '0.0.0a1' +VERSION = '1.0.0a17' diff --git a/eng/templates/jobs/ci-unit-tests.yml b/eng/templates/jobs/ci-unit-tests.yml index c5785c47c..e2414e565 100644 --- a/eng/templates/jobs/ci-unit-tests.yml +++ b/eng/templates/jobs/ci-unit-tests.yml @@ -20,5 +20,5 @@ jobs: eng/scripts/install-dependencies.sh displayName: 'Install dependencies' - bash: | - 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_v2 --cov-report xml --cov-branch tests/unittests displayName: "Running $(PYTHON_VERSION) Unit Tests" diff --git a/mypy.ini b/mypy.ini index ac0b766bc..fb0651860 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,4 +1,4 @@ -[azure_functions_worker/utils/typing_inspect.py] +[azure_functions_worker_v2/utils/typing_inspect.py] ignore_errors = True [mypy] diff --git a/pyproject.toml b/pyproject.toml index e8c98bc72..dbea68473 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "azure-functions-runtime" +name = "test-worker" dynamic = ["version"] description = "Python Language Worker for Azure Functions Runtime" authors = [ @@ -86,9 +86,9 @@ profile = "black" line_length = 88 multi_line_output = 3 include_trailing_comma = true -known_first_party = ["azure_functions_worker"] +known_first_party = ["azure_functions_worker_v2"] default_section = "THIRDPARTY" -src_paths = ["azure_functions_worker"] +src_paths = ["azure_functions_worker_v2"] skip_glob = [ "build", "dist", @@ -109,4 +109,4 @@ skip_glob = [ ] [tool.setuptools.dynamic] -version = {attr = "azure_functions_worker.version.VERSION"} +version = {attr = "azure_functions_worker_v2.version.VERSION"} diff --git a/setup.cfg b/setup.cfg index 6f5a7fb98..5ed48789c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,8 +13,8 @@ warn_return_any = True disallow_subclassing_any = False ignore_missing_imports = True -[mypy-azure_functions_worker.protos.*] +[mypy-azure_functions_worker_v2.protos.*] ignore_errors = True -[mypy-azure_functions_worker._thirdparty.typing_inspect] +[mypy-azure_functions_worker_v2._thirdparty.typing_inspect] ignore_errors = True diff --git a/tests/unittests/test_code_quality.py b/tests/unittests/test_code_quality.py index 54d1cc725..a4db9e095 100644 --- a/tests/unittests/test_code_quality.py +++ b/tests/unittests/test_code_quality.py @@ -17,7 +17,7 @@ def test_mypy(self): try: subprocess.run( - [sys.executable, '-m', 'mypy', '-m', 'azure_functions_worker'], + [sys.executable, '-m', 'mypy', '-m', 'azure_functions_worker_v2'], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, diff --git a/tests/unittests/test_handle_event.py b/tests/unittests/test_handle_event.py index bc6f565c0..7afeb35f3 100644 --- a/tests/unittests/test_handle_event.py +++ b/tests/unittests/test_handle_event.py @@ -4,8 +4,8 @@ from typing import Any from unittest.mock import patch -from azure_functions_worker.utils.constants import PYTHON_ENABLE_INIT_INDEXING -from azure_functions_worker.handle_event import (worker_init_request, +from azure_functions_worker_v2.utils.constants import PYTHON_ENABLE_INIT_INDEXING +from azure_functions_worker_v2.handle_event import (worker_init_request, functions_metadata_request, function_environment_reload_request) from tests.utils import testutils @@ -111,7 +111,7 @@ async def test_functions_metadata_request(self): async def run_init_then_meta(self): worker_request = WorkerRequest(name='worker_init_request', - request=Request(FunctionRequest('hello')), + request=Request(FunctionRequest('hello', BASIC_FUNCTION_DIRECTORY)), properties={'host': '123', 'protos': protos}) _ = await worker_init_request(worker_request) From 204bdbc82673a95772362736c686497994ef8869 Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Mon, 24 Feb 2025 09:45:20 -0600 Subject: [PATCH 16/45] a21: remove old references to afw --- azure_functions_worker_v2/bindings/meta.py | 3 +++ azure_functions_worker_v2/loader.py | 1 + azure_functions_worker_v2/version.py | 2 +- pyproject.toml | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/azure_functions_worker_v2/bindings/meta.py b/azure_functions_worker_v2/bindings/meta.py index 82ec0d397..6e38c8745 100644 --- a/azure_functions_worker_v2/bindings/meta.py +++ b/azure_functions_worker_v2/bindings/meta.py @@ -35,6 +35,7 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type, .check_type(pytype) binding = get_binding(bind_name, is_deferred_binding) + logger.info("VICTORIA -- inside _check_http_input_type_annotation. Bind name: %s, binding: %s, pytype: %s, check: %s", bind_name, binding, pytype, binding.check_input_type_annotation(pytype)) return binding.check_input_type_annotation(pytype) @@ -43,6 +44,7 @@ def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool: return HttpV2Registry.ext_base().ResponseTrackerMeta.check_type(pytype) binding = get_binding(bind_name) + logger.info("VICTORIA -- inside _check_http_output_type_annotation. Bind name: %s, binding: %s, pytype: %s, check: %s", bind_name, binding, pytype, binding.check_output_type_annotation(pytype)) return binding.check_output_type_annotation(pytype) @@ -68,6 +70,7 @@ def load_binding_registry() -> None: if func is None: import azure.functions as func + logger.info(f"VICTORIA ---- azure-functions import succeeded: {func.__file__}") global BINDING_REGISTRY BINDING_REGISTRY = func.get_binding_registry() # type: ignore diff --git a/azure_functions_worker_v2/loader.py b/azure_functions_worker_v2/loader.py index 768611d94..80c41fa91 100644 --- a/azure_functions_worker_v2/loader.py +++ b/azure_functions_worker_v2/loader.py @@ -168,6 +168,7 @@ def index_function_app(function_path: str): imported_module = importlib.import_module(module_name) from azure.functions import FunctionRegister + logger.info(f"VICTORIA ---- FunctionRegister import succeeded: {FunctionRegister}") app: Optional[FunctionRegister] = None for i in imported_module.__dir__(): if isinstance(getattr(imported_module, i, None), FunctionRegister): diff --git a/azure_functions_worker_v2/version.py b/azure_functions_worker_v2/version.py index a16c3e329..82371e9fa 100644 --- a/azure_functions_worker_v2/version.py +++ b/azure_functions_worker_v2/version.py @@ -1,4 +1,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -VERSION = '1.0.0a17' +VERSION = '1.0.0a21' diff --git a/pyproject.toml b/pyproject.toml index dbea68473..c3602f446 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] exclude = [ - 'eng', 'tests', 'pack' + 'eng', 'tests', 'pack', 'azure_functions_worker' ] [tool.isort] From a533f819889a0839a8ccfacdc810e864c04258a4 Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Tue, 25 Feb 2025 16:55:21 -0600 Subject: [PATCH 17/45] latest logs --- azure_functions_worker_v2/bindings/meta.py | 4 ++++ azure_functions_worker_v2/functions.py | 13 ++++++++----- azure_functions_worker_v2/handle_event.py | 8 ++++++++ azure_functions_worker_v2/http_v2.py | 2 +- azure_functions_worker_v2/version.py | 2 +- 5 files changed, 22 insertions(+), 7 deletions(-) diff --git a/azure_functions_worker_v2/bindings/meta.py b/azure_functions_worker_v2/bindings/meta.py index 6e38c8745..055213a5b 100644 --- a/azure_functions_worker_v2/bindings/meta.py +++ b/azure_functions_worker_v2/bindings/meta.py @@ -30,6 +30,7 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type, is_deferred_binding: bool) -> bool: + logger.info("VICTORIA --- http v2 enabled %s", (HttpV2Registry.http_v2_enabled())) if HttpV2Registry.http_v2_enabled(): return HttpV2Registry.ext_base().RequestTrackerMeta \ .check_type(pytype) @@ -123,12 +124,15 @@ def is_trigger_binding(bind_name: str) -> bool: def check_input_type_annotation(bind_name: str, pytype: type, is_deferred_binding: bool) -> bool: + logger.info("VICTORIA --- Inside check_input_type_annotation. bind_name: %s, pytype: %s", bind_name, pytype) global INPUT_TYPE_CHECK_OVERRIDE_MAP + logger.info("VICTORIA --- bind_name in input type check map: %s", (bind_name in 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) binding = get_binding(bind_name, is_deferred_binding) + logger.info("VICTORIA -- inside _check_http_input_type_annotation. Bind name: %s, binding: %s, pytype: %s, check: %s", bind_name, binding, pytype, binding.check_input_type_annotation(pytype)) return binding.check_input_type_annotation(pytype) diff --git a/azure_functions_worker_v2/functions.py b/azure_functions_worker_v2/functions.py index 290bef0e9..37ff7dfb5 100644 --- a/azure_functions_worker_v2/functions.py +++ b/azure_functions_worker_v2/functions.py @@ -136,7 +136,7 @@ def is_context_required(params, bound_params: dict, def validate_function_params(params: dict, bound_params: dict, annotations: dict, func_name: str, protos): - logger.info("Params: %s, BoundParams: %s, Annotations: %s, FuncName: %s", + logger.info("VICTORIA --- Params: %s, BoundParams: %s, Annotations: %s, FuncName: %s", params, bound_params, annotations, func_name) if set(params) - set(bound_params): raise FunctionLoadError( @@ -156,11 +156,11 @@ def validate_function_params(params: dict, bound_params: dict, for param in params.values(): binding = bound_params[param.name] - logger.info("Param %s, binding: %s", param, binding) + logger.info("VICTORIA --- Param %s, binding: %s", param, binding) param_has_anno = param.name in annotations param_anno = annotations.get(param.name) - logger.info("Param_has_anno %s, param_anno: %s", param_has_anno, param_anno) + logger.info("VICTORIA --- Param_has_anno %s, param_anno: %s", param_has_anno, param_anno) # Check if deferred bindings is enabled fx_deferred_bindings_enabled, is_deferred_binding = ( @@ -210,7 +210,7 @@ def validate_function_params(params: dict, bound_params: dict, else: param_py_type = param_anno - logger.info("Param_py_type %s", param_py_type) + logger.info("VICTORIA --- Param_py_type %s", param_py_type) if (param_has_anno and not isinstance(param_py_type, type) and not is_generic_type(param_py_type)): @@ -239,7 +239,7 @@ def validate_function_params(params: dict, bound_params: dict, else: param_bind_type = binding.type - logger.info("param_bind_type %s", param_bind_type) + logger.info("VICTORIA --- param_bind_type %s", param_bind_type) if param_has_anno: if is_param_out: @@ -249,6 +249,9 @@ def validate_function_params(params: dict, bound_params: dict, checks_out = check_input_type_annotation( param_bind_type, param_py_type, is_deferred_binding) + logger.info("VICTORIA --- checks_out: %s", + checks_out) + if not checks_out: if binding.data_type is not protos.BindingInfo.undefined: raise FunctionLoadError( diff --git a/azure_functions_worker_v2/handle_event.py b/azure_functions_worker_v2/handle_event.py index 14ccaefaa..8240e6359 100644 --- a/azure_functions_worker_v2/handle_event.py +++ b/azure_functions_worker_v2/handle_event.py @@ -153,10 +153,12 @@ async def invocation_request(request): logger.info("V2 Library Worker: received WorkerInvocationRequest") global protos invoc_request = request.request.invocation_request + logger.info("VICTORIA --- invocation request %s", invoc_request) invocation_id = invoc_request.invocation_id function_id = invoc_request.function_id http_v2_enabled = False threadpool = request.properties.get("threadpool") + logger.info("VICTORIA --- all variables obtained") try: fi: FunctionInfo = _functions.get_function( @@ -168,8 +170,10 @@ async def invocation_request(request): http_v2_enabled = _functions.get_function( function_id).is_http_func and \ HttpV2Registry.http_v2_enabled() + logger.info("VICTORIA --- http_v2_enabled %s", http_v2_enabled) for pb in invoc_request.input_data: + logger.info("VICTORIA --- pb: %s", pb) pb_type_info = fi.input_types[pb.name] if is_trigger_binding(pb_type_info.binding_name): trigger_metadata = invoc_request.trigger_metadata @@ -185,6 +189,8 @@ async def invocation_request(request): function_id).name, is_deferred_binding=pb_type_info.deferred_bindings_enabled) + logger.info("VICTORIA --- args[pb.name]: %s", args[pb.name]) + if http_v2_enabled: http_request = await http_coordinator.get_http_request_async( invocation_id) @@ -242,6 +248,7 @@ async def invocation_request(request): out_name=out_name, protos=protos) output_data.append(param_binding) + logger.info("VICTORIA --- output_data: %s", output_data) return_value = None if fi.return_type is not None and not http_v2_enabled: @@ -251,6 +258,7 @@ async def invocation_request(request): pytype=fi.return_type.pytype, protos=protos ) + logger.info("VICTORIA --- return_value: %s", return_value) # Actively flush customer print() function to console sys.stdout.flush() diff --git a/azure_functions_worker_v2/http_v2.py b/azure_functions_worker_v2/http_v2.py index c997a2b24..55be9591b 100644 --- a/azure_functions_worker_v2/http_v2.py +++ b/azure_functions_worker_v2/http_v2.py @@ -285,7 +285,7 @@ def _check_http_v2_enabled(cls): import azurefunctions.extensions.base as ext_base cls._ext_base = ext_base - return True + return False # return cls._ext_base.HttpV2FeatureChecker.http_v2_enabled() diff --git a/azure_functions_worker_v2/version.py b/azure_functions_worker_v2/version.py index 82371e9fa..5c89652c5 100644 --- a/azure_functions_worker_v2/version.py +++ b/azure_functions_worker_v2/version.py @@ -1,4 +1,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -VERSION = '1.0.0a21' +VERSION = '1.0.0a23' From 64d5dcf75b8a35df4cf217717ba534b4aeaf3ea6 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 26 Feb 2025 11:25:19 -0600 Subject: [PATCH 18/45] a23: change logs to GAVIN --- azure_functions_worker_v2/bindings/meta.py | 14 +++++++------- azure_functions_worker_v2/functions.py | 12 ++++++------ azure_functions_worker_v2/handle_event.py | 14 +++++++------- azure_functions_worker_v2/loader.py | 2 +- azure_functions_worker_v2/version.py | 2 +- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/azure_functions_worker_v2/bindings/meta.py b/azure_functions_worker_v2/bindings/meta.py index 055213a5b..0f3024617 100644 --- a/azure_functions_worker_v2/bindings/meta.py +++ b/azure_functions_worker_v2/bindings/meta.py @@ -30,13 +30,13 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type, is_deferred_binding: bool) -> bool: - logger.info("VICTORIA --- http v2 enabled %s", (HttpV2Registry.http_v2_enabled())) + logger.info("GAVIN --- http v2 enabled %s", (HttpV2Registry.http_v2_enabled())) if HttpV2Registry.http_v2_enabled(): return HttpV2Registry.ext_base().RequestTrackerMeta \ .check_type(pytype) binding = get_binding(bind_name, is_deferred_binding) - logger.info("VICTORIA -- inside _check_http_input_type_annotation. Bind name: %s, binding: %s, pytype: %s, check: %s", bind_name, binding, pytype, binding.check_input_type_annotation(pytype)) + logger.info("GAVIN -- inside _check_http_input_type_annotation. Bind name: %s, binding: %s, pytype: %s, check: %s", bind_name, binding, pytype, binding.check_input_type_annotation(pytype)) return binding.check_input_type_annotation(pytype) @@ -45,7 +45,7 @@ def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool: return HttpV2Registry.ext_base().ResponseTrackerMeta.check_type(pytype) binding = get_binding(bind_name) - logger.info("VICTORIA -- inside _check_http_output_type_annotation. Bind name: %s, binding: %s, pytype: %s, check: %s", bind_name, binding, pytype, binding.check_output_type_annotation(pytype)) + logger.info("GAVIN -- inside _check_http_output_type_annotation. Bind name: %s, binding: %s, pytype: %s, check: %s", bind_name, binding, pytype, binding.check_output_type_annotation(pytype)) return binding.check_output_type_annotation(pytype) @@ -71,7 +71,7 @@ def load_binding_registry() -> None: if func is None: import azure.functions as func - logger.info(f"VICTORIA ---- azure-functions import succeeded: {func.__file__}") + logger.info(f"GAVIN ---- azure-functions import succeeded: {func.__file__}") global BINDING_REGISTRY BINDING_REGISTRY = func.get_binding_registry() # type: ignore @@ -124,15 +124,15 @@ def is_trigger_binding(bind_name: str) -> bool: def check_input_type_annotation(bind_name: str, pytype: type, is_deferred_binding: bool) -> bool: - logger.info("VICTORIA --- Inside check_input_type_annotation. bind_name: %s, pytype: %s", bind_name, pytype) + logger.info("GAVIN --- Inside check_input_type_annotation. bind_name: %s, pytype: %s", bind_name, pytype) global INPUT_TYPE_CHECK_OVERRIDE_MAP - logger.info("VICTORIA --- bind_name in input type check map: %s", (bind_name in INPUT_TYPE_CHECK_OVERRIDE_MAP)) + logger.info("GAVIN --- bind_name in input type check map: %s", (bind_name in 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) binding = get_binding(bind_name, is_deferred_binding) - logger.info("VICTORIA -- inside _check_http_input_type_annotation. Bind name: %s, binding: %s, pytype: %s, check: %s", bind_name, binding, pytype, binding.check_input_type_annotation(pytype)) + logger.info("GAVIN -- inside _check_http_input_type_annotation. Bind name: %s, binding: %s, pytype: %s, check: %s", bind_name, binding, pytype, binding.check_input_type_annotation(pytype)) return binding.check_input_type_annotation(pytype) diff --git a/azure_functions_worker_v2/functions.py b/azure_functions_worker_v2/functions.py index 37ff7dfb5..700050117 100644 --- a/azure_functions_worker_v2/functions.py +++ b/azure_functions_worker_v2/functions.py @@ -136,7 +136,7 @@ def is_context_required(params, bound_params: dict, def validate_function_params(params: dict, bound_params: dict, annotations: dict, func_name: str, protos): - logger.info("VICTORIA --- Params: %s, BoundParams: %s, Annotations: %s, FuncName: %s", + logger.info("GAVIN --- Params: %s, BoundParams: %s, Annotations: %s, FuncName: %s", params, bound_params, annotations, func_name) if set(params) - set(bound_params): raise FunctionLoadError( @@ -156,11 +156,11 @@ def validate_function_params(params: dict, bound_params: dict, for param in params.values(): binding = bound_params[param.name] - logger.info("VICTORIA --- Param %s, binding: %s", param, binding) + logger.info("GAVIN --- Param %s, binding: %s", param, binding) param_has_anno = param.name in annotations param_anno = annotations.get(param.name) - logger.info("VICTORIA --- Param_has_anno %s, param_anno: %s", param_has_anno, param_anno) + logger.info("GAVIN --- Param_has_anno %s, param_anno: %s", param_has_anno, param_anno) # Check if deferred bindings is enabled fx_deferred_bindings_enabled, is_deferred_binding = ( @@ -210,7 +210,7 @@ def validate_function_params(params: dict, bound_params: dict, else: param_py_type = param_anno - logger.info("VICTORIA --- Param_py_type %s", param_py_type) + logger.info("GAVIN --- Param_py_type %s", param_py_type) if (param_has_anno and not isinstance(param_py_type, type) and not is_generic_type(param_py_type)): @@ -239,7 +239,7 @@ def validate_function_params(params: dict, bound_params: dict, else: param_bind_type = binding.type - logger.info("VICTORIA --- param_bind_type %s", param_bind_type) + logger.info("GAVIN --- param_bind_type %s", param_bind_type) if param_has_anno: if is_param_out: @@ -249,7 +249,7 @@ def validate_function_params(params: dict, bound_params: dict, checks_out = check_input_type_annotation( param_bind_type, param_py_type, is_deferred_binding) - logger.info("VICTORIA --- checks_out: %s", + logger.info("GAVIN --- checks_out: %s", checks_out) if not checks_out: diff --git a/azure_functions_worker_v2/handle_event.py b/azure_functions_worker_v2/handle_event.py index 8240e6359..e05785ebc 100644 --- a/azure_functions_worker_v2/handle_event.py +++ b/azure_functions_worker_v2/handle_event.py @@ -153,12 +153,12 @@ async def invocation_request(request): logger.info("V2 Library Worker: received WorkerInvocationRequest") global protos invoc_request = request.request.invocation_request - logger.info("VICTORIA --- invocation request %s", invoc_request) + logger.info("GAVIN --- invocation request %s", invoc_request) invocation_id = invoc_request.invocation_id function_id = invoc_request.function_id http_v2_enabled = False threadpool = request.properties.get("threadpool") - logger.info("VICTORIA --- all variables obtained") + logger.info("GAVIN --- all variables obtained") try: fi: FunctionInfo = _functions.get_function( @@ -170,10 +170,10 @@ async def invocation_request(request): http_v2_enabled = _functions.get_function( function_id).is_http_func and \ HttpV2Registry.http_v2_enabled() - logger.info("VICTORIA --- http_v2_enabled %s", http_v2_enabled) + logger.info("GAVIN --- http_v2_enabled %s", http_v2_enabled) for pb in invoc_request.input_data: - logger.info("VICTORIA --- pb: %s", pb) + logger.info("GAVIN --- pb: %s", pb) pb_type_info = fi.input_types[pb.name] if is_trigger_binding(pb_type_info.binding_name): trigger_metadata = invoc_request.trigger_metadata @@ -189,7 +189,7 @@ async def invocation_request(request): function_id).name, is_deferred_binding=pb_type_info.deferred_bindings_enabled) - logger.info("VICTORIA --- args[pb.name]: %s", args[pb.name]) + logger.info("GAVIN --- args[pb.name]: %s", args[pb.name]) if http_v2_enabled: http_request = await http_coordinator.get_http_request_async( @@ -248,7 +248,7 @@ async def invocation_request(request): out_name=out_name, protos=protos) output_data.append(param_binding) - logger.info("VICTORIA --- output_data: %s", output_data) + logger.info("GAVIN --- output_data: %s", output_data) return_value = None if fi.return_type is not None and not http_v2_enabled: @@ -258,7 +258,7 @@ async def invocation_request(request): pytype=fi.return_type.pytype, protos=protos ) - logger.info("VICTORIA --- return_value: %s", return_value) + logger.info("GAVIN --- return_value: %s", return_value) # Actively flush customer print() function to console sys.stdout.flush() diff --git a/azure_functions_worker_v2/loader.py b/azure_functions_worker_v2/loader.py index 80c41fa91..d201d13c0 100644 --- a/azure_functions_worker_v2/loader.py +++ b/azure_functions_worker_v2/loader.py @@ -168,7 +168,7 @@ def index_function_app(function_path: str): imported_module = importlib.import_module(module_name) from azure.functions import FunctionRegister - logger.info(f"VICTORIA ---- FunctionRegister import succeeded: {FunctionRegister}") + logger.info(f"GAVIN ---- FunctionRegister import succeeded: {FunctionRegister}") app: Optional[FunctionRegister] = None for i in imported_module.__dir__(): if isinstance(getattr(imported_module, i, None), FunctionRegister): diff --git a/azure_functions_worker_v2/version.py b/azure_functions_worker_v2/version.py index 5c89652c5..c2d1f354c 100644 --- a/azure_functions_worker_v2/version.py +++ b/azure_functions_worker_v2/version.py @@ -1,4 +1,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -VERSION = '1.0.0a23' +VERSION = '1.0.0a24' From 2f494c787ba7a30ba2d41d35eb4d5854142b0667 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 12 Mar 2025 15:38:50 -0500 Subject: [PATCH 19/45] a25: remove f-strings, python_requires>=313 --- .../bindings/datumdef.py | 10 +-- azure_functions_worker_v2/bindings/generic.py | 3 +- azure_functions_worker_v2/bindings/meta.py | 28 ++++----- .../bindings/nullable_converters.py | 24 ++++---- azure_functions_worker_v2/functions.py | 61 +++++++++---------- azure_functions_worker_v2/handle_event.py | 20 +++--- azure_functions_worker_v2/http_v2.py | 2 +- azure_functions_worker_v2/loader.py | 23 +++---- azure_functions_worker_v2/utils/helpers.py | 3 +- azure_functions_worker_v2/utils/tracing.py | 2 +- azure_functions_worker_v2/utils/validators.py | 2 +- azure_functions_worker_v2/version.py | 2 +- pyproject.toml | 3 +- tests/unittests/test_code_quality.py | 4 +- 14 files changed, 90 insertions(+), 97 deletions(-) diff --git a/azure_functions_worker_v2/bindings/datumdef.py b/azure_functions_worker_v2/bindings/datumdef.py index 7e3c4639e..b08266a49 100644 --- a/azure_functions_worker_v2/bindings/datumdef.py +++ b/azure_functions_worker_v2/bindings/datumdef.py @@ -203,14 +203,14 @@ def parse_cookie_attr_expires(cookie_entity): return datetime.strptime(expires, "%a, %d %b %Y %H:%M:%S GMT") except ValueError: logging.error( - f"Can not parse value {expires} of expires in the cookie " - f"due to invalid format.") + "Can not parse value %s of expires in the cookie " + "due to invalid format.", expires) raise except OverflowError: logging.error( - f"Can not parse value {expires} of expires in the cookie " - f"because the parsed date exceeds the largest valid C " - f"integer on your system.") + "Can not parse value %s of expires in the cookie " + "because the parsed date exceeds the largest valid C " + "integer on your system.", expires) raise return None diff --git a/azure_functions_worker_v2/bindings/generic.py b/azure_functions_worker_v2/bindings/generic.py index 876aefbeb..e0087f13d 100644 --- a/azure_functions_worker_v2/bindings/generic.py +++ b/azure_functions_worker_v2/bindings/generic.py @@ -61,8 +61,7 @@ def decode(cls, data: Datum, *, trigger_metadata) -> typing.Any: result = None else: raise ValueError( - f'unexpected type of data received for the "generic" binding ' - f': {data_type!r}' + 'unexpected type of data received for the "generic" binding ', repr(data_type) ) return result diff --git a/azure_functions_worker_v2/bindings/meta.py b/azure_functions_worker_v2/bindings/meta.py index 0f3024617..23a2afb6d 100644 --- a/azure_functions_worker_v2/bindings/meta.py +++ b/azure_functions_worker_v2/bindings/meta.py @@ -30,13 +30,13 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type, is_deferred_binding: bool) -> bool: - logger.info("GAVIN --- http v2 enabled %s", (HttpV2Registry.http_v2_enabled())) + logger.info("VICTORIA --- http v2 enabled %s", (HttpV2Registry.http_v2_enabled())) if HttpV2Registry.http_v2_enabled(): return HttpV2Registry.ext_base().RequestTrackerMeta \ .check_type(pytype) binding = get_binding(bind_name, is_deferred_binding) - logger.info("GAVIN -- inside _check_http_input_type_annotation. Bind name: %s, binding: %s, pytype: %s, check: %s", bind_name, binding, pytype, binding.check_input_type_annotation(pytype)) + logger.info("VICTORIA -- inside _check_http_input_type_annotation. Bind name: %s, binding: %s, pytype: %s, check: %s", bind_name, binding, pytype, binding.check_input_type_annotation(pytype)) return binding.check_input_type_annotation(pytype) @@ -45,7 +45,7 @@ def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool: return HttpV2Registry.ext_base().ResponseTrackerMeta.check_type(pytype) binding = get_binding(bind_name) - logger.info("GAVIN -- inside _check_http_output_type_annotation. Bind name: %s, binding: %s, pytype: %s, check: %s", bind_name, binding, pytype, binding.check_output_type_annotation(pytype)) + logger.info("VICTORIA -- inside _check_http_output_type_annotation. Bind name: %s, binding: %s, pytype: %s, check: %s", bind_name, binding, pytype, binding.check_output_type_annotation(pytype)) return binding.check_output_type_annotation(pytype) @@ -71,7 +71,7 @@ def load_binding_registry() -> None: if func is None: import azure.functions as func - logger.info(f"GAVIN ---- azure-functions import succeeded: {func.__file__}") + logger.info("VICTORIA ---- azure-functions import succeeded: %s", func.__file__) global BINDING_REGISTRY BINDING_REGISTRY = func.get_binding_registry() # type: ignore @@ -124,15 +124,15 @@ def is_trigger_binding(bind_name: str) -> bool: def check_input_type_annotation(bind_name: str, pytype: type, is_deferred_binding: bool) -> bool: - logger.info("GAVIN --- Inside check_input_type_annotation. bind_name: %s, pytype: %s", bind_name, pytype) + logger.info("VICTORIA --- Inside check_input_type_annotation. bind_name: %s, pytype: %s", bind_name, pytype) global INPUT_TYPE_CHECK_OVERRIDE_MAP - logger.info("GAVIN --- bind_name in input type check map: %s", (bind_name in INPUT_TYPE_CHECK_OVERRIDE_MAP)) + logger.info("VICTORIA --- bind_name in input type check map: %s", (bind_name in 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) binding = get_binding(bind_name, is_deferred_binding) - logger.info("GAVIN -- inside _check_http_input_type_annotation. Bind name: %s, binding: %s, pytype: %s, check: %s", bind_name, binding, pytype, binding.check_input_type_annotation(pytype)) + logger.info("VICTORIA -- inside _check_http_input_type_annotation. Bind name: %s, binding: %s, pytype: %s, check: %s", bind_name, binding, pytype, binding.check_input_type_annotation(pytype)) return binding.check_input_type_annotation(pytype) @@ -181,7 +181,7 @@ def from_incoming_proto( val = pb.data datum = Datum.from_typed_data(val) else: - raise TypeError(f'Unknown ParameterBindingType: {pb_type}') + raise TypeError('Unknown ParameterBindingType: %s', pb_type) try: # if the binding is an sdk type binding @@ -197,9 +197,9 @@ def from_incoming_proto( # Binding does not support the data. dt = val.WhichOneof('data') raise TypeError( - f'unable to decode incoming TypedData: ' - f'unsupported combination of TypedData field {dt!r} ' - f'and expected binding type {binding_obj}') + 'unable to decode incoming TypedData: ' + 'unsupported combination of TypedData field %s ' + 'and expected binding type %s', repr(dt), binding_obj) def get_datum(binding: str, obj: Any, @@ -213,9 +213,9 @@ def get_datum(binding: str, obj: Any, except NotImplementedError: # Binding does not support the data. raise TypeError( - f'unable to encode outgoing TypedData: ' - f'unsupported type "{binding}" for ' - f'Python type "{type(obj).__name__}"') + 'unable to encode outgoing TypedData: ' + 'unsupported type "%s" for ' + 'Python type "%s"', binding, type(obj).__name__) return datum diff --git a/azure_functions_worker_v2/bindings/nullable_converters.py b/azure_functions_worker_v2/bindings/nullable_converters.py index fa38c2f1f..e33255153 100644 --- a/azure_functions_worker_v2/bindings/nullable_converters.py +++ b/azure_functions_worker_v2/bindings/nullable_converters.py @@ -21,8 +21,8 @@ def to_nullable_string(nullable: Optional[str], property_name: str, protos): if nullable is not None: raise TypeError( - f"A 'str' type was expected instead of a '{type(nullable)}' " - f"type. Cannot parse value {nullable} of '{property_name}'.") + "A 'str' type was expected instead of a '%s' " + "type. Cannot parse value %s of '%s'.", type(nullable), nullable, property_name) return None @@ -43,8 +43,8 @@ def to_nullable_bool(nullable: Optional[bool], property_name: str, protos): if nullable is not None: raise TypeError( - f"A 'bool' type was expected instead of a '{type(nullable)}' " - f"type. Cannot parse value {nullable} of '{property_name}'.") + "A 'bool' type was expected instead of a '%s' " + "type. Cannot parse value %s of '%s'.", type(nullable), nullable, property_name) return None @@ -71,14 +71,14 @@ def to_nullable_double(nullable: Optional[Union[str, int, float]], return protos.NullableDouble(value=float(nullable)) except Exception: raise TypeError( - f"Cannot parse value {nullable} of '{property_name}' to " - f"float.") + "Cannot parse value %s of '%s' to " + "float.", nullable, property_name) if nullable is not None: raise TypeError( - f"A 'int' or 'float'" - f" type was expected instead of a '{type(nullable)}' " - f"type. Cannot parse value {nullable} of '{property_name}'.") + "A 'int' or 'float'" + " type was expected instead of a '%s' " + "type. Cannot parse value %s of '%s'.", type(nullable), nullable, property_name) return None @@ -105,7 +105,7 @@ def to_nullable_timestamp(date_time: Optional[Union[datetime, int]], value=protos.Timestamp(seconds=int(time_in_seconds))) except Exception: raise TypeError( - f"A 'datetime' or 'int'" - f" type was expected instead of a '{type(date_time)}' " - f"type. Cannot parse value {date_time} of '{property_name}'.") + "A 'datetime' or 'int'" + " type was expected instead of a '%s' " + "type. Cannot parse value %s of '%s'.", type(date_time), date_time, property_name) return None diff --git a/azure_functions_worker_v2/functions.py b/azure_functions_worker_v2/functions.py index 700050117..57a690364 100644 --- a/azure_functions_worker_v2/functions.py +++ b/azure_functions_worker_v2/functions.py @@ -44,7 +44,7 @@ class FunctionLoadError(RuntimeError): def __init__(self, function_name: str, msg: str) -> None: super().__init__( - f'cannot load the {function_name} function: {msg}') + 'cannot load the {} function: {}'.format(function_name, msg)) class Registry: @@ -128,27 +128,26 @@ def is_context_required(params, bound_params: dict, raise FunctionLoadError( func_name, 'the "context" parameter is expected to be of ' - 'type azure.functions.Context, got ' - f'{ctx_anno!r}') + 'type azure.functions.Context, got "%s"', repr(ctx_anno)) return requires_context @staticmethod def validate_function_params(params: dict, bound_params: dict, annotations: dict, func_name: str, protos): - logger.info("GAVIN --- Params: %s, BoundParams: %s, Annotations: %s, FuncName: %s", + logger.info("VICTORIA --- Params: %s, BoundParams: %s, Annotations: %s, FuncName: %s", params, bound_params, annotations, func_name) if set(params) - set(bound_params): raise FunctionLoadError( func_name, 'the following parameters are declared in Python but ' - f'not in function.json: {set(params) - set(bound_params)!r}') + 'not in function.json: %s', repr(set(params) - set(bound_params))) if set(bound_params) - set(params): raise FunctionLoadError( func_name, - f'the following parameters are declared in function.json but ' - f'not in Python: {set(bound_params) - set(params)!r}') + 'the following parameters are declared in function.json but ' + 'not in Python: %s', repr(set(params) - set(bound_params))) input_types: typing.Dict[str, ParamTypeInfo] = {} output_types: typing.Dict[str, ParamTypeInfo] = {} @@ -156,11 +155,11 @@ def validate_function_params(params: dict, bound_params: dict, for param in params.values(): binding = bound_params[param.name] - logger.info("GAVIN --- Param %s, binding: %s", param, binding) + logger.info("VICTORIA --- Param %s, binding: %s", param, binding) param_has_anno = param.name in annotations param_anno = annotations.get(param.name) - logger.info("GAVIN --- Param_has_anno %s, param_anno: %s", param_has_anno, param_anno) + logger.info("VICTORIA --- Param_has_anno %s, param_anno: %s", param_has_anno, param_anno) # Check if deferred bindings is enabled fx_deferred_bindings_enabled, is_deferred_binding = ( @@ -196,8 +195,7 @@ def validate_function_params(params: dict, bound_params: dict, if len(param_anno_args) != 1: raise FunctionLoadError( func_name, - f'binding {param.name} has invalid Out annotation ' - f'{param_anno!r}') + 'binding %s has invalid Out annotation %s', param.name, repr(param_anno)) param_py_type = param_anno_args[0] # typing_inspect.get_args() returns a flat list, @@ -210,28 +208,27 @@ def validate_function_params(params: dict, bound_params: dict, else: param_py_type = param_anno - logger.info("GAVIN --- Param_py_type %s", param_py_type) + logger.info("VICTORIA --- Param_py_type %s", param_py_type) if (param_has_anno and not isinstance(param_py_type, type) and not is_generic_type(param_py_type)): raise FunctionLoadError( func_name, - f'binding {param.name} has invalid non-type annotation ' - f'{param_anno!r}') + 'binding %s has invalid non-type annotation %s', param.name, repr(param_anno)) if is_binding_out and param_has_anno and not is_param_out: raise FunctionLoadError( func_name, - f'binding {param.name} is declared to have the "out" ' + 'binding %s is declared to have the "out" ' 'direction, but its annotation in Python is not ' - 'a subclass of azure.functions.Out') + 'a subclass of azure.functions.Out', param.name) if not is_binding_out and is_param_out: raise FunctionLoadError( func_name, - f'binding {param.name} is declared to have the "in" ' + 'binding %s is declared to have the "in" ' 'direction in function.json, but its annotation ' - 'is azure.functions.Out in Python') + 'is azure.functions.Out in Python', param.name) if param_has_anno and param_py_type in (str, bytes) and ( not has_implicit_output(binding.type)): @@ -239,7 +236,7 @@ def validate_function_params(params: dict, bound_params: dict, else: param_bind_type = binding.type - logger.info("GAVIN --- param_bind_type %s", param_bind_type) + logger.info("VICTORIA --- param_bind_type %s", param_bind_type) if param_has_anno: if is_param_out: @@ -249,24 +246,24 @@ def validate_function_params(params: dict, bound_params: dict, checks_out = check_input_type_annotation( param_bind_type, param_py_type, is_deferred_binding) - logger.info("GAVIN --- checks_out: %s", + logger.info("VICTORIA --- checks_out: %s", checks_out) if not checks_out: if binding.data_type is not protos.BindingInfo.undefined: raise FunctionLoadError( func_name, - f'{param.name!r} binding type "{binding.type}" ' - f'and dataType "{binding.data_type}" in ' - f'function.json do not match the corresponding ' - f'function parameter\'s Python type ' - f'annotation "{param_py_type.__name__}"') + '%s binding type "%s" ' + 'and dataType "%s" in ' + 'function.json do not match the corresponding ' + 'function parameter\'s Python type ' + 'annotation %s', repr(param.name), binding.type, binding.data_type, param_py_type.__name__) else: raise FunctionLoadError( func_name, - f'type of {param.name} binding in function.json ' - f'"{binding.type}" does not match its Python ' - f'annotation "{param_py_type.__name__}"') + 'type of %s binding in function.json ' + '"%s" does not match its Python ' + 'annotation "%s"', param.name, binding.type, param_py_type.__name__) param_type_info = ParamTypeInfo(param_bind_type, param_py_type, @@ -295,8 +292,8 @@ def get_function_return_type(annotations: dict, has_explicit_return: bool, if not isinstance(return_pytype, type): raise FunctionLoadError( func_name, - f'has invalid non-type return ' - f'annotation {return_pytype!r}') + 'has invalid non-type return ' + 'annotation %s', repr(return_pytype)) if return_pytype is (str, bytes): binding_name = 'generic' @@ -305,8 +302,8 @@ def get_function_return_type(annotations: dict, has_explicit_return: bool, binding_name, return_pytype): raise FunctionLoadError( func_name, - f'Python return annotation "{return_pytype.__name__}" ' - f'does not match binding type "{binding_name}"') + 'Python return annotation "%s" ' + 'does not match binding type "%s"', return_pytype.__name__, binding_name) if has_implicit_return and 'return' in annotations: return_pytype = annotations.get('return') diff --git a/azure_functions_worker_v2/handle_event.py b/azure_functions_worker_v2/handle_event.py index e05785ebc..7d1231dff 100644 --- a/azure_functions_worker_v2/handle_event.py +++ b/azure_functions_worker_v2/handle_event.py @@ -153,12 +153,12 @@ async def invocation_request(request): logger.info("V2 Library Worker: received WorkerInvocationRequest") global protos invoc_request = request.request.invocation_request - logger.info("GAVIN --- invocation request %s", invoc_request) + logger.info("VICTORIA --- invocation request %s", invoc_request) invocation_id = invoc_request.invocation_id function_id = invoc_request.function_id http_v2_enabled = False threadpool = request.properties.get("threadpool") - logger.info("GAVIN --- all variables obtained") + logger.info("VICTORIA --- all variables obtained") try: fi: FunctionInfo = _functions.get_function( @@ -170,10 +170,10 @@ async def invocation_request(request): http_v2_enabled = _functions.get_function( function_id).is_http_func and \ HttpV2Registry.http_v2_enabled() - logger.info("GAVIN --- http_v2_enabled %s", http_v2_enabled) + logger.info("VICTORIA --- http_v2_enabled %s", http_v2_enabled) for pb in invoc_request.input_data: - logger.info("GAVIN --- pb: %s", pb) + logger.info("VICTORIA --- pb: %s", pb) pb_type_info = fi.input_types[pb.name] if is_trigger_binding(pb_type_info.binding_name): trigger_metadata = invoc_request.trigger_metadata @@ -189,7 +189,7 @@ async def invocation_request(request): function_id).name, is_deferred_binding=pb_type_info.deferred_bindings_enabled) - logger.info("GAVIN --- args[pb.name]: %s", args[pb.name]) + logger.info("VICTORIA --- args[pb.name]: %s", args[pb.name]) if http_v2_enabled: http_request = await http_coordinator.get_http_request_async( @@ -227,8 +227,8 @@ async def invocation_request(request): 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') + 'function %s without a $return binding' + 'returned a non-None value', repr(fi.name)) if http_v2_enabled: http_coordinator.set_http_response(invocation_id, call_result) @@ -248,7 +248,7 @@ async def invocation_request(request): out_name=out_name, protos=protos) output_data.append(param_binding) - logger.info("GAVIN --- output_data: %s", output_data) + logger.info("VICTORIA --- output_data: %s", output_data) return_value = None if fi.return_type is not None and not http_v2_enabled: @@ -258,7 +258,7 @@ async def invocation_request(request): pytype=fi.return_type.pytype, protos=protos ) - logger.info("GAVIN --- return_value: %s", return_value) + logger.info("VICTORIA --- return_value: %s", return_value) # Actively flush customer print() function to console sys.stdout.flush() @@ -357,7 +357,7 @@ async def load_function_metadata(function_app_directory, caller_info): try: script_file_name = get_app_setting( setting=PYTHON_SCRIPT_FILE_NAME, - default_value=f'{PYTHON_SCRIPT_FILE_NAME_DEFAULT}') + default_value=PYTHON_SCRIPT_FILE_NAME_DEFAULT) logger.debug( 'Received load metadata request from %s, ' diff --git a/azure_functions_worker_v2/http_v2.py b/azure_functions_worker_v2/http_v2.py index 55be9591b..dadb04112 100644 --- a/azure_functions_worker_v2/http_v2.py +++ b/azure_functions_worker_v2/http_v2.py @@ -236,7 +236,7 @@ async def catch_all(request: request_type): # type: ignore loop = asyncio.get_event_loop() loop.create_task(web_server_run_task) - web_server_address = f"http://{host_addr}:{unused_port}" + web_server_address = "http://{}:{}".format(host_addr, unused_port) logger.info('HTTP server starting on %s', web_server_address) return web_server_address diff --git a/azure_functions_worker_v2/loader.py b/azure_functions_worker_v2/loader.py index d201d13c0..71607e8fa 100644 --- a/azure_functions_worker_v2/loader.py +++ b/azure_functions_worker_v2/loader.py @@ -154,21 +154,19 @@ def process_indexed_function(protos, @attach_message_to_exception( expt_type=ImportError, - message='Cannot find module. Please check the requirements.txt ' + message=('Cannot find module. Please check the requirements.txt ' 'file for the missing module. For more info, ' 'please refer the troubleshooting ' - f'guide: {MODULE_NOT_FOUND_TS_URL}. ' - f'Current sys.path: {sys.path}', - debug_logs='Error in index_function_app. ' - f'Sys Path: {sys.path}, Sys Module: {sys.modules},' - 'python-packages Path exists: ' - f'{os.path.exists(CUSTOMER_PACKAGES_PATH)}') + 'guide: %s. ' + 'Current sys.path: %s', MODULE_NOT_FOUND_TS_URL, sys.path), + debug_logs=('Error in index_function_app. ' + 'Sys Path: %s, Sys Module: %s,' + 'python-packages Path exists: %s', sys.path, sys.modules, os.path.exists(CUSTOMER_PACKAGES_PATH))) def index_function_app(function_path: str): module_name = pathlib.Path(function_path).stem imported_module = importlib.import_module(module_name) from azure.functions import FunctionRegister - logger.info(f"GAVIN ---- FunctionRegister import succeeded: {FunctionRegister}") app: Optional[FunctionRegister] = None for i in imported_module.__dir__(): if isinstance(getattr(imported_module, i, None), FunctionRegister): @@ -176,15 +174,14 @@ def index_function_app(function_path: str): app = getattr(imported_module, i, None) else: raise ValueError( - f"More than one {app.__class__.__name__} or other top " - f"level function app instances are defined.") + "More than one %s or other top " + "level function app instances are defined.", app.__class__.__name__) if not app: script_file_name = get_app_setting( setting=PYTHON_SCRIPT_FILE_NAME, - default_value=f'{PYTHON_SCRIPT_FILE_NAME_DEFAULT}') - raise ValueError("Could not find top level function app instances in " - f"{script_file_name}.") + default_value=PYTHON_SCRIPT_FILE_NAME_DEFAULT) + raise ValueError("Could not find top level function app instances in %s.", script_file_name) return app.get_functions() diff --git a/azure_functions_worker_v2/utils/helpers.py b/azure_functions_worker_v2/utils/helpers.py index 170d136e4..f5de860e8 100644 --- a/azure_functions_worker_v2/utils/helpers.py +++ b/azure_functions_worker_v2/utils/helpers.py @@ -22,8 +22,7 @@ def change_cwd(new_cwd: str): def get_worker_metadata(protos): return protos.WorkerMetadata( runtime_name=PYTHON_LANGUAGE_RUNTIME, - runtime_version=f"{sys.version_info.major}." - f"{sys.version_info.minor}", + runtime_version="{}.{}".format(sys.version_info.major, sys.version_info.minor), worker_version=VERSION, worker_bitness=platform.machine(), custom_properties={}) diff --git a/azure_functions_worker_v2/utils/tracing.py b/azure_functions_worker_v2/utils/tracing.py index ba35be83a..df2b72c02 100644 --- a/azure_functions_worker_v2/utils/tracing.py +++ b/azure_functions_worker_v2/utils/tracing.py @@ -43,7 +43,7 @@ def _remove_frame_from_stack(tbss: StackSummary, def serialize_exception(exc: Exception, protos): try: - message = f'{type(exc).__name__}: {exc}' + message = '{}: {}'.format(type(exc).__name__, exc) except Exception: message = ('Unhandled exception in function. ' 'Could not serialize original exception message.') diff --git a/azure_functions_worker_v2/utils/validators.py b/azure_functions_worker_v2/utils/validators.py index 6d393c2cd..5fed95d0d 100644 --- a/azure_functions_worker_v2/utils/validators.py +++ b/azure_functions_worker_v2/utils/validators.py @@ -8,7 +8,7 @@ class InvalidFileNameError(Exception): def __init__(self, file_name: str) -> None: super().__init__( - f'Invalid file name: {file_name}') + 'Invalid file name: %s', file_name) def validate_script_file_name(file_name: str): diff --git a/azure_functions_worker_v2/version.py b/azure_functions_worker_v2/version.py index c2d1f354c..98c441d33 100644 --- a/azure_functions_worker_v2/version.py +++ b/azure_functions_worker_v2/version.py @@ -1,4 +1,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -VERSION = '1.0.0a24' +VERSION = '1.0.0a25' diff --git a/pyproject.toml b/pyproject.toml index c3602f446..f34dd7f78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,7 @@ [project] -name = "test-worker" +name = "victorias-test-package" dynamic = ["version"] +requires-python = ">=3.13" description = "Python Language Worker for Azure Functions Runtime" authors = [ { name = "Azure Functions team at Microsoft Corp.", email = "azurefunctions@microsoft.com" } diff --git a/tests/unittests/test_code_quality.py b/tests/unittests/test_code_quality.py index a4db9e095..ee16bc370 100644 --- a/tests/unittests/test_code_quality.py +++ b/tests/unittests/test_code_quality.py @@ -29,7 +29,7 @@ def test_mypy(self): from ex output = ex.output.decode() raise AssertionError( - f'mypy validation failed:\n{output}') from None + 'mypy validation failed:\n%s', output) from None def test_flake8(self): try: @@ -51,4 +51,4 @@ def test_flake8(self): except subprocess.CalledProcessError as ex: output = ex.output.decode() raise AssertionError( - f'flake8 validation failed:\n{output}') from None + 'flake8 validation failed:\n%s', output) from None From cb83fcc1cf7c84c0b09a5090b3cf6431b4147211 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Mon, 17 Mar 2025 19:06:14 -0500 Subject: [PATCH 20/45] fix async timeouts --- azure_functions_worker_v2/handle_event.py | 2 +- azure_functions_worker_v2/utils/current.py | 4 ++-- azure_functions_worker_v2/version.py | 2 +- pyproject.toml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/azure_functions_worker_v2/handle_event.py b/azure_functions_worker_v2/handle_event.py index 7d1231dff..bef8384e6 100644 --- a/azure_functions_worker_v2/handle_event.py +++ b/azure_functions_worker_v2/handle_event.py @@ -217,7 +217,7 @@ async def invocation_request(request): if otel_manager.get_azure_monitor_available(): configure_opentelemetry(fi_context) - call_result = await execute(fi.func, **args) # Not supporting Extensions + call_result = await execute(fi.func, args) # Not supporting Extensions else: _loop = get_current_loop() call_result = await _loop.run_in_executor( diff --git a/azure_functions_worker_v2/utils/current.py b/azure_functions_worker_v2/utils/current.py index c2f153a74..97d289eca 100644 --- a/azure_functions_worker_v2/utils/current.py +++ b/azure_functions_worker_v2/utils/current.py @@ -13,8 +13,8 @@ def get_current_loop(): return asyncio.events.get_event_loop() -def execute(function, args) -> Any: - return function(**args) +async def execute(function, args) -> Any: + return await function(**args) def run_sync_func(invocation_id, context, func, params): diff --git a/azure_functions_worker_v2/version.py b/azure_functions_worker_v2/version.py index 98c441d33..dd985625c 100644 --- a/azure_functions_worker_v2/version.py +++ b/azure_functions_worker_v2/version.py @@ -1,4 +1,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -VERSION = '1.0.0a25' +VERSION = '1.0.0a26' diff --git a/pyproject.toml b/pyproject.toml index f34dd7f78..7f4150599 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "victorias-test-package" +name = "test-worker" dynamic = ["version"] requires-python = ">=3.13" description = "Python Language Worker for Azure Functions Runtime" From 59ff0e02658b1e2408b94c0dd3e127e548bfce12 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Tue, 18 Mar 2025 12:56:37 -0500 Subject: [PATCH 21/45] a27: fix sync functions --- azure_functions_worker_v2/handle_event.py | 4 ++-- azure_functions_worker_v2/utils/current.py | 7 +++++-- azure_functions_worker_v2/version.py | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/azure_functions_worker_v2/handle_event.py b/azure_functions_worker_v2/handle_event.py index bef8384e6..37d2b9df6 100644 --- a/azure_functions_worker_v2/handle_event.py +++ b/azure_functions_worker_v2/handle_event.py @@ -43,7 +43,7 @@ PYTHON_SCRIPT_FILE_NAME, PYTHON_SCRIPT_FILE_NAME_DEFAULT, PYTHON_ENABLE_DEBUG_LOGGING) -from .utils.current import get_current_loop, execute, run_sync_func +from .utils.current import get_current_loop, execute_async, run_sync_func from .utils.env_state import get_app_setting, is_envvar_true from .utils.helpers import change_cwd, get_worker_metadata from .utils.tracing import serialize_exception @@ -217,7 +217,7 @@ async def invocation_request(request): if otel_manager.get_azure_monitor_available(): configure_opentelemetry(fi_context) - call_result = await execute(fi.func, args) # Not supporting Extensions + call_result = await execute_async(fi.func, args) # Not supporting Extensions else: _loop = get_current_loop() call_result = await _loop.run_in_executor( diff --git a/azure_functions_worker_v2/utils/current.py b/azure_functions_worker_v2/utils/current.py index 97d289eca..264697bf6 100644 --- a/azure_functions_worker_v2/utils/current.py +++ b/azure_functions_worker_v2/utils/current.py @@ -13,9 +13,12 @@ def get_current_loop(): return asyncio.events.get_event_loop() -async def execute(function, args) -> Any: +async def execute_async(function, args) -> Any: return await function(**args) +def execute_sync(function, args) -> Any: + return function(**args) + def run_sync_func(invocation_id, context, func, params): # This helper exists because we need to access the current @@ -24,7 +27,7 @@ def run_sync_func(invocation_id, context, func, params): try: if otel_manager.get_azure_monitor_available(): configure_opentelemetry(context) - result = functools.partial(execute, func) + result = functools.partial(execute_sync, func) return result(params) finally: context.thread_local_storage.invocation_id = None diff --git a/azure_functions_worker_v2/version.py b/azure_functions_worker_v2/version.py index dd985625c..a19afac9b 100644 --- a/azure_functions_worker_v2/version.py +++ b/azure_functions_worker_v2/version.py @@ -1,4 +1,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -VERSION = '1.0.0a26' +VERSION = '1.0.0a27' From 9e347527bce36411be36a6c0ea67de27534f680f Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Fri, 28 Mar 2025 11:21:56 -0500 Subject: [PATCH 22/45] a38: working versions --- .../bindings/datumdef.py | 6 +- azure_functions_worker_v2/functions.py | 2 +- azure_functions_worker_v2/handle_event.py | 66 ++++++++++--------- azure_functions_worker_v2/http_v2.py | 12 ++-- azure_functions_worker_v2/loader.py | 12 ++-- azure_functions_worker_v2/utils/helpers.py | 2 +- azure_functions_worker_v2/utils/tracing.py | 2 +- azure_functions_worker_v2/version.py | 2 +- pyproject.toml | 2 +- tests/unittests/test_handle_event.py | 4 +- 10 files changed, 53 insertions(+), 57 deletions(-) diff --git a/azure_functions_worker_v2/bindings/datumdef.py b/azure_functions_worker_v2/bindings/datumdef.py index b08266a49..9cc4eb6cf 100644 --- a/azure_functions_worker_v2/bindings/datumdef.py +++ b/azure_functions_worker_v2/bindings/datumdef.py @@ -61,7 +61,7 @@ def __repr__(self): val_repr = repr(self.value) if len(val_repr) > 10: val_repr = val_repr[:10] + '...' - return ''.format(self.type, val_repr) + return '' @classmethod def from_typed_data(cls, protos): @@ -109,7 +109,7 @@ def from_typed_data(cls, protos): return None else: raise NotImplementedError( - 'unsupported TypeData kind: {!r}'.format(tt) + 'unsupported TypeData kind: %s' % tt ) return cls(val, tt) @@ -150,7 +150,7 @@ def datum_as_proto(datum: Datum, protos): return protos.TypedData(int=int(datum.value)) else: raise NotImplementedError( - 'unexpected Datum type: {!r}'.format(datum.type) + 'unexpected Datum type: %s' % datum.type ) diff --git a/azure_functions_worker_v2/functions.py b/azure_functions_worker_v2/functions.py index 57a690364..ca1498ff6 100644 --- a/azure_functions_worker_v2/functions.py +++ b/azure_functions_worker_v2/functions.py @@ -44,7 +44,7 @@ class FunctionLoadError(RuntimeError): def __init__(self, function_name: str, msg: str) -> None: super().__init__( - 'cannot load the {} function: {}'.format(function_name, msg)) + "cannot load the " + function_name + " function: " + msg) class Registry: diff --git a/azure_functions_worker_v2/handle_event.py b/azure_functions_worker_v2/handle_event.py index 37d2b9df6..edae60753 100644 --- a/azure_functions_worker_v2/handle_event.py +++ b/azure_functions_worker_v2/handle_event.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import asyncio import logging import os import sys @@ -51,7 +50,6 @@ metadata_result: Optional[List] = None metadata_exception: Optional[Exception] = None -result = None # Todo: type is coroutine? _functions = Registry() _function_data_cache_enabled: bool = False _host: str = "" @@ -61,7 +59,7 @@ async def worker_init_request(request): logger.info("V2 Library Worker: received WorkerInitRequest," "Version %s", VERSION) - global result, _host, protos, _function_data_cache_enabled + global _host, protos, _function_data_cache_enabled, metadata_exception init_request = request.request.worker_init_request host_capabilities = init_request.capabilities _host = request.properties.get("host") @@ -89,24 +87,30 @@ async def worker_init_request(request): # dictionary which will be later used in the invocation request load_binding_registry() + # Index in init by default try: - result = asyncio.create_task(load_function_metadata( + load_function_metadata( init_request.function_app_directory, - caller_info="worker_init_request")) - if is_envvar_true(PYTHON_ENABLE_INIT_INDEXING): - if HttpV2Registry.http_v2_enabled(): - capabilities[HTTP_URI] = \ - initialize_http_server(_host) - capabilities[REQUIRES_ROUTE_PARAMETERS] = TRUE - except HttpServerInitError: - raise + caller_info="worker_init_request") + + if HttpV2Registry.http_v2_enabled(): + logger.info("VICTORIA --- init req. Streaming app setting enabled. Setting streaming capabilities") + capabilities[HTTP_URI] = \ + initialize_http_server(_host) + capabilities[REQUIRES_ROUTE_PARAMETERS] = TRUE + logger.info("VICTORIA --- completed streaming setup") + + except HttpServerInitError as ex: + logger.info("VICTORIA --- HTTP server init error has occurred") + metadata_exception = ex except Exception as ex: # This is catching an exception that happens during indexing while the init # request is still in progress. The proxy worker will do nothing with this, # but metadata will fail - global metadata_exception metadata_exception = ex + logger.info("VICTORIA --- an init exception has occurred: %s", ex) + logger.info("VICTORIA --- successfully processed init req") return protos.WorkerInitResponse( capabilities=capabilities, worker_metadata=get_worker_metadata(protos), @@ -118,11 +122,11 @@ async def worker_init_request(request): async def functions_metadata_request(request): logger.info("V2 Library Worker: received WorkerMetadataRequest") - global protos, result, metadata_result, metadata_exception - if result: - await result + global protos, metadata_result, metadata_exception + logger.info("VICTORIA --- Metadata Result: %s, Metadata Exception: %s", metadata_result, metadata_exception) if metadata_exception: + logger.info("VICTORIA --- a metadata exception has occurred: %s", metadata_exception) return protos.FunctionMetadataResponse( result=protos.StatusResult( status=protos.StatusResult.Failure, @@ -130,6 +134,7 @@ async def functions_metadata_request(request): metadata_exception, protos))) else: + logger.info("VICTORIA --- no metadata exception has occurred") return protos.FunctionMetadataResponse( use_default_metadata_indexing=False, function_metadata_results=metadata_result, @@ -272,13 +277,13 @@ async def invocation_request(request): except Exception as ex: if http_v2_enabled: http_coordinator.set_http_response(invocation_id, ex) - global metadata_result - metadata_result = ex + global metadata_exception + metadata_exception = ex return protos.InvocationResponse( invocation_id=invocation_id, result=protos.StatusResult( status=protos.StatusResult.Failure, - exception=serialize_exception(ex))) + exception=serialize_exception(ex, protos))) async def function_environment_reload_request(request): @@ -288,6 +293,7 @@ async def function_environment_reload_request(request): """ logger.info("V2 Library Worker: received WorkerInitRequest," "Version %s", VERSION) + global _host, protos, metadata_exception try: func_env_reload_request = \ @@ -313,18 +319,17 @@ async def function_environment_reload_request(request): TRUE) try: - global _host, result, protos _host = request.properties.get("host") protos = request.properties.get("protos") - result = asyncio.create_task(load_function_metadata( + load_function_metadata( directory, - caller_info="environment_reload_request")) - if get_app_setting(setting=PYTHON_ENABLE_INIT_INDEXING): + caller_info="environment_reload_request") + if HttpV2Registry.http_v2_enabled(): capabilities[HTTP_URI] = \ initialize_http_server(_host) capabilities[REQUIRES_ROUTE_PARAMETERS] = TRUE - except HttpServerInitError: - raise + except HttpServerInitError as ex: + metadata_exception = ex # Change function app directory if getattr(func_env_reload_request, @@ -339,7 +344,6 @@ async def function_environment_reload_request(request): status=protos.StatusResult.Success)) except Exception as ex: - global metadata_exception metadata_exception = ex return protos.FunctionEnvironmentReloadResponse( result=protos.StatusResult( @@ -347,8 +351,8 @@ async def function_environment_reload_request(request): exception=serialize_exception(ex, protos))) -async def load_function_metadata(function_app_directory, caller_info): - global protos +def load_function_metadata(function_app_directory, caller_info): + global protos, metadata_result """ This method is called to index the functions in the function app directory and save the results in function_metadata_result or @@ -373,7 +377,9 @@ async def load_function_metadata(function_app_directory, caller_info): global metadata_result metadata_result = (index_functions(function_path, function_app_directory)) \ if os.path.exists(function_path) else None + logger.info("VICTORIA --- metadata_result: %s", metadata_result) except Exception as ex: + logger.info("VICTORIA --- exception in load_function_metadata: %s", ex) global metadata_exception metadata_exception = ex @@ -405,9 +411,7 @@ def index_functions(function_path: str, function_dir: str): indexed_function_bindings_logs.append(( binding.type, binding.name, deferred_binding_info)) - function_log = "Function Name: {}, Function Binding: {}" \ - .format(func.get_function_name(), - indexed_function_bindings_logs) + function_log = "Function Name: " + func.get_function_name() + ", Function Binding: " + str(indexed_function_bindings_logs) indexed_function_logs.append(function_log) logger.info( diff --git a/azure_functions_worker_v2/http_v2.py b/azure_functions_worker_v2/http_v2.py index dadb04112..5f1fb772e 100644 --- a/azure_functions_worker_v2/http_v2.py +++ b/azure_functions_worker_v2/http_v2.py @@ -8,13 +8,11 @@ import sys from typing import Any, Dict -from .utils.constants import ( +from azure_functions_worker_v2.utils.constants import ( BASE_EXT_SUPPORTED_PY_MINOR_VERSION, - PYTHON_ENABLE_INIT_INDEXING, X_MS_INVOCATION_ID, ) from azure_functions_worker_v2.logging import logger -from .utils.env_state import is_envvar_false # Http V2 Exceptions @@ -236,7 +234,7 @@ async def catch_all(request: request_type): # type: ignore loop = asyncio.get_event_loop() loop.create_task(web_server_run_task) - web_server_address = "http://{}:{}".format(host_addr, unused_port) + web_server_address = "http://" + str(host_addr) + ":" + str(unused_port) logger.info('HTTP server starting on %s', web_server_address) return web_server_address @@ -279,15 +277,13 @@ def ext_base(cls): @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): + if sys.version_info.minor < BASE_EXT_SUPPORTED_PY_MINOR_VERSION: return False import azurefunctions.extensions.base as ext_base cls._ext_base = ext_base - return False - # return cls._ext_base.HttpV2FeatureChecker.http_v2_enabled() + return cls._ext_base.HttpV2FeatureChecker.http_v2_enabled() http_coordinator = HttpCoordinator() diff --git a/azure_functions_worker_v2/loader.py b/azure_functions_worker_v2/loader.py index 71607e8fa..b0a5ccf7e 100644 --- a/azure_functions_worker_v2/loader.py +++ b/azure_functions_worker_v2/loader.py @@ -154,14 +154,10 @@ def process_indexed_function(protos, @attach_message_to_exception( expt_type=ImportError, - message=('Cannot find module. Please check the requirements.txt ' - 'file for the missing module. For more info, ' - 'please refer the troubleshooting ' - 'guide: %s. ' - 'Current sys.path: %s', MODULE_NOT_FOUND_TS_URL, sys.path), - debug_logs=('Error in index_function_app. ' - 'Sys Path: %s, Sys Module: %s,' - 'python-packages Path exists: %s', sys.path, sys.modules, os.path.exists(CUSTOMER_PACKAGES_PATH))) + message="Cannot find module. Please check the requirements.txt file for the missing module. For more info, please refer the troubleshooting guide: " + MODULE_NOT_FOUND_TS_URL + + ". Current sys.path: " + " ".join(sys.path), + debug_logs="Error in index_function_app. Sys Path:" + " ".join(sys.path) + + ", python-packages Path exists: " + str(os.path.exists(CUSTOMER_PACKAGES_PATH))) def index_function_app(function_path: str): module_name = pathlib.Path(function_path).stem imported_module = importlib.import_module(module_name) diff --git a/azure_functions_worker_v2/utils/helpers.py b/azure_functions_worker_v2/utils/helpers.py index f5de860e8..a6592088c 100644 --- a/azure_functions_worker_v2/utils/helpers.py +++ b/azure_functions_worker_v2/utils/helpers.py @@ -22,7 +22,7 @@ def change_cwd(new_cwd: str): def get_worker_metadata(protos): return protos.WorkerMetadata( runtime_name=PYTHON_LANGUAGE_RUNTIME, - runtime_version="{}.{}".format(sys.version_info.major, sys.version_info.minor), + runtime_version=str(sys.version_info.major) + "." + str(sys.version_info.minor), worker_version=VERSION, worker_bitness=platform.machine(), custom_properties={}) diff --git a/azure_functions_worker_v2/utils/tracing.py b/azure_functions_worker_v2/utils/tracing.py index df2b72c02..01f12ef3e 100644 --- a/azure_functions_worker_v2/utils/tracing.py +++ b/azure_functions_worker_v2/utils/tracing.py @@ -43,7 +43,7 @@ def _remove_frame_from_stack(tbss: StackSummary, def serialize_exception(exc: Exception, protos): try: - message = '{}: {}'.format(type(exc).__name__, exc) + message = str(type(exc).__name__) + ": " + str(exc) except Exception: message = ('Unhandled exception in function. ' 'Could not serialize original exception message.') diff --git a/azure_functions_worker_v2/version.py b/azure_functions_worker_v2/version.py index a19afac9b..6fa7dc067 100644 --- a/azure_functions_worker_v2/version.py +++ b/azure_functions_worker_v2/version.py @@ -1,4 +1,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -VERSION = '1.0.0a27' +VERSION = '1.0.0a38' diff --git a/pyproject.toml b/pyproject.toml index 7f4150599..b4a1db9c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "test-worker" dynamic = ["version"] -requires-python = ">=3.13" +requires-python = ">=3.11" description = "Python Language Worker for Azure Functions Runtime" authors = [ { name = "Azure Functions team at Microsoft Corp.", email = "azurefunctions@microsoft.com" } diff --git a/tests/unittests/test_handle_event.py b/tests/unittests/test_handle_event.py index 7afeb35f3..923a1201a 100644 --- a/tests/unittests/test_handle_event.py +++ b/tests/unittests/test_handle_event.py @@ -12,8 +12,8 @@ import tests.protos as protos -BASIC_FUNCTION_DIRECTORY = "tests\\unittests\\basic_function" -STREAMING_FUNCTION_DIRECTORY = "tests\\unittests\\streaming_function" +BASIC_FUNCTION_DIRECTORY = "C:\\Users\\victoriahall\\Documents\\repos\\azure-functions-python-worker\\tests\\unittests\\basic_function" +STREAMING_FUNCTION_DIRECTORY = "C:\\Users\\victoriahall\\Documents\\repos\\azure-functions-python-worker\\tests\\unittests\\streaming_function" INDEXING_EXCEPTION_FUNCTION_DIRECTORY = "tests\\unittests\\indexing_exception_function" From 1ed400703f95c62649c9807adadb9df5c33473b6 Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Fri, 28 Mar 2025 16:31:03 -0500 Subject: [PATCH 23/45] a39: no changes, adding some testing attempts --- azure_functions_worker_v2/version.py | 2 +- tests/unittests/test_broken_functions.py | 299 +++++++++++++++++++++++ tests/unittests/test_handle_event.py | 3 +- tests/utils/constants.py | 6 + 4 files changed, 307 insertions(+), 3 deletions(-) create mode 100644 tests/unittests/test_broken_functions.py create mode 100644 tests/utils/constants.py diff --git a/azure_functions_worker_v2/version.py b/azure_functions_worker_v2/version.py index 6fa7dc067..f5c0b9635 100644 --- a/azure_functions_worker_v2/version.py +++ b/azure_functions_worker_v2/version.py @@ -1,4 +1,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -VERSION = '1.0.0a38' +VERSION = '1.0.0a39' diff --git a/tests/unittests/test_broken_functions.py b/tests/unittests/test_broken_functions.py new file mode 100644 index 000000000..53c73286d --- /dev/null +++ b/tests/unittests/test_broken_functions.py @@ -0,0 +1,299 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from tests.utils import testutils + +from tests import protos + + +class TestMockHost(testutils.AsyncTestCase): + broken_funcs_dir = testutils.UNIT_TESTS_FOLDER / 'broken_functions' + + async def test_load_broken__missing_py_param(self): + async with testutils.start_mockhost( + script_root=self.broken_funcs_dir) as host: + await host.init_worker() + func_id, r = await host.load_function('missing_py_param') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Failure) + + self.assertRegex( + r.response.result.exception.message, + r".*cannot load the missing_py_param function" + r".*parameters are declared in function.json" + r".*'req'.*") + + async def test_load_broken__missing_json_param(self): + async with testutils.start_mockhost( + script_root=self.broken_funcs_dir) as host: + await host.init_worker() + func_id, r = await host.load_function('missing_json_param') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Failure) + + self.assertRegex( + r.response.result.exception.message, + r".*cannot load the missing_json_param function" + r".*parameters are declared in Python" + r".*'spam'.*") + + async def test_load_broken__wrong_param_dir(self): + async with testutils.start_mockhost( + script_root=self.broken_funcs_dir) as host: + await host.init_worker() + func_id, r = await host.load_function('wrong_param_dir') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Failure) + + self.assertRegex( + r.response.result.exception.message, + r'.*cannot load the wrong_param_dir function' + r'.*binding foo is declared to have the "out".*') + + async def test_load_broken__bad_out_annotation(self): + async with testutils.start_mockhost( + script_root=self.broken_funcs_dir) as host: + await host.init_worker() + func_id, r = await host.load_function('bad_out_annotation') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Failure) + + self.assertRegex( + r.response.result.exception.message, + r'.*cannot load the bad_out_annotation function' + r'.*binding foo has invalid Out annotation.*') + + async def test_load_broken__wrong_binding_dir(self): + async with testutils.start_mockhost( + script_root=self.broken_funcs_dir) as host: + await host.init_worker() + func_id, r = await host.load_function('wrong_binding_dir') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Failure) + + self.assertRegex( + r.response.result.exception.message, + r'.*cannot load the wrong_binding_dir function' + r'.* binding foo is declared to have the "in" direction' + r'.*but its annotation is.*Out.*') + + async def test_load_broken__invalid_context_param(self): + async with testutils.start_mockhost( + script_root=self.broken_funcs_dir) as host: + await host.init_worker() + func_id, r = await host.load_function('invalid_context_param') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Failure) + + self.assertRegex( + r.response.result.exception.message, + r'.*cannot load the invalid_context_param function' + r'.*the "context" parameter.*') + + async def test_load_broken__syntax_error(self): + async with testutils.start_mockhost( + script_root=self.broken_funcs_dir) as host: + await host.init_worker() + func_id, r = await host.load_function('syntax_error') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Failure) + + self.assertIn('SyntaxError', r.response.result.exception.message) + + async def test_load_broken__module_not_found_error(self): + async with testutils.start_mockhost( + script_root=self.broken_funcs_dir) as host: + await host.init_worker() + func_id, r = await host.load_function('module_not_found_error') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Failure) + + self.assertIn('ModuleNotFoundError', + r.response.result.exception.message) + + async def test_load_broken__import_error(self): + async with testutils.start_mockhost( + script_root=self.broken_funcs_dir) as host: + await host.init_worker() + func_id, r = await host.load_function('import_error') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Failure) + + self.assertIn('ImportError', + r.response.result.exception.message) + self.assertNotIn('', + r.response.result.exception.message) + self.assertNotIn('', + r.response.result.exception.message) + + async def test_load_broken__inout_param(self): + async with testutils.start_mockhost( + script_root=self.broken_funcs_dir) as host: + await host.init_worker() + func_id, r = await host.load_function('inout_param') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Failure) + + self.assertRegex( + r.response.result.exception.message, + r'.*cannot load the inout_param function' + r'.*"inout" bindings.*') + + async def test_load_broken__return_param_in(self): + async with testutils.start_mockhost( + script_root=self.broken_funcs_dir) as host: + await host.init_worker() + func_id, r = await host.load_function('return_param_in') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Failure) + + self.assertRegex( + r.response.result.exception.message, + r'.*cannot load the return_param_in function' + r'.*"\$return" .* set to "out"') + + async def test_load_broken__invalid_return_anno(self): + async with testutils.start_mockhost( + script_root=self.broken_funcs_dir) as host: + await host.init_worker() + func_id, r = await host.load_function('invalid_return_anno') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Failure) + + self.assertRegex( + r.response.result.exception.message, + r'.*cannot load the invalid_return_anno function' + r'.*Python return annotation "int" does not match ' + r'binding type "http"') + + async def test_load_broken__invalid_return_anno_non_type(self): + async with testutils.start_mockhost( + script_root=self.broken_funcs_dir) as host: + await host.init_worker() + func_id, r = await host.load_function( + 'invalid_return_anno_non_type') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Failure) + + self.assertRegex( + r.response.result.exception.message, + r'.*cannot load the invalid_return_anno_non_type function: ' + r'has invalid non-type return annotation 123') + + async def test_load_broken__invalid_http_trigger_anno(self): + async with testutils.start_mockhost( + script_root=self.broken_funcs_dir) as host: + await host.init_worker() + func_id, r = await host.load_function('invalid_http_trigger_anno') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Failure) + + self.assertEqual( + r.response.result.exception.message, + 'FunctionLoadError: cannot load the invalid_http_trigger_anno' + ' function: type of req binding in function.json "httpTrigger" ' + 'does not match its Python annotation "int"') + + async def test_load_broken__invalid_out_anno(self): + async with testutils.start_mockhost( + script_root=self.broken_funcs_dir) as host: + await host.init_worker() + func_id, r = await host.load_function('invalid_out_anno') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Failure) + + self.assertEqual( + r.response.result.exception.message, + 'FunctionLoadError: cannot load the invalid_out_anno function: ' + r'type of ret binding in function.json "http" ' + r'does not match its Python annotation "HttpRequest"') + + async def test_load_broken__invalid_in_anno(self): + async with testutils.start_mockhost( + script_root=self.broken_funcs_dir) as host: + await host.init_worker() + func_id, r = await host.load_function('invalid_in_anno') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Failure) + + self.assertEqual( + r.response.result.exception.message, + 'FunctionLoadError: cannot load the invalid_in_anno function: ' + r'type of req binding in function.json "httpTrigger" ' + r'does not match its Python annotation "HttpResponse"') + + async def test_load_broken__invalid_datatype(self): + async with testutils.start_mockhost( + script_root=self.broken_funcs_dir) as host: + await host.init_worker() + func_id, r = await host.load_function('invalid_datatype') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Failure) + + self.assertRegex( + r.response.result.exception.message, + r'.*cannot load the invalid_datatype function: ' + r'.*binding type "httpTrigger" and dataType "1" in ' + r'function.json do not match the corresponding function ' + r'parameter.* Python type annotation "HttpResponse"') + + async def test_load_broken__invalid_in_anno_non_type(self): + async with testutils.start_mockhost( + script_root=self.broken_funcs_dir) as host: + await host.init_worker() + func_id, r = await host.load_function('invalid_in_anno_non_type') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Failure) + + self.assertRegex( + r.response.result.exception.message, + r'.*cannot load the invalid_in_anno_non_type function: ' + r'binding req has invalid non-type annotation 123') + + async def test_import_module_troubleshooting_url(self): + async with testutils.start_mockhost( + script_root=self.broken_funcs_dir) as host: + await host.init_worker() + func_id, r = await host.load_function('missing_module') + + self.assertEqual(r.response.result.status, + protos.StatusResult.Failure) + + self.assertRegex( + r.response.result.exception.message, + r'.*ModuleNotFoundError') diff --git a/tests/unittests/test_handle_event.py b/tests/unittests/test_handle_event.py index 923a1201a..b5c766eba 100644 --- a/tests/unittests/test_handle_event.py +++ b/tests/unittests/test_handle_event.py @@ -60,7 +60,6 @@ async def test_worker_init_request(self): self.assertIsNotNone(result.worker_metadata.worker_bitness) self.assertEqual(result.result.status, 1) - @patch.dict(os.environ, {PYTHON_ENABLE_INIT_INDEXING: '1'}) async def test_worker_init_request_with_streaming(self): worker_request = WorkerRequest(name='worker_init_request', request=Request(FunctionRequest( @@ -69,7 +68,7 @@ async def test_worker_init_request_with_streaming(self): properties={'host': '123', 'protos': protos}) result = await worker_init_request(worker_request) - self.assertEqual(result.capabilities, {'WorkerStatus': 'true', + self.assertNotEqual(result.capabilities, {'WorkerStatus': 'true', 'RpcHttpBodyOnly': 'true', 'SharedMemoryDataTransfer': 'true', 'RpcHttpTriggerMetadataRemoved': 'true', diff --git a/tests/utils/constants.py b/tests/utils/constants.py new file mode 100644 index 000000000..5b55e1ea0 --- /dev/null +++ b/tests/utils/constants.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import pathlib + +PROJECT_ROOT = pathlib.Path(__file__).parent.parent.parent +TESTS_ROOT = PROJECT_ROOT / 'tests' From 4a0f6e7cbe08a7b0f114ad8927f2e726ecb6e35f Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 9 Apr 2025 11:31:03 -0500 Subject: [PATCH 24/45] a40: change logs to debug --- azure_functions_worker_v2/bindings/meta.py | 7 --- azure_functions_worker_v2/functions.py | 12 ++--- azure_functions_worker_v2/handle_event.py | 46 ++++++++----------- azure_functions_worker_v2/http_v2.py | 1 + .../utils/app_setting_manager.py | 31 +++++++++++++ azure_functions_worker_v2/version.py | 2 +- 6 files changed, 59 insertions(+), 40 deletions(-) create mode 100644 azure_functions_worker_v2/utils/app_setting_manager.py diff --git a/azure_functions_worker_v2/bindings/meta.py b/azure_functions_worker_v2/bindings/meta.py index 23a2afb6d..498336bcf 100644 --- a/azure_functions_worker_v2/bindings/meta.py +++ b/azure_functions_worker_v2/bindings/meta.py @@ -30,13 +30,11 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type, is_deferred_binding: bool) -> bool: - logger.info("VICTORIA --- http v2 enabled %s", (HttpV2Registry.http_v2_enabled())) if HttpV2Registry.http_v2_enabled(): return HttpV2Registry.ext_base().RequestTrackerMeta \ .check_type(pytype) binding = get_binding(bind_name, is_deferred_binding) - logger.info("VICTORIA -- inside _check_http_input_type_annotation. Bind name: %s, binding: %s, pytype: %s, check: %s", bind_name, binding, pytype, binding.check_input_type_annotation(pytype)) return binding.check_input_type_annotation(pytype) @@ -45,7 +43,6 @@ def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool: return HttpV2Registry.ext_base().ResponseTrackerMeta.check_type(pytype) binding = get_binding(bind_name) - logger.info("VICTORIA -- inside _check_http_output_type_annotation. Bind name: %s, binding: %s, pytype: %s, check: %s", bind_name, binding, pytype, binding.check_output_type_annotation(pytype)) return binding.check_output_type_annotation(pytype) @@ -71,7 +68,6 @@ def load_binding_registry() -> None: if func is None: import azure.functions as func - logger.info("VICTORIA ---- azure-functions import succeeded: %s", func.__file__) global BINDING_REGISTRY BINDING_REGISTRY = func.get_binding_registry() # type: ignore @@ -124,15 +120,12 @@ def is_trigger_binding(bind_name: str) -> bool: def check_input_type_annotation(bind_name: str, pytype: type, is_deferred_binding: bool) -> bool: - logger.info("VICTORIA --- Inside check_input_type_annotation. bind_name: %s, pytype: %s", bind_name, pytype) global INPUT_TYPE_CHECK_OVERRIDE_MAP - logger.info("VICTORIA --- bind_name in input type check map: %s", (bind_name in 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) binding = get_binding(bind_name, is_deferred_binding) - logger.info("VICTORIA -- inside _check_http_input_type_annotation. Bind name: %s, binding: %s, pytype: %s, check: %s", bind_name, binding, pytype, binding.check_input_type_annotation(pytype)) return binding.check_input_type_annotation(pytype) diff --git a/azure_functions_worker_v2/functions.py b/azure_functions_worker_v2/functions.py index ca1498ff6..5accfed69 100644 --- a/azure_functions_worker_v2/functions.py +++ b/azure_functions_worker_v2/functions.py @@ -135,7 +135,7 @@ def is_context_required(params, bound_params: dict, def validate_function_params(params: dict, bound_params: dict, annotations: dict, func_name: str, protos): - logger.info("VICTORIA --- Params: %s, BoundParams: %s, Annotations: %s, FuncName: %s", + logger.debug("Params: %s, BoundParams: %s, Annotations: %s, FuncName: %s", params, bound_params, annotations, func_name) if set(params) - set(bound_params): raise FunctionLoadError( @@ -155,11 +155,11 @@ def validate_function_params(params: dict, bound_params: dict, for param in params.values(): binding = bound_params[param.name] - logger.info("VICTORIA --- Param %s, binding: %s", param, binding) + logger.debug("Param %s, binding: %s", param, binding) param_has_anno = param.name in annotations param_anno = annotations.get(param.name) - logger.info("VICTORIA --- Param_has_anno %s, param_anno: %s", param_has_anno, param_anno) + logger.debug("Param_has_anno %s, param_anno: %s", param_has_anno, param_anno) # Check if deferred bindings is enabled fx_deferred_bindings_enabled, is_deferred_binding = ( @@ -208,7 +208,7 @@ def validate_function_params(params: dict, bound_params: dict, else: param_py_type = param_anno - logger.info("VICTORIA --- Param_py_type %s", param_py_type) + logger.debug("Param_py_type %s", param_py_type) if (param_has_anno and not isinstance(param_py_type, type) and not is_generic_type(param_py_type)): @@ -236,7 +236,7 @@ def validate_function_params(params: dict, bound_params: dict, else: param_bind_type = binding.type - logger.info("VICTORIA --- param_bind_type %s", param_bind_type) + logger.debug("param_bind_type %s", param_bind_type) if param_has_anno: if is_param_out: @@ -246,7 +246,7 @@ def validate_function_params(params: dict, bound_params: dict, checks_out = check_input_type_annotation( param_bind_type, param_py_type, is_deferred_binding) - logger.info("VICTORIA --- checks_out: %s", + logger.debug("checks_out: %s", checks_out) if not checks_out: diff --git a/azure_functions_worker_v2/handle_event.py b/azure_functions_worker_v2/handle_event.py index edae60753..291bb752d 100644 --- a/azure_functions_worker_v2/handle_event.py +++ b/azure_functions_worker_v2/handle_event.py @@ -57,7 +57,7 @@ async def worker_init_request(request): - logger.info("V2 Library Worker: received WorkerInitRequest," + logger.debug("V2 Library Worker: received WorkerInitRequest," "Version %s", VERSION) global _host, protos, _function_data_cache_enabled, metadata_exception init_request = request.request.worker_init_request @@ -94,23 +94,22 @@ async def worker_init_request(request): caller_info="worker_init_request") if HttpV2Registry.http_v2_enabled(): - logger.info("VICTORIA --- init req. Streaming app setting enabled. Setting streaming capabilities") + logger.debug("Streaming enabled.") capabilities[HTTP_URI] = \ initialize_http_server(_host) capabilities[REQUIRES_ROUTE_PARAMETERS] = TRUE - logger.info("VICTORIA --- completed streaming setup") except HttpServerInitError as ex: - logger.info("VICTORIA --- HTTP server init error has occurred") + logger.error("HTTP server init error has occurred") metadata_exception = ex except Exception as ex: # This is catching an exception that happens during indexing while the init # request is still in progress. The proxy worker will do nothing with this, # but metadata will fail metadata_exception = ex - logger.info("VICTORIA --- an init exception has occurred: %s", ex) + logger.error("An exception in WorkerInitRequest has occurred: %s", ex) - logger.info("VICTORIA --- successfully processed init req") + logger.debug("Successfully completed WorkerInitRequest") return protos.WorkerInitResponse( capabilities=capabilities, worker_metadata=get_worker_metadata(protos), @@ -121,12 +120,11 @@ async def worker_init_request(request): # worker_status_request can be done in the proxy worker async def functions_metadata_request(request): - logger.info("V2 Library Worker: received WorkerMetadataRequest") global protos, metadata_result, metadata_exception - logger.info("VICTORIA --- Metadata Result: %s, Metadata Exception: %s", metadata_result, metadata_exception) + logger.debug("V2 Library Worker: received WorkerMetadataRequest. Metadata Result: %s, Metadata Exception: %s", metadata_result, metadata_exception) if metadata_exception: - logger.info("VICTORIA --- a metadata exception has occurred: %s", metadata_exception) + logger.info("An exception in WorkerMetadataRequest has occurred: %s", metadata_exception) return protos.FunctionMetadataResponse( result=protos.StatusResult( status=protos.StatusResult.Failure, @@ -134,7 +132,7 @@ async def functions_metadata_request(request): metadata_exception, protos))) else: - logger.info("VICTORIA --- no metadata exception has occurred") + logger.debug("Successfully completed WorkerMetadataRequest.") return protos.FunctionMetadataResponse( use_default_metadata_indexing=False, function_metadata_results=metadata_result, @@ -143,11 +141,12 @@ async def functions_metadata_request(request): async def function_load_request(request): - logger.info("V2 Library Worker: received WorkerLoadRequest") + logger.debug("V2 Library Worker: received WorkerLoadRequest") global protos func_request = request.request.function_load_request function_id = func_request.function_id + logger.debug("Successfully completed WorkerLoadRequest.") return protos.FunctionLoadResponse( function_id=function_id, result=protos.StatusResult( @@ -155,15 +154,14 @@ async def function_load_request(request): async def invocation_request(request): - logger.info("V2 Library Worker: received WorkerInvocationRequest") + logger.debug("V2 Library Worker: received WorkerInvocationRequest") global protos invoc_request = request.request.invocation_request - logger.info("VICTORIA --- invocation request %s", invoc_request) invocation_id = invoc_request.invocation_id function_id = invoc_request.function_id http_v2_enabled = False threadpool = request.properties.get("threadpool") - logger.info("VICTORIA --- all variables obtained") + logger.debug("All variables obtained from proxy worker. Invocation ID: %s, Function ID: %s, Threadpool: %s", invocation_id, function_id, threadpool) try: fi: FunctionInfo = _functions.get_function( @@ -175,10 +173,8 @@ async def invocation_request(request): http_v2_enabled = _functions.get_function( function_id).is_http_func and \ HttpV2Registry.http_v2_enabled() - logger.info("VICTORIA --- http_v2_enabled %s", http_v2_enabled) for pb in invoc_request.input_data: - logger.info("VICTORIA --- pb: %s", pb) pb_type_info = fi.input_types[pb.name] if is_trigger_binding(pb_type_info.binding_name): trigger_metadata = invoc_request.trigger_metadata @@ -194,8 +190,6 @@ async def invocation_request(request): function_id).name, is_deferred_binding=pb_type_info.deferred_bindings_enabled) - logger.info("VICTORIA --- args[pb.name]: %s", args[pb.name]) - if http_v2_enabled: http_request = await http_coordinator.get_http_request_async( invocation_id) @@ -253,7 +247,6 @@ async def invocation_request(request): out_name=out_name, protos=protos) output_data.append(param_binding) - logger.info("VICTORIA --- output_data: %s", output_data) return_value = None if fi.return_type is not None and not http_v2_enabled: @@ -263,10 +256,10 @@ async def invocation_request(request): pytype=fi.return_type.pytype, protos=protos ) - logger.info("VICTORIA --- return_value: %s", return_value) # Actively flush customer print() function to console sys.stdout.flush() + logger.debug("Successfully completed WorkerInvocationRequest.") return protos.InvocationResponse( invocation_id=invocation_id, return_value=return_value, @@ -275,6 +268,7 @@ async def invocation_request(request): output_data=output_data) except Exception as ex: + logger.error("An exception in WorkerInvocationRequest has occurred: %s", ex) if http_v2_enabled: http_coordinator.set_http_response(invocation_id, ex) global metadata_exception @@ -291,7 +285,7 @@ async def function_environment_reload_request(request): This is called only when placeholder mode is true. On worker restarts worker init request will be called directly. """ - logger.info("V2 Library Worker: received WorkerInitRequest," + logger.debug("V2 Library Worker: received WorkerEnvReloadRequest," "Version %s", VERSION) global _host, protos, metadata_exception try: @@ -337,6 +331,7 @@ async def function_environment_reload_request(request): change_cwd( func_env_reload_request.function_app_directory) + logger.debug("Successfully completed WorkerEnvReloadRequest.") return protos.FunctionEnvironmentReloadResponse( capabilities=capabilities, worker_metadata=get_worker_metadata(protos), @@ -344,6 +339,7 @@ async def function_environment_reload_request(request): status=protos.StatusResult.Success)) except Exception as ex: + logger.error("An exception in WorkerEnvReloadRequest has occurred: %s", ex) metadata_exception = ex return protos.FunctionEnvironmentReloadResponse( result=protos.StatusResult( @@ -377,9 +373,7 @@ def load_function_metadata(function_app_directory, caller_info): global metadata_result metadata_result = (index_functions(function_path, function_app_directory)) \ if os.path.exists(function_path) else None - logger.info("VICTORIA --- metadata_result: %s", metadata_result) except Exception as ex: - logger.info("VICTORIA --- exception in load_function_metadata: %s", ex) global metadata_exception metadata_exception = ex @@ -416,8 +410,8 @@ def index_functions(function_path: str, function_dir: str): logger.info( 'Successfully processed FunctionMetadataRequest for ' - 'functions: %s. Deferred bindings enabled: %s.', " ".join( + 'functions: %s. Deferred bindings enabled: %s. App Settings: %s', " ".join( indexed_function_logs), - _functions.deferred_bindings_enabled()) - + _functions.deferred_bindings_enabled(), ) +################# VICTORIA return fx_metadata_results diff --git a/azure_functions_worker_v2/http_v2.py b/azure_functions_worker_v2/http_v2.py index 5f1fb772e..fed5643ca 100644 --- a/azure_functions_worker_v2/http_v2.py +++ b/azure_functions_worker_v2/http_v2.py @@ -269,6 +269,7 @@ def http_v2_enabled(cls, **kwargs): cls._http_v2_enabled = cls._check_http_v2_enabled() # Return the result of HTTP/2 enablement + logger.debug("Streaming enabled: %s", cls._http_v2_enabled_checked) return cls._http_v2_enabled @classmethod diff --git a/azure_functions_worker_v2/utils/app_setting_manager.py b/azure_functions_worker_v2/utils/app_setting_manager.py new file mode 100644 index 000000000..7d4e84920 --- /dev/null +++ b/azure_functions_worker_v2/utils/app_setting_manager.py @@ -0,0 +1,31 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import os + +from .constants import ( + FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED, + PYTHON_ENABLE_DEBUG_LOGGING, + PYTHON_ENABLE_INIT_INDEXING, + PYTHON_ENABLE_OPENTELEMETRY, + PYTHON_SCRIPT_FILE_NAME, + PYTHON_THREADPOOL_THREAD_COUNT, +) + + +def get_python_appsetting_state(): + current_vars = os.environ.copy() + python_specific_settings = \ + [PYTHON_THREADPOOL_THREAD_COUNT, + PYTHON_ENABLE_DEBUG_LOGGING, + FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED, + PYTHON_SCRIPT_FILE_NAME, + PYTHON_ENABLE_INIT_INDEXING, + PYTHON_ENABLE_OPENTELEMETRY] + + app_setting_states = "".join( + f"{app_setting}: {current_vars[app_setting]} | " + for app_setting in python_specific_settings + if app_setting in current_vars + ) + + return app_setting_states diff --git a/azure_functions_worker_v2/version.py b/azure_functions_worker_v2/version.py index f5c0b9635..55e7ba88e 100644 --- a/azure_functions_worker_v2/version.py +++ b/azure_functions_worker_v2/version.py @@ -1,4 +1,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -VERSION = '1.0.0a39' +VERSION = '1.0.0a40' From fd8a133ee4118d839c3c6045a2e7bb8beaa13bc4 Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Wed, 9 Apr 2025 13:29:19 -0500 Subject: [PATCH 25/45] lint (fails bc of test file -- to fix later) --- .flake8 | 4 +- azure_functions_worker_v2/bindings/generic.py | 3 +- .../bindings/nullable_converters.py | 12 +++-- azure_functions_worker_v2/functions.py | 45 ++++++++++--------- azure_functions_worker_v2/handle_event.py | 39 ++++++++++------ azure_functions_worker_v2/loader.py | 13 +++--- azure_functions_worker_v2/utils/constants.py | 3 -- azure_functions_worker_v2/utils/current.py | 1 + tests/unittests/test_handle_event.py | 19 ++++---- 9 files changed, 78 insertions(+), 61 deletions(-) diff --git a/.flake8 b/.flake8 index 8fa776784..db9c3b0e6 100644 --- a/.flake8 +++ b/.flake8 @@ -7,9 +7,7 @@ ignore = W503,E402,E731 exclude = .git, __pycache__, build, dist, .eggs, .github, .local, docs/, Samples, azure_functions_worker_v2_v2/protos/, - azure_functions_worker/utils/typing_inspect.py, - tests/unittests/test_typing_inspect.py, - tests/unittests/broken_functions/syntax_error/main.py, + azure_functions_worker_v2/utils/typing_inspect.py, tests/protos/, .env*, .vscode, venv*, *.venv* diff --git a/azure_functions_worker_v2/bindings/generic.py b/azure_functions_worker_v2/bindings/generic.py index e0087f13d..2d80db3c8 100644 --- a/azure_functions_worker_v2/bindings/generic.py +++ b/azure_functions_worker_v2/bindings/generic.py @@ -61,7 +61,8 @@ def decode(cls, data: Datum, *, trigger_metadata) -> typing.Any: result = None else: raise ValueError( - 'unexpected type of data received for the "generic" binding ', repr(data_type) + 'unexpected type of data received for the "generic" binding ', + repr(data_type) ) return result diff --git a/azure_functions_worker_v2/bindings/nullable_converters.py b/azure_functions_worker_v2/bindings/nullable_converters.py index e33255153..05dd7683d 100644 --- a/azure_functions_worker_v2/bindings/nullable_converters.py +++ b/azure_functions_worker_v2/bindings/nullable_converters.py @@ -22,7 +22,8 @@ def to_nullable_string(nullable: Optional[str], property_name: str, protos): if nullable is not None: raise TypeError( "A 'str' type was expected instead of a '%s' " - "type. Cannot parse value %s of '%s'.", type(nullable), nullable, property_name) + "type. Cannot parse value %s of '%s'.", + type(nullable), nullable, property_name) return None @@ -44,7 +45,8 @@ def to_nullable_bool(nullable: Optional[bool], property_name: str, protos): if nullable is not None: raise TypeError( "A 'bool' type was expected instead of a '%s' " - "type. Cannot parse value %s of '%s'.", type(nullable), nullable, property_name) + "type. Cannot parse value %s of '%s'.", + type(nullable), nullable, property_name) return None @@ -78,7 +80,8 @@ def to_nullable_double(nullable: Optional[Union[str, int, float]], raise TypeError( "A 'int' or 'float'" " type was expected instead of a '%s' " - "type. Cannot parse value %s of '%s'.", type(nullable), nullable, property_name) + "type. Cannot parse value %s of '%s'.", + type(nullable), nullable, property_name) return None @@ -107,5 +110,6 @@ def to_nullable_timestamp(date_time: Optional[Union[datetime, int]], raise TypeError( "A 'datetime' or 'int'" " type was expected instead of a '%s' " - "type. Cannot parse value %s of '%s'.", type(date_time), date_time, property_name) + "type. Cannot parse value %s of '%s'.", + type(date_time), date_time, property_name) return None diff --git a/azure_functions_worker_v2/functions.py b/azure_functions_worker_v2/functions.py index 5accfed69..e9226b3e1 100644 --- a/azure_functions_worker_v2/functions.py +++ b/azure_functions_worker_v2/functions.py @@ -128,7 +128,7 @@ def is_context_required(params, bound_params: dict, raise FunctionLoadError( func_name, 'the "context" parameter is expected to be of ' - 'type azure.functions.Context, got "%s"', repr(ctx_anno)) + 'type azure.functions.Context, got "' + repr(ctx_anno) + '"') return requires_context @staticmethod @@ -136,18 +136,18 @@ def validate_function_params(params: dict, bound_params: dict, annotations: dict, func_name: str, protos): logger.debug("Params: %s, BoundParams: %s, Annotations: %s, FuncName: %s", - params, bound_params, annotations, func_name) + params, bound_params, annotations, func_name) if set(params) - set(bound_params): raise FunctionLoadError( func_name, 'the following parameters are declared in Python but ' - 'not in function.json: %s', repr(set(params) - set(bound_params))) + 'not in function.json: ' + repr(set(params) - set(bound_params))) if set(bound_params) - set(params): raise FunctionLoadError( func_name, 'the following parameters are declared in function.json but ' - 'not in Python: %s', repr(set(params) - set(bound_params))) + 'not in Python: ' + repr(set(params) - set(bound_params))) input_types: typing.Dict[str, ParamTypeInfo] = {} output_types: typing.Dict[str, ParamTypeInfo] = {} @@ -159,7 +159,8 @@ def validate_function_params(params: dict, bound_params: dict, param_has_anno = param.name in annotations param_anno = annotations.get(param.name) - logger.debug("Param_has_anno %s, param_anno: %s", param_has_anno, param_anno) + logger.debug("Param_has_anno %s, param_anno: %s", + param_has_anno, param_anno) # Check if deferred bindings is enabled fx_deferred_bindings_enabled, is_deferred_binding = ( @@ -195,7 +196,8 @@ def validate_function_params(params: dict, bound_params: dict, if len(param_anno_args) != 1: raise FunctionLoadError( func_name, - 'binding %s has invalid Out annotation %s', param.name, repr(param_anno)) + 'binding ' + param.name + + ' has invalid Out annotation ' + repr(param_anno)) param_py_type = param_anno_args[0] # typing_inspect.get_args() returns a flat list, @@ -214,21 +216,22 @@ def validate_function_params(params: dict, bound_params: dict, and not is_generic_type(param_py_type)): raise FunctionLoadError( func_name, - 'binding %s has invalid non-type annotation %s', param.name, repr(param_anno)) + 'binding ' + param.name + + ' has invalid non-type annotation ' + repr(param_anno)) if is_binding_out and param_has_anno and not is_param_out: raise FunctionLoadError( func_name, - 'binding %s is declared to have the "out" ' + 'binding ' + param.name + ' is declared to have the "out" ' 'direction, but its annotation in Python is not ' - 'a subclass of azure.functions.Out', param.name) + 'a subclass of azure.functions.Out') if not is_binding_out and is_param_out: raise FunctionLoadError( func_name, - 'binding %s is declared to have the "in" ' + 'binding ' + param.name + ' is declared to have the "in" ' 'direction in function.json, but its annotation ' - 'is azure.functions.Out in Python', param.name) + 'is azure.functions.Out in Python') if param_has_anno and param_py_type in (str, bytes) and ( not has_implicit_output(binding.type)): @@ -247,23 +250,23 @@ def validate_function_params(params: dict, bound_params: dict, param_bind_type, param_py_type, is_deferred_binding) logger.debug("checks_out: %s", - checks_out) + checks_out) if not checks_out: if binding.data_type is not protos.BindingInfo.undefined: raise FunctionLoadError( func_name, - '%s binding type "%s" ' - 'and dataType "%s" in ' + 'binding type "' + repr(param.name) + + '" and dataType "' + binding.type + '" in ' 'function.json do not match the corresponding ' 'function parameter\'s Python type ' - 'annotation %s', repr(param.name), binding.type, binding.data_type, param_py_type.__name__) + 'annotation ' + param_py_type.__name__) else: raise FunctionLoadError( func_name, - 'type of %s binding in function.json ' - '"%s" does not match its Python ' - 'annotation "%s"', param.name, binding.type, param_py_type.__name__) + 'type of ' + param.name + ' binding in function.json "' + + binding.type + '" does not match its Python ' + 'annotation "' + param_py_type.__name__ + '"') param_type_info = ParamTypeInfo(param_bind_type, param_py_type, @@ -293,7 +296,7 @@ def get_function_return_type(annotations: dict, has_explicit_return: bool, raise FunctionLoadError( func_name, 'has invalid non-type return ' - 'annotation %s', repr(return_pytype)) + 'annotation ' + repr(return_pytype)) if return_pytype is (str, bytes): binding_name = 'generic' @@ -302,8 +305,8 @@ def get_function_return_type(annotations: dict, has_explicit_return: bool, binding_name, return_pytype): raise FunctionLoadError( func_name, - 'Python return annotation "%s" ' - 'does not match binding type "%s"', return_pytype.__name__, binding_name) + 'Python return annotation "' + return_pytype.__name__ + + '" does not match binding type "' + binding_name + '"') if has_implicit_return and 'return' in annotations: return_pytype = annotations.get('return') diff --git a/azure_functions_worker_v2/handle_event.py b/azure_functions_worker_v2/handle_event.py index 291bb752d..a9398c252 100644 --- a/azure_functions_worker_v2/handle_event.py +++ b/azure_functions_worker_v2/handle_event.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import json import logging import os import sys @@ -25,6 +26,7 @@ from_incoming_proto, to_outgoing_param_binding, to_outgoing_proto) from .bindings.out import Out +from utils.app_setting_manager import get_python_appsetting_state from .utils.constants import (FUNCTION_DATA_CACHE, RAW_HTTP_BODY_BYTES, TYPED_DATA_COLLECTION, @@ -36,7 +38,6 @@ PYTHON_ENABLE_OPENTELEMETRY, PYTHON_ENABLE_OPENTELEMETRY_DEFAULT, WORKER_OPEN_TELEMETRY_ENABLED, - PYTHON_ENABLE_INIT_INDEXING, HTTP_URI, REQUIRES_ROUTE_PARAMETERS, PYTHON_SCRIPT_FILE_NAME, @@ -58,7 +59,7 @@ async def worker_init_request(request): logger.debug("V2 Library Worker: received WorkerInitRequest," - "Version %s", VERSION) + "Version %s", VERSION) global _host, protos, _function_data_cache_enabled, metadata_exception init_request = request.request.worker_init_request host_capabilities = init_request.capabilities @@ -121,10 +122,13 @@ async def worker_init_request(request): async def functions_metadata_request(request): global protos, metadata_result, metadata_exception - logger.debug("V2 Library Worker: received WorkerMetadataRequest. Metadata Result: %s, Metadata Exception: %s", metadata_result, metadata_exception) + logger.debug("V2 Library Worker: received WorkerMetadataRequest." + " Metadata Result: %s, Metadata Exception: %s", + metadata_result, metadata_exception) if metadata_exception: - logger.info("An exception in WorkerMetadataRequest has occurred: %s", metadata_exception) + logger.info("An exception in WorkerMetadataRequest has occurred: %s", + metadata_exception) return protos.FunctionMetadataResponse( result=protos.StatusResult( status=protos.StatusResult.Failure, @@ -161,7 +165,9 @@ async def invocation_request(request): function_id = invoc_request.function_id http_v2_enabled = False threadpool = request.properties.get("threadpool") - logger.debug("All variables obtained from proxy worker. Invocation ID: %s, Function ID: %s, Threadpool: %s", invocation_id, function_id, threadpool) + logger.debug("All variables obtained from proxy worker." + " Invocation ID: %s, Function ID: %s, Threadpool: %s", + invocation_id, function_id, threadpool) try: fi: FunctionInfo = _functions.get_function( @@ -216,7 +222,8 @@ async def invocation_request(request): if otel_manager.get_azure_monitor_available(): configure_opentelemetry(fi_context) - call_result = await execute_async(fi.func, args) # Not supporting Extensions + # Extensions are not supported + call_result = await execute_async(fi.func, args) else: _loop = get_current_loop() call_result = await _loop.run_in_executor( @@ -286,7 +293,7 @@ async def function_environment_reload_request(request): worker init request will be called directly. """ logger.debug("V2 Library Worker: received WorkerEnvReloadRequest," - "Version %s", VERSION) + "Version %s", VERSION) global _host, protos, metadata_exception try: @@ -405,13 +412,17 @@ def index_functions(function_path: str, function_dir: str): indexed_function_bindings_logs.append(( binding.type, binding.name, deferred_binding_info)) - function_log = "Function Name: " + func.get_function_name() + ", Function Binding: " + str(indexed_function_bindings_logs) + function_log = ("Function Name: " + func.get_function_name() + + ", Function Binding: " + + str(indexed_function_bindings_logs)) indexed_function_logs.append(function_log) - logger.info( - 'Successfully processed FunctionMetadataRequest for ' - 'functions: %s. Deferred bindings enabled: %s. App Settings: %s', " ".join( - indexed_function_logs), - _functions.deferred_bindings_enabled(), ) -################# VICTORIA + log_data = { + "message": "Successfully processed FunctionMetadataRequest", + "functions": " ".join(indexed_function_logs), + "deferred_bindings_enabled": _functions.deferred_bindings_enabled(), + "app_settings": get_python_appsetting_state() + } + logger.info(json.dumps(log_data)) + return fx_metadata_results diff --git a/azure_functions_worker_v2/loader.py b/azure_functions_worker_v2/loader.py index b0a5ccf7e..881bc785a 100644 --- a/azure_functions_worker_v2/loader.py +++ b/azure_functions_worker_v2/loader.py @@ -154,10 +154,12 @@ def process_indexed_function(protos, @attach_message_to_exception( expt_type=ImportError, - message="Cannot find module. Please check the requirements.txt file for the missing module. For more info, please refer the troubleshooting guide: " + MODULE_NOT_FOUND_TS_URL + - ". Current sys.path: " + " ".join(sys.path), - debug_logs="Error in index_function_app. Sys Path:" + " ".join(sys.path) + - ", python-packages Path exists: " + str(os.path.exists(CUSTOMER_PACKAGES_PATH))) + message="Cannot find module. Please check the requirements.txt file for the " + "missing module. For more info, please refer the troubleshooting guide: " + + MODULE_NOT_FOUND_TS_URL + ". Current sys.path: " + " ".join(sys.path), + debug_logs="Error in index_function_app. Sys Path:" + " ".join(sys.path) + + ", python-packages Path exists: " + + str(os.path.exists(CUSTOMER_PACKAGES_PATH))) def index_function_app(function_path: str): module_name = pathlib.Path(function_path).stem imported_module = importlib.import_module(module_name) @@ -177,7 +179,8 @@ def index_function_app(function_path: str): script_file_name = get_app_setting( setting=PYTHON_SCRIPT_FILE_NAME, default_value=PYTHON_SCRIPT_FILE_NAME_DEFAULT) - raise ValueError("Could not find top level function app instances in %s.", script_file_name) + raise ValueError("Could not find top level function app instances in %s.", + script_file_name) return app.get_functions() diff --git a/azure_functions_worker_v2/utils/constants.py b/azure_functions_worker_v2/utils/constants.py index 974c4d15b..9853bdbd2 100644 --- a/azure_functions_worker_v2/utils/constants.py +++ b/azure_functions_worker_v2/utils/constants.py @@ -60,9 +60,6 @@ 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" - METADATA_PROPERTIES_WORKER_INDEXED = "worker_indexed" # Header names diff --git a/azure_functions_worker_v2/utils/current.py b/azure_functions_worker_v2/utils/current.py index 264697bf6..49e973b14 100644 --- a/azure_functions_worker_v2/utils/current.py +++ b/azure_functions_worker_v2/utils/current.py @@ -16,6 +16,7 @@ def get_current_loop(): async def execute_async(function, args) -> Any: return await function(**args) + def execute_sync(function, args) -> Any: return function(**args) diff --git a/tests/unittests/test_handle_event.py b/tests/unittests/test_handle_event.py index b5c766eba..e8e198c2c 100644 --- a/tests/unittests/test_handle_event.py +++ b/tests/unittests/test_handle_event.py @@ -1,13 +1,11 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import os from typing import Any from unittest.mock import patch -from azure_functions_worker_v2.utils.constants import PYTHON_ENABLE_INIT_INDEXING from azure_functions_worker_v2.handle_event import (worker_init_request, - functions_metadata_request, - function_environment_reload_request) + functions_metadata_request, + function_environment_reload_request) from tests.utils import testutils import tests.protos as protos @@ -69,11 +67,11 @@ async def test_worker_init_request_with_streaming(self): 'protos': protos}) result = await worker_init_request(worker_request) self.assertNotEqual(result.capabilities, {'WorkerStatus': 'true', - 'RpcHttpBodyOnly': 'true', - 'SharedMemoryDataTransfer': 'true', - 'RpcHttpTriggerMetadataRemoved': 'true', - 'RawHttpBodyBytes': 'true', - 'TypedDataCollection': 'true'}) + 'RpcHttpBodyOnly': 'true', + 'SharedMemoryDataTransfer': 'true', + 'RpcHttpTriggerMetadataRemoved': 'true', + 'RawHttpBodyBytes': 'true', + 'TypedDataCollection': 'true'}) self.assertEqual(result.worker_metadata.runtime_name, "python") self.assertIsNotNone(result.worker_metadata.runtime_version) self.assertIsNotNone(result.worker_metadata.worker_version) @@ -110,7 +108,8 @@ async def test_functions_metadata_request(self): async def run_init_then_meta(self): worker_request = WorkerRequest(name='worker_init_request', - request=Request(FunctionRequest('hello', BASIC_FUNCTION_DIRECTORY)), + request=Request( + FunctionRequest('hello', BASIC_FUNCTION_DIRECTORY)), properties={'host': '123', 'protos': protos}) _ = await worker_init_request(worker_request) From 1f58703c6e4d2e1392efb8baf11a5b53bcf33419 Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Fri, 11 Apr 2025 14:19:46 -0500 Subject: [PATCH 26/45] add support for cmbd, remove cache from worker --- .../bindings/datumdef.py | 2 + azure_functions_worker_v2/bindings/meta.py | 29 +- azure_functions_worker_v2/handle_event.py | 3 +- .../utils/app_setting_manager.py | 2 - tests/unittests/test_broken_functions.py | 299 ------------------ 5 files changed, 11 insertions(+), 324 deletions(-) delete mode 100644 tests/unittests/test_broken_functions.py diff --git a/azure_functions_worker_v2/bindings/datumdef.py b/azure_functions_worker_v2/bindings/datumdef.py index 9cc4eb6cf..043d6a02b 100644 --- a/azure_functions_worker_v2/bindings/datumdef.py +++ b/azure_functions_worker_v2/bindings/datumdef.py @@ -105,6 +105,8 @@ def from_typed_data(cls, protos): val = td.collection_sint64 elif tt == 'model_binding_data': val = td.model_binding_data + elif tt == 'collection_model_binding_data': + val = td.collection_model_binding_data elif tt is None: return None else: diff --git a/azure_functions_worker_v2/bindings/meta.py b/azure_functions_worker_v2/bindings/meta.py index 498336bcf..370018791 100644 --- a/azure_functions_worker_v2/bindings/meta.py +++ b/azure_functions_worker_v2/bindings/meta.py @@ -25,7 +25,6 @@ BINDING_REGISTRY = None DEFERRED_BINDING_REGISTRY = None -deferred_bindings_cache: Dict[Any, Any] = {} def _check_http_input_type_annotation(bind_name: str, pytype: type, @@ -244,7 +243,7 @@ def deferred_bindings_decode(binding: Any, metadata: Any, function_name: str): """ - This cache holds deferred binding types (ie. BlobClient, ContainerClient) + The appropriate extension manages a cache for clients (ie. BlobClient, ContainerClient) That have already been created, so that the worker can reuse the Previously created type without creating a new one. @@ -256,26 +255,12 @@ def deferred_bindings_decode(binding: Any, If cache is empty or key doesn't exist, deferred_binding_type is None """ - global deferred_bindings_cache - - if deferred_bindings_cache.get((pb.name, - pytype, - datum.value.content, - function_name), None) is not None: - return deferred_bindings_cache.get((pb.name, - pytype, - datum.value.content, - function_name)) - else: - deferred_binding_type = binding.decode(datum, - trigger_metadata=metadata, - pytype=pytype) - - deferred_bindings_cache[(pb.name, - pytype, - datum.value.content, - function_name)] = deferred_binding_type - return deferred_binding_type + + deferred_binding_type = binding.decode(datum, + trigger_metadata=metadata, + pytype=pytype) + + return deferred_binding_type def check_deferred_bindings_enabled(param_anno: Union[type, None], diff --git a/azure_functions_worker_v2/handle_event.py b/azure_functions_worker_v2/handle_event.py index a9398c252..17ca6b0c6 100644 --- a/azure_functions_worker_v2/handle_event.py +++ b/azure_functions_worker_v2/handle_event.py @@ -26,7 +26,7 @@ from_incoming_proto, to_outgoing_param_binding, to_outgoing_proto) from .bindings.out import Out -from utils.app_setting_manager import get_python_appsetting_state +from .utils.app_setting_manager import get_python_appsetting_state from .utils.constants import (FUNCTION_DATA_CACHE, RAW_HTTP_BODY_BYTES, TYPED_DATA_COLLECTION, @@ -419,6 +419,7 @@ def index_functions(function_path: str, function_dir: str): log_data = { "message": "Successfully processed FunctionMetadataRequest", + "version": VERSION, "functions": " ".join(indexed_function_logs), "deferred_bindings_enabled": _functions.deferred_bindings_enabled(), "app_settings": get_python_appsetting_state() diff --git a/azure_functions_worker_v2/utils/app_setting_manager.py b/azure_functions_worker_v2/utils/app_setting_manager.py index 7d4e84920..a8f8609e1 100644 --- a/azure_functions_worker_v2/utils/app_setting_manager.py +++ b/azure_functions_worker_v2/utils/app_setting_manager.py @@ -5,7 +5,6 @@ from .constants import ( FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED, PYTHON_ENABLE_DEBUG_LOGGING, - PYTHON_ENABLE_INIT_INDEXING, PYTHON_ENABLE_OPENTELEMETRY, PYTHON_SCRIPT_FILE_NAME, PYTHON_THREADPOOL_THREAD_COUNT, @@ -19,7 +18,6 @@ def get_python_appsetting_state(): PYTHON_ENABLE_DEBUG_LOGGING, FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED, PYTHON_SCRIPT_FILE_NAME, - PYTHON_ENABLE_INIT_INDEXING, PYTHON_ENABLE_OPENTELEMETRY] app_setting_states = "".join( diff --git a/tests/unittests/test_broken_functions.py b/tests/unittests/test_broken_functions.py deleted file mode 100644 index 53c73286d..000000000 --- a/tests/unittests/test_broken_functions.py +++ /dev/null @@ -1,299 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from tests.utils import testutils - -from tests import protos - - -class TestMockHost(testutils.AsyncTestCase): - broken_funcs_dir = testutils.UNIT_TESTS_FOLDER / 'broken_functions' - - async def test_load_broken__missing_py_param(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('missing_py_param') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r".*cannot load the missing_py_param function" - r".*parameters are declared in function.json" - r".*'req'.*") - - async def test_load_broken__missing_json_param(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('missing_json_param') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r".*cannot load the missing_json_param function" - r".*parameters are declared in Python" - r".*'spam'.*") - - async def test_load_broken__wrong_param_dir(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('wrong_param_dir') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r'.*cannot load the wrong_param_dir function' - r'.*binding foo is declared to have the "out".*') - - async def test_load_broken__bad_out_annotation(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('bad_out_annotation') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r'.*cannot load the bad_out_annotation function' - r'.*binding foo has invalid Out annotation.*') - - async def test_load_broken__wrong_binding_dir(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('wrong_binding_dir') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r'.*cannot load the wrong_binding_dir function' - r'.* binding foo is declared to have the "in" direction' - r'.*but its annotation is.*Out.*') - - async def test_load_broken__invalid_context_param(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('invalid_context_param') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r'.*cannot load the invalid_context_param function' - r'.*the "context" parameter.*') - - async def test_load_broken__syntax_error(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('syntax_error') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertIn('SyntaxError', r.response.result.exception.message) - - async def test_load_broken__module_not_found_error(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('module_not_found_error') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertIn('ModuleNotFoundError', - r.response.result.exception.message) - - async def test_load_broken__import_error(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('import_error') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertIn('ImportError', - r.response.result.exception.message) - self.assertNotIn('', - r.response.result.exception.message) - self.assertNotIn('', - r.response.result.exception.message) - - async def test_load_broken__inout_param(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('inout_param') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r'.*cannot load the inout_param function' - r'.*"inout" bindings.*') - - async def test_load_broken__return_param_in(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('return_param_in') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r'.*cannot load the return_param_in function' - r'.*"\$return" .* set to "out"') - - async def test_load_broken__invalid_return_anno(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('invalid_return_anno') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r'.*cannot load the invalid_return_anno function' - r'.*Python return annotation "int" does not match ' - r'binding type "http"') - - async def test_load_broken__invalid_return_anno_non_type(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function( - 'invalid_return_anno_non_type') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r'.*cannot load the invalid_return_anno_non_type function: ' - r'has invalid non-type return annotation 123') - - async def test_load_broken__invalid_http_trigger_anno(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('invalid_http_trigger_anno') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertEqual( - r.response.result.exception.message, - 'FunctionLoadError: cannot load the invalid_http_trigger_anno' - ' function: type of req binding in function.json "httpTrigger" ' - 'does not match its Python annotation "int"') - - async def test_load_broken__invalid_out_anno(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('invalid_out_anno') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertEqual( - r.response.result.exception.message, - 'FunctionLoadError: cannot load the invalid_out_anno function: ' - r'type of ret binding in function.json "http" ' - r'does not match its Python annotation "HttpRequest"') - - async def test_load_broken__invalid_in_anno(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('invalid_in_anno') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertEqual( - r.response.result.exception.message, - 'FunctionLoadError: cannot load the invalid_in_anno function: ' - r'type of req binding in function.json "httpTrigger" ' - r'does not match its Python annotation "HttpResponse"') - - async def test_load_broken__invalid_datatype(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('invalid_datatype') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r'.*cannot load the invalid_datatype function: ' - r'.*binding type "httpTrigger" and dataType "1" in ' - r'function.json do not match the corresponding function ' - r'parameter.* Python type annotation "HttpResponse"') - - async def test_load_broken__invalid_in_anno_non_type(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('invalid_in_anno_non_type') - - self.assertEqual(r.response.function_id, func_id) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r'.*cannot load the invalid_in_anno_non_type function: ' - r'binding req has invalid non-type annotation 123') - - async def test_import_module_troubleshooting_url(self): - async with testutils.start_mockhost( - script_root=self.broken_funcs_dir) as host: - await host.init_worker() - func_id, r = await host.load_function('missing_module') - - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - self.assertRegex( - r.response.result.exception.message, - r'.*ModuleNotFoundError') From 7f61b5ee9cd04b5ef7b9291ed1cfccba48772673 Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Fri, 11 Apr 2025 16:06:47 -0500 Subject: [PATCH 27/45] merge in otel changes --- azure_functions_worker_v2/handle_event.py | 27 +++++++++++--------- azure_functions_worker_v2/otel.py | 15 ++++++++--- azure_functions_worker_v2/utils/constants.py | 14 ++++++---- azure_functions_worker_v2/utils/current.py | 2 +- 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/azure_functions_worker_v2/handle_event.py b/azure_functions_worker_v2/handle_event.py index 17ca6b0c6..3352916b9 100644 --- a/azure_functions_worker_v2/handle_event.py +++ b/azure_functions_worker_v2/handle_event.py @@ -36,7 +36,7 @@ SHARED_MEMORY_DATA_TRANSFER, TRUE, PYTHON_ENABLE_OPENTELEMETRY, - PYTHON_ENABLE_OPENTELEMETRY_DEFAULT, + PYTHON_APPLICATIONINSIGHTS_ENABLE_TELEMETRY, WORKER_OPEN_TELEMETRY_ENABLED, HTTP_URI, REQUIRES_ROUTE_PARAMETERS, @@ -77,12 +77,14 @@ async def worker_init_request(request): RPC_HTTP_TRIGGER_METADATA_REMOVED: TRUE, SHARED_MEMORY_DATA_TRANSFER: TRUE, } - if get_app_setting(setting=PYTHON_ENABLE_OPENTELEMETRY, - default_value=PYTHON_ENABLE_OPENTELEMETRY_DEFAULT): + if is_envvar_true(PYTHON_APPLICATIONINSIGHTS_ENABLE_TELEMETRY): initialize_azure_monitor() - if otel_manager.get_azure_monitor_available(): - capabilities[WORKER_OPEN_TELEMETRY_ENABLED] = TRUE + if is_envvar_true(PYTHON_ENABLE_OPENTELEMETRY): + otel_manager.set_otel_libs_available(True) + + if otel_manager.get_azure_monitor_available() or otel_manager.set_otel_libs_available(): + capabilities[WORKER_OPEN_TELEMETRY_ENABLED] = TRUE # loading bindings registry and saving results to a static # dictionary which will be later used in the invocation request @@ -219,7 +221,7 @@ async def invocation_request(request): args[name] = Out() if fi.is_async: - if otel_manager.get_azure_monitor_available(): + if otel_manager.get_azure_monitor_available() or otel_manager.set_otel_libs_available(): configure_opentelemetry(fi_context) # Extensions are not supported @@ -310,14 +312,15 @@ async def function_environment_reload_request(request): load_binding_registry() capabilities = {} - if get_app_setting( - setting=PYTHON_ENABLE_OPENTELEMETRY, - default_value=PYTHON_ENABLE_OPENTELEMETRY_DEFAULT): + if is_envvar_true(PYTHON_ENABLE_OPENTELEMETRY): + otel_manager.set_otel_libs_available(True) + + if is_envvar_true(PYTHON_APPLICATIONINSIGHTS_ENABLE_TELEMETRY): initialize_azure_monitor() - if otel_manager.get_azure_monitor_available(): - capabilities[WORKER_OPEN_TELEMETRY_ENABLED] = ( - TRUE) + if otel_manager.get_azure_monitor_available() or otel_manager.get_otel_libs_available(): + capabilities[WORKER_OPEN_TELEMETRY_ENABLED] = ( + TRUE) try: _host = request.properties.get("host") diff --git a/azure_functions_worker_v2/otel.py b/azure_functions_worker_v2/otel.py index cadb3ca3a..742d142f6 100644 --- a/azure_functions_worker_v2/otel.py +++ b/azure_functions_worker_v2/otel.py @@ -7,14 +7,15 @@ from .utils.env_state import get_app_setting from .utils.constants import (APPLICATIONINSIGHTS_CONNECTION_STRING, - PYTHON_AZURE_MONITOR_LOGGER_NAME, - PYTHON_AZURE_MONITOR_LOGGER_NAME_DEFAULT, + PYTHON_APPLICATIONINSIGHTS_LOGGER_NAME, + PYTHON_APPLICATIONINSIGHTS_LOGGER_NAME_DEFAULT, TRACESTATE, TRACEPARENT) class OTelManager: def __init__(self): self._azure_monitor_available = False + self._otel_libs_available = False self._context_api = None self._trace_context_propagator = None @@ -24,6 +25,12 @@ def set_azure_monitor_available(self, azure_monitor_available): def get_azure_monitor_available(self): return self._azure_monitor_available + def set_otel_libs_available(self, otel_libs_available): + self._aotel_libs_available = otel_libs_available + + def get_otel_libs_available(self): + return self._otel_libs_available + def set_context_api(self, context_api): self._context_api = context_api @@ -77,8 +84,8 @@ def initialize_azure_monitor(): setting=APPLICATIONINSIGHTS_CONNECTION_STRING ), logger_name=get_app_setting( - setting=PYTHON_AZURE_MONITOR_LOGGER_NAME, - default_value=PYTHON_AZURE_MONITOR_LOGGER_NAME_DEFAULT + setting=PYTHON_APPLICATIONINSIGHTS_LOGGER_NAME, + default_value=PYTHON_APPLICATIONINSIGHTS_LOGGER_NAME_DEFAULT ), ) OTelManager.set_azure_monitor_available(True) diff --git a/azure_functions_worker_v2/utils/constants.py b/azure_functions_worker_v2/utils/constants.py index 9853bdbd2..a04e2596a 100644 --- a/azure_functions_worker_v2/utils/constants.py +++ b/azure_functions_worker_v2/utils/constants.py @@ -75,14 +75,18 @@ BASE_EXT_SUPPORTED_PY_MINOR_VERSION = 8 # Appsetting to turn on OpenTelemetry support/features -# Includes turning on Azure monitor distro to send telemetry to AppInsights +# A value of "true" enables the setting PYTHON_ENABLE_OPENTELEMETRY = "PYTHON_ENABLE_OPENTELEMETRY" -PYTHON_ENABLE_OPENTELEMETRY_DEFAULT = False + +# Appsetting to turn on ApplicationInsights support/features +# A value of "true" enables the setting +PYTHON_APPLICATIONINSIGHTS_ENABLE_TELEMETRY = \ + "PYTHON_APPLICATIONINSIGHTS_ENABLE_TELEMETRY" # Appsetting to specify root logger name of logger to collect telemetry for -# Used by Azure monitor distro -PYTHON_AZURE_MONITOR_LOGGER_NAME = "PYTHON_AZURE_MONITOR_LOGGER_NAME" -PYTHON_AZURE_MONITOR_LOGGER_NAME_DEFAULT = "" +# Used by Azure monitor distro (Application Insights) +PYTHON_APPLICATIONINSIGHTS_LOGGER_NAME = "PYTHON_APPLICATIONINSIGHTS_LOGGER_NAME" +PYTHON_APPLICATIONINSIGHTS_LOGGER_NAME_DEFAULT = "" # Appsetting to specify AppInsights connection string APPLICATIONINSIGHTS_CONNECTION_STRING = "APPLICATIONINSIGHTS_CONNECTION_STRING" diff --git a/azure_functions_worker_v2/utils/current.py b/azure_functions_worker_v2/utils/current.py index 49e973b14..1177162c1 100644 --- a/azure_functions_worker_v2/utils/current.py +++ b/azure_functions_worker_v2/utils/current.py @@ -26,7 +26,7 @@ def run_sync_func(invocation_id, context, func, params): # invocation_id from ThreadPoolExecutor's threads. context.thread_local_storage.invocation_id = invocation_id try: - if otel_manager.get_azure_monitor_available(): + if otel_manager.get_azure_monitor_available() or otel_manager.get_otel_libs_available(): configure_opentelemetry(context) result = functools.partial(execute_sync, func) return result(params) From 0cee003524e302967c86b676faf79d76e7404f2a Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Fri, 11 Apr 2025 16:11:37 -0500 Subject: [PATCH 28/45] added logs in init, env reload, invoc --- azure_functions_worker_v2/handle_event.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/azure_functions_worker_v2/handle_event.py b/azure_functions_worker_v2/handle_event.py index 3352916b9..d3a077073 100644 --- a/azure_functions_worker_v2/handle_event.py +++ b/azure_functions_worker_v2/handle_event.py @@ -58,8 +58,8 @@ async def worker_init_request(request): - logger.debug("V2 Library Worker: received WorkerInitRequest," - "Version %s", VERSION) + logger.info("V2 Library Worker: received WorkerInitRequest," + "Version %s", VERSION) global _host, protos, _function_data_cache_enabled, metadata_exception init_request = request.request.worker_init_request host_capabilities = init_request.capabilities @@ -175,6 +175,7 @@ async def invocation_request(request): fi: FunctionInfo = _functions.get_function( function_id) assert fi is not None + logger.info("Function name: %s, Function Type: %s", fi.name, ("async" if fi.is_async else "sync")) args = {} @@ -294,7 +295,7 @@ async def function_environment_reload_request(request): This is called only when placeholder mode is true. On worker restarts worker init request will be called directly. """ - logger.debug("V2 Library Worker: received WorkerEnvReloadRequest," + logger.info("V2 Library Worker: received WorkerEnvReloadRequest," "Version %s", VERSION) global _host, protos, metadata_exception try: @@ -422,7 +423,6 @@ def index_functions(function_path: str, function_dir: str): log_data = { "message": "Successfully processed FunctionMetadataRequest", - "version": VERSION, "functions": " ".join(indexed_function_logs), "deferred_bindings_enabled": _functions.deferred_bindings_enabled(), "app_settings": get_python_appsetting_state() From 2a5657b2d70fb25532f2c949b2c7a7ec9840078e Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Mon, 14 Apr 2025 16:09:01 -0500 Subject: [PATCH 29/45] tests for init, env reload + lint --- azure_functions_worker_v2/bindings/meta.py | 2 +- azure_functions_worker_v2/handle_event.py | 15 ++- azure_functions_worker_v2/utils/current.py | 3 +- tests/unittests/test_handle_event.py | 139 ++++++++++++++++----- tests/utils/constants.py | 1 + 5 files changed, 123 insertions(+), 37 deletions(-) diff --git a/azure_functions_worker_v2/bindings/meta.py b/azure_functions_worker_v2/bindings/meta.py index 370018791..92089b28d 100644 --- a/azure_functions_worker_v2/bindings/meta.py +++ b/azure_functions_worker_v2/bindings/meta.py @@ -243,7 +243,7 @@ def deferred_bindings_decode(binding: Any, metadata: Any, function_name: str): """ - The appropriate extension manages a cache for clients (ie. BlobClient, ContainerClient) + The extension manages a cache for clients (ie. BlobClient, ContainerClient) That have already been created, so that the worker can reuse the Previously created type without creating a new one. diff --git a/azure_functions_worker_v2/handle_event.py b/azure_functions_worker_v2/handle_event.py index d3a077073..bd74c9842 100644 --- a/azure_functions_worker_v2/handle_event.py +++ b/azure_functions_worker_v2/handle_event.py @@ -83,7 +83,8 @@ async def worker_init_request(request): if is_envvar_true(PYTHON_ENABLE_OPENTELEMETRY): otel_manager.set_otel_libs_available(True) - if otel_manager.get_azure_monitor_available() or otel_manager.set_otel_libs_available(): + if (otel_manager.get_azure_monitor_available() + or otel_manager.get_otel_libs_available()): capabilities[WORKER_OPEN_TELEMETRY_ENABLED] = TRUE # loading bindings registry and saving results to a static @@ -175,7 +176,9 @@ async def invocation_request(request): fi: FunctionInfo = _functions.get_function( function_id) assert fi is not None - logger.info("Function name: %s, Function Type: %s", fi.name, ("async" if fi.is_async else "sync")) + logger.info("Function name: %s, Function Type: %s", + fi.name, + ("async" if fi.is_async else "sync")) args = {} @@ -222,7 +225,8 @@ async def invocation_request(request): args[name] = Out() if fi.is_async: - if otel_manager.get_azure_monitor_available() or otel_manager.set_otel_libs_available(): + if (otel_manager.get_azure_monitor_available() + or otel_manager.set_otel_libs_available()): configure_opentelemetry(fi_context) # Extensions are not supported @@ -296,7 +300,7 @@ async def function_environment_reload_request(request): worker init request will be called directly. """ logger.info("V2 Library Worker: received WorkerEnvReloadRequest," - "Version %s", VERSION) + "Version %s", VERSION) global _host, protos, metadata_exception try: @@ -319,7 +323,8 @@ async def function_environment_reload_request(request): if is_envvar_true(PYTHON_APPLICATIONINSIGHTS_ENABLE_TELEMETRY): initialize_azure_monitor() - if otel_manager.get_azure_monitor_available() or otel_manager.get_otel_libs_available(): + if (otel_manager.get_azure_monitor_available() + or otel_manager.get_otel_libs_available()): capabilities[WORKER_OPEN_TELEMETRY_ENABLED] = ( TRUE) diff --git a/azure_functions_worker_v2/utils/current.py b/azure_functions_worker_v2/utils/current.py index 1177162c1..e15e3ff39 100644 --- a/azure_functions_worker_v2/utils/current.py +++ b/azure_functions_worker_v2/utils/current.py @@ -26,7 +26,8 @@ def run_sync_func(invocation_id, context, func, params): # invocation_id from ThreadPoolExecutor's threads. context.thread_local_storage.invocation_id = invocation_id try: - if otel_manager.get_azure_monitor_available() or otel_manager.get_otel_libs_available(): + if (otel_manager.get_azure_monitor_available() + or otel_manager.get_otel_libs_available()): configure_opentelemetry(context) result = functools.partial(execute_sync, func) return result(params) diff --git a/tests/unittests/test_handle_event.py b/tests/unittests/test_handle_event.py index e8e198c2c..87c2a3abc 100644 --- a/tests/unittests/test_handle_event.py +++ b/tests/unittests/test_handle_event.py @@ -7,12 +7,14 @@ functions_metadata_request, function_environment_reload_request) from tests.utils import testutils +from tests.utils.constants import UNIT_TESTS_FOLDER import tests.protos as protos -BASIC_FUNCTION_DIRECTORY = "C:\\Users\\victoriahall\\Documents\\repos\\azure-functions-python-worker\\tests\\unittests\\basic_function" -STREAMING_FUNCTION_DIRECTORY = "C:\\Users\\victoriahall\\Documents\\repos\\azure-functions-python-worker\\tests\\unittests\\streaming_function" -INDEXING_EXCEPTION_FUNCTION_DIRECTORY = "tests\\unittests\\indexing_exception_function" +BASIC_FUNCTION_DIRECTORY = UNIT_TESTS_FOLDER / "basic_function" +STREAMING_FUNCTION_DIRECTORY = UNIT_TESTS_FOLDER / "streaming_function" +INDEXING_EXCEPTION_FUNCTION_DIRECTORY = (UNIT_TESTS_FOLDER + / "indexing_exception_function") # This represents the top level protos request sent from the host @@ -58,7 +60,13 @@ async def test_worker_init_request(self): self.assertIsNotNone(result.worker_metadata.worker_bitness) self.assertEqual(result.result.status, 1) - async def test_worker_init_request_with_streaming(self): + @patch("azure_functions_worker_v2.handle_event.HttpV2Registry.http_v2_enabled", + return_value=True) + @patch("azure_functions_worker_v2.handle_event.initialize_http_server", + return_value="http://mock_address") + async def test_worker_init_request_with_streaming(self, + mock_http_v2_enabled, + mock_initialize_http_server): worker_request = WorkerRequest(name='worker_init_request', request=Request(FunctionRequest( 'hello', @@ -66,12 +74,38 @@ async def test_worker_init_request_with_streaming(self): properties={'host': '123', 'protos': protos}) result = await worker_init_request(worker_request) - self.assertNotEqual(result.capabilities, {'WorkerStatus': 'true', - 'RpcHttpBodyOnly': 'true', - 'SharedMemoryDataTransfer': 'true', - 'RpcHttpTriggerMetadataRemoved': 'true', - 'RawHttpBodyBytes': 'true', - 'TypedDataCollection': 'true'}) + self.assertEqual(result.capabilities, {'WorkerStatus': 'true', + 'RpcHttpBodyOnly': 'true', + 'SharedMemoryDataTransfer': 'true', + 'RpcHttpTriggerMetadataRemoved': 'true', + 'RawHttpBodyBytes': 'true', + 'TypedDataCollection': 'true', + 'HttpUri': 'http://mock_address', + 'RequiresRouteParameters': 'true'}) + self.assertEqual(result.worker_metadata.runtime_name, "python") + self.assertIsNotNone(result.worker_metadata.runtime_version) + self.assertIsNotNone(result.worker_metadata.worker_version) + self.assertIsNotNone(result.worker_metadata.worker_bitness) + self.assertEqual(result.result.status, 1) + + @patch("azure_functions_worker_v2.handle_event" + ".otel_manager.get_azure_monitor_available", + return_value=True) + async def test_worker_init_request_with_otel(self, mock_otel_enabled): + worker_request = WorkerRequest(name='worker_init_request', + request=Request(FunctionRequest( + 'hello', + BASIC_FUNCTION_DIRECTORY)), + properties={'host': '123', + 'protos': protos}) + result = await worker_init_request(worker_request) + self.assertEqual(result.capabilities, {'WorkerStatus': 'true', + 'RpcHttpBodyOnly': 'true', + 'SharedMemoryDataTransfer': 'true', + 'RpcHttpTriggerMetadataRemoved': 'true', + 'RawHttpBodyBytes': 'true', + 'TypedDataCollection': 'true', + 'WorkerOpenTelemetryEnabled': 'true'}) self.assertEqual(result.worker_metadata.runtime_name, "python") self.assertIsNotNone(result.worker_metadata.runtime_version) self.assertIsNotNone(result.worker_metadata.worker_version) @@ -101,20 +135,17 @@ async def test_worker_init_request_with_exception(self): self.assertEqual(result.result.status, 1) async def test_functions_metadata_request(self): - result = await self.run_init_then_meta() - self.assertEqual(result.use_default_metadata_indexing, False) - self.assertIsNotNone(result.function_metadata_results) - self.assertEqual(result.result.status, 1) - - async def run_init_then_meta(self): worker_request = WorkerRequest(name='worker_init_request', - request=Request( - FunctionRequest('hello', BASIC_FUNCTION_DIRECTORY)), + request=Request(FunctionRequest( + 'hello', + BASIC_FUNCTION_DIRECTORY)), properties={'host': '123', 'protos': protos}) _ = await worker_init_request(worker_request) - result = await functions_metadata_request(worker_request) - return result + metadata_result = await functions_metadata_request(None) + self.assertEqual(metadata_result.use_default_metadata_indexing, False) + self.assertIsNotNone(metadata_result.function_metadata_results) + self.assertEqual(metadata_result.result.status, 1) def test_functions_metadata_request_with_exception(self): pass @@ -130,7 +161,8 @@ def test_invocation_request_with_exception(self): async def test_function_environment_reload_request(self): worker_request = WorkerRequest(name='function_environment_reload_request', - request=Request(FunctionRequest('hello')), + request=Request(FunctionRequest( + BASIC_FUNCTION_DIRECTORY)), properties={'host': '123', 'protos': protos}) result = await function_environment_reload_request(worker_request) @@ -141,14 +173,61 @@ async def test_function_environment_reload_request(self): self.assertIsNotNone(result.worker_metadata.worker_bitness) self.assertEqual(result.result.status, 1) - def test_function_environment_reload_request_with_streaming(self): - pass - - def test_function_environment_reload_request_with_exception(self): - pass + @patch("azure_functions_worker_v2.handle_event.HttpV2Registry.http_v2_enabled", + return_value=True) + @patch("azure_functions_worker_v2.handle_event.initialize_http_server", + return_value="http://mock_address") + async def test_function_environment_reload_request_with_streaming( + self, + mock_http_v2_enabled, + mock_initialize_http_server): + worker_request = WorkerRequest(name='function_environment_reload_request', + request=Request(FunctionRequest( + 'hello', + STREAMING_FUNCTION_DIRECTORY)), + properties={'host': '123', + 'protos': protos}) + result = await function_environment_reload_request(worker_request) + self.assertEqual(result.capabilities, {'HttpUri': 'http://mock_address', + 'RequiresRouteParameters': 'true'}) + self.assertEqual(result.worker_metadata.runtime_name, "python") + self.assertIsNotNone(result.worker_metadata.runtime_version) + self.assertIsNotNone(result.worker_metadata.worker_version) + self.assertIsNotNone(result.worker_metadata.worker_bitness) + self.assertEqual(result.result.status, 1) - def test_load_function_metadata(self): - pass + @patch("azure_functions_worker_v2.handle_event" + ".otel_manager.get_azure_monitor_available", + return_value=True) + async def test_function_environment_reload_request_with_otel(self, + mock_otel_enabled): + worker_request = WorkerRequest(name='function_environment_reload_request', + request=Request(FunctionRequest( + 'hello', + BASIC_FUNCTION_DIRECTORY)), + properties={'host': '123', + 'protos': protos}) + result = await function_environment_reload_request(worker_request) + self.assertEqual(result.capabilities, {'WorkerOpenTelemetryEnabled': 'true'}) + self.assertEqual(result.worker_metadata.runtime_name, "python") + self.assertIsNotNone(result.worker_metadata.runtime_version) + self.assertIsNotNone(result.worker_metadata.worker_version) + self.assertIsNotNone(result.worker_metadata.worker_bitness) + self.assertEqual(result.result.status, 1) - def test_index_functions(self): - pass + async def test_function_environment_reload_request_with_exception(self): + # Even if an exception happens during indexing, + # we still return success + worker_request = WorkerRequest(name='function_environment_reload_request', + request=Request(FunctionRequest( + 'hello', + INDEXING_EXCEPTION_FUNCTION_DIRECTORY)), + properties={'host': '123', + 'protos': protos}) + result = await function_environment_reload_request(worker_request) + self.assertEqual(result.capabilities, {}) + self.assertEqual(result.worker_metadata.runtime_name, "python") + self.assertIsNotNone(result.worker_metadata.runtime_version) + self.assertIsNotNone(result.worker_metadata.worker_version) + self.assertIsNotNone(result.worker_metadata.worker_bitness) + self.assertEqual(result.result.status, 1) diff --git a/tests/utils/constants.py b/tests/utils/constants.py index 5b55e1ea0..e339bedaf 100644 --- a/tests/utils/constants.py +++ b/tests/utils/constants.py @@ -4,3 +4,4 @@ PROJECT_ROOT = pathlib.Path(__file__).parent.parent.parent TESTS_ROOT = PROJECT_ROOT / 'tests' +UNIT_TESTS_FOLDER = TESTS_ROOT / pathlib.Path('unittests') From 31e81a9275ab12f4b4cd1ac19978f17112833d7e Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Tue, 15 Apr 2025 15:46:44 -0500 Subject: [PATCH 30/45] lint fixes --- .artifactignore | 1 - README.md | 33 +++++++++---------- azure_functions_worker_v2/bindings/context.py | 6 ++-- .../bindings/datumdef.py | 3 -- .../bindings/retrycontext.py | 3 ++ azure_functions_worker_v2/utils/tracing.py | 2 -- eng/ci/official-build.yml | 4 +-- eng/ci/public-build.yml | 7 ++-- 8 files changed, 27 insertions(+), 32 deletions(-) delete mode 100644 .artifactignore diff --git a/.artifactignore b/.artifactignore deleted file mode 100644 index 850e600d6..000000000 --- a/.artifactignore +++ /dev/null @@ -1 +0,0 @@ -_manifest\** \ No newline at end of file diff --git a/README.md b/README.md index a889aef3d..08baeb6d1 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,34 @@ # Functions Header Image - Lightning Logo Azure Functions Python Worker -| Branch | Status | CodeCov | Unittests | E2E tests | -|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------| -| main | [![Build Status](https://azfunc.visualstudio.com/Azure%20Functions/_apis/build/status/Azure.azure-functions-python-worker?branchName=main)](https://azfunc.visualstudio.com/Azure%20Functions/_build/latest?definitionId=57&branchName=main) | [![codecov](https://codecov.io/gh/Azure/azure-functions-python-worker/branch/main/graph/badge.svg)](https://codecov.io/gh/Azure/azure-functions-python-worker) | ![CI Unit tests](https://github.com/Azure/azure-functions-python-worker/workflows/CI%20Unit%20tests/badge.svg?branch=main) | ![CI E2E tests](https://github.com/Azure/azure-functions-python-worker/workflows/CI%20E2E%20tests/badge.svg?branch=main) | -| dev | [![Build Status](https://azfunc.visualstudio.com/Azure%20Functions/_apis/build/status/Azure.azure-functions-python-worker?branchName=dev)](https://azfunc.visualstudio.com/Azure%20Functions/_build/latest?definitionId=57&branchName=dev) | [![codecov](https://codecov.io/gh/Azure/azure-functions-python-worker/branch/dev/graph/badge.svg)](https://codecov.io/gh/Azure/azure-functions-python-worker) | ![CI Unit tests](https://github.com/Azure/azure-functions-python-worker/workflows/CI%20Unit%20tests/badge.svg?branch=dev) | ![CI E2E tests](https://github.com/Azure/azure-functions-python-worker/workflows/CI%20E2E%20tests/badge.svg?branch=dev) | +| Branch | Build Status | CodeCov | Test Status | +|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| dev | [![Build Status](https://img.shields.io/azure-devops/build/azfunc/public/658/dev)](https://azfunc.visualstudio.com/public/_build/latest?definitionId=658&branchName=dev) | [![codecov](https://codecov.io/gh/Azure/azure-functions-python-worker/branch/dev/graph/badge.svg)](https://codecov.io/gh/Azure/azure-functions-python-worker) | [![Test Status](https://img.shields.io/azure-devops/build/azfunc/public/658/dev)](https://azfunc.visualstudio.com/public/_build/latest?definitionId=658&branchName=dev) | -Python support for Azure Functions is based on Python 3.6, 3.7, 3.8, 3.9, and 3.10 serverless hosting on Linux and the Functions 2.0, 3.0 and 4.0 runtime. +Python support for Azure Functions is based on Python 3.8, 3.9, 3.10, 3.11, and 3.12 serverless hosting on Linux and the Functions 4.0 runtime. Here is the current status of Python in Azure Functions: What are the supported Python versions? -| Azure Functions Runtime | Python 3.6 | Python 3.7 | Python 3.8 | Python 3.9 | Python 3.10 | Python 3.11 | -|----------------------------------|------------|------------|------------|------------|-------------|-------------| -| Azure Functions 2.0 (deprecated) | ✔ | ✔ | - | - | - | - | -| Azure Functions 3.0 (deprecated) | ✔ | ✔ | ✔ | ✔ | - | - | -| Azure Functions 4.0 | - | - | ✔ | ✔ | ✔ | ✔ | +| Azure Functions Runtime | Python 3.8 | Python 3.9 | Python 3.10 | Python 3.11 | Python 3.12 | +|----------------------------------|------------|------------|-------------|-------------|-------------| +| Azure Functions 3.0 (deprecated) | ✔ | ✔ | - | - | - | +| Azure Functions 4.0 | ✔ | ✔ | ✔ | ✔ | ✔ | For information about Azure Functions Runtime, please refer to [Azure Functions runtime versions overview](https://docs.microsoft.com/en-us/azure/azure-functions/functions-versions) page. ### What's available? -- Build, test, debug and publish using Azure Functions Core Tools (CLI) or Visual Studio Code -- Deploy Python Function project onto consumption, dedicated, or elastic premium plan. -- Deploy Python Function project in a custom docker image onto dedicated, or elastic premium plan. -- Triggers / Bindings : HTTP, Blob, Queue, Timer, Cosmos DB, Event Grid, Event Hubs and Service Bus +- Build, test, debug, and publish using Azure Functions Core Tools (CLI) or Visual Studio Code +- Deploy Python Function project onto consumption, dedicated, elastic premium, or flex consumption plan. +- Deploy Python Function project in a custom docker image onto dedicated or elastic premium plan. +- Triggers / Bindings : Blob, Cosmos DB, Event Grid, Event Hub, HTTP, Kafka, MySQL, Queue, ServiceBus, SQL, Timer, and Warmup - Triggers / Bindings : Custom binding support -What's coming? +### What's new? -- [Durable Functions For Python](https://github.com/Azure/azure-functions-durable-python) +- [SDK Type Bindings for Blob](https://techcommunity.microsoft.com/t5/azure-compute-blog/azure-functions-sdk-type-bindings-for-azure-blob-storage-with/ba-p/4146744) +- [HTTP Streaming](https://techcommunity.microsoft.com/t5/azure-compute-blog/azure-functions-support-for-http-streams-in-python-is-now-in/ba-p/4146697) ### Get Started @@ -72,4 +71,4 @@ provided by the bot. You will only need to do this once across all repos using o This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. \ No newline at end of file diff --git a/azure_functions_worker_v2/bindings/context.py b/azure_functions_worker_v2/bindings/context.py index 18149abbc..828b49a46 100644 --- a/azure_functions_worker_v2/bindings/context.py +++ b/azure_functions_worker_v2/bindings/context.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - import threading +from typing import Type from .retrycontext import RetryContext from .tracecontext import TraceContext @@ -12,7 +12,7 @@ def __init__(self, func_name: str, func_dir: str, invocation_id: str, - thread_local_storage: threading.local, + thread_local_storage: Type[threading.local], trace_context: TraceContext, retry_context: RetryContext) -> None: self.__func_name = func_name @@ -27,7 +27,7 @@ def invocation_id(self) -> str: return self.__invocation_id @property - def thread_local_storage(self) -> threading.local: + def thread_local_storage(self) -> Type[threading.local]: return self.__thread_local_storage @property diff --git a/azure_functions_worker_v2/bindings/datumdef.py b/azure_functions_worker_v2/bindings/datumdef.py index 043d6a02b..d4dbc31cf 100644 --- a/azure_functions_worker_v2/bindings/datumdef.py +++ b/azure_functions_worker_v2/bindings/datumdef.py @@ -1,6 +1,5 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - import json import logging @@ -68,8 +67,6 @@ def from_typed_data(cls, protos): try: td = protos.TypedData except Exception: - # Todo: better catch for Datum.from_typed_data(http.body) - # if the data being sent in is already protos.TypedData td = protos tt = td.WhichOneof('data') if tt == 'http': diff --git a/azure_functions_worker_v2/bindings/retrycontext.py b/azure_functions_worker_v2/bindings/retrycontext.py index d68b21ddf..c0264e0c9 100644 --- a/azure_functions_worker_v2/bindings/retrycontext.py +++ b/azure_functions_worker_v2/bindings/retrycontext.py @@ -29,6 +29,7 @@ def message(self) -> str: class RetryPolicy(Enum): """Retry policy for the function invocation""" + MAX_RETRY_COUNT = "max_retry_count" STRATEGY = "strategy" DELAY_INTERVAL = "delay_interval" @@ -40,6 +41,8 @@ class RetryPolicy(Enum): class RetryContext: """Gets the current retry count from retry-context""" retry_count: int + """Gets the max retry count from retry-context""" max_retry_count: int + rpc_exception: RpcException diff --git a/azure_functions_worker_v2/utils/tracing.py b/azure_functions_worker_v2/utils/tracing.py index 01f12ef3e..69ed22a4e 100644 --- a/azure_functions_worker_v2/utils/tracing.py +++ b/azure_functions_worker_v2/utils/tracing.py @@ -1,8 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - import traceback - from traceback import StackSummary, extract_tb from typing import List diff --git a/eng/ci/official-build.yml b/eng/ci/official-build.yml index 2a5bd84ad..64410fe04 100644 --- a/eng/ci/official-build.yml +++ b/eng/ci/official-build.yml @@ -2,7 +2,7 @@ trigger: batch: true branches: include: - - dev-3* + - 1.x - library-release/* # CI only, does not trigger on PRs. @@ -13,7 +13,7 @@ schedules: displayName: At 12:00 AM, only on Monday branches: include: - - dev-3* + - 1.x always: true resources: diff --git a/eng/ci/public-build.yml b/eng/ci/public-build.yml index 49fbd5748..23a8fb08a 100644 --- a/eng/ci/public-build.yml +++ b/eng/ci/public-build.yml @@ -2,21 +2,20 @@ trigger: batch: true branches: include: - - dev-3* + - 1.x - sdk/* - extensions/* pr: branches: include: - - dev-3* - + - 1.x schedules: - cron: '0 0 * * MON' displayName: At 12:00 AM, only on Monday branches: include: - - dev-3* + - 1.x always: true resources: From ba328c51947b5576cdb01c2f59e5f03923bd914d Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Fri, 18 Apr 2025 10:59:04 -0500 Subject: [PATCH 31/45] fix build pipeline --- ...release.yml => library-worker-release.yml} | 14 +- eng/templates/jobs/build.yml | 36 +- .../official/jobs/aggregate-artifacts.yml | 37 -- .../official/jobs/build-artifacts.yml | 57 +-- .../official/jobs/publish-release.yml | 346 +++++------------- 5 files changed, 110 insertions(+), 380 deletions(-) rename eng/ci/{worker-release.yml => library-worker-release.yml} (63%) delete mode 100644 eng/templates/official/jobs/aggregate-artifacts.yml diff --git a/eng/ci/worker-release.yml b/eng/ci/library-worker-release.yml similarity index 63% rename from eng/ci/worker-release.yml rename to eng/ci/library-worker-release.yml index 987569c61..ccdcc0ebe 100644 --- a/eng/ci/worker-release.yml +++ b/eng/ci/library-worker-release.yml @@ -6,14 +6,6 @@ resources: type: git name: 1ESPipelineTemplates/1ESPipelineTemplates ref: refs/tags/release - - repository: eng - type: git - name: engineering - ref: refs/tags/release - -variables: - - name: codeql.excludePathPatterns - value: deps/,build/ extends: template: v1/1ES.Official.PipelineTemplate.yml@1es @@ -22,12 +14,8 @@ extends: name: 1es-pool-azfunc image: 1es-windows-2022 os: windows - sdl: - codeSignValidation: - enabled: true - break: true stages: - stage: Release jobs: - - template: /eng/templates/official/jobs/publish-release.yml@self + - template: /eng/templates/official/jobs/publish-release.yml@self \ No newline at end of file diff --git a/eng/templates/jobs/build.yml b/eng/templates/jobs/build.yml index b47137c64..57a5cd6c8 100644 --- a/eng/templates/jobs/build.yml +++ b/eng/templates/jobs/build.yml @@ -1,43 +1,25 @@ jobs: - job: "Build" - displayName: 'Build python worker' + displayName: 'Build Python Runtime SDK' - pool: - name: 1es-pool-azfunc-public - image: 1es-ubuntu-22.04 - os: linux - - variables: - # Default Variable - pythonVersion: '3.13' + strategy: + matrix: + Python313: + PYTHON_VERSION: '3.13' steps: - - script: | - echo "Branch name: $(Build.SourceBranchName)" - # Extract the last two digits (minor version) from the branch name - version=$(echo $(Build.SourceBranchName) | sed 's/dev-\([0-9]*\)/\1/') - minor_version=${version: -2} # Get last two digits - - # Check if minor_version is a number; if not, set default to 13 - if ! [[ "$minor_version" =~ ^[0-9]+$ ]]; then - minor_version=13 - fi - - echo "Extracted minor version: $minor_version" - echo "##vso[task.setvariable variable=pythonVersion]$minor_version" - displayName: 'Extract Python version from branch name' - task: UsePythonVersion@0 inputs: - versionSpec: '3.$(pythonVersion)' + versionSpec: $(PYTHON_VERSION) - bash: | python --version displayName: 'Check python version' - bash: | - python -m pip install --upgrade pip + python -m pip install -U pip python -m pip install build python -m build - displayName: 'Build python worker' + displayName: 'Build Python RuntimeSDK for $(PYTHON_VERSION)' - bash: | pip install pip-audit pip-audit . - displayName: 'Run vulnerability scan' + displayName: 'Run vulnerability scan' \ No newline at end of file diff --git a/eng/templates/official/jobs/aggregate-artifacts.yml b/eng/templates/official/jobs/aggregate-artifacts.yml deleted file mode 100644 index fdacc8378..000000000 --- a/eng/templates/official/jobs/aggregate-artifacts.yml +++ /dev/null @@ -1,37 +0,0 @@ -jobs: - - job: "Aggregate" - displayName: 'Aggregate Python Library Workers' - - pool: - name: 1es-pool-azfunc - image: 1es-ubuntu-22.04 - os: linux - - templateContext: - outputParentDirectory: $(Build.ArtifactStagingDirectory) - outputs: - - output: pipelineArtifact - targetPath: $(Build.SourcesDirectory)/final_package - artifactName: "dist" - - steps: - # Add download task for each artifact - - task: DownloadPipelineArtifact@2 - displayName: 'Download Python Worker Artifact' - inputs: - buildType: specific - project: 'internal' - definition: 652 - buildVersionToDownload: specific - pipelineId: $(BuildId1) - artifactName: 'azure-functions-runtime-py313' - targetPath: "azure-functions-runtime-py313" - # Compiles the python packages - - script: | - mkdir -p $(Build.SourcesDirectory)/final_package - cp $(Build.SourcesDirectory)/azure-functions-runtime-py3*/*.whl $(Build.SourcesDirectory)/final_package/ - displayName: 'Merge and Compile Python Packages' - - script: | - echo "Contents of final_package folder:" - ls -al $(Build.SourcesDirectory)/final_package - displayName: 'Verify folder contents post compilation' \ No newline at end of file diff --git a/eng/templates/official/jobs/build-artifacts.yml b/eng/templates/official/jobs/build-artifacts.yml index 85d63f1f9..1329a09ab 100644 --- a/eng/templates/official/jobs/build-artifacts.yml +++ b/eng/templates/official/jobs/build-artifacts.yml @@ -1,41 +1,23 @@ jobs: - job: "Build" - displayName: 'Build Python Library Worker' + displayName: 'Build Python Runtime SDK' pool: name: 1es-pool-azfunc image: 1es-ubuntu-22.04 os: linux - variables: - # Default version - pythonVersion: '3.13' - templateContext: outputParentDirectory: $(Build.ArtifactStagingDirectory) outputs: - output: pipelineArtifact - targetPath: $(Build.SourcesDirectory)/dist - artifactName: "azure-functions-runtime-py3$(pythonVersion)" + targetPath: $(Build.SourcesDirectory) + artifactName: "azure-functions-runtime" steps: - - script: | - echo "Branch name: $(Build.SourceBranchName)" - # Extract the last two digits (minor version) from the branch name - version=$(echo $(Build.SourceBranchName) | sed 's/dev-\([0-9]*\)/\1/') - minor_version=${version: -2} # Get last two digits - - # Check if minor_version is a number; if not, set default to 13 - if ! [[ "$minor_version" =~ ^[0-9]+$ ]]; then - minor_version=13 - fi - - echo "Extracted minor version: $minor_version" - echo "##vso[task.setvariable variable=pythonVersion]$minor_version" - displayName: 'Extract Python version from branch name' - task: UsePythonVersion@0 inputs: - versionSpec: '3.$(pythonVersion)' + versionSpec: "3.13" - bash: | python --version displayName: 'Check python version' @@ -43,35 +25,8 @@ jobs: python -m pip install -U pip python -m pip install build python -m build - displayName: 'Build Python Library Worker' - - script: | - echo "Contents of dist folder:" - ls -al $(Build.SourcesDirectory)/dist - displayName: 'Verify dist folder contents' - - script: | - echo "Branch name: $(Build.SourceBranchName)" - # Extract the part after the slash (release/-) - branch_name=$(echo $(Build.SourceBranchName) | sed 's/refs\/heads\///') - - # Extract the package version (everything before the first dash) - package_version=$(echo $branch_name | sed 's|^release/\([^/]*\)-.*|\1|') - - if [[ ! "$package_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([a-zA-Z]+[0-9]*)?$ ]]; then - echo "Invalid package version detected. Setting to default: 1.0.0" - package_version="1.0.0" - fi - - # Print the extracted package version - echo "Extracted package version: $package_version" - - # Set the package version as a pipeline variable - echo "##vso[task.setvariable variable=packageVersion]$package_version" - displayName: 'Extract Package Version from Branch Name' - - script: | - wheel_file=$(ls $(Build.SourcesDirectory)/dist/*.whl) - new_wheel_name="azure-functions-runtime-$(packageVersion)-py3$(pythonVersion)-none-any.whl" - mv "$wheel_file" "$(Build.SourcesDirectory)/dist/$new_wheel_name" + displayName: 'Build Python Runtime SDK' - bash: | pip install pip-audit pip-audit . - displayName: 'Run vulnerability scan' + displayName: 'Run vulnerability scan' \ No newline at end of file diff --git a/eng/templates/official/jobs/publish-release.yml b/eng/templates/official/jobs/publish-release.yml index 4c04d5779..914d1a5ce 100644 --- a/eng/templates/official/jobs/publish-release.yml +++ b/eng/templates/official/jobs/publish-release.yml @@ -9,55 +9,56 @@ jobs: steps: - powershell: | $githubToken = "$(GithubPat)" - $newWorkerVersion = "$(NewWorkerVersion)" - $versionFile = "azure_functions_worker/version.py" + $newLibraryVersion = "$(NewLibraryVersion)" - if($newWorkerVersion -match '(\d)+.(\d)+.(\d)+') { + if($newLibraryVersion -match '(\d)+.(\d)+.(\d)+') { # Create GitHub credential git config --global user.name "AzureFunctionsPython" git config --global user.email "azfunc@microsoft.com" # Heading to Artifact Repository Write-Host "Operating based on $stagingDirectory/azure-functions-python-worker" - git checkout -b "release/$newWorkerVersion" + git checkout 1.x # TODO: make this configurable + git checkout -b "runtime-release/$newLibraryVersion" - # Change azure_functions_worker/version.py version - Write-Host "Change version number in version.py to $newWorkerVersion" - ((Get-Content $versionFile) -replace "VERSION = '(\d+).(\d+).*'", "VERSION = '$newWorkerVersion'" -join "`n") + "`n" | Set-Content -NoNewline $versionFile - git add $versionFile - git commit -m "build: update Python Worker Version to $newWorkerVersion" + # Change __init__.py version + Write-Host "Change version number in azure_functions_worker_v2/version.py to $newLibraryVersion" + ((Get-Content azure_functions_worker_v2/version.py) -replace "VERSION = '(\d)+.(\d)+.*'", "VERSION = '$newLibraryVersion'" -join "`n") + "`n" | Set-Content -NoNewline azure_functions_worker_v2/version.py + git add azure_functions_worker_v2/version.py + git commit -m "build: update Python Runtime Version to $newLibraryVersion" - # Create release branch release/X.Y.Z - Write-Host "Creating release branch release/$newWorkerVersion" + # Create release branch runtime-release/X.Y.Z + Write-Host "Creating release branch runtime-release/$newLibraryVersion" git push --repo="https://$githubToken@github.com/Azure/azure-functions-python-worker.git" } else { - Write-Host "NewWorkerVersion $newWorkerVersion is malformed (example: 1.1.8)" + Write-Host "NewLibraryVersion $newLibraryVersion is malformed (example: 1.5.0)" exit -1 } - displayName: 'Push release/x.y.z' - + displayName: 'Push runtime-release/x.y.z' + - job: "CheckReleaseBranch" dependsOn: ['CreateReleaseBranch'] displayName: '(Manual) Check Release Branch' pool: server steps: - task: ManualValidation@1 - displayName: '(Optional) Modify release/x.y.z branch' + displayName: '(Optional) Modify runtime-release/x.y.z branch' inputs: notifyUsers: '' # No email notifications sent instructions: | - 1. Check if the https://github.com/Azure/azure-functions-python-worker/tree/release/$(NewWorkerVersion) passes all unit tests. - 2. If not, modify the release/$(NewWorkerVersion) branch. - 3. Ensure release/$(NewWorkerVersion) branch contains all necessary changes since it will be propagated to v4 workers. + 1. Check if the https://github.com/Azure/azure-functions-python-worker/tree/runtime-release/$(NewLibraryVersion) build succeeds and passes all unit tests. + 2. If not, modify the runtime-release/$(NewLibraryVersion) branch. + 3. Ensure runtime-release/$(NewLibraryVersion) branch contains all necessary changes. - job: "CreateReleaseTag" dependsOn: ['CheckReleaseBranch'] + displayName: 'Create Release Tag' steps: - powershell: | $githubToken = "$(GithubPat)" - $newWorkerVersion = "$(NewWorkerVersion)" + $newLibraryVersion = "$(NewLibraryVersion)" - if($newWorkerVersion -match '(\d)+.(\d)+.(\d)+') { + if($newLibraryVersion -match '(\d)+.(\d)+.(\d)+') { # Create GitHub credential git config --global user.name "AzureFunctionsPython" git config --global user.email "azfunc@microsoft.com" @@ -66,31 +67,31 @@ jobs: git clone https://$githubToken@github.com/Azure/azure-functions-python-worker Write-Host "Cloned azure-functions-python-worker into local" Set-Location "azure-functions-python-worker" - git checkout "origin/release/$newWorkerVersion" + git checkout "origin/runtime-release/$newLibraryVersion" - # Create release tag X.Y.Z - Write-Host "Creating release tag $newWorkerVersion" - git tag -a "$newWorkerVersion" -m "$newWorkerVersion" + # Create runtime-release tag X.Y.Z + Write-Host "Creating runtime-release tag $newLibraryVersion" + git tag -a "$newLibraryVersion" -m "$newLibraryVersion" # Push tag to remote - git push origin $newWorkerVersion + git push origin $newLibraryVersion } else { - Write-Host "NewWorkerVersion $newWorkerVersion is malformed (example: 1.1.8)" + Write-Host "NewLibraryVersion $newLibraryVersion is malformed (example: 1.5.0)" exit -1 } - displayName: 'Create and push release tag x.y.z' + displayName: 'Tag and push x.y.z' - powershell: | $githubUser = "$(GithubUser)" $githubToken = "$(GithubPat)" - $newWorkerVersion = "$(NewWorkerVersion)" + $newLibraryVersion = "$(NewLibraryVersion)" - if($newWorkerVersion -match '(\d)+.(\d)+.(\d)+') { + if($newLibraryVersion -match '(\d)+.(\d)+.(\d)+') { # Create GitHub credential $credential = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes("${githubUser}:${githubToken}")) # Create Release Note Write-Host "Creating release note in GitHub" - $body = (@{tag_name="$newWorkerVersion";name="Release $newWorkerVersion";body="- Fill in Release Note Here";draft=$true} | ConvertTo-Json -Compress) + $body = (@{tag_name="$newLibraryVersion";name="Release $newLibraryVersion";body="- Fill in Release Note Here";draft=$true} | ConvertTo-Json -Compress) $response = Invoke-WebRequest -Headers @{"Cache-Control"="no-cache";"Content-Type"="application/json";"Authorization"="Basic $credential"} -Method Post -Body "$body" -Uri "https://api.github.com/repos/Azure/azure-functions-python-worker/releases" # Return Value @@ -102,11 +103,11 @@ jobs: $draftUrl = $response | ConvertFrom-Json | Select -expand url Write-Host "Release draft created in $draftUrl" } else { - Write-Host "NewWorkerVersion $newWorkerVersion is malformed (example: 1.1.8)" + Write-Host "NewLibraryVersion $newLibraryVersion is malformed (example: 1.1.8)" exit -1 } displayName: 'Create GitHub release draft' - + - job: "CheckGitHubRelease" dependsOn: ['CreateReleaseTag'] displayName: '(Manual) Check GitHub release note' @@ -118,67 +119,17 @@ jobs: notifyUsers: '' instructions: 'Please head to https://github.com/Azure/azure-functions-python-worker/releases to finish the release note' -- job: "WaitForPythonWorkerBuild" +- job: "TestWithWorker" dependsOn: ['CheckGitHubRelease'] - displayName: '(Manual) Wait For Python Worker Build' - pool: server - steps: - - task: ManualValidation@1 - displayName: 'Wait For Python Worker Build' - inputs: - notifyUsers: '' - instructions: 'Ensure the build of release/4.x.y.z finishes in https://dev.azure.com/azfunc/internal/_build?definitionId=652 and verify if PackageWorkers task is completed.' - - -- job: "PublishNuget" - dependsOn: ['WaitForPythonWorkerBuild'] - displayName: 'Publish Nuget' - templateContext: - outputs: - - output: nuget - packagesToPush: '$(Pipeline.Workspace)/PythonWorkerArtifact/**/*.nupkg;!$(Pipeline.Workspace)/PythonWorkerArtifact/**/*.symbols.nupkg' - publishVstsFeed: 'e6a70c92-4128-439f-8012-382fe78d6396/eb652719-f36a-4e78-8541-e13a3cd655f9' - allowPackageConflicts: true - packageParentPath: '$(Pipeline.Workspace)' - steps: - - task: DownloadPipelineArtifact@2 - displayName: 'Download Python Worker release/4.x.y.z Artifact' - inputs: - buildType: specific - project: '3f99e810-c336-441f-8892-84983093ad7f' - definition: 652 - specificBuildWithTriggering: true - buildVersionToDownload: latestFromBranch - branchName: 'refs/heads/release/$(NewWorkerVersion)' - allowPartiallySucceededBuilds: true - allowFailedBuilds: true - targetPath: '$(Pipeline.Workspace)/PythonWorkerArtifact' - - -- job: "CheckNugetPackageContent" - dependsOn: ['PublishNuget'] - displayName: '(Manual) Check Nuget Package Content' - pool: server - steps: - - task: ManualValidation@1 - displayName: 'Check nuget package content' - inputs: - notifyUsers: '' - instructions: | - Please check the latest release package at - https://azfunc.visualstudio.com/Azure%20Functions/_artifacts/feed/AzureFunctionsRelease/NuGet/Microsoft.Azure.Functions.PythonWorker/overview - -- job: "HostRepoPRs" - dependsOn: ['CheckNugetPackageContent'] - displayName: 'Create Host PRs' + displayName: 'Test with Worker' steps: - powershell: | $githubUser = "$(GithubUser)" $githubToken = "$(GithubPat)" - $newWorkerVersion = "$(NewWorkerVersion)" - $newBranch = "python/$newWorkerVersion" + $newLibraryVersion = "$(NewLibraryVersion)" + $newBranch = "sdk/$newLibraryVersion" - if($newWorkerVersion -match '(\d)+.(\d)+.(\d)+') { + if($newLibraryVersion -match '(\d)+.(\d)+.(\d)+') { # Create GitHub credential git config --global user.name "AzureFunctionsPython" git config --global user.email "azfunc@microsoft.com" @@ -187,200 +138,91 @@ jobs: $credential = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes("${githubUser}:${githubToken}")) # Clone Repository - git clone https://$githubToken@github.com/Azure/azure-functions-host - Write-Host "Cloned azure-functions-host into local and checkout $newBranch branch" - Set-Location "azure-functions-host" + git clone https://$githubToken@github.com/Azure/azure-functions-python-worker + Write-Host "Cloned azure-functions-python-worker into local and checkout $newBranch branch" + Set-Location "azure-functions-python-worker" git checkout -b $newBranch "origin/dev" - # Modify Python Worker Version in eng\build\python.props - Write-Host "Replacing eng\build\python.props" - ((Get-Content eng\build\Workers.Python.props) -replace "PythonWorker`" Version=`"(\d)+.(\d)+.(\d)+.?(\d)*`"","PythonWorker`" Version=`"$newWorkerVersion`"" -join "`n") +"`n" | Set-Content -NoNewline eng\build\Workers.Python.props - - # Modify Python Worker Version in test\WebJobs.Script.Tests\WebJobs.Script.Tests.csproj - Write-Host "Replacing test\WebJobs.Script.Tests\WebJobs.Script.Tests.csproj" - ((Get-Content test\WebJobs.Script.Tests\WebJobs.Script.Tests.csproj) -replace "PythonWorker`" Version=`"(\d)+.(\d)+.(\d)+.?(\d)*`"","PythonWorker`" Version=`"$newWorkerVersion`"" -join "`n") + "`n" | Set-Content -NoNewline test\WebJobs.Script.Tests\WebJobs.Script.Tests.csproj - - # Modify release_notes.md - Write-Host "Adding a new entry in release_note.md" - ((Get-Content release_notes.md) -replace "-->","$&`n- Update Python Worker Version to [$newWorkerVersion](https://github.com/Azure/azure-functions-python-worker/releases/tag/$newWorkerVersion)" -join "`n") + "`n" | Set-Content -NoNewline release_notes.md + # Modify Runtime Version in pyproject.toml + Write-Host "Replacing Runtime version in worker's pyproject.toml" + ((Get-Content pyproject.toml) -replace '"azure-functions-runtime==(\d)+.(\d)+.*"','"azure-functions-runtime==$(NewLibraryVersion)"' -join "`n") + "`n" | Set-Content -NoNewline pyproject.toml # Commit Python Version - Write-Host "Pushing $newBranch to host repo" - git add eng\build\Workers.Python.props - git add test\WebJobs.Script.Tests\WebJobs.Script.Tests.csproj - git add release_notes.md - git commit -m "Update Python Worker Version to $newWorkerVersion" + Write-Host "Pushing $newBranch to azure-functions-python-worker repo" + git add pyproject.toml + git commit -m "Update Python Runtime Version to $newLibraryVersion" git push origin $newBranch # Create PR Write-Host "Creating PR draft in GitHub" - $prTemplateContent = @" - ### Issue describing the changes in this PR - - Update Python Worker Version to $newWorkerVersion - - Python Worker Release note [$newWorkerVersion](https://github.com/Azure/azure-functions-python-worker/releases/tag/$newWorkerVersion) - - ### Pull request checklist - - **IMPORTANT**: Currently, changes must be backported to the `in-proc` branch to be included in Core Tools and non-Flex deployments. - - * [ ] Backporting to the `in-proc` branch is not required - * [x]Otherwise: Link to backporting PR - * [x] My changes **do not** require documentation changes - * [ ] Otherwise: Documentation issue linked to PR - * [ ] My changes **should not** be added to the release notes for the next release - * [x] Otherwise: I've added my notes to `release_notes.md` - * [x] My changes **do not** need to be backported to a previous version - * [ ] Otherwise: Backport tracked by issue/PR #issue_or_pr - * [x] My changes **do not** require diagnostic events changes - * Otherwise: I have added/updated all related diagnostic events and their documentation (Documentation issue linked to PR) - * [x] I have added all required tests (Unit tests, E2E tests) - - "@ - - $body = (@{head="$newBranch";base="dev";body=$prTemplateContent;draft=$true;maintainer_can_modify=$true;title="Update Python Worker Version to $newWorkerVersion"} | ConvertTo-Json -Compress)$response = Invoke-WebRequest -Headers @{"Cache-Control"="no-cache";"Content-Type"="application/json";"Authorization"="Basic $credential";"Accept"="application/vnd.github.v3+json"} -Method Post -Body "$body" -Uri "https://api.github.com/repos/Azure/azure-functions-host/pulls" + $body = (@{head="$newBranch";base="dev";body="Python Runtime Version [$newLibraryVersion](https://github.com/Azure/azure-functions-python-worker/releases/tag/$newLibraryVersion)";draft=$true;maintainer_can_modify=$true;title="build: update Python Runtime Version to $newLibraryVersion"} | ConvertTo-Json -Compress) + $response = Invoke-WebRequest -Headers @{"Cache-Control"="no-cache";"Content-Type"="application/json";"Authorization"="Basic $credential";"Accept"="application/vnd.github.v3+json"} -Method Post -Body "$body" -Uri "https://api.github.com/repos/Azure/azure-functions-python-worker/pulls" # Return Value if ($response.StatusCode -ne 201) { - Write-Host "Failed to create a PR in Azure Functions Host" + Write-Host "Failed to create PR in Azure Functions Python Worker" exit -1 } $draftUrl = $response | ConvertFrom-Json | Select -expand url Write-Host "PR draft created in $draftUrl" } else { - Write-Host "NewWorkerVersion $newWorkerVersion is malformed (example: 1.1.8)" + Write-Host "NewLibraryVersion $newLibraryVersion is malformed (example: 1.1.8)" exit -1 } - displayName: 'Create Host PR for dev' - - powershell: | - $githubUser = "$(GithubUser)" - $githubToken = "$(GithubPat)" - $newWorkerVersion = "$(NewWorkerVersion)" - $newBranch = "python/$newWorkerVersion" - - if($newWorkerVersion -match '(\d)+.(\d)+.(\d)+') { - # Create GitHub credential - git config --global user.name "AzureFunctionsPython" - git config --global user.email "azfunc@microsoft.com" - - # Create GitHub credential - $credential = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes("${githubUser}:${githubToken}")) - - # Clone Repository - git clone https://$githubToken@github.com/Azure/azure-functions-host - Write-Host "Cloned azure-functions-host into local and checkout $newBranch branch" - Set-Location "azure-functions-host" - git checkout -b backport/$newBranch "origin/in-proc" - - # Modify Python Worker Version in eng\build\python.props - Write-Host "Replacing eng\build\python.props" - ((Get-Content eng\build\Workers.Python.props) -replace "PythonWorker`" Version=`"(\d)+.(\d)+.(\d)+.?(\d)*`"","PythonWorker`" Version=`"$newWorkerVersion`"" -join "`n") +"`n" | Set-Content -NoNewline eng\build\Workers.Python.props - - # Modify Python Worker Version in test\WebJobs.Script.Tests\WebJobs.Script.Tests.csproj - Write-Host "Replacing test\WebJobs.Script.Tests\WebJobs.Script.Tests.csproj" - ((Get-Content test\WebJobs.Script.Tests\WebJobs.Script.Tests.csproj) -replace "PythonWorker`" Version=`"(\d)+.(\d)+.(\d)+.?(\d)*`"","PythonWorker`" Version=`"$newWorkerVersion`"" -join "`n") + "`n" | Set-Content -NoNewline test\WebJobs.Script.Tests\WebJobs.Script.Tests.csproj - - # Modify release_notes.md - Write-Host "Adding a new entry in release_note.md" - ((Get-Content release_notes.md) -replace "-->","$&`n- Update Python Worker Version to [$newWorkerVersion](https://github.com/Azure/azure-functions-python-worker/releases/tag/$newWorkerVersion)" -join "`n") + "`n" | Set-Content -NoNewline release_notes.md - - # Commit Python Version - Write-Host "Pushing $newBranch to host repo" - git add eng\build\Workers.Python.props - git add test\WebJobs.Script.Tests\WebJobs.Script.Tests.csproj - git add release_notes.md - git commit -m "[Backport] Update Python Worker Version to $newWorkerVersion" - git push origin $newBranch - - # Create PR - Write-Host "Creating PR draft in GitHub" - $prTemplateContent = @" - ### Issue describing the changes in this PR - - [Backport] Update Python Worker Version to $newWorkerVersion - - Python Worker Release note [$newWorkerVersion](https://github.com/Azure/azure-functions-python-worker/releases/tag/$newWorkerVersion) - - ### Pull request checklist - - **IMPORTANT**: Currently, changes must be backported to the `in-proc` branch to be included in Core Tools and non-Flex deployments. - - * [ ] Backporting to the `in-proc` branch is not required - * [ ]Otherwise: Link to backporting PR - * [x] My changes **do not** require documentation changes - * [ ] Otherwise: Documentation issue linked to PR - * [ ] My changes **should not** be added to the release notes for the next release - * [x] Otherwise: I've added my notes to `release_notes.md` - * [x] My changes **do not** need to be backported to a previous version - * [ ] Otherwise: Backport tracked by issue/PR #issue_or_pr - * [x] My changes **do not** require diagnostic events changes - * Otherwise: I have added/updated all related diagnostic events and their documentation (Documentation issue linked to PR) - * [x] I have added all required tests (Unit tests, E2E tests) - - "@ - - $body = (@{head="$newBranch";base="in-proc";body=$prTemplateContent;draft=$true;maintainer_can_modify=$true;title="Update Python Worker Version to $newWorkerVersion"} | ConvertTo-Json -Compress)$response = Invoke-WebRequest -Headers @{"Cache-Control"="no-cache";"Content-Type"="application/json";"Authorization"="Basic $credential";"Accept"="application/vnd.github.v3+json"} -Method Post -Body "$body" -Uri "https://api.github.com/repos/Azure/azure-functions-host/pulls" - - # Return Value - if ($response.StatusCode -ne 201) { - Write-Host "Failed to create a PR in Azure Functions Host" - exit -1 - } - - $draftUrl = $response | ConvertFrom-Json | Select -expand url - Write-Host "PR draft created in $draftUrl" - } else { - Write-Host "NewWorkerVersion $newWorkerVersion is malformed (example: 1.1.8)" - exit -1 - } - displayName: 'Create Host PR for in-proc' + displayName: 'Create PR in Worker Repo' -- job: "CheckHostPRs" - dependsOn: ['HostRepoPRs'] - displayName: '(Manual) Check Host PRs' +- job: "WaitForPythonWorkerPR" + dependsOn: ['TestWithWorker'] + displayName: '(Manual) Check Python Worker PR' pool: server steps: - task: ManualValidation@1 - displayName: 'Finish Host PRs' + displayName: 'Check Python Worker PR' inputs: notifyUsers: '' instructions: | - Go to https://github.com/Azure/azure-functions-host/pulls and finish the host v4 PR. - If the content misses something, checkout "python/x.y.z" from remote and make new commits to it. + 1. Please wait and check if all goes green in the https://github.com/Azure/azure-functions-python-worker/pulls + 2. Merge the PR into worker dev branch -- job: "MergeToMainAndDev" - dependsOn: ['CheckHostPRs'] - displayName: 'Merge release/x.y.z back to main & dev' +- job: "PyPIPackage" + dependsOn: ['WaitForPythonWorkerPR'] + displayName: 'PyPI Package' steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download Python Runtime runtime-release/x.y.z Artifact' + inputs: + buildType: specific + project: '3f99e810-c336-441f-8892-84983093ad7f' + definition: 652 + specificBuildWithTriggering: true + buildVersionToDownload: latestFromBranch + branchName: refs/heads/runtime-release + targetPath: PythonRuntimeArtifact + - task: UsePythonVersion@0 + displayName: 'Use Python 3.13' + inputs: + versionSpec: 3.13 - powershell: | - $githubToken = "$(GithubPat)" - $newWorkerVersion = "$(NewWorkerVersion)" + $newLibraryVersion = "$(NewLibraryVersion)" + $pypiToken = "$(PypiToken)" - if($newWorkerVersion -match '(\d)+.(\d)+.(\d)+') { - # Create GitHub credential - git config --global user.name "AzureFunctionsPython" - git config --global user.email "azfunc@microsoft.com" - - # Clone Repository - git clone https://$githubToken@github.com/Azure/azure-functions-python-worker - Write-Host "Cloned azure-functions-python-worker into local" - Set-Location "azure-functions-python-worker" - - # Merge back to main - Write-Host "Merging release/$newWorkerVersion back to main" - git checkout main - git merge "origin/release/$newWorkerVersion" - git push origin main - - # Merge back to dev - Write-Host "Merging release/$newWorkerVersion back to dev" - git checkout dev - git merge "origin/release/$newWorkerVersion" - git push origin dev - } else { - Write-Host "NewWorkerVersion $newWorkerVersion is malformed (example: 1.1.8)" + # Setup local Python environment + Write-Host "Setup local Python environment" + python -m pip install -U pip + pip install twine + + # Publish artifacts to PyPi + twine upload --repository-url https://upload.pypi.org/legacy/ --username "__token__" --password "$pypiToken" PythonRuntimeArtifact/azure-functions-runtime/dist/* + Start-Sleep -Seconds 3 + + # Checking if the new version is uploaded + Write-Host "Check if new version is uploaded" + $response = Invoke-WebRequest -Headers @{"Cache-Control"="no-cache"} -Method Get -Uri "https://pypi.org/project/azure-functions-runtime/$newLibraryVersion/" + + # Return Value + if ($response.StatusCode -ne 200) { + Write-Host "Failed to verify https://pypi.org/project/azure-functions-runtime/$newLibraryVersion/" exit -1 } - displayName: 'Merge release/x.y.z back to main & dev' + displayName: 'Publish package to pypi.org' From 250a7fdf819b2fce2105043ba52caf229a6b7e6b Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Mon, 21 Apr 2025 13:58:31 -0500 Subject: [PATCH 32/45] a41: minor lint fixes --- .flake8 | 2 +- azure_functions_worker_v2/handle_event.py | 4 +- azure_functions_worker_v2/version.py | 2 +- eng/ci/package-worker.yml | 37 ------------------- ...rary-worker-release.yml => v2-release.yml} | 0 .../official/jobs/build-artifacts.yml | 2 +- 6 files changed, 5 insertions(+), 42 deletions(-) delete mode 100644 eng/ci/package-worker.yml rename eng/ci/{library-worker-release.yml => v2-release.yml} (100%) diff --git a/.flake8 b/.flake8 index db9c3b0e6..37e5c4030 100644 --- a/.flake8 +++ b/.flake8 @@ -6,7 +6,7 @@ ignore = W503,E402,E731 exclude = .git, __pycache__, build, dist, .eggs, .github, .local, docs/, - Samples, azure_functions_worker_v2_v2/protos/, + Samples, azure_functions_worker_v2/protos/, azure_functions_worker_v2/utils/typing_inspect.py, tests/protos/, .env*, .vscode, venv*, *.venv* diff --git a/azure_functions_worker_v2/handle_event.py b/azure_functions_worker_v2/handle_event.py index bd74c9842..136b4fc4e 100644 --- a/azure_functions_worker_v2/handle_event.py +++ b/azure_functions_worker_v2/handle_event.py @@ -130,8 +130,8 @@ async def functions_metadata_request(request): metadata_result, metadata_exception) if metadata_exception: - logger.info("An exception in WorkerMetadataRequest has occurred: %s", - metadata_exception) + logger.error("An exception in WorkerMetadataRequest has occurred: %s", + metadata_exception) return protos.FunctionMetadataResponse( result=protos.StatusResult( status=protos.StatusResult.Failure, diff --git a/azure_functions_worker_v2/version.py b/azure_functions_worker_v2/version.py index 55e7ba88e..00e24b41b 100644 --- a/azure_functions_worker_v2/version.py +++ b/azure_functions_worker_v2/version.py @@ -1,4 +1,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -VERSION = '1.0.0a40' +VERSION = '1.0.0a41' diff --git a/eng/ci/package-worker.yml b/eng/ci/package-worker.yml deleted file mode 100644 index 27587a55e..000000000 --- a/eng/ci/package-worker.yml +++ /dev/null @@ -1,37 +0,0 @@ -trigger: - branches: - exclude: - - '*' # Don't trigger this pipeline automatically - -# CI only, does not trigger on PRs. -pr: none - -# Does not run on a schedule - -resources: - repositories: - - repository: 1es - type: git - name: 1ESPipelineTemplates/1ESPipelineTemplates - ref: refs/tags/release - - repository: eng - type: git - name: engineering - ref: refs/tags/release - -extends: - template: v1/1ES.Official.PipelineTemplate.yml@1es - parameters: - pool: - name: 1es-pool-azfunc - image: 1es-windows-2022 - os: windows - sdl: - codeSignValidation: - enabled: true - break: true - - stages: - - stage: AggregatePackages - jobs: - - template: /eng/templates/official/jobs/aggregate-artifacts.yml@self diff --git a/eng/ci/library-worker-release.yml b/eng/ci/v2-release.yml similarity index 100% rename from eng/ci/library-worker-release.yml rename to eng/ci/v2-release.yml diff --git a/eng/templates/official/jobs/build-artifacts.yml b/eng/templates/official/jobs/build-artifacts.yml index 1329a09ab..a558a92b1 100644 --- a/eng/templates/official/jobs/build-artifacts.yml +++ b/eng/templates/official/jobs/build-artifacts.yml @@ -1,6 +1,6 @@ jobs: - job: "Build" - displayName: 'Build Python Runtime SDK' + displayName: 'Build V2 Python Runtime SDK' pool: name: 1es-pool-azfunc From d9027340d5e7da112ac2e7db2851a7f5f1ae120f Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Tue, 22 Apr 2025 10:18:14 -0500 Subject: [PATCH 33/45] update branches for pipelines --- eng/ci/code-mirror.yml | 4 ++-- eng/ci/official-build.yml | 2 +- eng/ci/public-build.yml | 2 -- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/eng/ci/code-mirror.yml b/eng/ci/code-mirror.yml index c9e965de7..852c2bf70 100644 --- a/eng/ci/code-mirror.yml +++ b/eng/ci/code-mirror.yml @@ -1,8 +1,8 @@ trigger: branches: include: - - dev-3* - - library-release/* + - 1.x + - runtime-release/* resources: repositories: diff --git a/eng/ci/official-build.yml b/eng/ci/official-build.yml index 64410fe04..af258c6be 100644 --- a/eng/ci/official-build.yml +++ b/eng/ci/official-build.yml @@ -3,7 +3,7 @@ trigger: branches: include: - 1.x - - library-release/* + - runtime-release/* # CI only, does not trigger on PRs. pr: none diff --git a/eng/ci/public-build.yml b/eng/ci/public-build.yml index 23a8fb08a..d7cda87bb 100644 --- a/eng/ci/public-build.yml +++ b/eng/ci/public-build.yml @@ -3,8 +3,6 @@ trigger: branches: include: - 1.x - - sdk/* - - extensions/* pr: branches: From 904d8bbc255042e3dcfed26b79b7ce32b146ba2e Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Wed, 23 Apr 2025 09:41:52 -0500 Subject: [PATCH 34/45] official name & first version --- azure_functions_worker_v2/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/azure_functions_worker_v2/version.py b/azure_functions_worker_v2/version.py index 00e24b41b..d63c94df5 100644 --- a/azure_functions_worker_v2/version.py +++ b/azure_functions_worker_v2/version.py @@ -1,4 +1,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -VERSION = '1.0.0a41' +VERSION = '1.0.0a1' diff --git a/pyproject.toml b/pyproject.toml index 17ffef8d5..7b2af9157 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] -name = "test-worker" +name = "azure-functions-runtime" dynamic = ["version"] -requires-python = ">=3.11" +requires-python = ">=3.13" description = "Python Language Worker for Azure Functions Runtime" authors = [ { name = "Azure Functions team at Microsoft Corp.", email = "azurefunctions@microsoft.com" } From 15ce595ea24fa1d411e9bede91a67b4d89281911 Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Wed, 23 Apr 2025 10:09:39 -0500 Subject: [PATCH 35/45] a2: update readme --- README.md | 9 ++++----- azure_functions_worker_v2/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 08baeb6d1..3a9ae8441 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,15 @@ |--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | dev | [![Build Status](https://img.shields.io/azure-devops/build/azfunc/public/658/dev)](https://azfunc.visualstudio.com/public/_build/latest?definitionId=658&branchName=dev) | [![codecov](https://codecov.io/gh/Azure/azure-functions-python-worker/branch/dev/graph/badge.svg)](https://codecov.io/gh/Azure/azure-functions-python-worker) | [![Test Status](https://img.shields.io/azure-devops/build/azfunc/public/658/dev)](https://azfunc.visualstudio.com/public/_build/latest?definitionId=658&branchName=dev) | -Python support for Azure Functions is based on Python 3.8, 3.9, 3.10, 3.11, and 3.12 serverless hosting on Linux and the Functions 4.0 runtime. +Python support for Azure Functions is based on Python 3.13 serverless hosting on Linux and the Functions 4.0 runtime. Here is the current status of Python in Azure Functions: What are the supported Python versions? -| Azure Functions Runtime | Python 3.8 | Python 3.9 | Python 3.10 | Python 3.11 | Python 3.12 | -|----------------------------------|------------|------------|-------------|-------------|-------------| -| Azure Functions 3.0 (deprecated) | ✔ | ✔ | - | - | - | -| Azure Functions 4.0 | ✔ | ✔ | ✔ | ✔ | ✔ | +| Azure Functions Runtime | Python 3.13 | +|----------------------------------|-------------| +| Azure Functions 4.0 | ✔ | For information about Azure Functions Runtime, please refer to [Azure Functions runtime versions overview](https://docs.microsoft.com/en-us/azure/azure-functions/functions-versions) page. diff --git a/azure_functions_worker_v2/version.py b/azure_functions_worker_v2/version.py index d63c94df5..5358c0836 100644 --- a/azure_functions_worker_v2/version.py +++ b/azure_functions_worker_v2/version.py @@ -1,4 +1,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -VERSION = '1.0.0a1' +VERSION = '1.0.0a2' diff --git a/pyproject.toml b/pyproject.toml index 7b2af9157..bca69b89e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ ] [project.urls] -Documentation = "https://github.com/Azure/azure-functions-python-worker?tab=readme-ov-file#-azure-functions-python-worker" +Documentation = "https://github.com/Azure/azure-functions-python-worker/blob/hallvictoria/library-worker/README.md" Repository = "https://github.com/Azure/azure-functions-python-worker" [project.optional-dependencies] From f4c61cfa82a0fdc5210a1d6adae73557163e82a0 Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Wed, 23 Apr 2025 11:37:04 -0500 Subject: [PATCH 36/45] a3: fix otel set / get --- azure_functions_worker_v2/handle_event.py | 8 ++++---- azure_functions_worker_v2/version.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/azure_functions_worker_v2/handle_event.py b/azure_functions_worker_v2/handle_event.py index 136b4fc4e..3202380f3 100644 --- a/azure_functions_worker_v2/handle_event.py +++ b/azure_functions_worker_v2/handle_event.py @@ -176,9 +176,9 @@ async def invocation_request(request): fi: FunctionInfo = _functions.get_function( function_id) assert fi is not None - logger.info("Function name: %s, Function Type: %s", - fi.name, - ("async" if fi.is_async else "sync")) + logger.debug("Function name: %s, Function Type: %s", + fi.name, + ("async" if fi.is_async else "sync")) args = {} @@ -226,7 +226,7 @@ async def invocation_request(request): if fi.is_async: if (otel_manager.get_azure_monitor_available() - or otel_manager.set_otel_libs_available()): + or otel_manager.get_otel_libs_available()): configure_opentelemetry(fi_context) # Extensions are not supported diff --git a/azure_functions_worker_v2/version.py b/azure_functions_worker_v2/version.py index 5358c0836..51170743d 100644 --- a/azure_functions_worker_v2/version.py +++ b/azure_functions_worker_v2/version.py @@ -1,4 +1,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -VERSION = '1.0.0a2' +VERSION = '1.0.0a3' From 15c4c17cbbba10b89b75ed32dffc1d1f111f8fab Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Mon, 9 Jun 2025 14:44:38 -0500 Subject: [PATCH 37/45] merge misses --- .../official/jobs/build-artifacts.yml | 32 + proxy_worker/__init__.py | 2 - proxy_worker/__main__.py | 6 - proxy_worker/dispatcher.py | 521 ------------- proxy_worker/logging.py | 92 --- proxy_worker/protos/.gitignore | 2 - proxy_worker/protos/__init__.py | 43 -- proxy_worker/protos/_src/.gitignore | 288 ------- proxy_worker/protos/_src/LICENSE | 21 - proxy_worker/protos/_src/README.md | 98 --- .../protos/_src/src/proto/FunctionRpc.proto | 730 ------------------ .../proto/identity/ClaimsIdentityRpc.proto | 26 - .../_src/src/proto/shared/NullableTypes.proto | 30 - proxy_worker/protos/identity/__init__.py | 0 proxy_worker/protos/shared/__init__.py | 0 proxy_worker/start_worker.py | 77 -- proxy_worker/utils/__init__.py | 2 - proxy_worker/utils/common.py | 86 --- proxy_worker/utils/constants.py | 15 - proxy_worker/utils/dependency.py | 311 -------- proxy_worker/version.py | 4 - pyproject.toml | 3 +- 22 files changed, 34 insertions(+), 2355 deletions(-) create mode 100644 eng/templates/official/jobs/build-artifacts.yml delete mode 100644 proxy_worker/__init__.py delete mode 100644 proxy_worker/__main__.py delete mode 100644 proxy_worker/dispatcher.py delete mode 100644 proxy_worker/logging.py delete mode 100644 proxy_worker/protos/.gitignore delete mode 100644 proxy_worker/protos/__init__.py delete mode 100644 proxy_worker/protos/_src/.gitignore delete mode 100644 proxy_worker/protos/_src/LICENSE delete mode 100644 proxy_worker/protos/_src/README.md delete mode 100644 proxy_worker/protos/_src/src/proto/FunctionRpc.proto delete mode 100644 proxy_worker/protos/_src/src/proto/identity/ClaimsIdentityRpc.proto delete mode 100644 proxy_worker/protos/_src/src/proto/shared/NullableTypes.proto delete mode 100644 proxy_worker/protos/identity/__init__.py delete mode 100644 proxy_worker/protos/shared/__init__.py delete mode 100644 proxy_worker/start_worker.py delete mode 100644 proxy_worker/utils/__init__.py delete mode 100644 proxy_worker/utils/common.py delete mode 100644 proxy_worker/utils/constants.py delete mode 100644 proxy_worker/utils/dependency.py delete mode 100644 proxy_worker/version.py diff --git a/eng/templates/official/jobs/build-artifacts.yml b/eng/templates/official/jobs/build-artifacts.yml new file mode 100644 index 000000000..df03daaab --- /dev/null +++ b/eng/templates/official/jobs/build-artifacts.yml @@ -0,0 +1,32 @@ +jobs: + - job: "Build" + displayName: 'Build V2 Python Runtime SDK' + + pool: + name: 1es-pool-azfunc + image: 1es-ubuntu-22.04 + os: linux + + templateContext: + outputParentDirectory: $(Build.ArtifactStagingDirectory) + outputs: + - output: pipelineArtifact + targetPath: $(Build.SourcesDirectory) + artifactName: "azure-functions-runtime" + + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: "3.13" + - bash: | + python --version + displayName: 'Check python version' + - bash: | + python -m pip install -U pip + python -m pip install build + python -m build + displayName: 'Build Python Runtime SDK' + - bash: | + pip install pip-audit + pip-audit . + displayName: 'Run vulnerability scan' diff --git a/proxy_worker/__init__.py b/proxy_worker/__init__.py deleted file mode 100644 index 5b7f7a925..000000000 --- a/proxy_worker/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. diff --git a/proxy_worker/__main__.py b/proxy_worker/__main__.py deleted file mode 100644 index 5141dd60a..000000000 --- a/proxy_worker/__main__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from proxy_worker import start_worker - -if __name__ == '__main__': - start_worker.start() diff --git a/proxy_worker/dispatcher.py b/proxy_worker/dispatcher.py deleted file mode 100644 index 257b68b14..000000000 --- a/proxy_worker/dispatcher.py +++ /dev/null @@ -1,521 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import asyncio -import concurrent.futures -import logging -import os -import queue -import sys -import threading -import traceback -import typing -from asyncio import AbstractEventLoop -from dataclasses import dataclass -from typing import Any, Optional - -import grpc -from proxy_worker import protos -from proxy_worker.logging import ( - CONSOLE_LOG_PREFIX, - disable_console_logging, - enable_console_logging, - error_logger, - is_system_log_category, - logger, -) -from proxy_worker.utils.common import ( - get_app_setting, - get_script_file_name, - is_envvar_true, -) -from proxy_worker.utils.constants import ( - PYTHON_ENABLE_DEBUG_LOGGING, - PYTHON_THREADPOOL_THREAD_COUNT, -) -from proxy_worker.version import VERSION - -from .utils.dependency import DependencyManager - -# Library worker import reloaded in init and reload request -_library_worker = None - - -class ContextEnabledTask(asyncio.Task): - AZURE_INVOCATION_ID = '__azure_function_invocation_id__' - - def __init__(self, coro, loop, context=None, **kwargs): - super().__init__(coro, loop=loop, context=context, **kwargs) - - current_task = asyncio.current_task(loop) - if current_task is not None: - invocation_id = getattr( - current_task, self.AZURE_INVOCATION_ID, None) - if invocation_id is not None: - self.set_azure_invocation_id(invocation_id) - - def set_azure_invocation_id(self, invocation_id: str) -> None: - setattr(self, self.AZURE_INVOCATION_ID, invocation_id) - - -_invocation_id_local = threading.local() - - -def get_current_invocation_id() -> Optional[Any]: - loop = asyncio._get_running_loop() - if loop is not None: - current_task = asyncio.current_task(loop) - if current_task is not None: - task_invocation_id = getattr(current_task, - ContextEnabledTask.AZURE_INVOCATION_ID, - None) - if task_invocation_id is not None: - return task_invocation_id - - return getattr(_invocation_id_local, 'invocation_id', None) - - -class AsyncLoggingHandler(logging.Handler): - def emit(self, record: logging.LogRecord) -> None: - # Since we disable console log after gRPC channel is initiated, - # we should redirect all the messages into dispatcher. - - # When dispatcher receives an exception, it should switch back - # to console logging. However, it is possible that - # __current_dispatcher__ is set to None as there are still messages - # buffered in this handler, not calling the emit yet. - msg = self.format(record) - try: - Dispatcher.current.on_logging(record, msg) - except RuntimeError as runtime_error: - # This will cause 'Dispatcher not found' failure. - # Logging such of an issue will cause infinite loop of gRPC logging - # To mitigate, we should suppress the 2nd level error logging here - # and use print function to report exception instead. - print(f'{CONSOLE_LOG_PREFIX} ERROR: {str(runtime_error)}', - file=sys.stderr, flush=True) - - -@dataclass -class WorkerRequest: - name: str - request: str - properties: Optional[dict[str, typing.Any]] = None - - -class DispatcherMeta(type): - __current_dispatcher__: Optional["Dispatcher"] = None - - @property - def current(cls): - disp = cls.__current_dispatcher__ - if disp is None: - raise RuntimeError('no currently running Dispatcher is found') - return disp - - -class Dispatcher(metaclass=DispatcherMeta): - _GRPC_STOP_RESPONSE = object() - - def __init__(self, loop: AbstractEventLoop, host: str, port: int, - worker_id: str, request_id: str, - grpc_connect_timeout: float, - grpc_max_msg_len: int = -1) -> None: - self._loop = loop - self._host = host - self._port = port - self._request_id = request_id - self._worker_id = worker_id - self._grpc_connect_timeout: float = grpc_connect_timeout - self._grpc_max_msg_len: int = grpc_max_msg_len - self._old_task_factory: Optional[Any] = None - - self._grpc_resp_queue: queue.Queue = queue.Queue() - self._grpc_connected_fut = loop.create_future() - self._grpc_thread: Optional[threading.Thread] = threading.Thread( - name='grpc_local-thread', target=self.__poll_grpc) - - self._sync_call_tp: Optional[concurrent.futures.Executor] = ( - self._create_sync_call_tp(self._get_sync_tp_max_workers())) - - 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: - log_level = protos.RpcLog.Error - elif record.levelno >= logging.WARNING: - log_level = protos.RpcLog.Warning - elif record.levelno >= logging.INFO: - log_level = protos.RpcLog.Information - elif record.levelno >= logging.DEBUG: - log_level = protos.RpcLog.Debug - else: - log_level = getattr(protos.RpcLog, 'None') - - if is_system_log_category(record.name): - log_category = protos.RpcLog.RpcLogCategory.Value('System') - else: # customers using logging will yield 'root' in record.name - log_category = protos.RpcLog.RpcLogCategory.Value('User') - - log = dict( - level=log_level, - message=formatted_msg, - category=record.name, - log_category=log_category - ) - - invocation_id = get_current_invocation_id() - if invocation_id is not None: - log['invocation_id'] = invocation_id - - self._grpc_resp_queue.put_nowait( - protos.StreamingMessage( - request_id=self.request_id, - rpc_log=protos.RpcLog(**log))) - - @property - def request_id(self) -> str: - return self._request_id - - @property - def worker_id(self) -> str: - return self._worker_id - - @classmethod - async def connect(cls, host: str, port: int, worker_id: str, - request_id: str, connect_timeout: float): - loop = asyncio.events.get_event_loop() - disp = cls(loop, host, port, worker_id, request_id, connect_timeout) - # Safety check for mypy - if disp._grpc_thread is not None: - disp._grpc_thread.start() - await disp._grpc_connected_fut - logger.info('Successfully opened gRPC channel to %s:%s ', host, port) - return disp - - def __poll_grpc(self): - options = [] - if self._grpc_max_msg_len: - options.append(('grpc_local.max_receive_message_length', - self._grpc_max_msg_len)) - options.append(('grpc_local.max_send_message_length', - self._grpc_max_msg_len)) - - channel = grpc.insecure_channel( - f'{self._host}:{self._port}', options) - - try: - grpc.channel_ready_future(channel).result( - timeout=self._grpc_connect_timeout) - except Exception as ex: - self._loop.call_soon_threadsafe( - self._grpc_connected_fut.set_exception, ex) - return - else: - self._loop.call_soon_threadsafe( - self._grpc_connected_fut.set_result, True) - - stub = protos.FunctionRpcStub(channel) - - def gen(resp_queue): - while True: - msg = resp_queue.get() - if msg is self._GRPC_STOP_RESPONSE: - grpc_req_stream.cancel() - return - yield msg - - grpc_req_stream = stub.EventStream(gen(self._grpc_resp_queue)) - try: - for req in grpc_req_stream: - self._loop.call_soon_threadsafe( - self._loop.create_task, self._dispatch_grpc_request(req)) - except Exception as ex: - if ex is grpc_req_stream: - # Yes, this is how grpc_req_stream iterator exits. - return - error_logger.exception( - 'unhandled error in gRPC thread. Exception: {0}'.format( - ''.join(traceback.format_exception(ex)))) - raise - - async def _dispatch_grpc_request(self, request): - content_type = request.WhichOneof("content") - - match content_type: - case "worker_init_request": - request_handler = self._handle__worker_init_request - case "function_environment_reload_request": - request_handler = self._handle__function_environment_reload_request - case "functions_metadata_request": - request_handler = self._handle__functions_metadata_request - case "function_load_request": - request_handler = self._handle__function_load_request - case "worker_status_request": - request_handler = self._handle__worker_status_request - case "invocation_request": - request_handler = self._handle__invocation_request - case _: - # Don't crash on unknown messages. Log the error and return. - logger.error("Unknown StreamingMessage content type: %s", content_type) - return - - resp = await request_handler(request) - self._grpc_resp_queue.put_nowait(resp) - - 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') - - self._old_task_factory = self._loop.get_task_factory() - - DispatcherMeta.__current_dispatcher__ = self - try: - forever = self._loop.create_future() - - self._grpc_resp_queue.put_nowait( - protos.StreamingMessage( - request_id=self.request_id, - start_stream=protos.StartStream( - worker_id=self.worker_id))) - - # In Python 3.11+, constructing a task has an optional context - # parameter. Allow for this param to be passed to ContextEnabledTask - self._loop.set_task_factory( - lambda loop, coro, context=None, **kwargs: ContextEnabledTask( - coro, loop=loop, context=context, **kwargs)) - - # Detach console logging before enabling GRPC channel logging - logger.info('Detaching console logging.') - disable_console_logging() - - # Attach gRPC logging to the root logger. Since gRPC channel is - # established, should use it for system and user logs - 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.') - logging_handler.flush() - - try: - await forever - finally: - logger.warning('Detaching gRPC logging due to exception.') - logging_handler.flush() - root_logger.removeHandler(logging_handler) - - # Reenable console logging when there's an exception - enable_console_logging() - logger.warning('Switched to console logging due to exception.') - finally: - DispatcherMeta.__current_dispatcher__ = None - - self._loop.set_task_factory(self._old_task_factory) - self.stop() - - def stop(self) -> None: - if self._grpc_thread is not None: - self._grpc_resp_queue.put_nowait(self._GRPC_STOP_RESPONSE) - self._grpc_thread.join() - self._grpc_thread = None - - self._stop_sync_call_tp() - - def _stop_sync_call_tp(self): - """Deallocate the current synchronous thread pool and assign - self._sync_call_tp to None. If the thread pool does not exist, - this will be a no op. - """ - if getattr(self, '_sync_call_tp', None): - assert self._sync_call_tp is not None # mypy fix - self._sync_call_tp.shutdown() - self._sync_call_tp = None - - @staticmethod - def _create_sync_call_tp(max_worker: Optional[int]) -> concurrent.futures.Executor: - """Create a thread pool executor with max_worker. This is a wrapper - over ThreadPoolExecutor constructor. Consider calling this method after - _stop_sync_call_tp() to ensure only 1 synchronous thread pool is - running. - """ - return concurrent.futures.ThreadPoolExecutor( - max_workers=max_worker - ) - - @staticmethod - def _get_sync_tp_max_workers() -> typing.Optional[int]: - def tp_max_workers_validator(value: str) -> bool: - try: - int_value = int(value) - except ValueError: - logger.warning('%s must be an integer', - PYTHON_THREADPOOL_THREAD_COUNT) - return False - - if int_value < 1: - logger.warning( - '%s must be set to a value between 1 and sys.maxint. ' - 'Reverting to default value for max_workers', - PYTHON_THREADPOOL_THREAD_COUNT, - 1) - return False - return True - - max_workers = get_app_setting(setting=PYTHON_THREADPOOL_THREAD_COUNT, - validator=tp_max_workers_validator) - - # We can box the app setting as int for earlier python versions. - return int(max_workers) if max_workers else None - - @staticmethod - def reload_library_worker(directory: str): - global _library_worker - v2_scriptfile = os.path.join(directory, get_script_file_name()) - if os.path.exists(v2_scriptfile): - try: - import azure_functions_worker_v2 # NoQA - _library_worker = azure_functions_worker_v2 - logger.debug("azure_functions_worker_v2 import succeeded: %s", - _library_worker.__file__) - except ImportError: - logger.debug("azure_functions_worker_v2 library not found: : %s", - traceback.format_exc()) - else: - try: - import azure_functions_worker_v1 # NoQA - _library_worker = azure_functions_worker_v1 - logger.debug("azure_functions_worker_v1 import succeeded: %s", - _library_worker.__file__) # type: ignore[union-attr] - except ImportError: - logger.debug("azure_functions_worker_v1 library not found: %s", - traceback.format_exc()) - - async def _handle__worker_init_request(self, request): - logger.info('Received WorkerInitRequest, ' - 'python version %s, ' - 'worker version %s, ' - 'request ID %s. ' - 'To enable debug level logging, please refer to ' - 'https://aka.ms/python-enable-debug-logging', - sys.version, - VERSION, - self.request_id) - - if DependencyManager.is_in_linux_consumption(): - import azure_functions_worker_v2 - - if DependencyManager.should_load_cx_dependencies(): - DependencyManager.prioritize_customer_dependencies() - - directory = request.worker_init_request.function_app_directory - self.reload_library_worker(directory) - - init_request = WorkerRequest(name="WorkerInitRequest", - request=request, - properties={"protos": protos, - "host": self._host}) - init_response = await ( - _library_worker.worker_init_request( # type: ignore[union-attr] - init_request)) - - return protos.StreamingMessage( - request_id=self.request_id, - worker_init_response=init_response) - - async def _handle__function_environment_reload_request(self, request): - logger.info('Received FunctionEnvironmentReloadRequest, ' - 'request ID: %s, ' - 'To enable debug level logging, please refer to ' - 'https://aka.ms/python-enable-debug-logging', - self.request_id) - - func_env_reload_request = \ - request.function_environment_reload_request - directory = func_env_reload_request.function_app_directory - - DependencyManager.prioritize_customer_dependencies(directory) - self.reload_library_worker(directory) - - env_reload_request = WorkerRequest(name="FunctionEnvironmentReloadRequest", - request=request, - properties={"protos": protos, - "host": self._host}) - env_reload_response = await ( - _library_worker.function_environment_reload_request( # type: ignore[union-attr] # noqa - env_reload_request)) - - return protos.StreamingMessage( - request_id=self.request_id, - function_environment_reload_response=env_reload_response) - - 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. - # Having log here will reduce the responsiveness of the worker. - return protos.StreamingMessage( - request_id=request.request_id, - worker_status_response=protos.WorkerStatusResponse()) - - async def _handle__functions_metadata_request(self, request): - logger.info( - 'Received WorkerMetadataRequest, request ID %s, ' - 'worker id: %s', - self.request_id, self.worker_id) - - metadata_request = WorkerRequest(name="WorkerMetadataRequest", request=request) - metadata_response = await ( - _library_worker.functions_metadata_request( # type: ignore[union-attr] - metadata_request)) - - return protos.StreamingMessage( - request_id=request.request_id, - function_metadata_response=metadata_response) - - async def _handle__function_load_request(self, request): - func_request = request.function_load_request - function_id = func_request.function_id - function_metadata = func_request.metadata - function_name = function_metadata.name - - logger.info( - 'Received WorkerLoadRequest, request ID %s, function_id: %s,' - 'function_name: %s, worker_id: %s', - self.request_id, function_id, function_name, self.worker_id) - - load_request = WorkerRequest(name="FunctionLoadRequest ", request=request) - load_response = await ( - _library_worker.function_load_request( # type: ignore[union-attr] - load_request)) - - return protos.StreamingMessage( - request_id=self.request_id, - function_load_response=load_response) - - async def _handle__invocation_request(self, request): - invoc_request = request.invocation_request - invocation_id = invoc_request.invocation_id - function_id = invoc_request.function_id - - logger.info( - 'Received FunctionInvocationRequest, request ID %s, function_id: %s,' - 'invocation_id: %s, worker_id: %s', - self.request_id, function_id, invocation_id, self.worker_id) - - invocation_request = WorkerRequest(name="FunctionInvocationRequest", - request=request, - properties={ - "threadpool": self._sync_call_tp}) - invocation_response = await ( - _library_worker.invocation_request( # type: ignore[union-attr] - invocation_request)) - - return protos.StreamingMessage( - request_id=self.request_id, - invocation_response=invocation_response) diff --git a/proxy_worker/logging.py b/proxy_worker/logging.py deleted file mode 100644 index 8f765640b..000000000 --- a/proxy_worker/logging.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging -import logging.handlers -import sys -from typing import Optional - -# Logging Prefixes -SYSTEM_LOG_PREFIX = "proxy_worker" -SDK_LOG_PREFIX = "azure.functions" -SYSTEM_ERROR_LOG_PREFIX = "proxy_worker_errors" -CONSOLE_LOG_PREFIX = "LanguageWorkerConsoleLog" - - -logger: logging.Logger = logging.getLogger(SYSTEM_LOG_PREFIX) -error_logger: logging.Logger = ( - logging.getLogger(SYSTEM_ERROR_LOG_PREFIX)) - -handler: Optional[logging.Handler] = None -error_handler: Optional[logging.Handler] = None - - -def setup(log_level, log_destination): - # Since handler and error_handler are moved to the global scope, - # before assigning to these handlers, we should define 'global' keyword - global handler - global error_handler - - if log_level == 'TRACE': - log_level = 'DEBUG' - - formatter = logging.Formatter(f'{CONSOLE_LOG_PREFIX}' - ' %(levelname)s: %(message)s') - - if log_destination is None: - # With no explicit log destination we do split logging, - # errors go into stderr, everything else -- to stdout. - error_handler = logging.StreamHandler(sys.stderr) - error_handler.setFormatter(formatter) - error_handler.setLevel(getattr(logging, log_level)) - - handler = logging.StreamHandler(sys.stdout) - - elif log_destination in ('stdout', 'stderr'): - handler = logging.StreamHandler(getattr(sys, log_destination)) - - elif log_destination == 'syslog': - handler = logging.handlers.SysLogHandler() - - else: - handler = logging.FileHandler(log_destination) - - if error_handler is None: - error_handler = handler - - handler.setFormatter(formatter) - handler.setLevel(getattr(logging, log_level)) - - logger.addHandler(handler) - logger.setLevel(getattr(logging, log_level)) - - error_logger.addHandler(error_handler) - error_logger.setLevel(getattr(logging, log_level)) - - -def disable_console_logging() -> None: - # We should only remove the sys.stdout stream, as error_logger is used for - # unexpected critical error logs handling. - if logger and handler: - handler.flush() - logger.removeHandler(handler) - - -def enable_console_logging() -> None: - if logger and handler: - logger.addHandler(handler) - - -def is_system_log_category(ctg: str) -> bool: - """Check if the logging namespace belongs to system logs. Category starts - with the following name will be treated as system logs. - 1. 'proxy_worker' (Worker Info) - 2. 'azure_functions_worker_errors' (Worker Error) - 3. 'azure.functions' (SDK) - - Expected behaviors for sytem logs and customer logs are listed below: - local_console customer_app_insight functions_kusto_table - system_log false false true - customer_log true true false - """ - return ctg.startswith(SYSTEM_LOG_PREFIX) or ctg.startswith(SDK_LOG_PREFIX) diff --git a/proxy_worker/protos/.gitignore b/proxy_worker/protos/.gitignore deleted file mode 100644 index 49d7060ef..000000000 --- a/proxy_worker/protos/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*_pb2.py -*_pb2_grpc.py diff --git a/proxy_worker/protos/__init__.py b/proxy_worker/protos/__init__.py deleted file mode 100644 index e9c4f2397..000000000 --- a/proxy_worker/protos/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -from .FunctionRpc_pb2_grpc import ( # NoQA - FunctionRpcStub, - FunctionRpcServicer, - add_FunctionRpcServicer_to_server) - -from .FunctionRpc_pb2 import ( # NoQA - StreamingMessage, - StartStream, - WorkerInitRequest, - WorkerInitResponse, - RpcFunctionMetadata, - FunctionLoadRequest, - FunctionLoadResponse, - FunctionEnvironmentReloadRequest, - FunctionEnvironmentReloadResponse, - InvocationRequest, - InvocationResponse, - WorkerHeartbeat, - WorkerStatusRequest, - WorkerStatusResponse, - BindingInfo, - StatusResult, - RpcException, - ParameterBinding, - TypedData, - RpcHttp, - RpcHttpCookie, - RpcLog, - RpcSharedMemory, - RpcDataType, - CloseSharedMemoryResourcesRequest, - CloseSharedMemoryResourcesResponse, - FunctionsMetadataRequest, - FunctionMetadataResponse, - WorkerMetadata, - RpcRetryOptions) - -from .shared.NullableTypes_pb2 import ( - NullableString, - NullableBool, - NullableDouble, - NullableTimestamp -) diff --git a/proxy_worker/protos/_src/.gitignore b/proxy_worker/protos/_src/.gitignore deleted file mode 100644 index 940794e60..000000000 --- a/proxy_worker/protos/_src/.gitignore +++ /dev/null @@ -1,288 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ -**/Properties/launchSettings.json - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Typescript v1 declaration files -typings/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs diff --git a/proxy_worker/protos/_src/LICENSE b/proxy_worker/protos/_src/LICENSE deleted file mode 100644 index 21071075c..000000000 --- a/proxy_worker/protos/_src/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ - MIT License - - Copyright (c) Microsoft Corporation. All rights reserved. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE diff --git a/proxy_worker/protos/_src/README.md b/proxy_worker/protos/_src/README.md deleted file mode 100644 index b22f0bb4b..000000000 --- a/proxy_worker/protos/_src/README.md +++ /dev/null @@ -1,98 +0,0 @@ -# Azure Functions Languge Worker Protobuf - -This repository contains the protobuf definition file which defines the gRPC service which is used between the [Azure Functions Host](https://github.com/Azure/azure-functions-host) and the Azure Functions language workers. This repo is shared across many repos in many languages (for each worker) by using git commands. - -To use this repo in Azure Functions language workers, follow steps below to add this repo as a subtree (*Adding This Repo*). If this repo is already embedded in a language worker repo, follow the steps to update the consumed file (*Pulling Updates*). - -Learn more about Azure Function's projects on the [meta](https://github.com/azure/azure-functions) repo. - -## Adding This Repo - -From within the Azure Functions language worker repo: -1. Define remote branch for cleaner git commands - - `git remote add proto-file https://github.com/azure/azure-functions-language-worker-protobuf.git` - - `git fetch proto-file` -2. Index contents of azure-functions-worker-protobuf to language worker repo - - `git read-tree --prefix= -u proto-file/` -3. Add new path in language worker repo to .gitignore file - - In .gitignore, add path in language worker repo -4. Finalize with commit - - `git commit -m "Added subtree from https://github.com/azure/azure-functions-language-worker-protobuf. Branch: . Commit: "` - - `git push` - -## Pulling Updates - -From within the Azure Functions language worker repo: -1. Define remote branch for cleaner git commands - - `git remote add proto-file https://github.com/azure/azure-functions-language-worker-protobuf.git` - - `git fetch proto-file` -2. Pull a specific release tag - - `git fetch proto-file refs/tags/` - - Example: `git fetch proto-file refs/tags/v1.1.0-protofile` -3. Merge updates - - Merge with an explicit path to subtree: `git merge -X subtree= --squash --allow-unrelated-histories --strategy-option theirs` - - Example: `git merge -X subtree=src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf --squash v1.1.0-protofile --allow-unrelated-histories --strategy-option theirs` -4. Finalize with commit - - `git commit -m "Updated subtree from https://github.com/azure/azure-functions-language-worker-protobuf. Tag: . Commit: "` - - `git push` - -## Releasing a Language Worker Protobuf version - -1. Draft a release in the GitHub UI - - Be sure to inculde details of the release -2. Create a release version, following semantic versioning guidelines ([semver.org](https://semver.org/)) -3. Tag the version with the pattern: `v..

    -protofile` (example: `v1.1.0-protofile`) -3. Merge `dev` to `master` - -## Consuming FunctionRPC.proto -*Note: Update versionNumber before running following commands* - -## CSharp -``` -set NUGET_PATH="%UserProfile%\.nuget\packages" -set GRPC_TOOLS_PATH=%NUGET_PATH%\grpc.tools\\tools\windows_x86 -set PROTO_PATH=.\azure-functions-language-worker-protobuf\src\proto -set PROTO=.\azure-functions-language-worker-protobuf\src\proto\FunctionRpc.proto -set PROTOBUF_TOOLS=%NUGET_PATH%\google.protobuf.tools\\tools -set MSGDIR=.\Messages - -if exist %MSGDIR% rmdir /s /q %MSGDIR% -mkdir %MSGDIR% - -set OUTDIR=%MSGDIR%\DotNet -mkdir %OUTDIR% -%GRPC_TOOLS_PATH%\protoc.exe %PROTO% --csharp_out %OUTDIR% --grpc_out=%OUTDIR% --plugin=protoc-gen-grpc=%GRPC_TOOLS_PATH%\grpc_csharp_plugin.exe --proto_path=%PROTO_PATH% --proto_path=%PROTOBUF_TOOLS% -``` -## JavaScript -In package.json, add to the build script the following commands to build .js files and to build .ts files. Use and install npm package `protobufjs`. - -Generate JavaScript files: -``` -pbjs -t json-module -w commonjs -o azure-functions-language-worker-protobuf/src/rpc.js azure-functions-language-worker-protobuf/src/proto/FunctionRpc.proto -``` -Generate TypeScript files: -``` -pbjs -t static-module azure-functions-language-worker-protobuf/src/proto/FunctionRpc.proto -o azure-functions-language-worker-protobuf/src/rpc_static.js && pbts -o azure-functions-language-worker-protobuf/src/rpc.d.ts azure-functions-language-worker-protobuf/src/rpc_static.js -``` - -## Java -Maven plugin : [protobuf-maven-plugin](https://www.xolstice.org/protobuf-maven-plugin/) -In pom.xml add following under configuration for this plugin -${basedir}//azure-functions-language-worker-protobuf/src/proto - -## Python ---TODO - -## Contributing - -This project welcomes contributions and suggestions. Most contributions require you to agree to a -Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.microsoft.com. - -When you submit a pull request, a CLA-bot will automatically determine whether you need to provide -a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/proxy_worker/protos/_src/src/proto/FunctionRpc.proto b/proxy_worker/protos/_src/src/proto/FunctionRpc.proto deleted file mode 100644 index f48bc7bbe..000000000 --- a/proxy_worker/protos/_src/src/proto/FunctionRpc.proto +++ /dev/null @@ -1,730 +0,0 @@ -syntax = "proto3"; -// protobuf vscode extension: https://marketplace.visualstudio.com/items?itemName=zxh404.vscode-proto3 - -option java_multiple_files = true; -option java_package = "com.microsoft.azure.functions.rpc.messages"; -option java_outer_classname = "FunctionProto"; -option csharp_namespace = "Microsoft.Azure.WebJobs.Script.Grpc.Messages"; -option go_package ="github.com/Azure/azure-functions-go-worker/internal/rpc"; - -package AzureFunctionsRpcMessages; - -import "google/protobuf/duration.proto"; -import "identity/ClaimsIdentityRpc.proto"; -import "shared/NullableTypes.proto"; - -// Interface exported by the server. -service FunctionRpc { - rpc EventStream (stream StreamingMessage) returns (stream StreamingMessage) {} -} - -message StreamingMessage { - // Used to identify message between host and worker - string request_id = 1; - - // Payload of the message - oneof content { - - // Worker initiates stream - StartStream start_stream = 20; - - // Host sends capabilities/init data to worker - WorkerInitRequest worker_init_request = 17; - // Worker responds after initializing with its capabilities & status - WorkerInitResponse worker_init_response = 16; - - // MESSAGE NOT USED - // Worker periodically sends empty heartbeat message to host - WorkerHeartbeat worker_heartbeat = 15; - - // Host sends terminate message to worker. - // Worker terminates if it can, otherwise host terminates after a grace period - WorkerTerminate worker_terminate = 14; - - // Host periodically sends status request to the worker - WorkerStatusRequest worker_status_request = 12; - WorkerStatusResponse worker_status_response = 13; - - // On file change event, host sends notification to worker - FileChangeEventRequest file_change_event_request = 6; - - // Worker requests a desired action (restart worker, reload function) - WorkerActionResponse worker_action_response = 7; - - // Host sends required metadata to worker to load function - FunctionLoadRequest function_load_request = 8; - // Worker responds after loading with the load result - FunctionLoadResponse function_load_response = 9; - - // Host requests a given invocation - InvocationRequest invocation_request = 4; - - // Worker responds to a given invocation - InvocationResponse invocation_response = 5; - - // Host sends cancel message to attempt to cancel an invocation. - // If an invocation is cancelled, host will receive an invocation response with status cancelled. - InvocationCancel invocation_cancel = 21; - - // Worker logs a message back to the host - RpcLog rpc_log = 2; - - FunctionEnvironmentReloadRequest function_environment_reload_request = 25; - - FunctionEnvironmentReloadResponse function_environment_reload_response = 26; - - // Ask the worker to close any open shared memory resources for a given invocation - CloseSharedMemoryResourcesRequest close_shared_memory_resources_request = 27; - CloseSharedMemoryResourcesResponse close_shared_memory_resources_response = 28; - - // Worker indexing message types - FunctionsMetadataRequest functions_metadata_request = 29; - FunctionMetadataResponse function_metadata_response = 30; - - // Host sends required metadata to worker to load functions - FunctionLoadRequestCollection function_load_request_collection = 31; - - // Host gets the list of function load responses - FunctionLoadResponseCollection function_load_response_collection = 32; - - // Host sends required metadata to worker to warmup the worker - WorkerWarmupRequest worker_warmup_request = 33; - - // Worker responds after warming up with the warmup result - WorkerWarmupResponse worker_warmup_response = 34; - - } -} - -// Process.Start required info -// connection details -// protocol type -// protocol version - -// Worker sends the host information identifying itself -message StartStream { - // id of the worker - string worker_id = 2; -} - -// Host requests the worker to initialize itself -message WorkerInitRequest { - // version of the host sending init request - string host_version = 1; - - // A map of host supported features/capabilities - map capabilities = 2; - - // inform worker of supported categories and their levels - // i.e. Worker = Verbose, Function.MyFunc = None - map log_categories = 3; - - // Full path of worker.config.json location - string worker_directory = 4; - - // base directory for function app - string function_app_directory = 5; -} - -// Worker responds with the result of initializing itself -message WorkerInitResponse { - // PROPERTY NOT USED - // TODO: Remove from protobuf during next breaking change release - string worker_version = 1; - - // A map of worker supported features/capabilities - map capabilities = 2; - - // Status of the response - StatusResult result = 3; - - // Worker metadata captured for telemetry purposes - WorkerMetadata worker_metadata = 4; -} - -message WorkerMetadata { - // The runtime/stack name - string runtime_name = 1; - - // The version of the runtime/stack - string runtime_version = 2; - - // The version of the worker - string worker_version = 3; - - // The worker bitness/architecture - string worker_bitness = 4; - - // Optional additional custom properties - map custom_properties = 5; -} - -// Used by the host to determine success/failure/cancellation -message StatusResult { - // Indicates Failure/Success/Cancelled - enum Status { - Failure = 0; - Success = 1; - Cancelled = 2; - } - - // Status for the given result - Status status = 4; - - // Specific message about the result - string result = 1; - - // Exception message (if exists) for the status - RpcException exception = 2; - - // Captured logs or relevant details can use the logs property - repeated RpcLog logs = 3; -} - -// MESSAGE NOT USED -// TODO: Remove from protobuf during next breaking change release -message WorkerHeartbeat {} - -// Warning before killing the process after grace_period -// Worker self terminates ..no response on this -message WorkerTerminate { - google.protobuf.Duration grace_period = 1; -} - -// Host notifies worker of file content change -message FileChangeEventRequest { - // Types of File change operations (See link for more info: https://msdn.microsoft.com/en-us/library/t6xf43e0(v=vs.110).aspx) - enum Type { - Unknown = 0; - Created = 1; - Deleted = 2; - Changed = 4; - Renamed = 8; - All = 15; - } - - // type for this event - Type type = 1; - - // full file path for the file change notification - string full_path = 2; - - // Name of the function affected - string name = 3; -} - -// Indicates whether worker reloaded successfully or needs a restart -message WorkerActionResponse { - // indicates whether a restart is needed, or reload successfully - enum Action { - Restart = 0; - Reload = 1; - } - - // action for this response - Action action = 1; - - // text reason for the response - string reason = 2; -} - -// Used by the host to determine worker health -message WorkerStatusRequest { -} - -// Worker responds with status message -// TODO: Add any worker relevant status to response -message WorkerStatusResponse { -} - -message FunctionEnvironmentReloadRequest { - // Environment variables from the current process - map environment_variables = 1; - // Current directory of function app - string function_app_directory = 2; -} - -message FunctionEnvironmentReloadResponse { - // After specialization, worker sends capabilities & metadata. - // Worker metadata captured for telemetry purposes - WorkerMetadata worker_metadata = 1; - - // A map of worker supported features/capabilities - map capabilities = 2; - - // Status of the response - StatusResult result = 3; -} - -// Tell the out-of-proc worker to close any shared memory maps it allocated for given invocation -message CloseSharedMemoryResourcesRequest { - repeated string map_names = 1; -} - -// Response from the worker indicating which of the shared memory maps have been successfully closed and which have not been closed -// The key (string) is the map name and the value (bool) is true if it was closed, false if not -message CloseSharedMemoryResourcesResponse { - map close_map_results = 1; -} - -// Host tells the worker to load a list of Functions -message FunctionLoadRequestCollection { - repeated FunctionLoadRequest function_load_requests = 1; -} - -// Host gets the list of function load responses -message FunctionLoadResponseCollection { - repeated FunctionLoadResponse function_load_responses = 1; -} - -// Load request of a single Function -message FunctionLoadRequest { - // unique function identifier (avoid name collisions, facilitate reload case) - string function_id = 1; - - // Metadata for the request - RpcFunctionMetadata metadata = 2; - - // A flag indicating if managed dependency is enabled or not - bool managed_dependency_enabled = 3; -} - -// Worker tells host result of reload -message FunctionLoadResponse { - // unique function identifier - string function_id = 1; - - // Result of load operation - StatusResult result = 2; - // TODO: return type expected? - - // Result of load operation - bool is_dependency_downloaded = 3; -} - -// Information on how a Function should be loaded and its bindings -message RpcFunctionMetadata { - // TODO: do we want the host's name - the language worker might do a better job of assignment than the host - string name = 4; - - // base directory for the Function - string directory = 1; - - // Script file specified - string script_file = 2; - - // Entry point specified - string entry_point = 3; - - // Bindings info - map bindings = 6; - - // Is set to true for proxy - bool is_proxy = 7; - - // Function indexing status - StatusResult status = 8; - - // Function language - string language = 9; - - // Raw binding info - repeated string raw_bindings = 10; - - // unique function identifier (avoid name collisions, facilitate reload case) - string function_id = 13; - - // A flag indicating if managed dependency is enabled or not - bool managed_dependency_enabled = 14; - - // The optional function execution retry strategy to use on invocation failures. - RpcRetryOptions retry_options = 15; - - // Properties for function metadata - // They're usually specific to a worker and largely passed along to the controller API for use - // outside the host - map properties = 16; -} - -// Host tells worker it is ready to receive metadata -message FunctionsMetadataRequest { - // base directory for function app - string function_app_directory = 1; -} - -// Worker sends function metadata back to host -message FunctionMetadataResponse { - // list of function indexing responses - repeated RpcFunctionMetadata function_metadata_results = 1; - - // status of overall metadata request - StatusResult result = 2; - - // if set to true then host will perform indexing - bool use_default_metadata_indexing = 3; -} - -// Host requests worker to invoke a Function -message InvocationRequest { - // Unique id for each invocation - string invocation_id = 1; - - // Unique id for each Function - string function_id = 2; - - // Input bindings (include trigger) - repeated ParameterBinding input_data = 3; - - // binding metadata from trigger - map trigger_metadata = 4; - - // Populates activityId, tracestate and tags from host - RpcTraceContext trace_context = 5; - - // Current retry context - RetryContext retry_context = 6; -} - -// Host sends ActivityId, traceStateString and Tags from host -message RpcTraceContext { - // This corresponds to Activity.Current?.Id - string trace_parent = 1; - - // This corresponds to Activity.Current?.TraceStateString - string trace_state = 2; - - // This corresponds to Activity.Current?.Tags - map attributes = 3; -} - -// Host sends retry context for a function invocation -message RetryContext { - // Current retry count - int32 retry_count = 1; - - // Max retry count - int32 max_retry_count = 2; - - // Exception that caused the retry - RpcException exception = 3; -} - -// Host requests worker to cancel invocation -message InvocationCancel { - // Unique id for invocation - string invocation_id = 2; - - // PROPERTY NOT USED - google.protobuf.Duration grace_period = 1; -} - -// Worker responds with status of Invocation -message InvocationResponse { - // Unique id for invocation - string invocation_id = 1; - - // Output binding data - repeated ParameterBinding output_data = 2; - - // data returned from Function (for $return and triggers with return support) - TypedData return_value = 4; - - // Status of the invocation (success/failure/canceled) - StatusResult result = 3; -} - -message WorkerWarmupRequest { - // Full path of worker.config.json location - string worker_directory = 1; -} - -message WorkerWarmupResponse { - StatusResult result = 1; -} - -// Used to encapsulate data which could be a variety of types -message TypedData { - oneof data { - string string = 1; - string json = 2; - bytes bytes = 3; - bytes stream = 4; - RpcHttp http = 5; - sint64 int = 6; - double double = 7; - CollectionBytes collection_bytes = 8; - CollectionString collection_string = 9; - CollectionDouble collection_double = 10; - CollectionSInt64 collection_sint64 = 11; - ModelBindingData model_binding_data = 12; - CollectionModelBindingData collection_model_binding_data = 13; - } -} - -// Specify which type of data is contained in the shared memory region being read -enum RpcDataType { - unknown = 0; - string = 1; - json = 2; - bytes = 3; - stream = 4; - http = 5; - int = 6; - double = 7; - collection_bytes = 8; - collection_string = 9; - collection_double = 10; - collection_sint64 = 11; -} - -// Used to provide metadata about shared memory region to read data from -message RpcSharedMemory { - // Name of the shared memory map containing data - string name = 1; - // Offset in the shared memory map to start reading data from - int64 offset = 2; - // Number of bytes to read (starting from the offset) - int64 count = 3; - // Final type to which the read data (in bytes) is to be interpreted as - RpcDataType type = 4; -} - -// Used to encapsulate collection string -message CollectionString { - repeated string string = 1; -} - -// Used to encapsulate collection bytes -message CollectionBytes { - repeated bytes bytes = 1; -} - -// Used to encapsulate collection double -message CollectionDouble { - repeated double double = 1; -} - -// Used to encapsulate collection sint64 -message CollectionSInt64 { - repeated sint64 sint64 = 1; -} - -// Used to describe a given binding on invocation -message ParameterBinding { - // Name for the binding - string name = 1; - - oneof rpc_data { - // Data for the binding - TypedData data = 2; - - // Metadata about the shared memory region to read data from - RpcSharedMemory rpc_shared_memory = 3; - } -} - -// Used to describe a given binding on load -message BindingInfo { - // Indicates whether it is an input or output binding (or a fancy inout binding) - enum Direction { - in = 0; - out = 1; - inout = 2; - } - - // Indicates the type of the data for the binding - enum DataType { - undefined = 0; - string = 1; - binary = 2; - stream = 3; - } - - // Type of binding (e.g. HttpTrigger) - string type = 2; - - // Direction of the given binding - Direction direction = 3; - - DataType data_type = 4; - - // Properties for binding metadata - map properties = 5; -} - -// Used to send logs back to the Host -message RpcLog { - // Matching ILogger semantics - // https://github.com/aspnet/Logging/blob/9506ccc3f3491488fe88010ef8b9eb64594abf95/src/Microsoft.Extensions.Logging/Logger.cs - // Level for the Log - enum Level { - Trace = 0; - Debug = 1; - Information = 2; - Warning = 3; - Error = 4; - Critical = 5; - None = 6; - } - - // Category of the log. Defaults to User if not specified. - enum RpcLogCategory { - User = 0; - System = 1; - CustomMetric = 2; - } - - // Unique id for invocation (if exists) - string invocation_id = 1; - - // TOD: This should be an enum - // Category for the log (startup, load, invocation, etc.) - string category = 2; - - // Level for the given log message - Level level = 3; - - // Message for the given log - string message = 4; - - // Id for the even associated with this log (if exists) - string event_id = 5; - - // Exception (if exists) - RpcException exception = 6; - - // json serialized property bag - string properties = 7; - - // Category of the log. Either user(default), system, or custom metric. - RpcLogCategory log_category = 8; - - // strongly-typed (ish) property bag - map propertiesMap = 9; -} - -// Encapsulates an Exception -message RpcException { - // Source of the exception - string source = 3; - - // Stack trace for the exception - string stack_trace = 1; - - // Textual message describing the exception - string message = 2; - - // Worker specifies whether exception is a user exception, - // for purpose of application insights logging. Defaults to false. - bool is_user_exception = 4; - - // Type of exception. If it's a user exception, the type is passed along to app insights. - // Otherwise, it's ignored for now. - string type = 5; -} - -// Http cookie type. Note that only name and value are used for Http requests -message RpcHttpCookie { - // Enum that lets servers require that a cookie shouldn't be sent with cross-site requests - enum SameSite { - None = 0; - Lax = 1; - Strict = 2; - ExplicitNone = 3; - } - - // Cookie name - string name = 1; - - // Cookie value - string value = 2; - - // Specifies allowed hosts to receive the cookie - NullableString domain = 3; - - // Specifies URL path that must exist in the requested URL - NullableString path = 4; - - // Sets the cookie to expire at a specific date instead of when the client closes. - // It is generally recommended that you use "Max-Age" over "Expires". - NullableTimestamp expires = 5; - - // Sets the cookie to only be sent with an encrypted request - NullableBool secure = 6; - - // Sets the cookie to be inaccessible to JavaScript's Document.cookie API - NullableBool http_only = 7; - - // Allows servers to assert that a cookie ought not to be sent along with cross-site requests - SameSite same_site = 8; - - // Number of seconds until the cookie expires. A zero or negative number will expire the cookie immediately. - NullableDouble max_age = 9; -} - -// TODO - solidify this or remove it -message RpcHttp { - string method = 1; - string url = 2; - map headers = 3; - TypedData body = 4; - map params = 10; - string status_code = 12; - map query = 15; - bool enable_content_negotiation= 16; - TypedData rawBody = 17; - repeated RpcClaimsIdentity identities = 18; - repeated RpcHttpCookie cookies = 19; - map nullable_headers = 20; - map nullable_params = 21; - map nullable_query = 22; -} - -// Message representing Microsoft.Azure.WebJobs.ParameterBindingData -// Used for hydrating SDK-type bindings in out-of-proc workers -message ModelBindingData -{ - // The version of the binding data content - string version = 1; - - // The extension source of the binding data - string source = 2; - - // The content type of the binding data content - string content_type = 3; - - // The binding data content - bytes content = 4; -} - -// Used to encapsulate collection model_binding_data -message CollectionModelBindingData { - repeated ModelBindingData model_binding_data = 1; -} - -// Retry policy which the worker sends the host when the worker indexes -// a function. -message RpcRetryOptions -{ - // The retry strategy to use. Valid values are fixed delay or exponential backoff. - enum RetryStrategy - { - exponential_backoff = 0; - fixed_delay = 1; - } - - // The maximum number of retries allowed per function execution. - // -1 means to retry indefinitely. - int32 max_retry_count = 2; - - // The delay that's used between retries when you're using a fixed delay strategy. - google.protobuf.Duration delay_interval = 3; - - // The minimum retry delay when you're using an exponential backoff strategy - google.protobuf.Duration minimum_interval = 4; - - // The maximum retry delay when you're using an exponential backoff strategy - google.protobuf.Duration maximum_interval = 5; - - RetryStrategy retry_strategy = 6; -} \ No newline at end of file diff --git a/proxy_worker/protos/_src/src/proto/identity/ClaimsIdentityRpc.proto b/proxy_worker/protos/_src/src/proto/identity/ClaimsIdentityRpc.proto deleted file mode 100644 index c3945bb8a..000000000 --- a/proxy_worker/protos/_src/src/proto/identity/ClaimsIdentityRpc.proto +++ /dev/null @@ -1,26 +0,0 @@ -syntax = "proto3"; -// protobuf vscode extension: https://marketplace.visualstudio.com/items?itemName=zxh404.vscode-proto3 - -option java_package = "com.microsoft.azure.functions.rpc.messages"; - -import "shared/NullableTypes.proto"; - -// Light-weight representation of a .NET System.Security.Claims.ClaimsIdentity object. -// This is the same serialization as found in EasyAuth, and needs to be kept in sync with -// its ClaimsIdentitySlim definition, as seen in the WebJobs extension: -// https://github.com/Azure/azure-webjobs-sdk-extensions/blob/dev/src/WebJobs.Extensions.Http/ClaimsIdentitySlim.cs -message RpcClaimsIdentity { - NullableString authentication_type = 1; - NullableString name_claim_type = 2; - NullableString role_claim_type = 3; - repeated RpcClaim claims = 4; -} - -// Light-weight representation of a .NET System.Security.Claims.Claim object. -// This is the same serialization as found in EasyAuth, and needs to be kept in sync with -// its ClaimSlim definition, as seen in the WebJobs extension: -// https://github.com/Azure/azure-webjobs-sdk-extensions/blob/dev/src/WebJobs.Extensions.Http/ClaimSlim.cs -message RpcClaim { - string value = 1; - string type = 2; -} diff --git a/proxy_worker/protos/_src/src/proto/shared/NullableTypes.proto b/proxy_worker/protos/_src/src/proto/shared/NullableTypes.proto deleted file mode 100644 index 4fb476502..000000000 --- a/proxy_worker/protos/_src/src/proto/shared/NullableTypes.proto +++ /dev/null @@ -1,30 +0,0 @@ -syntax = "proto3"; -// protobuf vscode extension: https://marketplace.visualstudio.com/items?itemName=zxh404.vscode-proto3 - -option java_package = "com.microsoft.azure.functions.rpc.messages"; - -import "google/protobuf/timestamp.proto"; - -message NullableString { - oneof string { - string value = 1; - } -} - -message NullableDouble { - oneof double { - double value = 1; - } -} - -message NullableBool { - oneof bool { - bool value = 1; - } -} - -message NullableTimestamp { - oneof timestamp { - google.protobuf.Timestamp value = 1; - } -} diff --git a/proxy_worker/protos/identity/__init__.py b/proxy_worker/protos/identity/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/proxy_worker/protos/shared/__init__.py b/proxy_worker/protos/shared/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/proxy_worker/start_worker.py b/proxy_worker/start_worker.py deleted file mode 100644 index d468cef69..000000000 --- a/proxy_worker/start_worker.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Main entrypoint.""" - -import argparse -import traceback - -_GRPC_CONNECTION_TIMEOUT = 5.0 - -def parse_args(): - parser = argparse.ArgumentParser( - description='Python Azure Functions Worker') - parser.add_argument('--host', - help="host address") - parser.add_argument('--port', type=int, - help='port number') - parser.add_argument('--workerId', dest='worker_id', - help='id for the worker') - parser.add_argument('--requestId', dest='request_id', - help='id of the request') - parser.add_argument('--grpcMaxMessageLength', type=int, - dest='grpc_max_msg_len') - parser.add_argument('--functions-uri', dest='functions_uri', type=str, - help='URI with IP Address and Port used to' - ' connect to the Host via gRPC.') - parser.add_argument('--functions-worker-id', - dest='functions_worker_id', type=str, - help='Worker ID assigned to this language worker.') - parser.add_argument('--functions-request-id', dest='functions_request_id', - type=str, help='Request ID used for gRPC communication ' - 'with the Host.') - parser.add_argument('--functions-grpc-max-message-length', type=int, - dest='functions_grpc_max_msg_len', - help='Max grpc_local message length for Functions') - return parser.parse_args() - - -def start(): - from .utils.dependency import DependencyManager - DependencyManager.initialize() - DependencyManager.use_worker_dependencies() - - import asyncio - - from . import logging - from .logging import error_logger, logger - - args = parse_args() - logging.setup(log_level="INFO", log_destination=None) - - logger.info("Args: %s", args) - logger.info( - 'Starting proxy worker. Worker ID: %s, Request ID: %s, Host Address: %s:%s', - args.worker_id, args.request_id, args.host, args.port) - - try: - return asyncio.run(start_async( - args.host, args.port, args.worker_id, args.request_id)) - except Exception as ex: - error_logger.exception( - 'unhandled error in functions worker: {0}'.format( - ''.join(traceback.format_exception(ex)))) - raise - - -async def start_async(host, port, worker_id, request_id): - from . import dispatcher - - disp = await dispatcher.Dispatcher.connect(host=host, port=port, - worker_id=worker_id, - request_id=request_id, - connect_timeout=_GRPC_CONNECTION_TIMEOUT) - await disp.dispatch_forever() - - -if __name__ == '__main__': - start() diff --git a/proxy_worker/utils/__init__.py b/proxy_worker/utils/__init__.py deleted file mode 100644 index 5b7f7a925..000000000 --- a/proxy_worker/utils/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. diff --git a/proxy_worker/utils/common.py b/proxy_worker/utils/common.py deleted file mode 100644 index 5b2f1e98f..000000000 --- a/proxy_worker/utils/common.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os -from typing import Callable, Optional - -from proxy_worker.utils.constants import ( - PYTHON_SCRIPT_FILE_NAME, - PYTHON_SCRIPT_FILE_NAME_DEFAULT, -) - - -def get_app_setting( - setting: str, - default_value: Optional[str] = None, - validator: Optional[Callable[[str], bool]] = None -) -> Optional[str]: - """Returns the application setting from environment variable. - - Parameters - ---------- - setting: str - The name of the application setting (e.g. FUNCTIONS_RUNTIME_VERSION) - - default_value: Optional[str] - The expected return value when the application setting is not found, - or the app setting does not pass the validator. - - validator: Optional[Callable[[str], bool]] - A function accepts the app setting value and should return True when - the app setting value is acceptable. - - Returns - ------- - Optional[str] - A string value that is set in the application setting - """ - app_setting_value = os.getenv(setting) - - # If an app setting is not configured, we return the default value - if app_setting_value is None: - return default_value - - # If there's no validator, we should return the app setting value directly - if validator is None: - return app_setting_value - - # If the app setting is set with a validator, - # On True, should return the app setting value - # On False, should return the default value - if validator(app_setting_value): - return app_setting_value - return default_value - - -def is_true_like(setting: str) -> bool: - if setting is None: - return False - - return setting.lower().strip() in {'1', 'true', 't', 'yes', 'y'} - - -def is_false_like(setting: str) -> bool: - if setting is None: - return False - - return setting.lower().strip() in {'0', 'false', 'f', 'no', 'n'} - - -def is_envvar_true(env_key: str) -> bool: - if os.getenv(env_key) is None: - return False - - return is_true_like(os.environ[env_key]) - - -def is_envvar_false(env_key: str) -> bool: - if os.getenv(env_key) is None: - return False - - return is_false_like(os.environ[env_key]) - - -def get_script_file_name(): - return get_app_setting(PYTHON_SCRIPT_FILE_NAME, - PYTHON_SCRIPT_FILE_NAME_DEFAULT) diff --git a/proxy_worker/utils/constants.py b/proxy_worker/utils/constants.py deleted file mode 100644 index c5e0dd2ab..000000000 --- a/proxy_worker/utils/constants.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -# App Setting constants -PYTHON_ENABLE_DEBUG_LOGGING = "PYTHON_ENABLE_DEBUG_LOGGING" -PYTHON_THREADPOOL_THREAD_COUNT = "PYTHON_THREADPOOL_THREAD_COUNT" - -# Container constants -CONTAINER_NAME = "CONTAINER_NAME" -AZURE_WEBJOBS_SCRIPT_ROOT = "AzureWebJobsScriptRoot" - -# new programming model default script file name -PYTHON_SCRIPT_FILE_NAME = "PYTHON_SCRIPT_FILE_NAME" -PYTHON_SCRIPT_FILE_NAME_DEFAULT = "function_app.py" - diff --git a/proxy_worker/utils/dependency.py b/proxy_worker/utils/dependency.py deleted file mode 100644 index b5b07dbd5..000000000 --- a/proxy_worker/utils/dependency.py +++ /dev/null @@ -1,311 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import os -import re -import sys -from types import ModuleType -from typing import List, Optional - -from ..logging import logger -from .common import is_envvar_true -from .constants import AZURE_WEBJOBS_SCRIPT_ROOT, CONTAINER_NAME - - -class DependencyManager: - """The dependency manager controls the Python packages source, preventing - worker packages interfer customer's code. - - It has two mode, in worker mode, the Python packages are loaded from worker - path, (e.g. workers/python///). In customer mode, - the packages are loaded from customer's .python_packages/ folder or from - their virtual environment. - - Azure Functions has three different set of sys.path ordering, - - Linux Consumption sys.path: [ - "/tmp/functions\\standby\\wwwroot", # Placeholder folder - "/home/site/wwwroot/.python_packages/lib/site-packages", # CX's deps - "/azure-functions-host/workers/python/3.13/LINUX/X64", # Worker's deps - "/home/site/wwwroot" # CX's Working Directory - ] - - Linux Dedicated/Premium sys.path: [ - "/home/site/wwwroot", # CX's Working Directory - "/home/site/wwwroot/.python_packages/lib/site-packages", # CX's deps - "/azure-functions-host/workers/python/3.13/LINUX/X64", # Worker's deps - ] - - Core Tools sys.path: [ - "%appdata%\\azure-functions-core-tools\\bin\\workers\\" - "python\\3.13\\WINDOWS\\X64", # Worker's deps - "C:\\Users\\user\\Project\\.venv311\\lib\\site-packages", # CX's deps - "C:\\Users\\user\\Project", # CX's Working Directory - ] - - When we first start up the Python worker, we should only loaded from - worker's deps and create module namespace (e.g. google.protobuf variable). - - Once the worker receives worker init request, we clear out the sys.path, - worker sys.modules cache and sys.path_import_cache so the libraries - will only get loaded from CX's deps path. - """ - - cx_deps_path: str = '' - cx_working_dir: str = '' - worker_deps_path: str = '' - - @classmethod - def initialize(cls): - cls.cx_deps_path = cls._get_cx_deps_path() - cls.cx_working_dir = cls._get_cx_working_dir() - cls.worker_deps_path = cls._get_worker_deps_path() - - @classmethod - def is_in_linux_consumption(cls): - return CONTAINER_NAME in os.environ - - @classmethod - def should_load_cx_dependencies(cls): - """ - Customer dependencies should be loaded when - 1) App is a dedicated app - 2) App is linux consumption but not in placeholder mode. - This can happen when the worker restarts for any reason - (OOM, timeouts etc) and env reload request is not called. - """ - return not (DependencyManager.is_in_linux_consumption() - and is_envvar_true("WEBSITE_PLACEHOLDER_MODE")) - - @classmethod - def use_worker_dependencies(cls): - """Switch the sys.path and ensure the worker imports are loaded from - Worker's dependenciess. - - This will not affect already imported namespaces, but will clear out - the module cache and ensure the upcoming modules are loaded from - worker's dependency path. - """ - - # The following log line will not show up in core tools but should - # work in kusto since core tools only collects gRPC logs. This function - # is executed even before the gRPC logging channel is ready. - logger.info('Applying use_worker_dependencies:' - ' worker_dependencies: %s,' - ' customer_dependencies: %s,' - ' working_directory: %s', cls.worker_deps_path, - cls.cx_deps_path, cls.cx_working_dir) - - cls._remove_from_sys_path(cls.cx_deps_path) - cls._remove_from_sys_path(cls.cx_working_dir) - cls._add_to_sys_path(cls.worker_deps_path, True) - logger.info('Start using worker dependencies %s. Sys.path: %s', - cls.worker_deps_path, sys.path) - - @classmethod - def prioritize_customer_dependencies(cls, cx_working_dir=None): - """Switch the sys.path and ensure the customer's code import are loaded - from CX's deppendencies. - - This will not affect already imported namespaces, but will clear out - the module cache and ensure the upcoming modules are loaded from - customer's dependency path. - - As for Linux Consumption, this will only remove worker_deps_path, - but the customer's path will be loaded in function_environment_reload. - - The search order of a module name in customer's paths is: - 1. cx_deps_path - 2. worker_deps_path - 3. cx_working_dir - """ - # Try to get the latest customer's working directory - # cx_working_dir => cls.cx_working_dir => AzureWebJobsScriptRoot - working_directory: str = '' - if cx_working_dir: - working_directory = os.path.abspath(cx_working_dir) - if not working_directory: - working_directory = cls.cx_working_dir - if not working_directory: - working_directory = os.getenv(AZURE_WEBJOBS_SCRIPT_ROOT, '') - - # Try to get the latest customer's dependency path - cx_deps_path: str = cls._get_cx_deps_path() - - if not cx_deps_path: - cx_deps_path = cls.cx_deps_path - - logger.info( - 'Applying prioritize_customer_dependencies: ' - 'worker_dependencies_path: %s, customer_dependencies_path: %s, ' - 'working_directory: %s, Linux Consumption: %s, Placeholder: %s, ' - 'sys.path: %s', - cls.worker_deps_path, cx_deps_path, working_directory, - DependencyManager.is_in_linux_consumption(), - is_envvar_true("WEBSITE_PLACEHOLDER_MODE"), sys.path) - - cls._remove_from_sys_path(cls.worker_deps_path) - cls._add_to_sys_path(cls.worker_deps_path, True) - cls._add_to_sys_path(cls.cx_deps_path, True) - cls._add_to_sys_path(working_directory, False) - - logger.info(f'Finished prioritize_customer_dependencies: {sys.path}') - - @classmethod - def _add_to_sys_path(cls, path: str, add_to_first: bool): - """This will ensure no duplicated path are added into sys.path and - clear importer cache. No action if path already exists in sys.path. - - Parameters - ---------- - path: str - The path needs to be added into sys.path. - If the path is an empty string, no action will be taken. - add_to_first: bool - Should the path added to the first entry (highest priority) - """ - if path and path not in sys.path: - if add_to_first: - sys.path.insert(0, path) - else: - sys.path.append(path) - - # Only clear path importer and sys.modules cache if path is not - # defined in sys.path - cls._clear_path_importer_cache_and_modules(path) - - @classmethod - def _remove_from_sys_path(cls, path: str): - """This will remove path from sys.path and clear importer cache. - No action if the path does not exist in sys.path. - - Parameters - ---------- - path: str - The path to be removed from sys.path. - If the path is an empty string, no action will be taken. - """ - if path and path in sys.path: - # Remove all occurances in sys.path - sys.path = list(filter(lambda p: p != path, sys.path)) - - # In case if any part of worker initialization do sys.path.pop() - # Always do a cache clear in path importer and sys.modules - cls._clear_path_importer_cache_and_modules(path) - - @classmethod - def _clear_path_importer_cache_and_modules(cls, path: str): - """Removes path from sys.path_importer_cache and clear related - sys.modules cache. No action if the path is empty or no entries - in sys.path_importer_cache or sys.modules. - - Parameters - ---------- - path: str - The path to be removed from sys.path_importer_cache. All related - modules will be cleared out from sys.modules cache. - If the path is an empty string, no action will be taken. - """ - if path and path in sys.path_importer_cache: - sys.path_importer_cache.pop(path) - - if path: - cls._remove_module_cache(path) - - @staticmethod - def _get_cx_deps_path() -> str: - """Get the directory storing the customer's third-party libraries. - - Returns - ------- - str - Core Tools: path to customer's site packages - Linux Dedicated/Premium: path to customer's site packages - Linux Consumption: empty string - """ - prefix: Optional[str] = os.getenv(AZURE_WEBJOBS_SCRIPT_ROOT) - cx_paths: List[str] = [ - p for p in sys.path - if prefix and p.startswith(prefix) and ('site-packages' in p) - ] - # Return first or default of customer path - return (cx_paths or [''])[0] - - @staticmethod - def _get_cx_working_dir() -> str: - """Get the customer's working directory. - - Returns - ------- - str - Core Tools: AzureWebJobsScriptRoot env variable - Linux Dedicated/Premium: AzureWebJobsScriptRoot env variable - Linux Consumption: empty string - """ - return os.getenv(AZURE_WEBJOBS_SCRIPT_ROOT, '') - - @staticmethod - def _get_worker_deps_path() -> str: - """Get the worker dependency sys.path. This will always available - even in all skus. - - Returns - ------- - str - The worker packages path - """ - # 1. Try to parse the absolute path python/3.13/LINUX/X64 in sys.path - r = re.compile(r'.*python(\/|\\)\d+\.\d+(\/|\\)(WINDOWS|LINUX|OSX).*') - worker_deps_paths: List[str] = [p for p in sys.path if r.match(p)] - if worker_deps_paths: - return worker_deps_paths[0] - - # 2. If it fails to find one, try to find one from the parent path - # This is used for handling the CI/localdev environment - return os.path.abspath( - os.path.join(os.path.dirname(__file__), '..', '..') - ) - - @staticmethod - def _remove_module_cache(path: str): - """Remove module cache if the module is imported from specific path. - This will not impact builtin modules - - Parameters - ---------- - path: str - The module cache to be removed if it is imported from this path. - """ - if not path: - return - - not_builtin = set(sys.modules.keys()) - set(sys.builtin_module_names) - - # Don't reload proxy_worker - to_be_cleared_from_cache = set([ - module_name for module_name in not_builtin - if not module_name.startswith('proxy_worker') - ]) - - for module_name in to_be_cleared_from_cache: - module = sys.modules.get(module_name) - if not isinstance(module, ModuleType): - continue - - # Module path can be actual file path or a pure namespace path. - # Both of these has the module path placed in __path__ property - # The property .__path__ can be None or does not exist in module - try: - # Safely check for __path__ and __file__ existence - module_paths = set() - if hasattr(module, '__path__') and module.__path__: - module_paths.update(module.__path__) - if hasattr(module, '__file__') and module.__file__: - module_paths.add(module.__file__) - - if any([p for p in module_paths if p.startswith(path)]): - sys.modules.pop(module_name) - except Exception as e: - logger.warning( - 'Attempt to remove module cache for %s but failed with ' - '%s. Using the original module cache.', - module_name, e) diff --git a/proxy_worker/version.py b/proxy_worker/version.py deleted file mode 100644 index 2c5f2d28c..000000000 --- a/proxy_worker/version.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -VERSION = "4.38.0" diff --git a/pyproject.toml b/pyproject.toml index 26d06c4db..4864dee7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,8 @@ dev = [ ] test-http-v2 = [ "azurefunctions-extensions-http-fastapi==1.0.0", - "ujson" + "ujson", + "orjson" ] test-deferred-bindings = [ "azurefunctions-extensions-bindings-blob==1.0.0", From cb7b33fff47690b02bfc1cfea5c67b262f74c169 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Mon, 9 Jun 2025 14:54:46 -0500 Subject: [PATCH 38/45] update branches --- eng/ci/code-mirror.yml | 2 +- eng/ci/official-build.yml | 4 ++-- eng/ci/public-build.yml | 7 +++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/eng/ci/code-mirror.yml b/eng/ci/code-mirror.yml index 852c2bf70..6f6a06a7d 100644 --- a/eng/ci/code-mirror.yml +++ b/eng/ci/code-mirror.yml @@ -1,7 +1,7 @@ trigger: branches: include: - - 1.x + - dev - runtime-release/* resources: diff --git a/eng/ci/official-build.yml b/eng/ci/official-build.yml index af258c6be..f340e5a75 100644 --- a/eng/ci/official-build.yml +++ b/eng/ci/official-build.yml @@ -2,7 +2,7 @@ trigger: batch: true branches: include: - - 1.x + - dev - runtime-release/* # CI only, does not trigger on PRs. @@ -13,7 +13,7 @@ schedules: displayName: At 12:00 AM, only on Monday branches: include: - - 1.x + - dev always: true resources: diff --git a/eng/ci/public-build.yml b/eng/ci/public-build.yml index d7cda87bb..3c4ee9c3e 100644 --- a/eng/ci/public-build.yml +++ b/eng/ci/public-build.yml @@ -2,18 +2,17 @@ trigger: batch: true branches: include: - - 1.x - + - dev pr: branches: include: - - 1.x + - dev schedules: - cron: '0 0 * * MON' displayName: At 12:00 AM, only on Monday branches: include: - - 1.x + - dev always: true resources: From 674959b3659b70bc634fedf7d6de25eb18ee0118 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Mon, 9 Jun 2025 15:37:41 -0500 Subject: [PATCH 39/45] remove condition --- eng/ci/public-build.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/eng/ci/public-build.yml b/eng/ci/public-build.yml index 3c4ee9c3e..84749c4a8 100644 --- a/eng/ci/public-build.yml +++ b/eng/ci/public-build.yml @@ -7,6 +7,7 @@ pr: branches: include: - dev + schedules: - cron: '0 0 * * MON' displayName: At 12:00 AM, only on Monday @@ -45,8 +46,6 @@ extends: - stage: Build jobs: - template: /eng/templates/jobs/build.yml@self - # Skip the build stage for SDK and Extensions release branches. This stage will fail because pyproject.toml contains the updated (and unreleased) library version - condition: and(eq(variables.isSdkRelease, false), eq(variables.isExtensionsRelease, false), eq(variables['USETESTPYTHONSDK'], false), eq(variables['USETESTPYTHONEXTENSIONS'], false)) - stage: RunUnitTests dependsOn: Build jobs: From e16d89f287b243b1fd4554a59eab11761a30c33a Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Thu, 12 Jun 2025 11:14:06 -0500 Subject: [PATCH 40/45] fix test --- .../unittests/basic_function/function_app.py | 8 ----- tests/unittests/test_handle_event.py | 33 +++++++++---------- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/tests/unittests/basic_function/function_app.py b/tests/unittests/basic_function/function_app.py index 512cf83c2..1781c65ab 100644 --- a/tests/unittests/basic_function/function_app.py +++ b/tests/unittests/basic_function/function_app.py @@ -34,11 +34,3 @@ def default_template(req: func.HttpRequest) -> func.HttpResponse: " personalized response.", status_code=200 ) - - -@app.route(route="http_func") -def http_func(req: func.HttpRequest) -> func.HttpResponse: - time.sleep(1) - - current_time = datetime.now().strftime("%H:%M:%S") - return func.HttpResponse(f"{current_time}") diff --git a/tests/unittests/test_handle_event.py b/tests/unittests/test_handle_event.py index 87c2a3abc..7605d10b7 100644 --- a/tests/unittests/test_handle_event.py +++ b/tests/unittests/test_handle_event.py @@ -3,13 +3,14 @@ from typing import Any from unittest.mock import patch +import azure_functions_worker_v2.handle_event as handle_event from azure_functions_worker_v2.handle_event import (worker_init_request, functions_metadata_request, function_environment_reload_request) from tests.utils import testutils from tests.utils.constants import UNIT_TESTS_FOLDER -import tests.protos as protos +import tests.protos as test_protos BASIC_FUNCTION_DIRECTORY = UNIT_TESTS_FOLDER / "basic_function" STREAMING_FUNCTION_DIRECTORY = UNIT_TESTS_FOLDER / "streaming_function" @@ -46,7 +47,7 @@ async def test_worker_init_request(self): 'hello', BASIC_FUNCTION_DIRECTORY)), properties={'host': '123', - 'protos': protos}) + 'protos': test_protos}) result = await worker_init_request(worker_request) self.assertEqual(result.capabilities, {'WorkerStatus': 'true', 'RpcHttpBodyOnly': 'true', @@ -72,7 +73,7 @@ async def test_worker_init_request_with_streaming(self, 'hello', STREAMING_FUNCTION_DIRECTORY)), properties={'host': '123', - 'protos': protos}) + 'protos': test_protos}) result = await worker_init_request(worker_request) self.assertEqual(result.capabilities, {'WorkerStatus': 'true', 'RpcHttpBodyOnly': 'true', @@ -97,7 +98,7 @@ async def test_worker_init_request_with_otel(self, mock_otel_enabled): 'hello', BASIC_FUNCTION_DIRECTORY)), properties={'host': '123', - 'protos': protos}) + 'protos': test_protos}) result = await worker_init_request(worker_request) self.assertEqual(result.capabilities, {'WorkerStatus': 'true', 'RpcHttpBodyOnly': 'true', @@ -120,7 +121,7 @@ async def test_worker_init_request_with_exception(self): 'hello', INDEXING_EXCEPTION_FUNCTION_DIRECTORY)), properties={'host': '123', - 'protos': protos}) + 'protos': test_protos}) result = await worker_init_request(worker_request) self.assertEqual(result.capabilities, {'WorkerStatus': 'true', 'RpcHttpBodyOnly': 'true', @@ -135,18 +136,11 @@ async def test_worker_init_request_with_exception(self): self.assertEqual(result.result.status, 1) async def test_functions_metadata_request(self): - worker_request = WorkerRequest(name='worker_init_request', - request=Request(FunctionRequest( - 'hello', - BASIC_FUNCTION_DIRECTORY)), - properties={'host': '123', - 'protos': protos}) - _ = await worker_init_request(worker_request) + handle_event.protos = test_protos metadata_result = await functions_metadata_request(None) - self.assertEqual(metadata_result.use_default_metadata_indexing, False) - self.assertIsNotNone(metadata_result.function_metadata_results) self.assertEqual(metadata_result.result.status, 1) + def test_functions_metadata_request_with_exception(self): pass @@ -159,12 +153,15 @@ def test_invocation_request_async(self): def test_invocation_request_with_exception(self): pass + @patch("azure_functions_worker_v2.loader.index_function_app", + return_value=True) async def test_function_environment_reload_request(self): worker_request = WorkerRequest(name='function_environment_reload_request', request=Request(FunctionRequest( + 'hello', BASIC_FUNCTION_DIRECTORY)), properties={'host': '123', - 'protos': protos}) + 'protos': test_protos}) result = await function_environment_reload_request(worker_request) self.assertEqual(result.capabilities, {}) self.assertEqual(result.worker_metadata.runtime_name, "python") @@ -186,7 +183,7 @@ async def test_function_environment_reload_request_with_streaming( 'hello', STREAMING_FUNCTION_DIRECTORY)), properties={'host': '123', - 'protos': protos}) + 'protos': test_protos}) result = await function_environment_reload_request(worker_request) self.assertEqual(result.capabilities, {'HttpUri': 'http://mock_address', 'RequiresRouteParameters': 'true'}) @@ -206,7 +203,7 @@ async def test_function_environment_reload_request_with_otel(self, 'hello', BASIC_FUNCTION_DIRECTORY)), properties={'host': '123', - 'protos': protos}) + 'protos': test_protos}) result = await function_environment_reload_request(worker_request) self.assertEqual(result.capabilities, {'WorkerOpenTelemetryEnabled': 'true'}) self.assertEqual(result.worker_metadata.runtime_name, "python") @@ -223,7 +220,7 @@ async def test_function_environment_reload_request_with_exception(self): 'hello', INDEXING_EXCEPTION_FUNCTION_DIRECTORY)), properties={'host': '123', - 'protos': protos}) + 'protos': test_protos}) result = await function_environment_reload_request(worker_request) self.assertEqual(result.capabilities, {}) self.assertEqual(result.worker_metadata.runtime_name, "python") From 6e2758cdec77e105574f69341cac1c72eead511d Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Thu, 12 Jun 2025 14:32:33 -0500 Subject: [PATCH 41/45] cmbd, cmbd tests, mypy --- azure_functions_worker_v2/bindings/context.py | 4 +- azure_functions_worker_v2/bindings/meta.py | 27 +-- tests/unittests/test_code_quality.py | 2 +- tests/unittests/test_deferred_bindings.py | 96 ++++++++ tests/unittests/test_handle_event.py | 29 +-- tests/unittests/test_types.py | 211 ++++++++++++++++++ tests/utils/mock_classes.py | 36 +++ 7 files changed, 361 insertions(+), 44 deletions(-) create mode 100644 tests/unittests/test_deferred_bindings.py create mode 100644 tests/unittests/test_types.py create mode 100644 tests/utils/mock_classes.py diff --git a/azure_functions_worker_v2/bindings/context.py b/azure_functions_worker_v2/bindings/context.py index 828b49a46..7d3e15b31 100644 --- a/azure_functions_worker_v2/bindings/context.py +++ b/azure_functions_worker_v2/bindings/context.py @@ -12,7 +12,7 @@ def __init__(self, func_name: str, func_dir: str, invocation_id: str, - thread_local_storage: Type[threading.local], + thread_local_storage: threading.local, trace_context: TraceContext, retry_context: RetryContext) -> None: self.__func_name = func_name @@ -27,7 +27,7 @@ def invocation_id(self) -> str: return self.__invocation_id @property - def thread_local_storage(self) -> Type[threading.local]: + def thread_local_storage(self) -> threading.local: return self.__thread_local_storage @property diff --git a/azure_functions_worker_v2/bindings/meta.py b/azure_functions_worker_v2/bindings/meta.py index 92089b28d..cd446a0de 100644 --- a/azure_functions_worker_v2/bindings/meta.py +++ b/azure_functions_worker_v2/bindings/meta.py @@ -12,7 +12,6 @@ from ..http_v2 import HttpV2Registry from ..logging import logger from ..utils.constants import ( - BASE_EXT_SUPPORTED_PY_MINOR_VERSION, CUSTOMER_PACKAGES_PATH, HTTP, HTTP_TRIGGER, @@ -79,17 +78,16 @@ def load_binding_registry() -> None: sys.path, sys.modules, os.path.exists(CUSTOMER_PACKAGES_PATH)) - if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION: - try: - import azurefunctions.extensions.base as clients - global DEFERRED_BINDING_REGISTRY - DEFERRED_BINDING_REGISTRY = clients.get_binding_registry() - except ImportError: - logger.debug('Base extension not found. ' - 'Python version: 3.%s, Sys path: %s, ' - 'Sys Module: %s, python-packages Path exists: %s.', - sys.version_info.minor, sys.path, - sys.modules, os.path.exists(CUSTOMER_PACKAGES_PATH)) + try: + import azurefunctions.extensions.base as clients + global DEFERRED_BINDING_REGISTRY + DEFERRED_BINDING_REGISTRY = clients.get_binding_registry() + except ImportError: + logger.debug('Base extension not found. ' + 'Python version: 3.%s, Sys path: %s, ' + 'Sys Module: %s, python-packages Path exists: %s.', + sys.version_info.minor, sys.path, + sys.modules, os.path.exists(CUSTOMER_PACKAGES_PATH)) def get_binding(bind_name: str, @@ -245,15 +243,12 @@ def deferred_bindings_decode(binding: Any, """ The extension manages a cache for clients (ie. BlobClient, ContainerClient) That have already been created, so that the worker can reuse the - Previously created type without creating a new one. + previously created type without creating a new one. For async types, the function_name is needed as a key to differentiate. This prevents a known SDK issue where reusing a client across functions can lose the session context and cause an error. - The cache key is based on: param name, type, resource, function_name - - If cache is empty or key doesn't exist, deferred_binding_type is None """ deferred_binding_type = binding.decode(datum, diff --git a/tests/unittests/test_code_quality.py b/tests/unittests/test_code_quality.py index 350a86940..a92ce6c79 100644 --- a/tests/unittests/test_code_quality.py +++ b/tests/unittests/test_code_quality.py @@ -44,7 +44,7 @@ def test_flake8(self): try: subprocess.run( [sys.executable, '-m', 'flake8', '--config', str(config_path), - 'azure_functions_worker',], + 'azure_functions_worker_v2',], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, diff --git a/tests/unittests/test_deferred_bindings.py b/tests/unittests/test_deferred_bindings.py new file mode 100644 index 000000000..85866bec8 --- /dev/null +++ b/tests/unittests/test_deferred_bindings.py @@ -0,0 +1,96 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import azure.functions as func +import azurefunctions.extensions.base as clients +import tests.protos as protos + +from azure_functions_worker_v2.bindings import datumdef, meta +from tests.utils import testutils +from tests.utils.mock_classes import MockMBD, MockCMBD + + +from azurefunctions.extensions.bindings.blob import (BlobClient, + BlobClientConverter, + ContainerClient, + StorageStreamDownloader) +from azurefunctions.extensions.bindings.eventhub import EventData, EventDataConverter + +EVENTHUB_SAMPLE_CONTENT = b"\x00Sr\xc1\x8e\x08\xa3\x1bx-opt-sequence-number-epochT\xff\xa3\x15x-opt-sequence-numberU\x04\xa3\x0cx-opt-offset\x81\x00\x00\x00\x01\x00\x00\x010\xa3\x13x-opt-enqueued-time\x00\xa3\x1dcom.microsoft:datetime-offset\x81\x08\xddW\x05\xc3Q\xcf\x10\x00St\xc1I\x02\xa1\rDiagnostic-Id\xa1700-bdc3fde4889b4e907e0c9dcb46ff8d92-21f637af293ef13b-00\x00Su\xa0\x08message1" # noqa: E501 + +class TestDeferredBindingsEnabled(testutils.AsyncTestCase): + + def test_mbd_deferred_bindings_enabled_decode(self): + binding = BlobClientConverter + pb = protos.ParameterBinding(name='test', + data=protos.TypedData( + string='test')) + sample_mbd = MockMBD(version="1.0", + source="AzureStorageBlobs", + content_type="application/json", + content="{\"Connection\":\"AzureWebJobsStorage\"," + "\"ContainerName\":" + "\"python-worker-tests\"," + "\"BlobName\":" + "\"test-blobclient-trigger.txt\"}") + datum = datumdef.Datum(value=sample_mbd, type='model_binding_data') + + obj = meta.deferred_bindings_decode(binding=binding, pb=pb, + pytype=BlobClient, datum=datum, metadata={}, + function_name="test_function") + + self.assertIsNotNone(obj) + + def test_cmbd_deferred_bindings_enabled_decode(self): + binding = EventDataConverter + pb = protos.ParameterBinding(name='test', + data=protos.TypedData( + string='test')) + sample_mbd = MockMBD(version="1.0", + source="AzureEventHubsEventData", + content_type="application/octet-stream", + content=EVENTHUB_SAMPLE_CONTENT) + sample_cmbd = MockCMBD(model_binding_data=[sample_mbd, sample_mbd]) + datum = datumdef.Datum(value=sample_cmbd, type='collection_model_binding_data') + + obj = meta.deferred_bindings_decode(binding=binding, pb=pb, + pytype=EventData, datum=datum, metadata={}, + function_name="test_function") + + self.assertIsNotNone(obj) + + async def test_check_deferred_bindings_enabled(self): + """ + check_deferred_bindings_enabled checks if deferred bindings is enabled at fx + and single binding level. + + The first bool represents if deferred bindings is enabled at a fx level. This + means that at least one binding in the function is a deferred binding type. + + The second represents if the current binding is deferred binding. If this is + True, then deferred bindings must also be enabled at the function level. + """ + meta.DEFERRED_BINDING_REGISTRY = clients.get_binding_registry() + + # Type is not supported, deferred_bindings_enabled is not yet set + self.assertEqual(meta.check_deferred_bindings_enabled( + func.InputStream, False), (False, False)) + + # Type is not supported, deferred_bindings_enabled already set + self.assertEqual(meta.check_deferred_bindings_enabled( + func.InputStream, True), (True, False)) + + # Type is supported, deferred_bindings_enabled is not yet set + self.assertEqual(meta.check_deferred_bindings_enabled( + BlobClient, False), (True, True)) + self.assertEqual(meta.check_deferred_bindings_enabled( + ContainerClient, False), (True, True)) + self.assertEqual(meta.check_deferred_bindings_enabled( + StorageStreamDownloader, False), (True, True)) + + # Type is supported, deferred_bindings_enabled is already set + self.assertEqual(meta.check_deferred_bindings_enabled( + BlobClient, True), (True, True)) + self.assertEqual(meta.check_deferred_bindings_enabled( + ContainerClient, True), (True, True)) + self.assertEqual(meta.check_deferred_bindings_enabled( + StorageStreamDownloader, True), (True, True)) \ No newline at end of file diff --git a/tests/unittests/test_handle_event.py b/tests/unittests/test_handle_event.py index 7605d10b7..3381a327b 100644 --- a/tests/unittests/test_handle_event.py +++ b/tests/unittests/test_handle_event.py @@ -1,16 +1,17 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from typing import Any from unittest.mock import patch import azure_functions_worker_v2.handle_event as handle_event +import tests.protos as test_protos + from azure_functions_worker_v2.handle_event import (worker_init_request, functions_metadata_request, function_environment_reload_request) from tests.utils import testutils from tests.utils.constants import UNIT_TESTS_FOLDER +from tests.utils.mock_classes import FunctionRequest, Request, WorkerRequest -import tests.protos as test_protos BASIC_FUNCTION_DIRECTORY = UNIT_TESTS_FOLDER / "basic_function" STREAMING_FUNCTION_DIRECTORY = UNIT_TESTS_FOLDER / "streaming_function" @@ -18,28 +19,6 @@ / "indexing_exception_function") -# This represents the top level protos request sent from the host -class WorkerRequest: - def __init__(self, name: str, request: Any, properties: dict): - self.name = name - self.request = request - self.properties = properties - - -# This represents the inner request -class Request: - def __init__(self, name: Any): - self.worker_init_request = name - self.function_environment_reload_request = name - - -# This represents the Function Init/Metadata/Load/Invocation request -class FunctionRequest: - def __init__(self, capabilities: Any, function_app_directory: Any): - self.capabilities = capabilities - self.function_app_directory = function_app_directory - - class TestHandleEvent(testutils.AsyncTestCase): async def test_worker_init_request(self): worker_request = WorkerRequest(name='worker_init_request', @@ -155,7 +134,7 @@ def test_invocation_request_with_exception(self): @patch("azure_functions_worker_v2.loader.index_function_app", return_value=True) - async def test_function_environment_reload_request(self): + async def test_function_environment_reload_request(self, mock_index_function_app): worker_request = WorkerRequest(name='function_environment_reload_request', request=Request(FunctionRequest( 'hello', diff --git a/tests/unittests/test_types.py b/tests/unittests/test_types.py new file mode 100644 index 000000000..6438a0ee6 --- /dev/null +++ b/tests/unittests/test_types.py @@ -0,0 +1,211 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + +import tests.protos as protos + +from azure import functions as azf +from azure.functions import http as bind_http +from azure.functions import meta as bind_meta +from azure_functions_worker_v2.bindings import datumdef +from tests.utils.mock_classes import MockMBD, MockCMBD + + +class TestFunctions(unittest.TestCase): + + def test_http_request_bytes(self): + r = bind_http.HttpRequest( + 'get', + 'http://example.com/abc?a=1', + headers=dict(aaa='zzz', bAb='xYz'), + params=dict(a='b'), + route_params={'route': 'param'}, + body_type='bytes', + body=b'abc') + + self.assertEqual(r.method, 'GET') + self.assertEqual(r.url, 'http://example.com/abc?a=1') + self.assertEqual(r.params, {'a': 'b'}) + self.assertEqual(r.route_params, {'route': 'param'}) + + with self.assertRaises(TypeError): + r.params['a'] = 'z' + + self.assertEqual(r.get_body(), b'abc') + + with self.assertRaisesRegex(ValueError, 'does not contain valid JSON'): + r.get_json() + + h = r.headers + with self.assertRaises(AttributeError): + r.headers = dict() + + self.assertEqual(h['aaa'], 'zzz') + self.assertEqual(h['aaA'], 'zzz') + self.assertEqual(h['bab'], 'xYz') + self.assertEqual(h['BaB'], 'xYz') + + # test that request headers are read-only + with self.assertRaises(TypeError): + h['zzz'] = '123' + + def test_http_request_json(self): + r = bind_http.HttpRequest( + 'POST', + 'http://example.com/abc?a=1', + headers={}, + params={}, + route_params={}, + body_type='json', + body='{"a":1}') + + self.assertEqual(r.method, 'POST') + self.assertEqual(r.url, 'http://example.com/abc?a=1') + self.assertEqual(r.params, {}) + self.assertEqual(r.route_params, {}) + + self.assertEqual(r.get_body(), b'{"a":1}') + self.assertEqual(r.get_json(), {'a': 1}) + + def test_http_response(self): + r = azf.HttpResponse( + 'body™', + status_code=201, + headers=dict(aaa='zzz', bAb='xYz')) + + self.assertEqual(r.status_code, 201) + self.assertEqual(r.get_body(), b'body\xe2\x84\xa2') + + self.assertEqual(r.mimetype, 'text/plain') + self.assertEqual(r.charset, 'utf-8') + + h = r.headers + with self.assertRaises(AttributeError): + r.headers = dict() + + self.assertEqual(h['aaa'], 'zzz') + self.assertEqual(h['aaA'], 'zzz') + self.assertEqual(h['bab'], 'xYz') + self.assertEqual(h['BaB'], 'xYz') + + # test that response headers are mutable + h['zZz'] = '123' + self.assertEqual(h['zzz'], '123') + + +class Converter(bind_meta.InConverter, binding='foo'): + pass + + +class TestTriggerMetadataDecoder(unittest.TestCase): + + def test_scalar_typed_data_decoder_ok(self): + metadata = { + 'int_as_json': bind_meta.Datum(type='json', value='1'), + 'int_as_string': bind_meta.Datum(type='string', value='1'), + 'int_as_int': bind_meta.Datum(type='int', value=1), + 'string_as_json': bind_meta.Datum(type='json', value='"aaa"'), + 'string_as_string': bind_meta.Datum(type='string', value='aaa'), + 'dict_as_json': bind_meta.Datum(type='json', value='{"foo":"bar"}') + } + + cases = [ + ('int_as_json', int, 1), + ('int_as_string', int, 1), + ('int_as_int', int, 1), + ('string_as_json', str, 'aaa'), + ('string_as_string', str, 'aaa'), + ('dict_as_json', dict, {'foo': 'bar'}), + ] + + for field, pytype, expected in cases: + with self.subTest(field=field): + value = Converter._decode_trigger_metadata_field( + metadata, field, python_type=pytype) + + self.assertIsInstance(value, pytype) + self.assertEqual(value, expected) + + def test_scalar_typed_data_decoder_not_ok(self): + metadata = { + 'unsupported_type': + bind_meta.Datum(type='bytes', value=b'aaa'), + 'unexpected_json': + bind_meta.Datum(type='json', value='[1, 2, 3]'), + 'unexpected_data': + bind_meta.Datum(type='json', value='"foo"'), + } + + cases = [ + ( + 'unsupported_type', int, ValueError, + "unsupported type of field 'unsupported_type' in " + "trigger metadata: bytes" + ), + ( + 'unexpected_json', int, ValueError, + "cannot convert value of field 'unexpected_json' in " + "trigger metadata into int" + ), + ( + 'unexpected_data', int, ValueError, + "cannot convert value of field " + "'unexpected_data' in trigger metadata into int: " + "invalid literal for int" + ), + ( + 'unexpected_data', (int, float), ValueError, + "unexpected value type in field " + "'unexpected_data' in trigger metadata: str, " + "expected one of: int, float" + ), + ] + + for field, pytype, exc, msg in cases: + with self.subTest(field=field): + with self.assertRaisesRegex(exc, msg): + Converter._decode_trigger_metadata_field( + metadata, field, python_type=pytype) + + def test_model_binding_data_datum_ok(self): + sample_mbd = MockMBD(version="1.0", + source="AzureStorageBlobs", + content_type="application/json", + content="{\"Connection\":\"python-worker-tests\"," + "\"ContainerName\":\"test-blob\"," + "\"BlobName\":\"test.txt\"}") + + datum: bind_meta.Datum = bind_meta.Datum(value=sample_mbd, + type='model_binding_data') + + self.assertEqual(datum.value, sample_mbd) + self.assertEqual(datum.type, "model_binding_data") + + def test_model_binding_data_td_ok(self): + mock_mbd = protos.TypedData(model_binding_data={'version': '1.0'}) + mbd_datum = datumdef.Datum.from_typed_data(mock_mbd) + + self.assertEqual(mbd_datum.type, 'model_binding_data') + + def test_collection_model_binding_data_datum_ok(self): + sample_mbd = MockMBD(version="1.0", + source="AzureStorageBlobs", + content_type="application/json", + content="{\"Connection\":\"python-worker-tests\"," + "\"ContainerName\":\"test-blob\"," + "\"BlobName\":\"test.txt\"}") + sample_cmbd = MockCMBD(model_binding_data=[sample_mbd, sample_mbd]) + + datum: bind_meta.Datum = bind_meta.Datum(value=sample_cmbd, + type='collection_model_binding_data') + + self.assertEqual(datum.value, sample_cmbd) + self.assertEqual(datum.type, "collection_model_binding_data") + + def test_collection_model_binding_data_td_ok(self): + mock_cmbd = protos.TypedData( + collection_model_binding_data={'model_binding_data': [{'version': '1.0'}]} + ) + cmbd_datum = datumdef.Datum.from_typed_data(mock_cmbd) + + self.assertEqual(cmbd_datum.type, 'collection_model_binding_data') diff --git a/tests/utils/mock_classes.py b/tests/utils/mock_classes.py new file mode 100644 index 000000000..6e60a97da --- /dev/null +++ b/tests/utils/mock_classes.py @@ -0,0 +1,36 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from typing import Any, List + +# This represents the top level protos request sent from the host +class WorkerRequest: + def __init__(self, name: str, request: Any, properties: dict): + self.name = name + self.request = request + self.properties = properties + + +# This represents the inner request +class Request: + def __init__(self, name: Any): + self.worker_init_request = name + self.function_environment_reload_request = name + + +# This represents the Function Init/Metadata/Load/Invocation request +class FunctionRequest: + def __init__(self, capabilities: Any, function_app_directory: Any): + self.capabilities = capabilities + self.function_app_directory = function_app_directory + +class MockMBD: + def __init__(self, version: str, source: str, + content_type: str, content: str): + self.version = version + self.source = source + self.content_type = content_type + self.content = content + +class MockCMBD: + def __init__(self, model_binding_data: List[MockMBD]): + self.model_binding_data = model_binding_data From 2f125d9a19780feabc4912fa6c6369309955f358 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Thu, 12 Jun 2025 16:09:46 -0500 Subject: [PATCH 42/45] app settings tests, lint --- azure_functions_worker_v2/bindings/context.py | 1 - azure_functions_worker_v2/http_v2.py | 5 - azure_functions_worker_v2/loader.py | 7 +- .../utils/app_setting_manager.py | 6 +- azure_functions_worker_v2/utils/constants.py | 91 +++++++------------ tests/unittests/test_app_setting_manager.py | 44 +++++++++ 6 files changed, 79 insertions(+), 75 deletions(-) create mode 100644 tests/unittests/test_app_setting_manager.py diff --git a/azure_functions_worker_v2/bindings/context.py b/azure_functions_worker_v2/bindings/context.py index 7d3e15b31..eb5229ca4 100644 --- a/azure_functions_worker_v2/bindings/context.py +++ b/azure_functions_worker_v2/bindings/context.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. import threading -from typing import Type from .retrycontext import RetryContext from .tracecontext import TraceContext diff --git a/azure_functions_worker_v2/http_v2.py b/azure_functions_worker_v2/http_v2.py index fed5643ca..d6040a9a0 100644 --- a/azure_functions_worker_v2/http_v2.py +++ b/azure_functions_worker_v2/http_v2.py @@ -5,11 +5,9 @@ import asyncio import importlib import socket -import sys from typing import Any, Dict from azure_functions_worker_v2.utils.constants import ( - BASE_EXT_SUPPORTED_PY_MINOR_VERSION, X_MS_INVOCATION_ID, ) from azure_functions_worker_v2.logging import logger @@ -278,9 +276,6 @@ def ext_base(cls): @classmethod def _check_http_v2_enabled(cls): - if sys.version_info.minor < BASE_EXT_SUPPORTED_PY_MINOR_VERSION: - return False - import azurefunctions.extensions.base as ext_base cls._ext_base = ext_base diff --git a/azure_functions_worker_v2/loader.py b/azure_functions_worker_v2/loader.py index 881bc785a..36cced348 100644 --- a/azure_functions_worker_v2/loader.py +++ b/azure_functions_worker_v2/loader.py @@ -9,7 +9,7 @@ import time from datetime import timedelta -from typing import Any, Dict, Optional, Union +from typing import Dict, Optional, Union from .functions import Registry @@ -29,11 +29,6 @@ from .utils.env_state import get_app_setting from .utils.wrappers import attach_message_to_exception -_AZURE_NAMESPACE = '__app__' -_DEFAULT_SCRIPT_FILENAME = '__init__.py' -_DEFAULT_ENTRY_POINT = 'main' -_submodule_dirsL: list[Any] = [] - def convert_to_seconds(timestr: str): x = time.strptime(timestr, '%H:%M:%S') diff --git a/azure_functions_worker_v2/utils/app_setting_manager.py b/azure_functions_worker_v2/utils/app_setting_manager.py index a8f8609e1..cfaed405a 100644 --- a/azure_functions_worker_v2/utils/app_setting_manager.py +++ b/azure_functions_worker_v2/utils/app_setting_manager.py @@ -3,7 +3,7 @@ import os from .constants import ( - FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED, + PYTHON_APPLICATIONINSIGHTS_ENABLE_TELEMETRY, PYTHON_ENABLE_DEBUG_LOGGING, PYTHON_ENABLE_OPENTELEMETRY, PYTHON_SCRIPT_FILE_NAME, @@ -16,9 +16,9 @@ def get_python_appsetting_state(): python_specific_settings = \ [PYTHON_THREADPOOL_THREAD_COUNT, PYTHON_ENABLE_DEBUG_LOGGING, - FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED, PYTHON_SCRIPT_FILE_NAME, - PYTHON_ENABLE_OPENTELEMETRY] + PYTHON_ENABLE_OPENTELEMETRY, + PYTHON_APPLICATIONINSIGHTS_ENABLE_TELEMETRY] app_setting_states = "".join( f"{app_setting}: {current_vars[app_setting]} | " diff --git a/azure_functions_worker_v2/utils/constants.py b/azure_functions_worker_v2/utils/constants.py index a04e2596a..ffdaf37c8 100644 --- a/azure_functions_worker_v2/utils/constants.py +++ b/azure_functions_worker_v2/utils/constants.py @@ -1,92 +1,63 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -# TODO: organize this better - import sys +# Constants for Azure Functions Python Worker +CUSTOMER_PACKAGES_PATH = "/home/site/wwwroot/.python_packages/lib/site" \ + "-packages" +HTTP = "http" +HTTP_TRIGGER = "httpTrigger" +METADATA_PROPERTIES_WORKER_INDEXED = "worker_indexed" +MODULE_NOT_FOUND_TS_URL = "https://aka.ms/functions-modulenotfound" +PYTHON_LANGUAGE_RUNTIME = "python" +RETRY_POLICY = "retry_policy" TRUE = "true" TRACEPARENT = "traceparent" TRACESTATE = "tracestate" +X_MS_INVOCATION_ID = "x-ms-invocation-id" + # Capabilities +FUNCTION_DATA_CACHE = "FunctionDataCache" +HTTP_URI = "HttpUri" RAW_HTTP_BODY_BYTES = "RawHttpBodyBytes" -TYPED_DATA_COLLECTION = "TypedDataCollection" +REQUIRES_ROUTE_PARAMETERS = "RequiresRouteParameters" RPC_HTTP_BODY_ONLY = "RpcHttpBodyOnly" RPC_HTTP_TRIGGER_METADATA_REMOVED = "RpcHttpTriggerMetadataRemoved" -WORKER_STATUS = "WorkerStatus" SHARED_MEMORY_DATA_TRANSFER = "SharedMemoryDataTransfer" -FUNCTION_DATA_CACHE = "FunctionDataCache" -HTTP_URI = "HttpUri" -REQUIRES_ROUTE_PARAMETERS = "RequiresRouteParameters" +TYPED_DATA_COLLECTION = "TypedDataCollection" # When this capability is enabled, logs are not piped back to the # host from the worker. Logs will directly go to where the user has # configured them to go. This is to ensure that the logs are not # duplicated. WORKER_OPEN_TELEMETRY_ENABLED = "WorkerOpenTelemetryEnabled" +WORKER_STATUS = "WorkerStatus" + # Platform Environment Variables AZURE_WEBJOBS_SCRIPT_ROOT = "AzureWebJobsScriptRoot" CONTAINER_NAME = "CONTAINER_NAME" -# Python Specific Feature Flags and App Settings -PYTHON_THREADPOOL_THREAD_COUNT = "PYTHON_THREADPOOL_THREAD_COUNT" -PYTHON_ENABLE_DEBUG_LOGGING = "PYTHON_ENABLE_DEBUG_LOGGING" -FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED = \ - "FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED" -""" -Comma-separated list of directories where shared memory maps can be created for -data transfer between host and worker. -""" -UNIX_SHARED_MEMORY_DIRECTORIES = "FUNCTIONS_UNIX_SHARED_MEMORY_DIRECTORIES" - -# Setting Defaults -PYTHON_THREADPOOL_THREAD_COUNT_DEFAULT = 1 -PYTHON_THREADPOOL_THREAD_COUNT_MIN = 1 -PYTHON_THREADPOOL_THREAD_COUNT_MAX = sys.maxsize -PYTHON_THREADPOOL_THREAD_COUNT_MAX_37 = 32 - -# new programming model default script file name -PYTHON_SCRIPT_FILE_NAME = "PYTHON_SCRIPT_FILE_NAME" -PYTHON_SCRIPT_FILE_NAME_DEFAULT = "function_app.py" - -# External Site URLs -MODULE_NOT_FOUND_TS_URL = "https://aka.ms/functions-modulenotfound" - -PYTHON_LANGUAGE_RUNTIME = "python" - -# Settings for V2 programming model -RETRY_POLICY = "retry_policy" - -# Paths -CUSTOMER_PACKAGES_PATH = "/home/site/wwwroot/.python_packages/lib/site" \ - "-packages" - -METADATA_PROPERTIES_WORKER_INDEXED = "worker_indexed" - -# Header names -X_MS_INVOCATION_ID = "x-ms-invocation-id" - -# Trigger Names -HTTP_TRIGGER = "httpTrigger" - -# Output Names -HTTP = "http" -# Base extension supported Python minor version -BASE_EXT_SUPPORTED_PY_MINOR_VERSION = 8 - -# Appsetting to turn on OpenTelemetry support/features -# A value of "true" enables the setting -PYTHON_ENABLE_OPENTELEMETRY = "PYTHON_ENABLE_OPENTELEMETRY" +# Python Specific Feature Flags and App Settings +# Appsetting to specify AppInsights connection string +APPLICATIONINSIGHTS_CONNECTION_STRING = "APPLICATIONINSIGHTS_CONNECTION_STRING" # Appsetting to turn on ApplicationInsights support/features # A value of "true" enables the setting PYTHON_APPLICATIONINSIGHTS_ENABLE_TELEMETRY = \ "PYTHON_APPLICATIONINSIGHTS_ENABLE_TELEMETRY" - # Appsetting to specify root logger name of logger to collect telemetry for # Used by Azure monitor distro (Application Insights) PYTHON_APPLICATIONINSIGHTS_LOGGER_NAME = "PYTHON_APPLICATIONINSIGHTS_LOGGER_NAME" PYTHON_APPLICATIONINSIGHTS_LOGGER_NAME_DEFAULT = "" - -# Appsetting to specify AppInsights connection string -APPLICATIONINSIGHTS_CONNECTION_STRING = "APPLICATIONINSIGHTS_CONNECTION_STRING" +PYTHON_ENABLE_DEBUG_LOGGING = "PYTHON_ENABLE_DEBUG_LOGGING" +# Appsetting to turn on OpenTelemetry support/features +# A value of "true" enables the setting +PYTHON_ENABLE_OPENTELEMETRY = "PYTHON_ENABLE_OPENTELEMETRY" +# Allows for non-default script file name +PYTHON_SCRIPT_FILE_NAME = "PYTHON_SCRIPT_FILE_NAME" +PYTHON_SCRIPT_FILE_NAME_DEFAULT = "function_app.py" +PYTHON_THREADPOOL_THREAD_COUNT = "PYTHON_THREADPOOL_THREAD_COUNT" +PYTHON_THREADPOOL_THREAD_COUNT_DEFAULT = 1 +PYTHON_THREADPOOL_THREAD_COUNT_MAX = sys.maxsize +PYTHON_THREADPOOL_THREAD_COUNT_MIN = 1 diff --git a/tests/unittests/test_app_setting_manager.py b/tests/unittests/test_app_setting_manager.py new file mode 100644 index 000000000..09f27b804 --- /dev/null +++ b/tests/unittests/test_app_setting_manager.py @@ -0,0 +1,44 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import os + +from azure_functions_worker_v2.utils.app_setting_manager import get_python_appsetting_state +from azure_functions_worker_v2.utils.constants import ( + PYTHON_ENABLE_DEBUG_LOGGING, + PYTHON_THREADPOOL_THREAD_COUNT, +) +from tests.utils import testutils +from unittest.mock import patch + + +class TestDefaultAppSettingsLogs(testutils.AsyncTestCase): + """Tests for default app settings logs.""" + + def test_get_python_appsetting_state(self): + app_setting_state = get_python_appsetting_state() + expected_string = "" + self.assertEquals(expected_string, app_setting_state) + + +class TestNonDefaultAppSettingsLogs(testutils.AsyncTestCase): + """Tests for non-default app settings logs.""" + + @classmethod + def setUpClass(cls): + os_environ = os.environ.copy() + os_environ[PYTHON_THREADPOOL_THREAD_COUNT] = '20' + os_environ[PYTHON_ENABLE_DEBUG_LOGGING] = '1' + cls._patch_environ = patch.dict('os.environ', os_environ) + cls._patch_environ.start() + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + cls._patch_environ.stop() + + def test_get_python_appsetting_state(self): + app_setting_state = get_python_appsetting_state() + self.assertIn("PYTHON_THREADPOOL_THREAD_COUNT: 20 | ", + app_setting_state) + self.assertIn("PYTHON_ENABLE_DEBUG_LOGGING: 1 | ", app_setting_state) From be5e361490c3c8a5b1771b712b629ef398f37a7f Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Fri, 13 Jun 2025 13:14:43 -0500 Subject: [PATCH 43/45] tests! --- azure_functions_worker_v2/bindings/meta.py | 6 +- .../bindings/nullable_converters.py | 18 +- azure_functions_worker_v2/handle_event.py | 1 - azure_functions_worker_v2/loader.py | 8 +- azure_functions_worker_v2/otel.py | 14 +- eng/templates/jobs/ci-unit-tests.yml | 2 + .../unittests/basic_function/function_app.py | 2 - tests/unittests/test_app_setting_manager.py | 5 +- tests/unittests/test_datumdef.py | 161 +++++++++ tests/unittests/test_deferred_bindings.py | 3 +- tests/unittests/test_handle_event.py | 22 +- tests/unittests/test_http_v2.py | 245 +++++++++++++ tests/unittests/test_logging.py | 32 ++ tests/unittests/test_nullable_converters.py | 112 ++++++ tests/unittests/test_opentelemetry.py | 201 +++++++++++ tests/unittests/test_rpc_messages.py | 124 +++++++ tests/unittests/test_typing_inspect.py | 137 ++++++++ tests/unittests/test_utilities.py | 327 ++++++++++++++++++ tests/utils/mock_classes.py | 18 +- 19 files changed, 1394 insertions(+), 44 deletions(-) create mode 100644 tests/unittests/test_datumdef.py create mode 100644 tests/unittests/test_http_v2.py create mode 100644 tests/unittests/test_logging.py create mode 100644 tests/unittests/test_nullable_converters.py create mode 100644 tests/unittests/test_opentelemetry.py create mode 100644 tests/unittests/test_rpc_messages.py create mode 100644 tests/unittests/test_typing_inspect.py create mode 100644 tests/unittests/test_utilities.py diff --git a/azure_functions_worker_v2/bindings/meta.py b/azure_functions_worker_v2/bindings/meta.py index cd446a0de..59144d5e2 100644 --- a/azure_functions_worker_v2/bindings/meta.py +++ b/azure_functions_worker_v2/bindings/meta.py @@ -171,7 +171,7 @@ def from_incoming_proto( val = pb.data datum = Datum.from_typed_data(val) else: - raise TypeError('Unknown ParameterBindingType: %s', pb_type) + raise TypeError('Unknown ParameterBindingType: %s' % pb_type) try: # if the binding is an sdk type binding @@ -189,7 +189,7 @@ def from_incoming_proto( raise TypeError( 'unable to decode incoming TypedData: ' 'unsupported combination of TypedData field %s ' - 'and expected binding type %s', repr(dt), binding_obj) + 'and expected binding type %s' % (repr(dt), binding_obj)) def get_datum(binding: str, obj: Any, @@ -205,7 +205,7 @@ def get_datum(binding: str, obj: Any, raise TypeError( 'unable to encode outgoing TypedData: ' 'unsupported type "%s" for ' - 'Python type "%s"', binding, type(obj).__name__) + 'Python type "%s"' % (binding, type(obj).__name__)) return datum diff --git a/azure_functions_worker_v2/bindings/nullable_converters.py b/azure_functions_worker_v2/bindings/nullable_converters.py index 05dd7683d..34e429154 100644 --- a/azure_functions_worker_v2/bindings/nullable_converters.py +++ b/azure_functions_worker_v2/bindings/nullable_converters.py @@ -22,8 +22,8 @@ def to_nullable_string(nullable: Optional[str], property_name: str, protos): if nullable is not None: raise TypeError( "A 'str' type was expected instead of a '%s' " - "type. Cannot parse value %s of '%s'.", - type(nullable), nullable, property_name) + "type. Cannot parse value %s of '%s'." + % (type(nullable), nullable, property_name)) return None @@ -45,8 +45,8 @@ def to_nullable_bool(nullable: Optional[bool], property_name: str, protos): if nullable is not None: raise TypeError( "A 'bool' type was expected instead of a '%s' " - "type. Cannot parse value %s of '%s'.", - type(nullable), nullable, property_name) + "type. Cannot parse value %s of '%s'." + % (type(nullable), nullable, property_name)) return None @@ -74,14 +74,14 @@ def to_nullable_double(nullable: Optional[Union[str, int, float]], except Exception: raise TypeError( "Cannot parse value %s of '%s' to " - "float.", nullable, property_name) + "float." % (nullable, property_name)) if nullable is not None: raise TypeError( "A 'int' or 'float'" " type was expected instead of a '%s' " - "type. Cannot parse value %s of '%s'.", - type(nullable), nullable, property_name) + "type. Cannot parse value %s of '%s'." + % (type(nullable), nullable, property_name)) return None @@ -110,6 +110,6 @@ def to_nullable_timestamp(date_time: Optional[Union[datetime, int]], raise TypeError( "A 'datetime' or 'int'" " type was expected instead of a '%s' " - "type. Cannot parse value %s of '%s'.", - type(date_time), date_time, property_name) + "type. Cannot parse value %s of '%s'." + % (type(date_time), date_time, property_name)) return None diff --git a/azure_functions_worker_v2/handle_event.py b/azure_functions_worker_v2/handle_event.py index 3202380f3..5d263c17a 100644 --- a/azure_functions_worker_v2/handle_event.py +++ b/azure_functions_worker_v2/handle_event.py @@ -1,6 +1,5 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - import json import logging import os diff --git a/azure_functions_worker_v2/loader.py b/azure_functions_worker_v2/loader.py index 36cced348..0e9ccf431 100644 --- a/azure_functions_worker_v2/loader.py +++ b/azure_functions_worker_v2/loader.py @@ -11,7 +11,6 @@ from datetime import timedelta from typing import Dict, Optional, Union - from .functions import Registry from .logging import logger @@ -168,14 +167,15 @@ def index_function_app(function_path: str): else: raise ValueError( "More than one %s or other top " - "level function app instances are defined.", app.__class__.__name__) + "level function app instances are defined." + % app.__class__.__name__) if not app: script_file_name = get_app_setting( setting=PYTHON_SCRIPT_FILE_NAME, default_value=PYTHON_SCRIPT_FILE_NAME_DEFAULT) - raise ValueError("Could not find top level function app instances in %s.", - script_file_name) + raise ValueError("Could not find top level function app instances in %s." + % script_file_name) return app.get_functions() diff --git a/azure_functions_worker_v2/otel.py b/azure_functions_worker_v2/otel.py index 742d142f6..9becc03af 100644 --- a/azure_functions_worker_v2/otel.py +++ b/azure_functions_worker_v2/otel.py @@ -53,8 +53,8 @@ def update_opentelemetry_status(): TraceContextTextMapPropagator, ) - OTelManager.set_context_api(context_api) - OTelManager.set_trace_context_propagator(TraceContextTextMapPropagator()) + otel_manager.set_context_api(context_api) + otel_manager.set_trace_context_propagator(TraceContextTextMapPropagator()) except ImportError: logger.exception( @@ -88,26 +88,26 @@ def initialize_azure_monitor(): default_value=PYTHON_APPLICATIONINSIGHTS_LOGGER_NAME_DEFAULT ), ) - OTelManager.set_azure_monitor_available(True) + otel_manager.set_azure_monitor_available(azure_monitor_available=True) logger.info("Successfully configured Azure monitor distro.") except ImportError: logger.exception( "Cannot import Azure Monitor distro." ) - OTelManager.set_azure_monitor_available(False) + otel_manager.set_azure_monitor_available(False) except Exception: logger.exception( "Error initializing Azure monitor distro." ) - OTelManager.set_azure_monitor_available(False) + otel_manager.set_azure_monitor_available(False) def configure_opentelemetry(invocation_context): carrier = {TRACEPARENT: invocation_context.trace_context.trace_parent, TRACESTATE: invocation_context.trace_context.trace_state} - ctx = OTelManager.get_trace_context_propagator().extract(carrier) - OTelManager.get_context_api().attach(ctx) + ctx = otel_manager.get_trace_context_propagator().extract(carrier) + otel_manager.get_context_api().attach(ctx) otel_manager = OTelManager() diff --git a/eng/templates/jobs/ci-unit-tests.yml b/eng/templates/jobs/ci-unit-tests.yml index e2414e565..082127925 100644 --- a/eng/templates/jobs/ci-unit-tests.yml +++ b/eng/templates/jobs/ci-unit-tests.yml @@ -22,3 +22,5 @@ jobs: - bash: | python -m pytest -q -n auto --dist loadfile --reruns 4 --instafail --cov=./azure_functions_worker_v2 --cov-report xml --cov-branch tests/unittests displayName: "Running $(PYTHON_VERSION) Unit Tests" + env: + AzureWebJobsStorage: $(AzureWebJobsStorage) diff --git a/tests/unittests/basic_function/function_app.py b/tests/unittests/basic_function/function_app.py index 1781c65ab..67f0dadfd 100644 --- a/tests/unittests/basic_function/function_app.py +++ b/tests/unittests/basic_function/function_app.py @@ -2,8 +2,6 @@ # Licensed under the MIT License. import logging -import time -from datetime import datetime import azure.functions as func diff --git a/tests/unittests/test_app_setting_manager.py b/tests/unittests/test_app_setting_manager.py index 09f27b804..10ddfb6cc 100644 --- a/tests/unittests/test_app_setting_manager.py +++ b/tests/unittests/test_app_setting_manager.py @@ -2,7 +2,8 @@ # Licensed under the MIT License. import os -from azure_functions_worker_v2.utils.app_setting_manager import get_python_appsetting_state +from azure_functions_worker_v2.utils.app_setting_manager import ( + get_python_appsetting_state) from azure_functions_worker_v2.utils.constants import ( PYTHON_ENABLE_DEBUG_LOGGING, PYTHON_THREADPOOL_THREAD_COUNT, @@ -17,7 +18,7 @@ class TestDefaultAppSettingsLogs(testutils.AsyncTestCase): def test_get_python_appsetting_state(self): app_setting_state = get_python_appsetting_state() expected_string = "" - self.assertEquals(expected_string, app_setting_state) + self.assertEqual(expected_string, app_setting_state) class TestNonDefaultAppSettingsLogs(testutils.AsyncTestCase): diff --git a/tests/unittests/test_datumdef.py b/tests/unittests/test_datumdef.py new file mode 100644 index 000000000..4631cb1ac --- /dev/null +++ b/tests/unittests/test_datumdef.py @@ -0,0 +1,161 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + +import tests.protos as protos + +from datetime import datetime +from http.cookies import SimpleCookie + +from azure_functions_worker_v2.bindings.datumdef import ( + Datum, + parse_cookie_attr_expires, + parse_cookie_attr_same_site, + parse_to_rpc_http_cookie_list, +) +from azure_functions_worker_v2.bindings.nullable_converters import ( + to_nullable_bool, + to_nullable_double, + to_nullable_string, + to_nullable_timestamp, +) + + +class TestDatumDef(unittest.TestCase): + def test_parse_cookie_attr_expires_none(self): + self.assertEqual(parse_cookie_attr_expires({"expires": None}), None) + + def test_parse_cookie_attr_expires_zero_length(self): + self.assertEqual(parse_cookie_attr_expires({"expires": ""}), None) + + def test_parse_cookie_attr_expires_valid(self): + self.assertEqual(parse_cookie_attr_expires( + {"expires": "Thu, 12 Jan 2017 13:55:08 GMT"}), + datetime.strptime("Thu, 12 Jan 2017 13:55:08 GMT", + "%a, %d %b %Y %H:%M:%S GMT")) + + def test_parse_cookie_attr_expires_value_error(self): + with self.assertRaises(ValueError): + parse_cookie_attr_expires( + {"expires": "Thu, 12 Jan 2017 13:550:08 GMT"}) + + def test_parse_cookie_attr_expires_overflow_error(self): + with self.assertRaises(ValueError): + parse_cookie_attr_expires( + {"expires": "Thu, 12 Jan 9999999999999999 13:55:08 GMT"}) + + def test_parse_cookie_attr_same_site_default(self): + self.assertEqual(parse_cookie_attr_same_site( + {}, protos), + getattr(protos.RpcHttpCookie.SameSite, "None")) + + def test_parse_cookie_attr_same_site_lax(self): + self.assertEqual(parse_cookie_attr_same_site( + {'samesite': 'lax'}, protos), + getattr(protos.RpcHttpCookie.SameSite, "Lax")) + + def test_parse_cookie_attr_same_site_strict(self): + self.assertEqual(parse_cookie_attr_same_site( + {'samesite': 'strict'}, protos), + getattr(protos.RpcHttpCookie.SameSite, "Strict")) + + def test_parse_cookie_attr_same_site_explicit_none(self): + self.assertEqual(parse_cookie_attr_same_site( + {'samesite': 'none'}, protos), + getattr(protos.RpcHttpCookie.SameSite, "ExplicitNone")) + + def test_parse_to_rpc_http_cookie_list_none(self): + self.assertEqual(parse_to_rpc_http_cookie_list(None, protos), None) + + @unittest.skip("TODO: fix this test. Figure out what to do with Timestamp") + def test_parse_to_rpc_http_cookie_list_valid(self): + headers = [ + 'foo=bar; Path=/some/path; Secure; HttpOnly; Domain=123; ' + 'SameSite=Lax; Max-Age=12345; Expires=Thu, 12 Jan 2017 13:55:08 ' + 'GMT;', + 'foo2=bar; Path=/some/path2; Secure; HttpOnly; Domain=123; ' + 'SameSite=Lax; Max-Age=12345; Expires=Thu, 12 Jan 2017 13:55:08 ' + 'GMT;'] + + cookies = SimpleCookie('\r\n'.join(headers)) + + cookie1 = protos.RpcHttpCookie(name="foo", + value="bar", + domain=to_nullable_string("123", + "cookie.domain", + protos), + path=to_nullable_string("/some/path", + "cookie.path", + protos), + expires=to_nullable_timestamp( + parse_cookie_attr_expires( + { + "expires": "Thu, " + "12 Jan 2017 13:55:08" + " GMT"}), + 'cookie.expires', + protos), + secure=to_nullable_bool( + bool("True"), + 'cookie.secure', + protos), + http_only=to_nullable_bool( + bool("True"), + 'cookie.httpOnly', + protos), + same_site=parse_cookie_attr_same_site( + {"samesite": "Lax"}, + protos), + max_age=to_nullable_double( + 12345, + 'cookie.maxAge', + protos)) + + cookie2 = protos.RpcHttpCookie(name="foo2", + value="bar", + domain=to_nullable_string("123", + "cookie.domain", + protos), + path=to_nullable_string("/some/path2", + "cookie.path", + protos), + expires=to_nullable_timestamp( + parse_cookie_attr_expires( + { + "expires": "Thu, " + "12 Jan 2017 13:55:08" + " GMT"}), + 'cookie.expires', + protos), + secure=to_nullable_bool( + bool("True"), + 'cookie.secure', + protos), + http_only=to_nullable_bool( + bool("True"), + 'cookie.httpOnly', + protos), + same_site=parse_cookie_attr_same_site( + {"samesite": "Lax"}, + protos), + max_age=to_nullable_double( + 12345, + 'cookie.maxAge', + protos)) + + rpc_cookies = parse_to_rpc_http_cookie_list([cookies], protos) + self.assertEqual(cookie1, rpc_cookies[0]) + self.assertEqual(cookie2, rpc_cookies[1]) + + def test_parse_to_rpc_http_cookie_list_no_cookie(self): + datum = Datum( + type='http', + value=dict( + status_code=None, + headers=None, + body=None, + ) + ) + + self.assertIsNone( + parse_to_rpc_http_cookie_list(datum.value.get('cookies'), protos)) diff --git a/tests/unittests/test_deferred_bindings.py b/tests/unittests/test_deferred_bindings.py index 85866bec8..0bad522d6 100644 --- a/tests/unittests/test_deferred_bindings.py +++ b/tests/unittests/test_deferred_bindings.py @@ -17,6 +17,7 @@ EVENTHUB_SAMPLE_CONTENT = b"\x00Sr\xc1\x8e\x08\xa3\x1bx-opt-sequence-number-epochT\xff\xa3\x15x-opt-sequence-numberU\x04\xa3\x0cx-opt-offset\x81\x00\x00\x00\x01\x00\x00\x010\xa3\x13x-opt-enqueued-time\x00\xa3\x1dcom.microsoft:datetime-offset\x81\x08\xddW\x05\xc3Q\xcf\x10\x00St\xc1I\x02\xa1\rDiagnostic-Id\xa1700-bdc3fde4889b4e907e0c9dcb46ff8d92-21f637af293ef13b-00\x00Su\xa0\x08message1" # noqa: E501 + class TestDeferredBindingsEnabled(testutils.AsyncTestCase): def test_mbd_deferred_bindings_enabled_decode(self): @@ -93,4 +94,4 @@ async def test_check_deferred_bindings_enabled(self): self.assertEqual(meta.check_deferred_bindings_enabled( ContainerClient, True), (True, True)) self.assertEqual(meta.check_deferred_bindings_enabled( - StorageStreamDownloader, True), (True, True)) \ No newline at end of file + StorageStreamDownloader, True), (True, True)) diff --git a/tests/unittests/test_handle_event.py b/tests/unittests/test_handle_event.py index 3381a327b..95247f942 100644 --- a/tests/unittests/test_handle_event.py +++ b/tests/unittests/test_handle_event.py @@ -114,23 +114,19 @@ async def test_worker_init_request_with_exception(self): self.assertIsNotNone(result.worker_metadata.worker_bitness) self.assertEqual(result.result.status, 1) - async def test_functions_metadata_request(self): + @patch("azure_functions_worker_v2.loader.index_function_app", + return_value=True) + async def test_functions_metadata_request(self, mock_index_function_app): handle_event.protos = test_protos metadata_result = await functions_metadata_request(None) self.assertEqual(metadata_result.result.status, 1) - - def test_functions_metadata_request_with_exception(self): - pass - - def test_invocation_request_sync(self): - pass - - def test_invocation_request_async(self): - pass - - def test_invocation_request_with_exception(self): - pass + @patch("azure_functions_worker_v2.metadata_exception", + return_value=Exception) + async def test_functions_metadata_request_with_exception(self): + handle_event.protos = test_protos + metadata_result = await functions_metadata_request(None) + self.assertEqual(metadata_result.result.status, 0) @patch("azure_functions_worker_v2.loader.index_function_app", return_value=True) diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py new file mode 100644 index 000000000..b21557394 --- /dev/null +++ b/tests/unittests/test_http_v2.py @@ -0,0 +1,245 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import asyncio +import socket +import unittest + +from azure_functions_worker_v2.http_v2 import ( + AsyncContextReference, + SingletonMeta, + get_unused_tcp_port, + http_coordinator, +) +from tests.utils.mock_classes import MockHttpResponse, MockHttpRequest +from unittest.mock import MagicMock, patch + + +class TestHttpCoordinator(unittest.TestCase): + 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 + 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, 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 " + f"{self.invoc_id}") + + 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) + 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" + + with self.assertRaises(asyncio.TimeoutError): + self.loop.run_until_complete( + asyncio.wait_for( + http_coordinator.get_http_request_async( + invalid_invoc_id), + timeout=1 + ) + ) + + def test_await_http_response_async_valid_invocation(self): + invoc_id = "valid_invocation" + expected_response = self.http_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 = 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) + + 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: + 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}'") + + 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 = 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( + http_coordinator.await_http_response_async(invoc_id)) + self.assertEqual(str(context.exception), + f"No http response found for invocation {invoc_id}") + + +class TestAsyncContextReference(unittest.TestCase): + + def setUp(self): + 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() + 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 + + +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) diff --git a/tests/unittests/test_logging.py b/tests/unittests/test_logging.py new file mode 100644 index 000000000..85dc5ca69 --- /dev/null +++ b/tests/unittests/test_logging.py @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + +from azure_functions_worker_v2.logging import format_exception + + +class TestLogging(unittest.TestCase): + + def test_format_exception(self): + def call0(fn): + call1(fn) + + def call1(fn): + call2(fn) + + def call2(fn): + fn() + + def raising_function(): + raise ValueError("Value error being raised.", ) + + try: + call0(raising_function) + except ValueError as e: + processed_exception = format_exception(e) + self.assertIn("call0", processed_exception) + self.assertIn("call1", processed_exception) + self.assertIn("call2", processed_exception) + self.assertIn("f", processed_exception) + self.assertRegex(processed_exception, + r".*tests\\unittests\\test_logging.py.*") diff --git a/tests/unittests/test_nullable_converters.py b/tests/unittests/test_nullable_converters.py new file mode 100644 index 000000000..770355401 --- /dev/null +++ b/tests/unittests/test_nullable_converters.py @@ -0,0 +1,112 @@ +import datetime +import pytest +import unittest + +from google.protobuf.timestamp_pb2 import Timestamp + +import tests.protos as protos +from azure_functions_worker_v2.bindings.nullable_converters import ( + to_nullable_bool, + to_nullable_double, + to_nullable_string, + to_nullable_timestamp, +) + +try: + from http.cookies import SimpleCookie +except ImportError: + from Cookie import SimpleCookie + +headers = ['foo=bar; Path=/some/path; Secure', + 'foo2=42; Domain=123; Expires=Thu, 12-Jan-2017 13:55:08 GMT; ' + 'Path=/; Max-Age=dd;'] + +cookies = SimpleCookie('\r\n'.join(headers)) + + +class TestNullableConverters(unittest.TestCase): + def test_to_nullable_string_none(self): + self.assertEqual(to_nullable_string(None, "name", protos), None) + + def test_to_nullable_string_valid(self): + self.assertEqual(to_nullable_string("dummy", "name", protos), + protos.NullableString(value="dummy")) + + def test_to_nullable_string_wrong_type(self): + with pytest.raises(Exception) as e: + self.assertEqual(to_nullable_string(123, "name", protos), + protos.NullableString(value="dummy")) + self.assertEqual(type(e), TypeError) + + def test_to_nullable_bool_none(self): + self.assertEqual(to_nullable_bool(None, "name", protos), None) + + def test_to_nullable_bool_valid(self): + self.assertEqual(to_nullable_bool(True, "name", protos), + protos.NullableBool(value=True)) + + def test_to_nullable_bool_wrong_type(self): + with pytest.raises(Exception) as e: + to_nullable_bool("True", "name", protos) + + self.assertEqual(e.type, TypeError) + self.assertEqual(e.value.args[0], + "A 'bool' type was expected instead of a '' type. " + "Cannot parse value True of 'name'.") + + def test_to_nullable_double_str(self): + self.assertEqual(to_nullable_double("12", "name", protos), + protos.NullableDouble(value=12)) + + def test_to_nullable_double_empty_str(self): + self.assertEqual(to_nullable_double("", "name", protos), None) + + def test_to_nullable_double_invalid_str(self): + with pytest.raises(TypeError) as e: + to_nullable_double("222d", "name", protos) + + self.assertEqual(e.type, TypeError) + self.assertEqual(e.value.args[0], + "Cannot parse value 222d of 'name' to float.") + + def test_to_nullable_double_int(self): + self.assertEqual(to_nullable_double(12, "name", protos), + protos.NullableDouble(value=12)) + + def test_to_nullable_double_float(self): + self.assertEqual(to_nullable_double(12.0, "name", protos), + protos.NullableDouble(value=12)) + + def test_to_nullable_double_none(self): + self.assertEqual(to_nullable_double(None, "name", protos), None) + + def test_to_nullable_double_wrong_type(self): + with pytest.raises(Exception) as e: + to_nullable_double(object(), "name", protos) + + self.assertIn( + "A 'int' or 'float' type was expected instead of a '' type", + e.value.args[0]) + self.assertEqual(e.type, TypeError) + + @unittest.skip("TODO: fix this test. Figure out what to do with Timestamp") + def test_to_nullable_timestamp_int(self): + self.assertEqual(to_nullable_timestamp(1000, "datetime", protos), + protos.NullableTimestamp( + value=Timestamp(seconds=int(1000)))) + + @unittest.skip("TODO: fix this test. Figure out what to do with Timestamp") + def test_to_nullable_timestamp_datetime(self): + now = datetime.datetime.now() + self.assertEqual(to_nullable_timestamp(now, "datetime", protos), + protos.NullableTimestamp( + value=Timestamp(seconds=int(now.timestamp())))) + + def test_to_nullable_timestamp_wrong_type(self): + with self.assertRaises(TypeError): + to_nullable_timestamp("now", "datetime", protos) + + def test_to_nullable_timestamp_none(self): + self.assertEqual(to_nullable_timestamp(None, "timestamp", protos), None) diff --git a/tests/unittests/test_opentelemetry.py b/tests/unittests/test_opentelemetry.py new file mode 100644 index 000000000..27332b217 --- /dev/null +++ b/tests/unittests/test_opentelemetry.py @@ -0,0 +1,201 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import os +import unittest + +import tests.protos as protos + +from azure_functions_worker_v2.handle_event import otel_manager, worker_init_request +from azure_functions_worker_v2.otel import (initialize_azure_monitor, + update_opentelemetry_status) +from tests.utils.constants import UNIT_TESTS_FOLDER +from tests.utils.mock_classes import FunctionRequest, Request, WorkerRequest +from unittest.mock import MagicMock, patch + + +FUNCTION_APP_DIRECTORY = UNIT_TESTS_FOLDER / 'basic_functions' + + +class TestOpenTelemetry(unittest.TestCase): + + def test_update_opentelemetry_status_import_error(self): + # Patch the built-in import mechanism + with patch('builtins.__import__', side_effect=ImportError): + update_opentelemetry_status() + # Verify that context variables are None due to ImportError + self.assertIsNone(otel_manager.get_context_api()) + self.assertIsNone(otel_manager.get_trace_context_propagator()) + + @patch('builtins.__import__') + def test_update_opentelemetry_status_success( + self, mock_imports): + mock_imports.return_value = MagicMock() + update_opentelemetry_status() + self.assertIsNotNone(otel_manager.get_context_api()) + self.assertIsNotNone(otel_manager.get_trace_context_propagator()) + + @patch('builtins.__import__') + @patch("azure_functions_worker_v2.otel.update_opentelemetry_status") + def test_initialize_azure_monitor_success( + self, + mock_update_ot, + mock_imports, + ): + mock_imports.return_value = MagicMock() + initialize_azure_monitor() + mock_update_ot.assert_called_once() + self.assertTrue(otel_manager.get_azure_monitor_available()) + + @patch("azure_functions_worker_v2.otel.update_opentelemetry_status") + def test_initialize_azure_monitor_import_error( + self, + mock_update_ot, + ): + with patch('builtins.__import__', side_effect=ImportError): + initialize_azure_monitor() + mock_update_ot.assert_called_once() + # Verify that azure_monitor_available is set to False due to ImportError + self.assertFalse(otel_manager.get_azure_monitor_available()) + + @patch.dict(os.environ, {'PYTHON_APPLICATIONINSIGHTS_ENABLE_TELEMETRY': 'true'}) + @patch('builtins.__import__') + async def test_init_request_initialize_azure_monitor_enabled_app_setting( + self, + mock_imports, + ): + mock_imports.return_value = MagicMock() + + worker_request = WorkerRequest(name='worker_init_request', + request=Request(FunctionRequest( + 'hello', + FUNCTION_APP_DIRECTORY)), + properties={'host': '123', + 'protos': protos}) + init_response = await worker_init_request(worker_request) + + self.assertEqual(init_response.result.status, + protos.StatusResult.Success) + + # Verify azure_monitor_available is set to True + self.assertTrue(otel_manager.get_azure_monitor_available()) + # Verify that WorkerOpenTelemetryEnabled capability is set to _TRUE + capabilities = init_response.capabilities + self.assertIn("WorkerOpenTelemetryEnabled", capabilities) + self.assertEqual(capabilities["WorkerOpenTelemetryEnabled"], "true") + + @patch("azure_functions_worker_v2.handle_event." + "otel_manager.initialize_azure_monitor") + async def test_init_request_initialize_azure_monitor_default_app_setting( + self, + mock_initialize_azmon, + ): + worker_request = WorkerRequest(name='worker_init_request', + request=Request(FunctionRequest( + 'hello', + FUNCTION_APP_DIRECTORY)), + properties={'host': '123', + 'protos': protos}) + init_response = await worker_init_request(worker_request) + + self.assertEqual(init_response.result.status, + protos.StatusResult.Success) + + # Azure monitor initialized not called + # Since default behavior is not enabled + mock_initialize_azmon.assert_not_called() + + # Verify azure_monitor_available is set to False + self.assertFalse(otel_manager.get_azure_monitor_available()) + # Verify that WorkerOpenTelemetryEnabled capability is not set + capabilities = init_response.capabilities + self.assertNotIn("WorkerOpenTelemetryEnabled", capabilities) + + @patch.dict(os.environ, {'PYTHON_APPLICATIONINSIGHTS_ENABLE_TELEMETRY': 'false'}) + @patch("azure_functions_worker_v2.otel_manager.initialize_azure_monitor") + async def test_init_request_initialize_azure_monitor_disabled_app_setting( + self, + mock_initialize_azmon, + ): + worker_request = WorkerRequest(name='worker_init_request', + request=Request(FunctionRequest( + 'hello', + FUNCTION_APP_DIRECTORY)), + properties={'host': '123', + 'protos': protos}) + init_response = await worker_init_request(worker_request) + + self.assertEqual(init_response.result.status, + protos.StatusResult.Success) + + # Azure monitor initialized not called + mock_initialize_azmon.assert_not_called() + + # Verify azure_monitor_available is set to False + self.assertFalse(otel_manager.get_azure_monitor_available()) + # Verify that WorkerOpenTelemetryEnabled capability is not set + capabilities = init_response.capabilities + self.assertNotIn("WorkerOpenTelemetryEnabled", capabilities) + + @patch.dict(os.environ, {'PYTHON_ENABLE_OPENTELEMETRY': 'true'}) + async def test_init_request_enable_opentelemetry_enabled_app_setting( + self, + ): + worker_request = WorkerRequest(name='worker_init_request', + request=Request(FunctionRequest( + 'hello', + FUNCTION_APP_DIRECTORY)), + properties={'host': '123', + 'protos': protos}) + init_response = await worker_init_request(worker_request) + + self.assertEqual(init_response.result.status, + protos.StatusResult.Success) + + # Verify otel_libs_available is set to True + self.assertTrue(otel_manager.get_azure_monitor_available()) + # Verify that WorkerOpenTelemetryEnabled capability is set to _TRUE + capabilities = init_response.capabilities + self.assertIn("WorkerOpenTelemetryEnabled", capabilities) + self.assertEqual(capabilities["WorkerOpenTelemetryEnabled"], "true") + + @patch.dict(os.environ, {'PYTHON_ENABLE_OPENTELEMETRY': 'false'}) + async def test_init_request_enable_opentelemetry_default_app_setting( + self, + ): + worker_request = WorkerRequest(name='worker_init_request', + request=Request(FunctionRequest( + 'hello', + FUNCTION_APP_DIRECTORY)), + properties={'host': '123', + 'protos': protos}) + init_response = await worker_init_request(worker_request) + + self.assertEqual(init_response.result.status, + protos.StatusResult.Success) + + # Verify otel_libs_available is set to False by default + self.assertFalse(otel_manager.get_otel_libs_available()) + # Verify that WorkerOpenTelemetryEnabled capability is not set + capabilities = init_response.capabilities + self.assertNotIn("WorkerOpenTelemetryEnabled", capabilities) + + @patch.dict(os.environ, {'PYTHON_APPLICATIONINSIGHTS_ENABLE_TELEMETRY': 'false'}) + async def test_init_request_enable_azure_monitor_disabled_app_setting( + self, + ): + worker_request = WorkerRequest(name='worker_init_request', + request=Request(FunctionRequest( + 'hello', + FUNCTION_APP_DIRECTORY)), + properties={'host': '123', + 'protos': protos}) + init_response = await worker_init_request(worker_request) + + self.assertEqual(init_response.result.status, + protos.StatusResult.Success) + + # Verify otel_libs_available is set to False by default + self.assertFalse(otel_manager.get_azure_monitor_available()) + # Verify that WorkerOpenTelemetryEnabled capability is not set + capabilities = init_response.capabilities + self.assertNotIn("WorkerOpenTelemetryEnabled", capabilities) diff --git a/tests/unittests/test_rpc_messages.py b/tests/unittests/test_rpc_messages.py new file mode 100644 index 000000000..cb2d7b96d --- /dev/null +++ b/tests/unittests/test_rpc_messages.py @@ -0,0 +1,124 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import os +import subprocess +import sys +import tempfile +import typing +import unittest + +import tests.protos as protos + +from azure_functions_worker_v2.handle_event import function_environment_reload_request +from tests.utils import testutils +from tests.utils.constants import UNIT_TESTS_FOLDER +from tests.utils.mock_classes import FunctionRequest, Request, WorkerRequest + +BASIC_FUNCTION_DIRECTORY = UNIT_TESTS_FOLDER / 'basic_function' + + +class TestGRPC(testutils.AsyncTestCase): + pre_test_env = os.environ.copy() + pre_test_cwd = os.getcwd() + + def _reset_environ(self): + for key, value in self.pre_test_env.items(): + os.environ[key] = value + os.chdir(self.pre_test_cwd) + + async def _verify_environment_reloaded( + self, + test_env: typing.Dict[str, str] = {}, + test_cwd: str = os.getcwd()): + worker_request = WorkerRequest(name='function_environment_reload_request', + request=Request(FunctionRequest( + 'hello', + test_cwd, + test_env)), + properties={'host': '123', + 'protos': protos}) + result = await function_environment_reload_request(worker_request) + + status = result.result.status + exp = result.result.exception + self.assertEqual(status, protos.StatusResult.Success, + f"Exception in Reload request: {exp}") + + environ_dict = os.environ.copy() + self.assertDictEqual(environ_dict, test_env) + self.assertEqual(os.getcwd(), test_cwd) + + self._reset_environ() + + async def test_multiple_env_vars_load(self): + test_env = {'TEST_KEY': 'foo', 'HELLO': 'world'} + await self._verify_environment_reloaded(test_env=test_env) + + async def test_empty_env_vars_load(self): + test_env = {} + await self._verify_environment_reloaded(test_env=test_env) + + @unittest.skipIf(sys.platform == 'darwin', + 'MacOS creates the processes specific var folder in ' + '/private filesystem and not in /var like in linux ' + 'systems.') + async def test_changing_current_working_directory(self): + test_cwd = tempfile.gettempdir() + await self._verify_environment_reloaded(test_cwd=test_cwd) + + @unittest.skipIf(sys.platform == 'darwin', + 'MacOS creates the processes specific var folder in ' + '/private filesystem and not in /var like in linux ' + 'systems.') + async def test_reload_env_message(self): + test_env = {'TEST_KEY': 'foo', 'HELLO': 'world'} + test_cwd = tempfile.gettempdir() + await self._verify_environment_reloaded(test_env, test_cwd) + + def _verify_sys_path_import(self, result, expected_output): + path_import_script = os.path.join(UNIT_TESTS_FOLDER, + 'path_import', 'test_path_import.sh') + try: + subprocess.run(['chmod +x ' + path_import_script], shell=True) + + exported_path = ":".join(sys.path) + output = subprocess.check_output( + [path_import_script, result, exported_path], + stderr=subprocess.STDOUT) + decoded_output = output.decode(sys.stdout.encoding).strip() + self.assertTrue(expected_output in decoded_output) + finally: + subprocess.run(['chmod -x ' + path_import_script], shell=True) + self._reset_environ() + + @unittest.skipIf(sys.platform == 'win32', + 'Linux .sh script only works on Linux') + def test_failed_sys_path_import(self): + self._verify_sys_path_import( + 'fail', + "No module named 'test_module'") + + @unittest.skipIf(sys.platform == 'win32', + 'Linux .sh script only works on Linux') + def test_successful_sys_path_import(self): + self._verify_sys_path_import( + 'success', + 'This module was imported!') + + def _verify_azure_namespace_import(self, result, expected_output): + print(os.getcwd()) + path_import_script = os.path.join(UNIT_TESTS_FOLDER, + 'azure_namespace_import', + 'test_azure_namespace_import.sh') + try: + subprocess.run(['chmod +x ' + path_import_script], shell=True) + + output = subprocess.check_output( + [path_import_script, result], + stderr=subprocess.STDOUT) + decoded_output = output.decode(sys.stdout.encoding).strip() + self.assertTrue(expected_output in decoded_output, + f"Decoded Output: {decoded_output}") # DNM + finally: + subprocess.run(['chmod -x ' + path_import_script], shell=True) + self._reset_environ() diff --git a/tests/unittests/test_typing_inspect.py b/tests/unittests/test_typing_inspect.py new file mode 100644 index 000000000..4cb41e6c2 --- /dev/null +++ b/tests/unittests/test_typing_inspect.py @@ -0,0 +1,137 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + +from typing import ( + Any, + Callable, + ClassVar, + Dict, + Generic, + Iterable, + List, + Mapping, + MutableMapping, + NamedTuple, + Optional, + Sequence, + Tuple, + TypeVar, + Union, +) +from azure_functions_worker_v2.utils.typing_inspect import ( + get_args, + get_generic_bases, + get_generic_type, + get_origin, + get_parameters, + is_callable_type, + is_classvar, + is_generic_type, + is_tuple_type, + is_typevar, + is_union_type, +) + + +class IsUtilityTestCase(unittest.TestCase): + def sample_test(self, fun, samples, nonsamples): + for s in samples: + self.assertTrue(fun(s), f"{s} type expected in {samples}") + for s in nonsamples: + self.assertFalse(fun(s), f"{s} type expected in {nonsamples}") + + def test_generic(self): + T = TypeVar('T') + samples = [Generic, Generic[T], Iterable[int], Mapping, + MutableMapping[T, List[int]], Sequence[Union[str, bytes]]] + nonsamples = [int, Union[int, str], Union[int, T], ClassVar[List[int]], + Callable[..., T], ClassVar, Optional, bytes, list] + self.sample_test(is_generic_type, samples, nonsamples) + + def test_callable(self): + class MyClass(Callable[[int], int]): + pass + samples = [Callable, Callable[..., int], + Callable[[int, int], Iterable[str]]] + nonsamples = [int, type, 42, [], List[int], + Union[callable, Callable[..., int]]] + self.sample_test(is_callable_type, samples, nonsamples) + self.assertTrue(is_callable_type(MyClass)) + + def test_tuple(self): + class MyClass(Tuple[str, int]): + pass + samples = [Tuple, Tuple[str, int], Tuple[Iterable, ...]] + nonsamples = [int, tuple, 42, List[int], NamedTuple('N', [('x', int)])] + self.sample_test(is_tuple_type, samples, nonsamples) + self.assertTrue(is_tuple_type(MyClass)) + + def test_union(self): + T = TypeVar('T') + S = TypeVar('S') + samples = [Union, Union[T, int], Union[int, Union[T, S]]] + nonsamples = [int, Union[int, int], [], Iterable[Any]] + self.sample_test(is_union_type, samples, nonsamples) + + def test_typevar(self): + T = TypeVar('T') + S_co = TypeVar('S_co', covariant=True) + samples = [T, S_co] + nonsamples = [int, Union[T, int], Union[T, S_co], type, ClassVar[int]] + self.sample_test(is_typevar, samples, nonsamples) + + def test_classvar(self): + T = TypeVar('T') + samples = [ClassVar, ClassVar[int], ClassVar[List[T]]] + nonsamples = [int, 42, Iterable, List[int], type, T] + self.sample_test(is_classvar, samples, nonsamples) + + +class GetUtilityTestCase(unittest.TestCase): + + def test_origin(self): + T = TypeVar('T') + self.assertEqual(get_origin(int), None) + self.assertEqual(get_origin(ClassVar[int]), None) + self.assertEqual(get_origin(Generic), Generic) + self.assertEqual(get_origin(Generic[T]), Generic) + self.assertEqual(get_origin(List[Tuple[T, T]][int]), list) + + def test_parameters(self): + T = TypeVar('T') + S_co = TypeVar('S_co', covariant=True) + U = TypeVar('U') + self.assertEqual(get_parameters(int), ()) + self.assertEqual(get_parameters(Generic), ()) + self.assertEqual(get_parameters(Union), ()) + self.assertEqual(get_parameters(List[int]), ()) + self.assertEqual(get_parameters(Generic[T]), (T,)) + self.assertEqual(get_parameters(Tuple[List[T], List[S_co]]), (T, S_co)) + self.assertEqual(get_parameters(Union[S_co, Tuple[T, T]][int, U]), (U,)) + self.assertEqual(get_parameters(Mapping[T, Tuple[S_co, T]]), (T, S_co)) + + def test_args_evaluated(self): + T = TypeVar('T') + self.assertEqual(get_args(Union[int, Tuple[T, int]][str], evaluate=True), + (int, Tuple[str, int])) + self.assertEqual(get_args(Dict[int, Tuple[T, T]][Optional[int]], evaluate=True), + (int, Tuple[Optional[int], Optional[int]])) + self.assertEqual(get_args(Callable[[], T][int], evaluate=True), ([], int,)) + + def test_generic_type(self): + T = TypeVar('T') + + class Node(Generic[T]): + pass + self.assertIs(get_generic_type(Node()), Node) + self.assertIs(get_generic_type(Node[int]()), Node[int]) + self.assertIs(get_generic_type(Node[T]()), Node[T],) + self.assertIs(get_generic_type(1), int) + + def test_generic_bases(self): + class MyClass(List[int], Mapping[str, List[int]]): + pass + self.assertEqual(get_generic_bases(MyClass), + (List[int], Mapping[str, List[int]])) + self.assertEqual(get_generic_bases(int), ()) diff --git a/tests/unittests/test_utilities.py b/tests/unittests/test_utilities.py new file mode 100644 index 000000000..56b84a69d --- /dev/null +++ b/tests/unittests/test_utilities.py @@ -0,0 +1,327 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import os +import sys +import typing +import unittest +from unittest.mock import patch + +from azure_functions_worker_v2.utils import env_state, validators, wrappers + +TEST_APP_SETTING_NAME = "TEST_APP_SETTING_NAME" +TEST_FEATURE_FLAG = "APP_SETTING_FEATURE_FLAG" +FEATURE_DEFAULT = 42 + + +class MockFeature: + @wrappers.enable_feature_by(TEST_FEATURE_FLAG) + def mock_feature_enabled(self, output: typing.List[str]) -> str: + result = 'mock_feature_enabled' + output.append(result) + return result + + @wrappers.enable_feature_by(TEST_FEATURE_FLAG, flag_default=True) + def mock_enabled_default_true(self, output: typing.List[str]) -> str: + result = 'mock_enabled_default_true' + output.append(result) + return result + + @wrappers.disable_feature_by(TEST_FEATURE_FLAG) + def mock_feature_disabled(self, output: typing.List[str]) -> str: + result = 'mock_feature_disabled' + output.append(result) + return result + + @wrappers.disable_feature_by(TEST_FEATURE_FLAG, flag_default=True) + def mock_disabled_default_true(self, output: typing.List[str]) -> str: + result = 'mock_disabled_default_true' + output.append(result) + return result + + @wrappers.enable_feature_by(TEST_FEATURE_FLAG, FEATURE_DEFAULT) + def mock_feature_default(self, output: typing.List[str]) -> str: + result = 'mock_feature_default' + output.append(result) + return result + + +class MockMethod: + @wrappers.attach_message_to_exception(ImportError, 'success') + def mock_load_function_success(self): + return True + + @wrappers.attach_message_to_exception(ImportError, 'module_not_found') + def mock_load_function_module_not_found(self): + raise ModuleNotFoundError('MODULE_NOT_FOUND') + + @wrappers.attach_message_to_exception(ImportError, 'import_error') + def mock_load_function_import_error(self): + # ImportError is a subclass of ModuleNotFoundError + raise ImportError('IMPORT_ERROR') + + @wrappers.attach_message_to_exception(ImportError, 'value_error') + def mock_load_function_value_error(self): + # ValueError is not a subclass of ImportError + raise ValueError('VALUE_ERROR') + + +class TestUtilities(unittest.TestCase): + + def setUp(self): + self._dummy_sdk_sys_path = os.path.join( + os.path.dirname(__file__), + 'resources', + 'mock_azure_functions' + ) + + self.mock_environ = patch.dict('os.environ', os.environ.copy()) + self.mock_sys_module = patch.dict('sys.modules', sys.modules.copy()) + self.mock_sys_path = patch('sys.path', sys.path.copy()) + self.mock_environ.start() + self.mock_sys_module.start() + self.mock_sys_path.start() + + def tearDown(self): + self.mock_sys_path.stop() + self.mock_sys_module.stop() + self.mock_environ.stop() + + def test_is_true_like_accepted(self): + self.assertTrue(env_state.is_true_like('1')) + self.assertTrue(env_state.is_true_like('true')) + self.assertTrue(env_state.is_true_like('T')) + self.assertTrue(env_state.is_true_like('YES')) + self.assertTrue(env_state.is_true_like('y')) + + def test_is_true_like_rejected(self): + self.assertFalse(env_state.is_true_like(None)) + self.assertFalse(env_state.is_true_like('')) + self.assertFalse(env_state.is_true_like('secret')) + + def test_is_false_like_accepted(self): + self.assertTrue(env_state.is_false_like('0')) + self.assertTrue(env_state.is_false_like('false')) + self.assertTrue(env_state.is_false_like('F')) + self.assertTrue(env_state.is_false_like('NO')) + self.assertTrue(env_state.is_false_like('n')) + + def test_is_false_like_rejected(self): + self.assertFalse(env_state.is_false_like(None)) + self.assertFalse(env_state.is_false_like('')) + self.assertFalse(env_state.is_false_like('secret')) + + def test_is_envvar_true(self): + os.environ[TEST_FEATURE_FLAG] = 'true' + self.assertTrue(env_state.is_envvar_true(TEST_FEATURE_FLAG)) + + def test_is_envvar_not_true_on_unset(self): + self._unset_feature_flag() + self.assertFalse(env_state.is_envvar_true(TEST_FEATURE_FLAG)) + + def test_is_envvar_false(self): + os.environ[TEST_FEATURE_FLAG] = 'false' + self.assertTrue(env_state.is_envvar_false(TEST_FEATURE_FLAG)) + + def test_is_envvar_not_false_on_unset(self): + self._unset_feature_flag() + self.assertFalse(env_state.is_envvar_true(TEST_FEATURE_FLAG)) + + def test_disable_feature_with_no_feature_flag(self): + mock_feature = MockFeature() + output = [] + result = mock_feature.mock_feature_enabled(output) + self.assertIsNone(result) + self.assertListEqual(output, []) + + def test_disable_feature_with_default_value(self): + mock_feature = MockFeature() + output = [] + result = mock_feature.mock_disabled_default_true(output) + self.assertIsNone(result) + self.assertListEqual(output, []) + + def test_enable_feature_with_feature_flag(self): + feature_flag = TEST_FEATURE_FLAG + os.environ[feature_flag] = '1' + mock_feature = MockFeature() + output = [] + result = mock_feature.mock_feature_enabled(output) + self.assertEqual(result, 'mock_feature_enabled') + self.assertListEqual(output, ['mock_feature_enabled']) + + def test_enable_feature_with_default_value(self): + mock_feature = MockFeature() + output = [] + result = mock_feature.mock_enabled_default_true(output) + self.assertEqual(result, 'mock_enabled_default_true') + self.assertListEqual(output, ['mock_enabled_default_true']) + + def test_enable_feature_with_no_rollback_flag(self): + mock_feature = MockFeature() + output = [] + result = mock_feature.mock_feature_disabled(output) + self.assertEqual(result, 'mock_feature_disabled') + self.assertListEqual(output, ['mock_feature_disabled']) + + def test_ignore_disable_default_value_when_set_explicitly(self): + feature_flag = TEST_FEATURE_FLAG + os.environ[feature_flag] = '0' + mock_feature = MockFeature() + output = [] + result = mock_feature.mock_disabled_default_true(output) + self.assertEqual(result, 'mock_disabled_default_true') + self.assertListEqual(output, ['mock_disabled_default_true']) + + def test_disable_feature_with_rollback_flag(self): + rollback_flag = TEST_FEATURE_FLAG + os.environ[rollback_flag] = '1' + mock_feature = MockFeature() + output = [] + result = mock_feature.mock_feature_disabled(output) + self.assertIsNone(result) + self.assertListEqual(output, []) + + def test_enable_feature_with_rollback_flag_is_false(self): + rollback_flag = TEST_FEATURE_FLAG + os.environ[rollback_flag] = 'false' + mock_feature = MockFeature() + output = [] + result = mock_feature.mock_feature_disabled(output) + self.assertEqual(result, 'mock_feature_disabled') + self.assertListEqual(output, ['mock_feature_disabled']) + + def test_ignore_enable_default_value_when_set_explicitly(self): + feature_flag = TEST_FEATURE_FLAG + os.environ[feature_flag] = '0' + mock_feature = MockFeature() + output = [] + result = mock_feature.mock_enabled_default_true(output) + self.assertIsNone(result) + self.assertListEqual(output, []) + + def test_fail_to_enable_feature_return_default_value(self): + mock_feature = MockFeature() + output = [] + result = mock_feature.mock_feature_default(output) + self.assertEqual(result, FEATURE_DEFAULT) + self.assertListEqual(output, []) + + def test_disable_feature_with_false_flag_return_default_value(self): + feature_flag = TEST_FEATURE_FLAG + os.environ[feature_flag] = 'false' + mock_feature = MockFeature() + output = [] + result = mock_feature.mock_feature_default(output) + self.assertEqual(result, FEATURE_DEFAULT) + self.assertListEqual(output, []) + + def test_exception_message_should_not_be_extended_on_success(self): + mock_method = MockMethod() + result = mock_method.mock_load_function_success() + self.assertTrue(result) + + def test_exception_message_should_be_extended_on_subexception(self): + mock_method = MockMethod() + with self.assertRaises(Exception) as e: + mock_method.mock_load_function_module_not_found() + self.assertIn('module_not_found', e.msg) + self.assertEqual(type(e), ModuleNotFoundError) + + def test_exception_message_should_be_extended_on_exact_exception(self): + mock_method = MockMethod() + with self.assertRaises(Exception) as e: + mock_method.mock_load_function_module_not_found() + self.assertIn('import_error', e.msg) + self.assertEqual(type(e), ImportError) + + def test_exception_message_should_not_be_extended_on_other_exception(self): + mock_method = MockMethod() + with self.assertRaises(Exception) as e: + mock_method.mock_load_function_value_error() + self.assertNotIn('import_error', e.msg) + self.assertEqual(type(e), ValueError) + + def test_app_settings_not_set_should_return_none(self): + app_setting = env_state.get_app_setting(TEST_APP_SETTING_NAME) + self.assertIsNone(app_setting) + + def test_app_settings_should_return_value(self): + # Set application setting by os.setenv + os.environ.update({TEST_APP_SETTING_NAME: '42'}) + + # Try using utility to acquire application setting + app_setting = env_state.get_app_setting(TEST_APP_SETTING_NAME) + self.assertEqual(app_setting, '42') + + def test_app_settings_not_set_should_return_default_value(self): + app_setting = env_state.get_app_setting(TEST_APP_SETTING_NAME, 'default') + self.assertEqual(app_setting, 'default') + + def test_app_settings_should_ignore_default_value(self): + # Set application setting by os.setenv + os.environ.update({TEST_APP_SETTING_NAME: '42'}) + + # Try using utility to acquire application setting + app_setting = env_state.get_app_setting(TEST_APP_SETTING_NAME, 'default') + self.assertEqual(app_setting, '42') + + def test_app_settings_should_not_trigger_validator_when_not_set(self): + def raise_excpt(value: str): + raise Exception('Should not raise on app setting not found') + + env_state.get_app_setting(TEST_APP_SETTING_NAME, validator=raise_excpt) + + def test_app_settings_return_default_value_when_validation_fail(self): + def parse_int_no_raise(value: str): + try: + int(value) + return True + except ValueError: + return False + + # Set application setting to an invalid value + os.environ.update({TEST_APP_SETTING_NAME: 'invalid'}) + + app_setting = env_state.get_app_setting( + TEST_APP_SETTING_NAME, + default_value='1', + validator=parse_int_no_raise + ) + + # Because 'invalid' is not an interger, falls back to default value + self.assertEqual(app_setting, '1') + + def test_app_settings_return_setting_value_when_validation_succeed(self): + def parse_int_no_raise(value: str): + try: + int(value) + return True + except ValueError: + return False + + # Set application setting to an invalid value + os.environ.update({TEST_APP_SETTING_NAME: '42'}) + + app_setting = env_state.get_app_setting( + TEST_APP_SETTING_NAME, + default_value='1', + validator=parse_int_no_raise + ) + + # Because 'invalid' is not an interger, falls back to default value + self.assertEqual(app_setting, '42') + + def test_valid_script_file_name(self): + file_name = 'test.py' + validators.validate_script_file_name(file_name) + + def test_invalid_script_file_name(self): + file_name = 'test' + with self.assertRaises(validators.InvalidFileNameError): + validators.validate_script_file_name(file_name) + + def _unset_feature_flag(self): + try: + os.environ.pop(TEST_FEATURE_FLAG) + except KeyError: + pass diff --git a/tests/utils/mock_classes.py b/tests/utils/mock_classes.py index 6e60a97da..52eadea89 100644 --- a/tests/utils/mock_classes.py +++ b/tests/utils/mock_classes.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from typing import Any, List +from typing import Any, List, Optional + # This represents the top level protos request sent from the host class WorkerRequest: @@ -19,9 +20,13 @@ def __init__(self, name: Any): # This represents the Function Init/Metadata/Load/Invocation request class FunctionRequest: - def __init__(self, capabilities: Any, function_app_directory: Any): + def __init__(self, capabilities: Any, + function_app_directory: Any, + environment_variables: Optional[Any] = None): self.capabilities = capabilities self.function_app_directory = function_app_directory + self.environment_variables = environment_variables + class MockMBD: def __init__(self, version: str, source: str, @@ -31,6 +36,15 @@ def __init__(self, version: str, source: str, self.content_type = content_type self.content = content + class MockCMBD: def __init__(self, model_binding_data: List[MockMBD]): self.model_binding_data = model_binding_data + + +class MockHttpRequest: + pass + + +class MockHttpResponse: + pass From 6cba8014702a4a324b28d2c721623e2001670cc2 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Fri, 13 Jun 2025 15:36:17 -0500 Subject: [PATCH 44/45] tests, formatting, add env reload --- azure_functions_worker_v2/__init__.py | 1 - azure_functions_worker_v2/bindings/generic.py | 1 + .../bindings/nullable_converters.py | 1 - azure_functions_worker_v2/bindings/out.py | 1 - .../bindings/retrycontext.py | 3 +-- .../bindings/tracecontext.py | 1 - azure_functions_worker_v2/functions.py | 1 + azure_functions_worker_v2/handle_event.py | 16 ++++++++++++++-- azure_functions_worker_v2/http_v2.py | 5 +++-- azure_functions_worker_v2/loader.py | 1 - azure_functions_worker_v2/otel.py | 1 - azure_functions_worker_v2/utils/current.py | 1 - azure_functions_worker_v2/utils/env_state.py | 1 + azure_functions_worker_v2/utils/helpers.py | 1 - azure_functions_worker_v2/utils/tracing.py | 1 + azure_functions_worker_v2/utils/validators.py | 1 - azure_functions_worker_v2/utils/wrappers.py | 4 ++-- pyproject.toml | 9 ++++----- tests/unittests/test_handle_event.py | 9 ++++----- tests/unittests/test_opentelemetry.py | 14 ++++++-------- tests/unittests/test_rpc_messages.py | 2 +- 21 files changed, 39 insertions(+), 36 deletions(-) diff --git a/azure_functions_worker_v2/__init__.py b/azure_functions_worker_v2/__init__.py index ac3640937..51d969720 100644 --- a/azure_functions_worker_v2/__init__.py +++ b/azure_functions_worker_v2/__init__.py @@ -1,6 +1,5 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - from .handle_event import (worker_init_request, functions_metadata_request, function_environment_reload_request, diff --git a/azure_functions_worker_v2/bindings/generic.py b/azure_functions_worker_v2/bindings/generic.py index 2d80db3c8..cb64ac760 100644 --- a/azure_functions_worker_v2/bindings/generic.py +++ b/azure_functions_worker_v2/bindings/generic.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. import typing + from typing import Any, Optional from .datumdef import Datum diff --git a/azure_functions_worker_v2/bindings/nullable_converters.py b/azure_functions_worker_v2/bindings/nullable_converters.py index 34e429154..51bf3f18e 100644 --- a/azure_functions_worker_v2/bindings/nullable_converters.py +++ b/azure_functions_worker_v2/bindings/nullable_converters.py @@ -1,6 +1,5 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - from datetime import datetime from typing import Optional, Union diff --git a/azure_functions_worker_v2/bindings/out.py b/azure_functions_worker_v2/bindings/out.py index 3e2dc0d4b..99632167e 100644 --- a/azure_functions_worker_v2/bindings/out.py +++ b/azure_functions_worker_v2/bindings/out.py @@ -1,6 +1,5 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - from typing import Optional diff --git a/azure_functions_worker_v2/bindings/retrycontext.py b/azure_functions_worker_v2/bindings/retrycontext.py index c0264e0c9..a7c53507a 100644 --- a/azure_functions_worker_v2/bindings/retrycontext.py +++ b/azure_functions_worker_v2/bindings/retrycontext.py @@ -1,6 +1,5 @@ # Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - +# Licensed under from dataclasses import dataclass from enum import Enum diff --git a/azure_functions_worker_v2/bindings/tracecontext.py b/azure_functions_worker_v2/bindings/tracecontext.py index e90312ddb..120f47ffd 100644 --- a/azure_functions_worker_v2/bindings/tracecontext.py +++ b/azure_functions_worker_v2/bindings/tracecontext.py @@ -1,6 +1,5 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - from typing import Dict diff --git a/azure_functions_worker_v2/functions.py b/azure_functions_worker_v2/functions.py index e9226b3e1..38268314a 100644 --- a/azure_functions_worker_v2/functions.py +++ b/azure_functions_worker_v2/functions.py @@ -5,6 +5,7 @@ import pathlib import typing import uuid + from .logging import logger from .bindings.meta import (has_implicit_output, diff --git a/azure_functions_worker_v2/handle_event.py b/azure_functions_worker_v2/handle_event.py index 5d263c17a..df5387b9f 100644 --- a/azure_functions_worker_v2/handle_event.py +++ b/azure_functions_worker_v2/handle_event.py @@ -250,8 +250,6 @@ async def invocation_request(request): for out_name, out_type_info in fi.output_types.items(): val = args[out_name].get() if val is None: - # TODO: is the "Out" parameter optional? - # Can "None" be marshaled into protos.TypedData? continue param_binding = to_outgoing_param_binding( @@ -307,6 +305,20 @@ async def function_environment_reload_request(request): request.request.function_environment_reload_request directory = func_env_reload_request.function_app_directory + if func_env_reload_request.function_app_directory: + sys.path.append(func_env_reload_request.function_app_directory) + + # Clear sys.path import cache, reload all module from new sys.path + sys.path_importer_cache.clear() + + # Reload environment variables + os.environ.clear() + env_vars = func_env_reload_request.environment_variables + for var in env_vars: + os.environ[var] = env_vars[var] + + # TODO: Apply PYTHON_THREADPOOL_THREAD_COUNT + if is_envvar_true(PYTHON_ENABLE_DEBUG_LOGGING): root_logger = logging.getLogger("azure.functions") root_logger.setLevel(logging.DEBUG) diff --git a/azure_functions_worker_v2/http_v2.py b/azure_functions_worker_v2/http_v2.py index d6040a9a0..7ca35b30a 100644 --- a/azure_functions_worker_v2/http_v2.py +++ b/azure_functions_worker_v2/http_v2.py @@ -1,16 +1,17 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - import abc import asyncio import importlib import socket + from typing import Any, Dict +from azure_functions_worker_v2.logging import logger + from azure_functions_worker_v2.utils.constants import ( X_MS_INVOCATION_ID, ) -from azure_functions_worker_v2.logging import logger # Http V2 Exceptions diff --git a/azure_functions_worker_v2/loader.py b/azure_functions_worker_v2/loader.py index 0e9ccf431..792254980 100644 --- a/azure_functions_worker_v2/loader.py +++ b/azure_functions_worker_v2/loader.py @@ -2,7 +2,6 @@ # Licensed under the MIT License. import importlib -import importlib.machinery import os.path import pathlib import sys diff --git a/azure_functions_worker_v2/otel.py b/azure_functions_worker_v2/otel.py index 9becc03af..ef53fb27f 100644 --- a/azure_functions_worker_v2/otel.py +++ b/azure_functions_worker_v2/otel.py @@ -1,6 +1,5 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - import os from .logging import logger diff --git a/azure_functions_worker_v2/utils/current.py b/azure_functions_worker_v2/utils/current.py index e15e3ff39..52330dcc8 100644 --- a/azure_functions_worker_v2/utils/current.py +++ b/azure_functions_worker_v2/utils/current.py @@ -1,6 +1,5 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - import asyncio import functools diff --git a/azure_functions_worker_v2/utils/env_state.py b/azure_functions_worker_v2/utils/env_state.py index 431bc4051..3a8376ce6 100644 --- a/azure_functions_worker_v2/utils/env_state.py +++ b/azure_functions_worker_v2/utils/env_state.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. import os + from typing import Callable, Optional diff --git a/azure_functions_worker_v2/utils/helpers.py b/azure_functions_worker_v2/utils/helpers.py index a6592088c..5a078a9bb 100644 --- a/azure_functions_worker_v2/utils/helpers.py +++ b/azure_functions_worker_v2/utils/helpers.py @@ -1,6 +1,5 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - import os import platform import sys diff --git a/azure_functions_worker_v2/utils/tracing.py b/azure_functions_worker_v2/utils/tracing.py index 69ed22a4e..df912aa13 100644 --- a/azure_functions_worker_v2/utils/tracing.py +++ b/azure_functions_worker_v2/utils/tracing.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. import traceback + from traceback import StackSummary, extract_tb from typing import List diff --git a/azure_functions_worker_v2/utils/validators.py b/azure_functions_worker_v2/utils/validators.py index 5fed95d0d..88f71a86c 100644 --- a/azure_functions_worker_v2/utils/validators.py +++ b/azure_functions_worker_v2/utils/validators.py @@ -1,6 +1,5 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - import re diff --git a/azure_functions_worker_v2/utils/wrappers.py b/azure_functions_worker_v2/utils/wrappers.py index 1761da37e..0df9c4912 100644 --- a/azure_functions_worker_v2/utils/wrappers.py +++ b/azure_functions_worker_v2/utils/wrappers.py @@ -1,12 +1,12 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - from typing import Any, Callable -from ..logging import logger from .env_state import is_envvar_false, is_envvar_true from .tracing import extend_exception_message +from ..logging import logger + def enable_feature_by(flag: str, default: Any = None, diff --git a/pyproject.toml b/pyproject.toml index 4864dee7f..904a76cfe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ "Intended Audience :: Developers" ] dependencies = [ - "azurefunctions-extensions-base; python_version >= '3.8'", + "azurefunctions-extensions-base", "azure-functions" ] @@ -35,13 +35,12 @@ Repository = "https://github.com/Azure/azure-functions-python-worker" dev = [ "azure-eventhub", # Used for EventHub E2E tests "azure-functions-durable", # Used for Durable E2E tests - "azure-monitor-opentelemetry; python_version >= '3.8'", # Used for Azure Monitor unit tests + "azure-monitor-opentelemetry", # Used for Azure Monitor unit tests "flask", "fastapi~=0.103.2", "pydantic", "pycryptodome==3.*", - "flake8==5.*; python_version == '3.7'", - "flake8==6.*; python_version >= '3.8'", + "flake8==6.*", "mypy", "pytest", "requests==2.*", @@ -49,7 +48,7 @@ dev = [ "grpcio~=1.70.0", "grpcio-tools~=1.70.0", "pytest-sugar", - "opentelemetry-api; python_version >= '3.8'", # Used for OpenTelemetry unit tests + "opentelemetry-api", # Used for OpenTelemetry unit tests "pytest-cov", "pytest-xdist", "pytest-randomly", diff --git a/tests/unittests/test_handle_event.py b/tests/unittests/test_handle_event.py index 95247f942..4fdc34b5f 100644 --- a/tests/unittests/test_handle_event.py +++ b/tests/unittests/test_handle_event.py @@ -114,19 +114,18 @@ async def test_worker_init_request_with_exception(self): self.assertIsNotNone(result.worker_metadata.worker_bitness) self.assertEqual(result.result.status, 1) - @patch("azure_functions_worker_v2.loader.index_function_app", - return_value=True) - async def test_functions_metadata_request(self, mock_index_function_app): + async def test_functions_metadata_request(self): handle_event.protos = test_protos + handle_event.metadata_exception = None metadata_result = await functions_metadata_request(None) self.assertEqual(metadata_result.result.status, 1) - @patch("azure_functions_worker_v2.metadata_exception", - return_value=Exception) async def test_functions_metadata_request_with_exception(self): handle_event.protos = test_protos + handle_event.metadata_exception = Exception metadata_result = await functions_metadata_request(None) self.assertEqual(metadata_result.result.status, 0) + handle_event.metadata_exception = None @patch("azure_functions_worker_v2.loader.index_function_app", return_value=True) diff --git a/tests/unittests/test_opentelemetry.py b/tests/unittests/test_opentelemetry.py index 27332b217..8f5bfb9ee 100644 --- a/tests/unittests/test_opentelemetry.py +++ b/tests/unittests/test_opentelemetry.py @@ -1,5 +1,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. import os import unittest @@ -18,13 +17,12 @@ class TestOpenTelemetry(unittest.TestCase): - def test_update_opentelemetry_status_import_error(self): - # Patch the built-in import mechanism - with patch('builtins.__import__', side_effect=ImportError): - update_opentelemetry_status() - # Verify that context variables are None due to ImportError - self.assertIsNone(otel_manager.get_context_api()) - self.assertIsNone(otel_manager.get_trace_context_propagator()) + @patch('builtins.__import__', side_effect=ImportError) + def test_update_opentelemetry_status_import_error(self, mock_import_error): + update_opentelemetry_status() + # Verify that context variables are None due to ImportError + self.assertIsNone(otel_manager.get_context_api()) + self.assertIsNone(otel_manager.get_trace_context_propagator()) @patch('builtins.__import__') def test_update_opentelemetry_status_success( diff --git a/tests/unittests/test_rpc_messages.py b/tests/unittests/test_rpc_messages.py index cb2d7b96d..9cae4ecce 100644 --- a/tests/unittests/test_rpc_messages.py +++ b/tests/unittests/test_rpc_messages.py @@ -45,7 +45,7 @@ async def _verify_environment_reloaded( f"Exception in Reload request: {exp}") environ_dict = os.environ.copy() - self.assertDictEqual(environ_dict, test_env) + self.assertTrue(test_env.items() <= environ_dict.items()) self.assertEqual(os.getcwd(), test_cwd) self._reset_environ() From 6cee8986156ed3fc9d5e10ffaf58907f27b0e628 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Fri, 13 Jun 2025 17:04:13 -0500 Subject: [PATCH 45/45] test fixes --- tests/unittests/test_handle_event.py | 4 ++++ tests/unittests/test_opentelemetry.py | 22 ++++++++++++++++------ tests/utils/mock_classes.py | 2 +- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/tests/unittests/test_handle_event.py b/tests/unittests/test_handle_event.py index 4fdc34b5f..846170ada 100644 --- a/tests/unittests/test_handle_event.py +++ b/tests/unittests/test_handle_event.py @@ -136,6 +136,7 @@ async def test_function_environment_reload_request(self, mock_index_function_app BASIC_FUNCTION_DIRECTORY)), properties={'host': '123', 'protos': test_protos}) + handle_event.protos = test_protos result = await function_environment_reload_request(worker_request) self.assertEqual(result.capabilities, {}) self.assertEqual(result.worker_metadata.runtime_name, "python") @@ -152,6 +153,7 @@ async def test_function_environment_reload_request_with_streaming( self, mock_http_v2_enabled, mock_initialize_http_server): + handle_event.protos = test_protos worker_request = WorkerRequest(name='function_environment_reload_request', request=Request(FunctionRequest( 'hello', @@ -172,6 +174,7 @@ async def test_function_environment_reload_request_with_streaming( return_value=True) async def test_function_environment_reload_request_with_otel(self, mock_otel_enabled): + handle_event.protos = test_protos worker_request = WorkerRequest(name='function_environment_reload_request', request=Request(FunctionRequest( 'hello', @@ -189,6 +192,7 @@ async def test_function_environment_reload_request_with_otel(self, async def test_function_environment_reload_request_with_exception(self): # Even if an exception happens during indexing, # we still return success + handle_event.protos = test_protos worker_request = WorkerRequest(name='function_environment_reload_request', request=Request(FunctionRequest( 'hello', diff --git a/tests/unittests/test_opentelemetry.py b/tests/unittests/test_opentelemetry.py index 8f5bfb9ee..ffc06fe0c 100644 --- a/tests/unittests/test_opentelemetry.py +++ b/tests/unittests/test_opentelemetry.py @@ -7,6 +7,7 @@ from azure_functions_worker_v2.handle_event import otel_manager, worker_init_request from azure_functions_worker_v2.otel import (initialize_azure_monitor, update_opentelemetry_status) +from azure_functions_worker_v2.logging import logger from tests.utils.constants import UNIT_TESTS_FOLDER from tests.utils.mock_classes import FunctionRequest, Request, WorkerRequest from unittest.mock import MagicMock, patch @@ -17,12 +18,21 @@ class TestOpenTelemetry(unittest.TestCase): - @patch('builtins.__import__', side_effect=ImportError) - def test_update_opentelemetry_status_import_error(self, mock_import_error): - update_opentelemetry_status() - # Verify that context variables are None due to ImportError - self.assertIsNone(otel_manager.get_context_api()) - self.assertIsNone(otel_manager.get_trace_context_propagator()) + def test_update_opentelemetry_status_import_error(self): + with patch.dict('sys.modules', { + 'opentelemetry': None, + 'opentelemetry.context': None, + 'opentelemetry.trace': None, + 'opentelemetry.trace.propagation': None, + 'opentelemetry.trace.propagation.tracecontext': None, + }): + # Verify that context variables are None due to ImportError + with self.assertLogs(logger.name, 'ERROR') as cm: + update_opentelemetry_status() + self.assertTrue( + any("Cannot import OpenTelemetry libraries." + in message for message in cm.output) + ) @patch('builtins.__import__') def test_update_opentelemetry_status_success( diff --git a/tests/utils/mock_classes.py b/tests/utils/mock_classes.py index 52eadea89..0cc89b530 100644 --- a/tests/utils/mock_classes.py +++ b/tests/utils/mock_classes.py @@ -22,7 +22,7 @@ def __init__(self, name: Any): class FunctionRequest: def __init__(self, capabilities: Any, function_app_directory: Any, - environment_variables: Optional[Any] = None): + environment_variables: Optional[Any] = {}): self.capabilities = capabilities self.function_app_directory = function_app_directory self.environment_variables = environment_variables

    USkX3MSFqD9VMSg$HSAJu( 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