├── azure ├── functions_worker │ ├── __init__.py │ ├── protos │ │ ├── .gitignore │ │ ├── __init__.py │ │ └── _src │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ └── src │ │ │ └── proto │ │ │ └── google │ │ │ └── protobuf │ │ │ └── duration.proto │ ├── __main__.py │ ├── bindings │ │ ├── out.py │ │ ├── context.py │ │ ├── __init__.py │ │ ├── timer.py │ │ ├── eventgrid.py │ │ ├── cosmosdb.py │ │ ├── eventhub.py │ │ ├── blob.py │ │ └── queue.py │ ├── logging.py │ ├── main.py │ ├── loader.py │ └── aio_compat.py └── __init__.py ├── docs ├── .gitignore ├── usage.rst ├── Makefile ├── make.bat └── api.rst ├── tests ├── load_functions │ ├── relimport │ │ ├── relative.py │ │ ├── main.py │ │ └── function.json │ ├── simple │ │ ├── main.py │ │ └── function.json │ ├── subdir │ │ ├── sub │ │ │ └── main.py │ │ └── function.json │ └── entrypoint │ │ ├── main.py │ │ └── function.json ├── ping │ ├── main.py │ ├── README.md │ └── function.json ├── broken_functions │ ├── syntax_error │ │ ├── main.py │ │ └── function.json │ ├── inout_param │ │ ├── main.py │ │ └── function.json │ ├── missing_py_param │ │ ├── main.py │ │ └── function.json │ ├── return_param_in │ │ ├── main.py │ │ └── function.json │ ├── unsupported_ret_type │ │ ├── main.py │ │ └── function.json │ ├── invalid_return_anno │ │ ├── main.py │ │ └── function.json │ ├── missing_json_param │ │ ├── main.py │ │ └── function.json │ ├── unsupported_bind_type │ │ ├── main.py │ │ └── function.json │ ├── wrong_param_dir │ │ ├── main.py │ │ └── function.json │ ├── invalid_http_trigger_anno │ │ ├── main.py │ │ └── function.json │ ├── invalid_context_param │ │ ├── main.py │ │ └── function.json │ ├── invalid_return_anno_non_type │ │ ├── main.py │ │ └── function.json │ ├── invalid_in_anno_non_type │ │ ├── main.py │ │ └── function.json │ ├── import_error │ │ ├── main.py │ │ └── function.json │ ├── bad_out_annotation │ │ ├── main.py │ │ └── function.json │ ├── module_not_found_error │ │ ├── main.py │ │ └── function.json │ ├── wrong_binding_dir │ │ ├── main.py │ │ └── function.json │ ├── invalid_out_anno │ │ ├── main.py │ │ └── function.json │ ├── invalid_in_anno │ │ ├── main.py │ │ └── function.json │ └── README.md ├── .gitignore ├── http_functions │ ├── no_return_returns │ │ ├── main.py │ │ └── function.json │ ├── remapped_context │ │ ├── main.py │ │ └── function.json │ ├── unhandled_error │ │ ├── main.py │ │ └── function.json │ ├── no_return │ │ ├── main.py │ │ └── function.json │ ├── return_http_no_body │ │ ├── main.py │ │ └── function.json │ ├── return_bytes │ │ ├── main.py │ │ └── function.json │ ├── return_str │ │ ├── main.py │ │ └── function.json │ ├── return_http_404 │ │ ├── main.py │ │ └── function.json │ ├── return_route_params │ │ ├── main.py │ │ └── function.json │ ├── return_out │ │ ├── main.py │ │ └── function.json │ ├── async_return_str │ │ ├── main.py │ │ └── function.json │ ├── return_http │ │ ├── main.py │ │ └── function.json │ ├── return_http_auth_admin │ │ ├── main.py │ │ └── function.json │ ├── unhandled_urllib_error │ │ ├── main.py │ │ └── function.json │ ├── return_http_redirect │ │ ├── main.py │ │ └── function.json │ ├── unhandled_unserializable_error │ │ ├── main.py │ │ └── function.json │ ├── accept_json │ │ ├── function.json │ │ └── main.py │ ├── async_logging │ │ ├── function.json │ │ └── main.py │ ├── return_context │ │ ├── function.json │ │ └── main.py │ ├── return_request │ │ ├── function.json │ │ └── main.py │ └── sync_logging │ │ ├── function.json │ │ └── main.py ├── blob_functions │ ├── put_blob_return │ │ ├── main.py │ │ └── function.json │ ├── get_blob_bytes │ │ ├── main.py │ │ └── function.json │ ├── get_blob_return │ │ ├── main.py │ │ └── function.json │ ├── get_blob_str │ │ ├── main.py │ │ └── function.json │ ├── get_blob_as_str │ │ ├── main.py │ │ └── function.json │ ├── get_blob_filelike │ │ ├── main.py │ │ └── function.json │ ├── get_blob_triggered │ │ ├── main.py │ │ └── function.json │ ├── put_blob_bytes │ │ ├── main.py │ │ └── function.json │ ├── put_blob_str │ │ ├── main.py │ │ └── function.json │ ├── put_blob_trigger │ │ ├── main.py │ │ └── function.json │ ├── get_blob_as_bytes │ │ ├── main.py │ │ └── function.json │ ├── put_blob_filelike │ │ ├── main.py │ │ └── function.json │ └── blob_trigger │ │ ├── main.py │ │ └── function.json ├── queue_functions │ ├── put_queue_return │ │ ├── main.py │ │ └── function.json │ ├── queue_trigger_return │ │ ├── main.py │ │ └── function.json │ ├── queue_trigger_message_return │ │ ├── main.py │ │ └── function.json │ ├── put_queue │ │ ├── main.py │ │ └── function.json │ ├── put_queue_message_return │ │ ├── main.py │ │ └── function.json │ ├── get_queue_blob_return │ │ ├── main.py │ │ └── function.json │ ├── get_queue_blob_message_return │ │ ├── main.py │ │ └── function.json │ ├── put_queue_return_multiple │ │ ├── main.py │ │ └── function.json │ ├── get_queue_blob │ │ ├── main.py │ │ └── function.json │ ├── queue_trigger_return_multiple │ │ ├── main.py │ │ └── function.json │ ├── put_queue_multiple_out │ │ ├── main.py │ │ └── function.json │ └── queue_trigger │ │ ├── function.json │ │ └── main.py ├── cosmosdb_functions │ ├── cosmosdb_trigger │ │ ├── __init__.py │ │ └── function.json │ ├── get_cosmosdb_triggered │ │ ├── main.py │ │ └── function.json │ ├── put_document │ │ ├── __init__.py │ │ └── function.json │ └── cosmosdb_input │ │ ├── __init__.py │ │ └── function.json ├── servicebus_functions │ ├── put_message_return │ │ ├── __init__.py │ │ └── function.json │ ├── put_message │ │ ├── __init__.py │ │ └── function.json │ ├── get_servicebus_triggered │ │ ├── __init__.py │ │ └── function.json │ └── servicebus_trigger │ │ ├── function.json │ │ └── __init__.py ├── eventhub_functions │ ├── eventhub_trigger │ │ ├── __init__.py │ │ └── function.json │ ├── get_eventhub_triggered │ │ ├── main.py │ │ └── function.json │ └── eventhub_output │ │ ├── __init__.py │ │ └── function.json ├── timer_functions │ └── return_pastdue │ │ ├── main.py │ │ └── function.json ├── eventgrid_functions │ ├── get_eventgrid_triggered │ │ ├── main.py │ │ └── function.json │ └── eventgrid_trigger │ │ ├── __init__.py │ │ └── function.json ├── __init__.py ├── test_loader.py ├── test_servicebus_functions.py ├── test_eventhub_functions.py ├── test_mock_timer_functions.py ├── test_code_quality.py ├── test_cosmosdb_functions.py ├── test_queue_functions.py ├── test_eventgrid_functions.py ├── test_mock_http_functions.py └── test_blob_functions.py ├── MANIFEST.in ├── requirements.txt ├── .ci ├── linux_devops_build.sh ├── linux_devops_tools.sh ├── linux_devops_tests.sh └── e2e │ ├── publish_tests │ ├── dev_docker_setup │ │ ├── dev.Dockerfile │ │ └── setup.sh │ ├── test_runners │ │ ├── prod_func_prod_docker │ │ │ ├── new_packapp.sh │ │ │ ├── new_no_bundler.sh │ │ │ ├── customer_no_bundler.sh │ │ │ ├── new_build_native_deps.sh │ │ │ └── customer_build_native_deps.sh │ │ ├── prod_func_dev_docker │ │ │ ├── new_packapp.sh │ │ │ ├── new_no_bundler.sh │ │ │ ├── customer_no_bundler.sh │ │ │ ├── new_build_native_deps.sh │ │ │ └── customer_build_native_deps.sh │ │ ├── dev_func_dev_docker │ │ │ ├── new_packapp.sh │ │ │ ├── new_no_bundler.sh │ │ │ ├── customer_no_bundler.sh │ │ │ ├── new_build_native_deps.sh │ │ │ └── customer_build_native_deps.sh │ │ ├── dev_func_prod_docker │ │ │ ├── new_packapp.sh │ │ │ ├── new_no_bundler.sh │ │ │ ├── customer_no_bundler.sh │ │ │ ├── new_build_native_deps.sh │ │ │ └── customer_build_native_deps.sh │ │ ├── setup_container_environment.sh │ │ ├── setup_test_environment.sh │ │ ├── helpers │ │ │ ├── helper.sh │ │ │ └── get_config_variables.sh │ │ ├── run_all_parallel.sh │ │ └── run_all_serial.sh │ ├── dev_func_setup │ │ └── setup.sh │ ├── func_tests_core │ │ ├── customer_churn_app │ │ │ ├── build_native_deps_test.sh │ │ │ └── no_bundler_test.sh │ │ ├── helpers │ │ │ └── helper.sh │ │ └── new_functionapp │ │ │ ├── packapp_test.sh │ │ │ ├── build_native_deps_test.sh │ │ │ └── no_bundler_test.sh │ └── publish_config.json │ └── running-environments │ └── Docker │ ├── start_tests_docker.sh │ └── test_runner.Dockerfile ├── pack ├── scripts │ ├── nix_deps.sh │ └── win_deps.ps1 ├── Microsoft.Azure.Functions.PythonWorkerRunEnvironments.targets ├── Microsoft.Azure.Functions.PythonWorkerRunEnvironments.nuspec └── templates │ ├── win_env_gen.yml │ └── nix_env_gen.yml ├── python ├── worker.config.json └── worker.py ├── .flake8 ├── setup.cfg ├── LICENSE ├── ISSUE_TEMPLATE.md ├── .gitignore ├── azure-pipelines-nightly.yml ├── azure-pipelines.yml └── README.md /azure/functions_worker/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | _templates 3 | -------------------------------------------------------------------------------- /tests/load_functions/relimport/relative.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/ping/main.py: -------------------------------------------------------------------------------- 1 | def main(req): 2 | return 3 | -------------------------------------------------------------------------------- /azure/functions_worker/protos/.gitignore: -------------------------------------------------------------------------------- 1 | /_src 2 | *_pb2.py 3 | *_pb2_grpc.py 4 | -------------------------------------------------------------------------------- /tests/broken_functions/syntax_error/main.py: -------------------------------------------------------------------------------- 1 | def main(req): 2 | 1 / # noqa 3 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | *_functions/bin/ 2 | *_functions/host.json 3 | *_functions/ping/ 4 | -------------------------------------------------------------------------------- /tests/http_functions/no_return_returns/main.py: -------------------------------------------------------------------------------- 1 | def main(req): 2 | return 'ABC' 3 | -------------------------------------------------------------------------------- /tests/load_functions/simple/main.py: -------------------------------------------------------------------------------- 1 | def main(req) -> str: 2 | return __name__ 3 | -------------------------------------------------------------------------------- /tests/load_functions/subdir/sub/main.py: -------------------------------------------------------------------------------- 1 | def main(req) -> str: 2 | return __name__ 3 | -------------------------------------------------------------------------------- /tests/http_functions/remapped_context/main.py: -------------------------------------------------------------------------------- 1 | def main(context): 2 | return context.method 3 | -------------------------------------------------------------------------------- /tests/load_functions/entrypoint/main.py: -------------------------------------------------------------------------------- 1 | def customentry(req) -> str: 2 | return __name__ 3 | -------------------------------------------------------------------------------- /tests/broken_functions/inout_param/main.py: -------------------------------------------------------------------------------- 1 | def main(req, abc): 2 | return 'trust me, it is OK!' 3 | -------------------------------------------------------------------------------- /tests/broken_functions/missing_py_param/main.py: -------------------------------------------------------------------------------- 1 | def main(): 2 | return 'trust me, it is OK!' 3 | -------------------------------------------------------------------------------- /tests/broken_functions/return_param_in/main.py: -------------------------------------------------------------------------------- 1 | def main(req): 2 | return 'trust me, it is OK!' 3 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | .. _azure-functions-usage: 2 | 3 | 4 | Azure Functions Usage 5 | ===================== 6 | -------------------------------------------------------------------------------- /tests/broken_functions/unsupported_ret_type/main.py: -------------------------------------------------------------------------------- 1 | def main(req): 2 | return 'trust me, it is OK!' 3 | -------------------------------------------------------------------------------- /tests/broken_functions/invalid_return_anno/main.py: -------------------------------------------------------------------------------- 1 | def main(req) -> int: 2 | return 'trust me, it is OK!' 3 | -------------------------------------------------------------------------------- /tests/broken_functions/missing_json_param/main.py: -------------------------------------------------------------------------------- 1 | def main(req, spam): 2 | return 'trust me, it is OK!' 3 | -------------------------------------------------------------------------------- /tests/broken_functions/unsupported_bind_type/main.py: -------------------------------------------------------------------------------- 1 | def main(req, ret): 2 | return 'trust me, it is OK!' 3 | -------------------------------------------------------------------------------- /tests/broken_functions/wrong_param_dir/main.py: -------------------------------------------------------------------------------- 1 | def main(req, foo: int): 2 | return 'trust me, it is OK!' 3 | -------------------------------------------------------------------------------- /tests/ping/README.md: -------------------------------------------------------------------------------- 1 | This function is used to check the host availability in tests. 2 | Please do not remove. 3 | -------------------------------------------------------------------------------- /tests/broken_functions/invalid_http_trigger_anno/main.py: -------------------------------------------------------------------------------- 1 | def main(req: int): 2 | return 'trust me, it is OK!' 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include azure *.py *.pyi 2 | recursive-include tests *.py *.json 3 | include LICENSE README.md 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Please list runtime dependencies in setup.py 'install_requires' and 2 | # 'extras_require'. 3 | . 4 | -------------------------------------------------------------------------------- /tests/broken_functions/invalid_context_param/main.py: -------------------------------------------------------------------------------- 1 | def main(req, context: int): 2 | return 'trust me, it is OK!' 3 | -------------------------------------------------------------------------------- /tests/broken_functions/invalid_return_anno_non_type/main.py: -------------------------------------------------------------------------------- 1 | def main(req) -> 123: 2 | return 'trust me, it is OK!' 3 | -------------------------------------------------------------------------------- /.ci/linux_devops_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -x 4 | 5 | python -m pip install -U -e .[dev] 6 | python setup.py webhost -------------------------------------------------------------------------------- /azure/functions_worker/__main__.py: -------------------------------------------------------------------------------- 1 | from azure.functions_worker import main 2 | 3 | if __name__ == '__main__': 4 | main.main() 5 | -------------------------------------------------------------------------------- /tests/http_functions/unhandled_error/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest): 5 | 1 / 0 6 | -------------------------------------------------------------------------------- /tests/load_functions/relimport/main.py: -------------------------------------------------------------------------------- 1 | from . import relative 2 | 3 | 4 | def main(req) -> str: 5 | return relative.__name__ 6 | -------------------------------------------------------------------------------- /azure/__init__.py: -------------------------------------------------------------------------------- 1 | from pkgutil import extend_path 2 | import typing 3 | __path__: typing.Iterable[str] = extend_path(__path__, __name__) 4 | -------------------------------------------------------------------------------- /tests/broken_functions/invalid_in_anno_non_type/main.py: -------------------------------------------------------------------------------- 1 | def main(req: 123): # annotations must be types! 2 | return 'trust me, it is OK!' 3 | -------------------------------------------------------------------------------- /tests/broken_functions/import_error/main.py: -------------------------------------------------------------------------------- 1 | from sys import __nonexistent # should raise ImportError 2 | 3 | 4 | def main(req): 5 | __nonexistent() 6 | -------------------------------------------------------------------------------- /tests/blob_functions/put_blob_return/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest) -> str: 5 | return 'FROM RETURN' 6 | -------------------------------------------------------------------------------- /tests/broken_functions/bad_out_annotation/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req, foo: azf.Out): 5 | return 'trust me, it is OK!' 6 | -------------------------------------------------------------------------------- /tests/http_functions/no_return/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | logger = logging.getLogger('test') 5 | 6 | 7 | def main(req): 8 | logger.error('hi') 9 | -------------------------------------------------------------------------------- /tests/http_functions/return_http_no_body/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest): 5 | return azf.HttpResponse() 6 | -------------------------------------------------------------------------------- /tests/broken_functions/module_not_found_error/main.py: -------------------------------------------------------------------------------- 1 | from __nonexistent import foo # should raise ModuleNotFoundError 2 | 3 | 4 | def main(req): 5 | foo() 6 | -------------------------------------------------------------------------------- /tests/broken_functions/wrong_binding_dir/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req, foo: azf.Out[str]): 5 | return 'trust me, it is OK!' 6 | -------------------------------------------------------------------------------- /tests/queue_functions/put_queue_return/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest) -> bytes: 5 | return req.get_body() 6 | -------------------------------------------------------------------------------- /tests/http_functions/return_bytes/main.py: -------------------------------------------------------------------------------- 1 | def main(req): 2 | # This function will fail, as we don't auto-convert "bytes" to "http". 3 | return b'Hello World!' 4 | -------------------------------------------------------------------------------- /tests/http_functions/return_str/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions 2 | 3 | 4 | def main(req: azure.functions.HttpRequest, context) -> str: 5 | return 'Hello World!' 6 | -------------------------------------------------------------------------------- /tests/queue_functions/queue_trigger_return/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(msg: azf.QueueMessage) -> bytes: 5 | return msg.get_body() 6 | -------------------------------------------------------------------------------- /tests/broken_functions/invalid_out_anno/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req, ret: azf.Out[azf.HttpRequest]): 5 | return 'trust me, it is OK!' 6 | -------------------------------------------------------------------------------- /tests/cosmosdb_functions/cosmosdb_trigger/__init__.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(docs: azf.DocumentList) -> str: 5 | return docs[0].to_json() 6 | -------------------------------------------------------------------------------- /tests/queue_functions/queue_trigger_message_return/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(msg: azf.QueueMessage) -> bytes: 5 | return msg.get_body() 6 | -------------------------------------------------------------------------------- /tests/servicebus_functions/put_message_return/__init__.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest) -> bytes: 5 | return req.get_body() 6 | -------------------------------------------------------------------------------- /tests/eventhub_functions/eventhub_trigger/__init__.py: -------------------------------------------------------------------------------- 1 | import azure.functions as func 2 | 3 | 4 | def main(event: func.EventHubEvent) -> bytes: 5 | return event.get_body() 6 | -------------------------------------------------------------------------------- /tests/http_functions/return_http_404/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest): 5 | return azf.HttpResponse('bye', status_code=404) 6 | -------------------------------------------------------------------------------- /tests/broken_functions/invalid_in_anno/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpResponse): # should be azf.HttpRequest 5 | return 'trust me, it is OK!' 6 | -------------------------------------------------------------------------------- /tests/timer_functions/return_pastdue/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(timer: azf.TimerRequest, pastdue: azf.Out[str]): 5 | pastdue.set(str(timer.past_due)) 6 | -------------------------------------------------------------------------------- /tests/blob_functions/get_blob_bytes/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest, file: azf.InputStream) -> str: 5 | return file.read().decode('utf-8') 6 | -------------------------------------------------------------------------------- /tests/blob_functions/get_blob_return/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest, file: azf.InputStream) -> str: 5 | return file.read().decode('utf-8') 6 | -------------------------------------------------------------------------------- /tests/blob_functions/get_blob_str/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest, file: azf.InputStream) -> str: 5 | return file.read().decode('utf-8') 6 | -------------------------------------------------------------------------------- /tests/queue_functions/put_queue/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest, msg: azf.Out[str]): 5 | msg.set(req.get_body()) 6 | 7 | return 'OK' 8 | -------------------------------------------------------------------------------- /tests/queue_functions/put_queue_message_return/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest) -> bytes: 5 | return azf.QueueMessage(body=req.get_body()) 6 | -------------------------------------------------------------------------------- /tests/blob_functions/get_blob_as_str/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest, file: str) -> str: 5 | assert isinstance(file, str) 6 | return file 7 | -------------------------------------------------------------------------------- /tests/blob_functions/get_blob_filelike/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest, file: azf.InputStream) -> str: 5 | return file.read().decode('utf-8') 6 | -------------------------------------------------------------------------------- /tests/blob_functions/get_blob_triggered/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest, file: azf.InputStream) -> str: 5 | return file.read().decode('utf-8') 6 | -------------------------------------------------------------------------------- /tests/blob_functions/put_blob_bytes/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest, file: azf.Out[str]) -> str: 5 | file.set(req.get_body()) 6 | return 'OK' 7 | -------------------------------------------------------------------------------- /tests/blob_functions/put_blob_str/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest, file: azf.Out[str]) -> str: 5 | file.set(req.get_body()) 6 | return 'OK' 7 | -------------------------------------------------------------------------------- /pack/scripts/nix_deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python -m venv .env 4 | source .env/bin/activate 5 | python -m pip install . 6 | 7 | python -m pip install . --no-compile --target "$BUILD_SOURCESDIRECTORY/deps" -------------------------------------------------------------------------------- /tests/blob_functions/put_blob_trigger/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest, file: azf.Out[str]) -> str: 5 | file.set(req.get_body()) 6 | return 'OK' 7 | -------------------------------------------------------------------------------- /tests/http_functions/return_route_params/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import azure.functions 3 | 4 | 5 | def main(req: azure.functions.HttpRequest) -> str: 6 | return json.dumps(dict(req.route_params)) 7 | -------------------------------------------------------------------------------- /tests/queue_functions/get_queue_blob_return/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest, file: azf.InputStream) -> str: 5 | return file.read().decode('utf-8') 6 | -------------------------------------------------------------------------------- /tests/cosmosdb_functions/get_cosmosdb_triggered/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as func 2 | 3 | 4 | def main(req: func.HttpRequest, file: func.InputStream) -> str: 5 | return file.read().decode('utf-8') 6 | -------------------------------------------------------------------------------- /tests/eventgrid_functions/get_eventgrid_triggered/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as func 2 | 3 | 4 | def main(req: func.HttpRequest, file: func.InputStream) -> str: 5 | return file.read().decode('utf-8') 6 | -------------------------------------------------------------------------------- /tests/eventhub_functions/get_eventhub_triggered/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as func 2 | 3 | 4 | def main(req: func.HttpRequest, file: func.InputStream) -> str: 5 | return file.read().decode('utf-8') 6 | -------------------------------------------------------------------------------- /tests/queue_functions/get_queue_blob_message_return/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest, file: azf.InputStream) -> str: 5 | return file.read().decode('utf-8') 6 | -------------------------------------------------------------------------------- /tests/http_functions/return_out/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest, foo: azf.Out[azf.HttpResponse]): 5 | foo.set(azf.HttpResponse(body='hello', status_code=201)) 6 | -------------------------------------------------------------------------------- /tests/blob_functions/get_blob_as_bytes/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest, file: bytes) -> str: 5 | assert isinstance(file, bytes) 6 | return file.decode('utf-8') 7 | -------------------------------------------------------------------------------- /tests/queue_functions/put_queue_return_multiple/main.py: -------------------------------------------------------------------------------- 1 | import typing 2 | import azure.functions as azf 3 | 4 | 5 | def main(req: azf.HttpRequest, msgs: azf.Out[typing.List[str]]): 6 | msgs.set(['one', 'two']) 7 | -------------------------------------------------------------------------------- /tests/servicebus_functions/put_message/__init__.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest, msg: azf.Out[str]): 5 | msg.set(req.get_body().decode('utf-8')) 6 | 7 | return 'OK' 8 | -------------------------------------------------------------------------------- /python/worker.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description":{ 3 | "language":"python", 4 | "extensions":[".py"], 5 | "defaultExecutablePath":"python", 6 | "defaultWorkerPath":"worker.py" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/eventhub_functions/eventhub_output/__init__.py: -------------------------------------------------------------------------------- 1 | import azure.functions as func 2 | 3 | 4 | def main(req: func.HttpRequest, event: func.Out[str]): 5 | event.set(req.get_body().decode('utf-8')) 6 | 7 | return 'OK' 8 | -------------------------------------------------------------------------------- /tests/http_functions/async_return_str/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import azure.functions 3 | 4 | 5 | async def main(req: azure.functions.HttpRequest, context): 6 | await asyncio.sleep(0.1) 7 | return 'Hello Async World!' 8 | -------------------------------------------------------------------------------- /tests/http_functions/return_http/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest): 5 | return azf.HttpResponse('

Hello World™

', 6 | mimetype='text/html') 7 | -------------------------------------------------------------------------------- /tests/blob_functions/put_blob_filelike/main.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | import azure.functions as azf 4 | 5 | 6 | def main(req: azf.HttpRequest, file: azf.Out[str]) -> str: 7 | file.set(io.StringIO('filelike')) 8 | return 'OK' 9 | -------------------------------------------------------------------------------- /tests/ping/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tests/cosmosdb_functions/put_document/__init__.py: -------------------------------------------------------------------------------- 1 | import azure.functions as func 2 | 3 | 4 | def main(req: func.HttpRequest, doc: func.Out[func.Document]): 5 | doc.set(func.Document.from_json(req.get_body())) 6 | 7 | return 'OK' 8 | -------------------------------------------------------------------------------- /tests/http_functions/return_http_auth_admin/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest): 5 | return azf.HttpResponse('

Hello World™

', 6 | mimetype='text/html') 7 | -------------------------------------------------------------------------------- /tests/cosmosdb_functions/cosmosdb_input/__init__.py: -------------------------------------------------------------------------------- 1 | import azure.functions as func 2 | 3 | 4 | def main(req: func.HttpRequest, docs: func.DocumentList) -> str: 5 | return func.HttpResponse(docs[0].to_json(), mimetype='application/json') 6 | -------------------------------------------------------------------------------- /tests/broken_functions/README.md: -------------------------------------------------------------------------------- 1 | Functions in this directory are purposefully "broken". They either have 2 | missing information in `function.json`, or invalid signatures, or even 3 | syntax errors. They are tested in "test_broken_functions.py". 4 | -------------------------------------------------------------------------------- /pack/scripts/win_deps.ps1: -------------------------------------------------------------------------------- 1 | py -3.6 -m venv .env 2 | .env\scripts\activate 3 | python -m pip install . 4 | 5 | $depsPath = Join-Path -Path $env:BUILD_SOURCESDIRECTORY -ChildPath "deps" 6 | 7 | python -m pip install . --no-compile --target $depsPath.ToString() -------------------------------------------------------------------------------- /tests/http_functions/no_return/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tests/http_functions/unhandled_urllib_error/main.py: -------------------------------------------------------------------------------- 1 | from urllib.request import urlopen 2 | 3 | import azure.functions as func 4 | 5 | 6 | def main(req: func.HttpRequest) -> str: 7 | image_url = req.params.get('img') 8 | urlopen(image_url).read() 9 | -------------------------------------------------------------------------------- /tests/broken_functions/invalid_in_anno/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tests/http_functions/no_return_returns/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.ci/linux_devops_tools.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo add-apt-repository -y \ 4 | 'deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-xenial-prod xenial main' \ 5 | && sudo apt-get update \ 6 | && sudo apt-get install -y \ 7 | azure-functions-core-tools -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = W503,E402,E731 3 | exclude = 4 | .git, __pycache__, build, dist, .eggs, .github, .local, docs/, 5 | Samples, azure/functions_worker/protos/, 6 | azure/functions_worker/typing_inspect.py, 7 | tests/test_typing_inspect.py 8 | -------------------------------------------------------------------------------- /tests/broken_functions/invalid_in_anno_non_type/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tests/servicebus_functions/get_servicebus_triggered/__init__.py: -------------------------------------------------------------------------------- 1 | import azure.functions as func 2 | 3 | 4 | def main(req: func.HttpRequest, file: func.InputStream) -> str: 5 | return func.HttpResponse( 6 | file.read().decode('utf-8'), mimetype='application/json') 7 | -------------------------------------------------------------------------------- /tests/queue_functions/get_queue_blob/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import azure.functions as azf 4 | 5 | 6 | def main(req: azf.HttpRequest, file: azf.InputStream) -> str: 7 | return json.dumps({ 8 | 'queue': json.loads(file.read().decode('utf-8')) 9 | }) 10 | -------------------------------------------------------------------------------- /tests/http_functions/return_http_redirect/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as azf 2 | 3 | 4 | def main(req: azf.HttpRequest): 5 | location = 'return_http?code={}'.format(req.params['code']) 6 | return azf.HttpResponse( 7 | status_code=302, 8 | headers={'location': location}) 9 | -------------------------------------------------------------------------------- /tests/queue_functions/queue_trigger_return_multiple/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import azure.functions as azf 3 | 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | def main(msg: azf.QueueMessage) -> None: 9 | logging.info('trigger on message: %s', msg.get_body().decode('utf-8')) 10 | -------------------------------------------------------------------------------- /tests/blob_functions/blob_trigger/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import azure.functions as azf 4 | 5 | 6 | def main(file: azf.InputStream) -> str: 7 | return json.dumps({ 8 | 'name': file.name, 9 | 'length': file.length, 10 | 'content': file.read().decode('utf-8') 11 | }) 12 | -------------------------------------------------------------------------------- /azure/functions_worker/bindings/out.py: -------------------------------------------------------------------------------- 1 | from azure.functions import _abc as azf_abc 2 | 3 | 4 | class Out(azf_abc.Out): 5 | 6 | def __init__(self): 7 | self.__value = None 8 | 9 | def set(self, val): 10 | self.__value = val 11 | 12 | def get(self): 13 | return self.__value 14 | -------------------------------------------------------------------------------- /tests/http_functions/unhandled_unserializable_error/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as func 2 | 3 | 4 | class UnserializableException(Exception): 5 | def __str__(self): 6 | raise RuntimeError('cannot serialize me') 7 | 8 | 9 | def main(req: func.HttpRequest) -> str: 10 | raise UnserializableException('foo') 11 | -------------------------------------------------------------------------------- /tests/http_functions/return_out/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "direction": "out", 12 | "name": "foo", 13 | "type": "http" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/load_functions/relimport/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/load_functions/simple/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/broken_functions/import_error/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/broken_functions/inout_param/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "inout", 13 | "name": "abc" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/broken_functions/invalid_out_anno/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "ret" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/broken_functions/syntax_error/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/http_functions/accept_json/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/http_functions/async_logging/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/http_functions/return_bytes/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/http_functions/return_context/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/http_functions/return_http/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/http_functions/return_request/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/http_functions/return_str/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/http_functions/sync_logging/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/load_functions/subdir/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "sub/main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/broken_functions/missing_py_param/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/broken_functions/return_param_in/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "in", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/broken_functions/unsupported_bind_type/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "yolo", 12 | "direction": "out", 13 | "name": "ret" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/http_functions/async_return_str/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/http_functions/return_http_404/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/http_functions/return_http_no_body/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/http_functions/unhandled_error/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.ci/linux_devops_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -x 4 | export AzureWebJobsStorage=$LINUXSTORAGECONNECTIONSTRING 5 | export AzureWebJobsCosmosDBConnectionString=$LINUXCOSMOSDBCONNECTIONSTRING 6 | export AzureWebJobsEventHubConnectionString=$LINUXEVENTHUBCONNECTIONSTRING 7 | export AzureWebJobsServiceBusConnectionString=$LINUXSERVICEBUSCONNECTIONSTRING 8 | python setup.py test -------------------------------------------------------------------------------- /tests/broken_functions/invalid_context_param/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/broken_functions/invalid_return_anno/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/broken_functions/missing_json_param/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/broken_functions/module_not_found_error/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/broken_functions/unsupported_ret_type/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "yolo", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/http_functions/remapped_context/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "context" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/http_functions/return_http_redirect/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/http_functions/unhandled_urllib_error/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/broken_functions/invalid_http_trigger_anno/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/queue_functions/put_queue_multiple_out/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as func 2 | 3 | 4 | def main(req: func.HttpRequest, resp: func.Out[func.HttpResponse], 5 | msg: func.Out[func.QueueMessage]) -> None: 6 | data = req.get_body().decode() 7 | msg.set(func.QueueMessage(body=data)) 8 | resp.set(func.HttpResponse(body='HTTP response: {}'.format(data))) 9 | -------------------------------------------------------------------------------- /tests/queue_functions/queue_trigger_return_multiple/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | 5 | "bindings": [ 6 | { 7 | "type": "queueTrigger", 8 | "direction": "in", 9 | "name": "msg", 10 | "queueName": "testqueue-return-multiple", 11 | "connection": "AzureWebJobsStorage", 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tests/broken_functions/invalid_return_anno_non_type/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/http_functions/unhandled_unserializable_error/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/load_functions/entrypoint/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "entryPoint": "customentry", 4 | "disabled": false, 5 | "bindings": [ 6 | { 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "name": "req" 10 | }, 11 | { 12 | "type": "http", 13 | "direction": "out", 14 | "name": "$return" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tests/http_functions/return_http_auth_admin/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "authLevel": "admin", 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "name": "req" 10 | }, 11 | { 12 | "type": "http", 13 | "direction": "out", 14 | "name": "$return" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tests/timer_functions/return_pastdue/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "timerTrigger", 7 | "direction": "in", 8 | "name": "timer", 9 | "schedule": "*/5 * * * * *" 10 | }, 11 | { 12 | "direction": "out", 13 | "name": "pastdue", 14 | "type": "http" 15 | } 16 | ] 17 | } 18 | 19 | -------------------------------------------------------------------------------- /tests/http_functions/sync_logging/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | import azure.functions 5 | 6 | 7 | logger = logging.getLogger('my function') 8 | 9 | 10 | def main(req: azure.functions.HttpRequest): 11 | try: 12 | 1 / 0 13 | except ZeroDivisionError: 14 | logger.error('a gracefully handled error', exc_info=True) 15 | time.sleep(0.05) 16 | return 'OK-sync' 17 | -------------------------------------------------------------------------------- /tests/http_functions/accept_json/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import azure.functions 4 | 5 | 6 | def main(req: azure.functions.HttpRequest): 7 | return json.dumps({ 8 | 'method': req.method, 9 | 'url': req.url, 10 | 'headers': dict(req.headers), 11 | 'params': dict(req.params), 12 | 'get_body': req.get_body().decode(), 13 | 'get_json': req.get_json() 14 | }) 15 | -------------------------------------------------------------------------------- /tests/http_functions/return_context/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import azure.functions 4 | 5 | 6 | def main(req: azure.functions.HttpRequest, context: azure.functions.Context): 7 | return json.dumps({ 8 | 'method': req.method, 9 | 'ctx_func_name': context.function_name, 10 | 'ctx_func_dir': context.function_directory, 11 | 'ctx_invocation_id': context.invocation_id, 12 | }) 13 | -------------------------------------------------------------------------------- /tests/eventgrid_functions/eventgrid_trigger/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import azure.functions as func 4 | 5 | 6 | def main(event: func.EventGridEvent) -> str: 7 | result = json.dumps({ 8 | 'id': event.id, 9 | 'data': event.get_json(), 10 | 'topic': event.topic, 11 | 'subject': event.subject, 12 | 'event_type': event.event_type, 13 | }) 14 | 15 | return result 16 | -------------------------------------------------------------------------------- /tests/http_functions/return_route_params/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req", 9 | "route": "return_route_params/{param1}/{param2}" 10 | }, 11 | { 12 | "type": "http", 13 | "direction": "out", 14 | "name": "$return" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/dev_docker_setup/dev.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=mcr.microsoft.com/azure-functions/python:2.0 2 | ARG PYTHON_WORKER_LOCATION=. 3 | FROM ${BASE_IMAGE} 4 | 5 | COPY ${PYTHON_WORKER_LOCATION} /python-worker-dev/ 6 | 7 | RUN pip install -e /python-worker-dev/ && \ 8 | rm -rf /azure-functions-host/workers/python/worker.py && \ 9 | mv /python-worker-dev/python/worker.py /azure-functions-host/workers/python/ 10 | -------------------------------------------------------------------------------- /tests/queue_functions/put_queue_return/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | 5 | "bindings": [ 6 | { 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "name": "req" 10 | }, 11 | { 12 | "direction": "out", 13 | "name": "$return", 14 | "queueName": "testqueue-return", 15 | "connection": "AzureWebJobsStorage", 16 | "type": "queue" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/blob_functions/put_blob_return/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "blob", 12 | "direction": "out", 13 | "name": "$return", 14 | "connection": "AzureWebJobsStorage", 15 | "path": "python-worker-tests/test-return.txt" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tests/broken_functions/wrong_binding_dir/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "direction": "in", 12 | "name": "foo", 13 | "type": "int" 14 | }, 15 | { 16 | "type": "http", 17 | "direction": "out", 18 | "name": "$return" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /tests/broken_functions/wrong_param_dir/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "direction": "out", 12 | "name": "foo", 13 | "type": "int" 14 | }, 15 | { 16 | "type": "http", 17 | "direction": "out", 18 | "name": "$return" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /tests/broken_functions/bad_out_annotation/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "direction": "out", 12 | "name": "foo", 13 | "type": "int" 14 | }, 15 | { 16 | "type": "http", 17 | "direction": "out", 18 | "name": "$return" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /tests/queue_functions/put_queue_message_return/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | 5 | "bindings": [ 6 | { 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "name": "req" 10 | }, 11 | { 12 | "direction": "out", 13 | "name": "$return", 14 | "queueName": "testqueue-message-return", 15 | "connection": "AzureWebJobsStorage", 16 | "type": "queue" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/queue_functions/put_queue_return_multiple/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | 5 | "bindings": [ 6 | { 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "name": "req" 10 | }, 11 | { 12 | "direction": "out", 13 | "name": "msgs", 14 | "queueName": "testqueue-return-multiple", 15 | "connection": "AzureWebJobsStorage", 16 | "type": "queue" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/prod_func_prod_docker/new_packapp.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the configuration variables 4 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/get_config_variables.sh" 5 | 6 | FUNCTION_APP="$(cat "${GLOBAL_CONFIG}" | jq -r '.publish_function_app.prod_func_prod_docker.new_function.packapp')" 7 | 8 | "$(dirname "${BASH_SOURCE[0]}")/../../func_tests_core/new_functionapp/packapp_test.sh" ${WORKING_DIR}/pfpdnpa ${FUNCTION_APP} func ${PROD_DOCKER_IMAGE} -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/prod_func_dev_docker/new_packapp.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the configuration variables 4 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/get_config_variables.sh" 5 | 6 | FUNCTION_APP="$(cat "${GLOBAL_CONFIG}" | jq -r '.publish_function_app.prod_func_dev_docker.new_function.packapp')" 7 | 8 | "$(dirname "${BASH_SOURCE[0]}")/../../func_tests_core/new_functionapp/packapp_test.sh" ${WORKING_DIR}/pfddnpa ${FUNCTION_APP} func ${DEV_DOCKER_IMAGE} 9 | -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/prod_func_dev_docker/new_no_bundler.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the configuration variables 4 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/get_config_variables.sh" 5 | 6 | FUNCTION_APP="$(cat "${GLOBAL_CONFIG}" | jq -r '.publish_function_app.prod_func_dev_docker.new_function.no_bundler')" 7 | 8 | "$(dirname "${BASH_SOURCE[0]}")/../../func_tests_core/new_functionapp/no_bundler_test.sh" ${WORKING_DIR}/pfddnnb ${FUNCTION_APP} func ${DEV_DOCKER_IMAGE} -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/prod_func_prod_docker/new_no_bundler.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the configuration variables 4 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/get_config_variables.sh" 5 | 6 | FUNCTION_APP="$(cat "${GLOBAL_CONFIG}" | jq -r '.publish_function_app.prod_func_prod_docker.new_function.no_bundler')" 7 | 8 | "$(dirname "${BASH_SOURCE[0]}")/../../func_tests_core/new_functionapp/no_bundler_test.sh" ${WORKING_DIR}/pfpdnnb ${FUNCTION_APP} func ${PROD_DOCKER_IMAGE} -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/dev_func_dev_docker/new_packapp.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the configuration variables 4 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/get_config_variables.sh" 5 | 6 | FUNCTION_APP="$(cat "${GLOBAL_CONFIG}" | jq -r '.publish_function_app.dev_func_dev_docker.new_function.packapp')" 7 | 8 | "$(dirname "${BASH_SOURCE[0]}")/../../func_tests_core/new_functionapp/packapp_test.sh" ${WORKING_DIR}/dfddnpa ${FUNCTION_APP} ${FUNC_DEV_DIR}/func ${DEV_DOCKER_IMAGE} -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/dev_func_prod_docker/new_packapp.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the configuration variables 4 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/get_config_variables.sh" 5 | 6 | FUNCTION_APP="$(cat "${GLOBAL_CONFIG}" | jq -r '.publish_function_app.dev_func_prod_docker.new_function.packapp')" 7 | 8 | "$(dirname "${BASH_SOURCE[0]}")/../../func_tests_core/new_functionapp/packapp_test.sh" ${WORKING_DIR}/dfpdnpa ${FUNCTION_APP} ${FUNC_DEV_DIR}/func ${PROD_DOCKER_IMAGE} -------------------------------------------------------------------------------- /tests/servicebus_functions/put_message_return/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "disabled": false, 4 | 5 | "bindings": [ 6 | { 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "name": "req" 10 | }, 11 | { 12 | "direction": "out", 13 | "name": "$return", 14 | "queueName": "testqueue-return", 15 | "connection": "AzureWebJobsServiceBusConnectionString", 16 | "type": "serviceBus" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/dev_func_dev_docker/new_no_bundler.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the configuration variables 4 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/get_config_variables.sh" 5 | 6 | FUNCTION_APP="$(cat "${GLOBAL_CONFIG}" | jq -r '.publish_function_app.dev_func_dev_docker.new_function.no_bundler')" 7 | 8 | "$(dirname "${BASH_SOURCE[0]}")/../../func_tests_core/new_functionapp/no_bundler_test.sh" ${WORKING_DIR}/dfddnnb ${FUNCTION_APP} ${FUNC_DEV_DIR}/func ${DEV_DOCKER_IMAGE} -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/prod_func_dev_docker/customer_no_bundler.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the configuration variables 4 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/get_config_variables.sh" 5 | 6 | FUNCTION_APP="$(cat "${GLOBAL_CONFIG}" | jq -r '.publish_function_app.prod_func_dev_docker.customer_churn.no_bundler')" 7 | 8 | "$(dirname "${BASH_SOURCE[0]}")/../../func_tests_core/customer_churn_app/no_bundler_test.sh" ${WORKING_DIR}/pfddcnb ${FUNCTION_APP} func ${DEV_DOCKER_IMAGE} -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/prod_func_prod_docker/customer_no_bundler.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the configuration variables 4 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/get_config_variables.sh" 5 | 6 | FUNCTION_APP="$(cat "${GLOBAL_CONFIG}" | jq -r '.publish_function_app.prod_func_prod_docker.customer_churn.no_bundler')" 7 | 8 | "$(dirname "${BASH_SOURCE[0]}")/../../func_tests_core/customer_churn_app/no_bundler_test.sh" ${WORKING_DIR}/pfpdcnb ${FUNCTION_APP} func ${PROD_DOCKER_IMAGE} -------------------------------------------------------------------------------- /tests/eventgrid_functions/eventgrid_trigger/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "disabled": false, 4 | 5 | "bindings": [ 6 | { 7 | "type": "eventGridTrigger", 8 | "direction": "in", 9 | "name": "event", 10 | }, 11 | { 12 | "type": "blob", 13 | "direction": "out", 14 | "name": "$return", 15 | "connection": "AzureWebJobsStorage", 16 | "path": "python-worker-tests/test-eventgrid-triggered.txt" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/dev_func_prod_docker/new_no_bundler.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the configuration variables 4 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/get_config_variables.sh" 5 | 6 | FUNCTION_APP="$(cat "${GLOBAL_CONFIG}" | jq -r '.publish_function_app.dev_func_prod_docker.new_function.no_bundler')" 7 | 8 | "$(dirname "${BASH_SOURCE[0]}")/../../func_tests_core/new_functionapp/no_bundler_test.sh" ${WORKING_DIR}/dfpdnnb ${FUNCTION_APP} ${FUNC_DEV_DIR}/func ${PROD_DOCKER_IMAGE} -------------------------------------------------------------------------------- /.ci/e2e/running-environments/Docker/start_tests_docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This needs to happen after the docker container is built. This is because the setup uses Docker deamon 4 | # And the /var/run/docker.sock needs to be mounted for the setup script to use Docker. 5 | # This is done when the Docker container is run. 6 | /azure-functions-python-worker/.ci/e2e/publish_tests/test_runners/setup_test_environment.sh 7 | 8 | /azure-functions-python-worker/.ci/e2e/publish_tests/test_runners/run_all_serial.sh -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/dev_func_dev_docker/customer_no_bundler.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the configuration variables 4 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/get_config_variables.sh" 5 | 6 | FUNCTION_APP="$(cat "${GLOBAL_CONFIG}" | jq -r '.publish_function_app.dev_func_dev_docker.customer_churn.no_bundler')" 7 | 8 | "$(dirname "${BASH_SOURCE[0]}")/../../func_tests_core/customer_churn_app/no_bundler_test.sh" ${WORKING_DIR}/dfddcnb ${FUNCTION_APP} ${FUNC_DEV_DIR}/func ${DEV_DOCKER_IMAGE} -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/prod_func_dev_docker/new_build_native_deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the configuration variables 4 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/get_config_variables.sh" 5 | 6 | FUNCTION_APP="$(cat "${GLOBAL_CONFIG}" | jq -r '.publish_function_app.prod_func_dev_docker.new_function.build_native_deps')" 7 | 8 | "$(dirname "${BASH_SOURCE[0]}")/../../func_tests_core/new_functionapp/build_native_deps_test.sh" ${WORKING_DIR}/pfddnbnd ${FUNCTION_APP} func ${DEV_DOCKER_IMAGE} -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/dev_func_prod_docker/customer_no_bundler.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the configuration variables 4 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/get_config_variables.sh" 5 | 6 | FUNCTION_APP="$(cat "${GLOBAL_CONFIG}" | jq -r '.publish_function_app.dev_func_prod_docker.customer_churn.no_bundler')" 7 | 8 | "$(dirname "${BASH_SOURCE[0]}")/../../func_tests_core/customer_churn_app/no_bundler_test.sh" ${WORKING_DIR}/dfpdcnb ${FUNCTION_APP} ${FUNC_DEV_DIR}/func ${PROD_DOCKER_IMAGE} -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/prod_func_prod_docker/new_build_native_deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the configuration variables 4 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/get_config_variables.sh" 5 | 6 | FUNCTION_APP="$(cat "${GLOBAL_CONFIG}" | jq -r '.publish_function_app.prod_func_prod_docker.new_function.build_native_deps')" 7 | 8 | "$(dirname "${BASH_SOURCE[0]}")/../../func_tests_core/new_functionapp/build_native_deps_test.sh" ${WORKING_DIR}/pfpdnbnd ${FUNCTION_APP} func ${PROD_DOCKER_IMAGE} -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/prod_func_dev_docker/customer_build_native_deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the configuration variables 4 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/get_config_variables.sh" 5 | 6 | FUNCTION_APP="$(cat "${GLOBAL_CONFIG}" | jq -r '.publish_function_app.prod_func_dev_docker.customer_churn.build_native_deps')" 7 | 8 | "$(dirname "${BASH_SOURCE[0]}")/../../func_tests_core/customer_churn_app/build_native_deps_test.sh" ${WORKING_DIR}/pfddcbnd ${FUNCTION_APP} func ${DEV_DOCKER_IMAGE} -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/dev_func_dev_docker/new_build_native_deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the configuration variables 4 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/get_config_variables.sh" 5 | 6 | FUNCTION_APP="$(cat "${GLOBAL_CONFIG}" | jq -r '.publish_function_app.dev_func_dev_docker.new_function.build_native_deps')" 7 | 8 | "$(dirname "${BASH_SOURCE[0]}")/../../func_tests_core/new_functionapp/build_native_deps_test.sh" ${WORKING_DIR}/dfddnbnd ${FUNCTION_APP} ${FUNC_DEV_DIR}/func ${DEV_DOCKER_IMAGE} -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/dev_func_prod_docker/new_build_native_deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the configuration variables 4 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/get_config_variables.sh" 5 | 6 | FUNCTION_APP="$(cat "${GLOBAL_CONFIG}" | jq -r '.publish_function_app.dev_func_prod_docker.new_function.build_native_deps')" 7 | 8 | "$(dirname "${BASH_SOURCE[0]}")/../../func_tests_core/new_functionapp/build_native_deps_test.sh" ${WORKING_DIR}/dfpdnbnd ${FUNCTION_APP} ${FUNC_DEV_DIR}/func ${PROD_DOCKER_IMAGE} -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/prod_func_prod_docker/customer_build_native_deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the configuration variables 4 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/get_config_variables.sh" 5 | 6 | FUNCTION_APP="$(cat "${GLOBAL_CONFIG}" | jq -r '.publish_function_app.prod_func_prod_docker.customer_churn.build_native_deps')" 7 | 8 | "$(dirname "${BASH_SOURCE[0]}")/../../func_tests_core/customer_churn_app/build_native_deps_test.sh" ${WORKING_DIR}/pfpdcbnd ${FUNCTION_APP} func ${PROD_DOCKER_IMAGE} -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/dev_func_dev_docker/customer_build_native_deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the configuration variables 4 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/get_config_variables.sh" 5 | 6 | FUNCTION_APP="$(cat "${GLOBAL_CONFIG}" | jq -r '.publish_function_app.dev_func_dev_docker.customer_churn.build_native_deps')" 7 | 8 | "$(dirname "${BASH_SOURCE[0]}")/../../func_tests_core/customer_churn_app/build_native_deps_test.sh" ${WORKING_DIR}/dfddcbnd ${FUNCTION_APP} ${FUNC_DEV_DIR}/func ${DEV_DOCKER_IMAGE} -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/dev_func_prod_docker/customer_build_native_deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the configuration variables 4 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/get_config_variables.sh" 5 | 6 | FUNCTION_APP="$(cat "${GLOBAL_CONFIG}" | jq -r '.publish_function_app.dev_func_prod_docker.customer_churn.build_native_deps')" 7 | 8 | "$(dirname "${BASH_SOURCE[0]}")/../../func_tests_core/customer_churn_app/build_native_deps_test.sh" ${WORKING_DIR}/dfpdcbnd ${FUNCTION_APP} ${FUNC_DEV_DIR}/func ${PROD_DOCKER_IMAGE} -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Bootstrap for '$ python setup.py test' command.""" 2 | 3 | import os.path 4 | import sys 5 | import unittest 6 | import unittest.runner 7 | 8 | 9 | def suite(): 10 | test_loader = unittest.TestLoader() 11 | test_suite = test_loader.discover( 12 | os.path.dirname(__file__), pattern='test_*.py') 13 | return test_suite 14 | 15 | 16 | if __name__ == '__main__': 17 | runner = unittest.runner.TextTestRunner() 18 | result = runner.run(suite()) 19 | sys.exit(not result.wasSuccessful()) 20 | -------------------------------------------------------------------------------- /tests/http_functions/return_request/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import hashlib 3 | 4 | import azure.functions 5 | 6 | 7 | def main(req: azure.functions.HttpRequest): 8 | params = dict(req.params) 9 | params.pop('code', None) 10 | body = req.get_body() 11 | return json.dumps({ 12 | 'method': req.method, 13 | 'url': req.url, 14 | 'headers': dict(req.headers), 15 | 'params': params, 16 | 'get_body': body.decode(), 17 | 'body_hash': hashlib.sha256(body).hexdigest(), 18 | }) 19 | -------------------------------------------------------------------------------- /tests/queue_functions/put_queue/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | 5 | "bindings": [ 6 | { 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "name": "req" 10 | }, 11 | { 12 | "direction": "out", 13 | "name": "msg", 14 | "queueName": "testqueue", 15 | "connection": "AzureWebJobsStorage", 16 | "type": "queue" 17 | }, 18 | { 19 | "direction": "out", 20 | "name": "$return", 21 | "type": "http" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tests/queue_functions/queue_trigger/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | 5 | "bindings": [ 6 | { 7 | "type": "queueTrigger", 8 | "direction": "in", 9 | "name": "msg", 10 | "queueName": "testqueue", 11 | "connection": "AzureWebJobsStorage", 12 | }, 13 | { 14 | "type": "blob", 15 | "direction": "out", 16 | "name": "$return", 17 | "connection": "AzureWebJobsStorage", 18 | "path": "python-worker-tests/test-queue-blob.txt" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/dev_func_setup/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | if [[ -z "$1" ]] 5 | then 6 | echo "Missing name of the directory to install func CLI" 7 | echo "usage ./script " 8 | exit 1 9 | fi 10 | 11 | if [[ -z "$2" ]] 12 | then 13 | echo "Missing the URL to download functions" 14 | fi 15 | 16 | wget $2 17 | echo "Retrieving func CLI version: " 18 | curl https://functionsclibuilds.blob.core.windows.net/builds/2/latest/version.txt 19 | echo "" 20 | unzip *.zip -d "$1" 21 | rm -rf *.zip 22 | chmod a+x "$1"/func -------------------------------------------------------------------------------- /tests/blob_functions/get_blob_str/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "blob", 12 | "direction": "in", 13 | "name": "file", 14 | "connection": "AzureWebJobsStorage", 15 | "path": "python-worker-tests/test-str.txt" 16 | }, 17 | { 18 | "type": "http", 19 | "direction": "out", 20 | "name": "$return", 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/blob_functions/put_blob_str/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "blob", 12 | "direction": "out", 13 | "name": "file", 14 | "connection": "AzureWebJobsStorage", 15 | "path": "python-worker-tests/test-str.txt" 16 | }, 17 | { 18 | "type": "http", 19 | "direction": "out", 20 | "name": "$return", 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/blob_functions/get_blob_bytes/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "blob", 12 | "direction": "in", 13 | "name": "file", 14 | "connection": "AzureWebJobsStorage", 15 | "path": "python-worker-tests/test-bytes.txt" 16 | }, 17 | { 18 | "type": "http", 19 | "direction": "out", 20 | "name": "$return", 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/blob_functions/get_blob_return/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "blob", 12 | "direction": "in", 13 | "name": "file", 14 | "connection": "AzureWebJobsStorage", 15 | "path": "python-worker-tests/test-return.txt" 16 | }, 17 | { 18 | "type": "http", 19 | "direction": "out", 20 | "name": "$return", 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/blob_functions/put_blob_bytes/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "blob", 12 | "direction": "out", 13 | "name": "file", 14 | "connection": "AzureWebJobsStorage", 15 | "path": "python-worker-tests/test-bytes.txt" 16 | }, 17 | { 18 | "type": "http", 19 | "direction": "out", 20 | "name": "$return", 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/blob_functions/get_blob_filelike/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "blob", 12 | "direction": "in", 13 | "name": "file", 14 | "connection": "AzureWebJobsStorage", 15 | "path": "python-worker-tests/test-filelike.txt" 16 | }, 17 | { 18 | "type": "http", 19 | "direction": "out", 20 | "name": "$return", 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/blob_functions/put_blob_filelike/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "blob", 12 | "direction": "out", 13 | "name": "file", 14 | "connection": "AzureWebJobsStorage", 15 | "path": "python-worker-tests/test-filelike.txt" 16 | }, 17 | { 18 | "type": "http", 19 | "direction": "out", 20 | "name": "$return", 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/queue_functions/get_queue_blob/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "blob", 12 | "direction": "in", 13 | "name": "file", 14 | "connection": "AzureWebJobsStorage", 15 | "path": "python-worker-tests/test-queue-blob.txt" 16 | }, 17 | { 18 | "type": "http", 19 | "direction": "out", 20 | "name": "$return", 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/blob_functions/get_blob_triggered/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "blob", 12 | "direction": "in", 13 | "name": "file", 14 | "connection": "AzureWebJobsStorage", 15 | "path": "python-worker-tests/test-blob-triggered.txt" 16 | }, 17 | { 18 | "type": "http", 19 | "direction": "out", 20 | "name": "$return", 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/blob_functions/put_blob_trigger/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "blob", 12 | "direction": "out", 13 | "name": "file", 14 | "connection": "AzureWebJobsStorage", 15 | "path": "python-worker-tests/test-blob-trigger.txt" 16 | }, 17 | { 18 | "type": "http", 19 | "direction": "out", 20 | "name": "$return", 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/queue_functions/queue_trigger_return/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | 5 | "bindings": [ 6 | { 7 | "type": "queueTrigger", 8 | "direction": "in", 9 | "name": "msg", 10 | "queueName": "testqueue-return", 11 | "connection": "AzureWebJobsStorage", 12 | }, 13 | { 14 | "type": "blob", 15 | "direction": "out", 16 | "name": "$return", 17 | "connection": "AzureWebJobsStorage", 18 | "path": "python-worker-tests/test-queue-blob-return.txt" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /tests/blob_functions/blob_trigger/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "blobTrigger", 7 | "direction": "in", 8 | "name": "file", 9 | "connection": "AzureWebJobsStorage", 10 | "path": "python-worker-tests/test-blob-trigger.txt" 11 | }, 12 | { 13 | "type": "blob", 14 | "direction": "out", 15 | "name": "$return", 16 | "connection": "AzureWebJobsStorage", 17 | "path": "python-worker-tests/test-blob-triggered.txt" 18 | }, 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tests/queue_functions/put_queue_multiple_out/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | 5 | "bindings": [ 6 | { 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "name": "req" 10 | }, 11 | { 12 | "name": "resp", 13 | "type": "http", 14 | "direction": "out" 15 | }, 16 | { 17 | "direction": "out", 18 | "name": "msg", 19 | "queueName": "testqueue-return-multiple-outparam", 20 | "connection": "AzureWebJobsStorage", 21 | "type": "queue" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tests/servicebus_functions/put_message/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "disabled": false, 4 | 5 | "bindings": [ 6 | { 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "name": "req" 10 | }, 11 | { 12 | "direction": "out", 13 | "name": "msg", 14 | "queueName": "testqueue", 15 | "connection": "AzureWebJobsServiceBusConnectionString", 16 | "type": "serviceBus" 17 | }, 18 | { 19 | "direction": "out", 20 | "name": "$return", 21 | "type": "http" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tests/queue_functions/get_queue_blob_return/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "blob", 12 | "direction": "in", 13 | "name": "file", 14 | "connection": "AzureWebJobsStorage", 15 | "path": "python-worker-tests/test-queue-blob-return.txt" 16 | }, 17 | { 18 | "type": "http", 19 | "direction": "out", 20 | "name": "$return", 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/cosmosdb_functions/get_cosmosdb_triggered/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "blob", 12 | "direction": "in", 13 | "name": "file", 14 | "connection": "AzureWebJobsStorage", 15 | "path": "python-worker-tests/test-cosmosdb-triggered.txt" 16 | }, 17 | { 18 | "type": "http", 19 | "direction": "out", 20 | "name": "$return", 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/eventgrid_functions/get_eventgrid_triggered/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "blob", 12 | "direction": "in", 13 | "name": "file", 14 | "connection": "AzureWebJobsStorage", 15 | "path": "python-worker-tests/test-eventgrid-triggered.txt" 16 | }, 17 | { 18 | "type": "http", 19 | "direction": "out", 20 | "name": "$return", 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/eventhub_functions/eventhub_output/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "disabled": false, 4 | 5 | "bindings": [ 6 | { 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "name": "req" 10 | }, 11 | { 12 | "type": "eventHub", 13 | "name": "event", 14 | "direction": "out", 15 | "eventHubName": "python-worker-ci", 16 | "connection": "AzureWebJobsEventHubConnectionString", 17 | }, 18 | { 19 | "direction": "out", 20 | "name": "$return", 21 | "type": "http" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tests/eventhub_functions/get_eventhub_triggered/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "blob", 12 | "direction": "in", 13 | "name": "file", 14 | "connection": "AzureWebJobsStorage", 15 | "path": "python-worker-tests/test-eventhub-triggered.txt" 16 | }, 17 | { 18 | "type": "http", 19 | "direction": "out", 20 | "name": "$return", 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/blob_functions/get_blob_as_str/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "blob", 12 | "direction": "in", 13 | "name": "file", 14 | "dataType": "string", 15 | "connection": "AzureWebJobsStorage", 16 | "path": "python-worker-tests/test-str.txt" 17 | }, 18 | { 19 | "type": "http", 20 | "direction": "out", 21 | "name": "$return" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tests/blob_functions/get_blob_as_bytes/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "blob", 12 | "direction": "in", 13 | "name": "file", 14 | "dataType": "binary", 15 | "connection": "AzureWebJobsStorage", 16 | "path": "python-worker-tests/test-bytes.txt" 17 | }, 18 | { 19 | "type": "http", 20 | "direction": "out", 21 | "name": "$return" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tests/queue_functions/get_queue_blob_message_return/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "blob", 12 | "direction": "in", 13 | "name": "file", 14 | "connection": "AzureWebJobsStorage", 15 | "path": "python-worker-tests/test-queue-blob-message-return.txt" 16 | }, 17 | { 18 | "type": "http", 19 | "direction": "out", 20 | "name": "$return", 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/queue_functions/queue_trigger_message_return/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "disabled": false, 4 | 5 | "bindings": [ 6 | { 7 | "type": "queueTrigger", 8 | "direction": "in", 9 | "name": "msg", 10 | "queueName": "testqueue-message-return", 11 | "connection": "AzureWebJobsStorage", 12 | }, 13 | { 14 | "type": "blob", 15 | "direction": "out", 16 | "name": "$return", 17 | "connection": "AzureWebJobsStorage", 18 | "path": "python-worker-tests/test-queue-blob-message-return.txt" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /tests/servicebus_functions/get_servicebus_triggered/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "disabled": false, 4 | "bindings": [ 5 | { 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "blob", 12 | "direction": "in", 13 | "name": "file", 14 | "connection": "AzureWebJobsStorage", 15 | "path": "python-worker-tests/test-servicebus-triggered.txt" 16 | }, 17 | { 18 | "type": "http", 19 | "direction": "out", 20 | "name": "$return", 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/eventhub_functions/eventhub_trigger/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "disabled": false, 4 | 5 | "bindings": [ 6 | { 7 | "type": "eventHubTrigger", 8 | "name": "event", 9 | "direction": "in", 10 | "eventHubName": "python-worker-ci", 11 | "connection": "AzureWebJobsEventHubConnectionString", 12 | }, 13 | { 14 | "type": "blob", 15 | "direction": "out", 16 | "name": "$return", 17 | "connection": "AzureWebJobsStorage", 18 | "path": "python-worker-tests/test-eventhub-triggered.txt" 19 | }, 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /tests/servicebus_functions/servicebus_trigger/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "disabled": false, 4 | 5 | "bindings": [ 6 | { 7 | "type": "serviceBusTrigger", 8 | "direction": "in", 9 | "name": "msg", 10 | "queueName": "testqueue", 11 | "connection": "AzureWebJobsServiceBusConnectionString", 12 | }, 13 | { 14 | "type": "blob", 15 | "direction": "out", 16 | "name": "$return", 17 | "connection": "AzureWebJobsStorage", 18 | "path": "python-worker-tests/test-servicebus-triggered.txt" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | addopts = --capture=no --assert=plain --strict --tb native 3 | testpaths = tests 4 | 5 | [mypy] 6 | python_version = 3.6 7 | check_untyped_defs = True 8 | warn_redundant_casts = True 9 | warn_unused_ignores = True 10 | warn_unused_configs = True 11 | strict_optional = True 12 | warn_return_any = True 13 | disallow_subclassing_any = False 14 | ignore_missing_imports = True 15 | 16 | [mypy-azure.functions_worker.aio_compat] 17 | ignore_errors = True 18 | 19 | [mypy-azure.functions_worker.protos.*] 20 | ignore_errors = True 21 | 22 | [mypy-azure.functions_worker.typing_inspect] 23 | ignore_errors = True 24 | -------------------------------------------------------------------------------- /azure/functions_worker/protos/__init__.py: -------------------------------------------------------------------------------- 1 | from .FunctionRpc_pb2_grpc import ( # NoQA 2 | FunctionRpcStub, 3 | FunctionRpcServicer, 4 | add_FunctionRpcServicer_to_server) 5 | 6 | from .FunctionRpc_pb2 import ( # NoQA 7 | StreamingMessage, 8 | StartStream, 9 | WorkerInitRequest, 10 | WorkerInitResponse, 11 | RpcFunctionMetadata, 12 | FunctionLoadRequest, 13 | FunctionLoadResponse, 14 | InvocationRequest, 15 | InvocationResponse, 16 | WorkerHeartbeat, 17 | BindingInfo, 18 | StatusResult, 19 | RpcException, 20 | ParameterBinding, 21 | TypedData, 22 | RpcHttp, 23 | RpcLog) 24 | -------------------------------------------------------------------------------- /azure/functions_worker/bindings/context.py: -------------------------------------------------------------------------------- 1 | from azure.functions import _abc as azf_abc 2 | 3 | 4 | class Context(azf_abc.Context): 5 | 6 | def __init__(self, func_name: str, func_dir: str, 7 | invocation_id: str) -> None: 8 | self.__func_name = func_name 9 | self.__func_dir = func_dir 10 | self.__invocation_id = invocation_id 11 | 12 | @property 13 | def invocation_id(self) -> str: 14 | return self.__invocation_id 15 | 16 | @property 17 | def function_name(self) -> str: 18 | return self.__func_name 19 | 20 | @property 21 | def function_directory(self) -> str: 22 | return self.__func_dir 23 | -------------------------------------------------------------------------------- /tests/http_functions/async_logging/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | import azure.functions 5 | 6 | 7 | logger = logging.getLogger('my function') 8 | 9 | 10 | async def main(req: azure.functions.HttpRequest): 11 | logger.info('hello %s', 'info') 12 | 13 | await asyncio.sleep(0.1) 14 | 15 | # Create a nested task to check if invocation_id is still 16 | # logged correctly. 17 | await asyncio.ensure_future(nested()) 18 | 19 | await asyncio.sleep(0.1) 20 | 21 | return 'OK-async' 22 | 23 | 24 | async def nested(): 25 | try: 26 | 1 / 0 27 | except ZeroDivisionError: 28 | logger.error('and another error', exc_info=True) 29 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = AzureFunctionsforPython 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/setup_container_environment.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | apt-get update 5 | apt-get install azure-functions-core-tools jq git unzip gettext -y 6 | curl -sSL https://get.docker.com/ | sh 7 | 8 | # Install azure CLI 9 | apt-get install apt-transport-https lsb-release software-properties-common dirmngr -y 10 | AZ_REPO=$(lsb_release -cs) 11 | echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $AZ_REPO main" | tee /etc/apt/sources.list.d/azure-cli.list 12 | apt-key --keyring /etc/apt/trusted.gpg.d/Microsoft.gpg adv --keyserver packages.microsoft.com --recv-keys BC528686B50D79E339D3721CEB3E94ADBE1229CF 13 | apt-get update 14 | apt-get install azure-cli -y -------------------------------------------------------------------------------- /tests/cosmosdb_functions/put_document/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "disabled": false, 4 | 5 | "bindings": [ 6 | { 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "name": "req" 10 | }, 11 | { 12 | "direction": "out", 13 | "type": "cosmosDB", 14 | "name": "doc", 15 | "databaseName": "test", 16 | "collectionName": "items", 17 | "leaseCollectionName": "leases", 18 | "createLeaseCollectionIfNotExists": true, 19 | "connectionStringSetting": "AzureWebJobsCosmosDBConnectionString", 20 | "createIfNotExists": true 21 | }, 22 | { 23 | "direction": "out", 24 | "name": "$return", 25 | "type": "http" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tests/cosmosdb_functions/cosmosdb_input/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "disabled": false, 4 | 5 | "bindings": [ 6 | { 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "name": "req" 10 | }, 11 | { 12 | "direction": "in", 13 | "type": "cosmosDB", 14 | "name": "docs", 15 | "databaseName": "test", 16 | "collectionName": "items", 17 | "id": "cosmosdb-input-test", 18 | "leaseCollectionName": "leases", 19 | "connectionStringSetting": "AzureWebJobsCosmosDBConnectionString", 20 | "createLeaseCollectionIfNotExists": true 21 | }, 22 | { 23 | "type": "http", 24 | "direction": "out", 25 | "name": "$return" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tests/cosmosdb_functions/cosmosdb_trigger/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "disabled": false, 4 | 5 | "bindings": [ 6 | { 7 | "direction": "in", 8 | "type": "cosmosDBTrigger", 9 | "name": "docs", 10 | "databaseName": "test", 11 | "collectionName": "items", 12 | "id": "cosmosdb-trigger-test", 13 | "leaseCollectionName": "leases", 14 | "connectionStringSetting": "AzureWebJobsCosmosDBConnectionString", 15 | "createLeaseCollectionIfNotExists": true 16 | }, 17 | { 18 | "type": "blob", 19 | "direction": "out", 20 | "name": "$return", 21 | "connection": "AzureWebJobsStorage", 22 | "path": "python-worker-tests/test-cosmosdb-triggered.txt" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tests/queue_functions/queue_trigger/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import azure.functions as azf 4 | 5 | 6 | def main(msg: azf.QueueMessage) -> str: 7 | result = json.dumps({ 8 | 'id': msg.id, 9 | 'body': msg.get_body().decode('utf-8'), 10 | 'expiration_time': (msg.expiration_time.isoformat() 11 | if msg.expiration_time else None), 12 | 'insertion_time': (msg.insertion_time.isoformat() 13 | if msg.insertion_time else None), 14 | 'time_next_visible': (msg.time_next_visible.isoformat() 15 | if msg.time_next_visible else None), 16 | 'pop_receipt': msg.pop_receipt, 17 | 'dequeue_count': msg.dequeue_count 18 | }) 19 | 20 | return result 21 | -------------------------------------------------------------------------------- /pack/Microsoft.Azure.Functions.PythonWorkerRunEnvironments.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/setup_test_environment.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | # Get the configuration variables 5 | source "$(dirname "${BASH_SOURCE[0]}")/helpers/get_config_variables.sh" 6 | 7 | # Login to the Service Principal Azure Account 8 | az login --service-principal -u ${SP_USER_NAME} -p ${SP_PASSWORD} --tenant ${SP_TENANT} 9 | 10 | # Update the ACR Docker Image with the dev branch of Docker image and python worker 11 | chmod a+x $(dirname "${BASH_SOURCE[0]}")/../dev_docker_setup/setup.sh 12 | $(dirname "${BASH_SOURCE[0]}")/../dev_docker_setup/setup.sh ${ACR_NAME} ${DEV_IMAGE_NAME} ${DOCKER_WORKING_DIR} 13 | 14 | # Setup the dev func executables 15 | chmod a+x $(dirname "${BASH_SOURCE[0]}")/../dev_func_setup/setup.sh 16 | $(dirname "${BASH_SOURCE[0]}")/../dev_func_setup/setup.sh ${FUNC_DEV_DIR} ${FUNC_DEV_URL} -------------------------------------------------------------------------------- /tests/servicebus_functions/servicebus_trigger/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import azure.functions as azf 4 | 5 | 6 | def main(msg: azf.ServiceBusMessage) -> str: 7 | result = json.dumps({ 8 | 'message_id': msg.message_id, 9 | 'body': msg.get_body().decode('utf-8'), 10 | 'content_type': msg.content_type, 11 | 'expiration_time': msg.expiration_time, 12 | 'label': msg.label, 13 | 'partition_key': msg.partition_key, 14 | 'reply_to': msg.reply_to, 15 | 'reply_to_session_id': msg.reply_to_session_id, 16 | 'scheduled_enqueue_time': msg.scheduled_enqueue_time, 17 | 'session_id': msg.session_id, 18 | 'time_to_live': msg.time_to_live, 19 | 'to': msg.to, 20 | 'user_properties': msg.user_properties, 21 | }) 22 | 23 | return result 24 | -------------------------------------------------------------------------------- /azure/functions_worker/bindings/__init__.py: -------------------------------------------------------------------------------- 1 | from .context import Context 2 | from .meta import check_input_type_annotation 3 | from .meta import check_output_type_annotation 4 | from .meta import is_binding, is_trigger_binding 5 | from .meta import from_incoming_proto, to_outgoing_proto 6 | from .out import Out 7 | 8 | # Import type implementations and converters 9 | # to get them registered and available: 10 | from . import blob # NoQA 11 | from . import cosmosdb # NoQA 12 | from . import eventgrid # NoQA 13 | from . import eventhub # NoQA 14 | from . import http # NoQA 15 | from . import queue # NoQA 16 | from . import servicebus # NoQA 17 | from . import timer # NoQA 18 | 19 | 20 | __all__ = ( 21 | 'Out', 'Context', 22 | 'is_binding', 'is_trigger_binding', 23 | 'check_input_type_annotation', 'check_output_type_annotation', 24 | 'from_incoming_proto', 'to_outgoing_proto', 25 | ) 26 | -------------------------------------------------------------------------------- /pack/Microsoft.Azure.Functions.PythonWorkerRunEnvironments.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Microsoft.Azure.Functions.PythonWorkerRunEnvironments 5 | 1.0.0-beta0 6 | Microsoft 7 | Microsoft 8 | false 9 | Microsoft Azure Functions Python Worker Run Environments 10 | © .NET Foundation. All rights reserved. 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=AzureFunctionsforPython 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /pack/templates/win_env_gen.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | jobName: 'WindowsEnvGen' 3 | dependency: 'Tests' 4 | vmImage: 'vs2017-win2016' 5 | pythonVersion: '3.6' 6 | artifactName: 'Windows' 7 | 8 | jobs: 9 | - job: ${{ parameters.jobName }} 10 | dependsOn: ${{ parameters.dependency }} 11 | pool: 12 | vmImage: ${{ parameters.vmImage }} 13 | steps: 14 | - task: UsePythonVersion@0 15 | inputs: 16 | versionSpec: ${{ parameters.pythonVersion }} 17 | addToPath: true 18 | - task: PowerShell@2 19 | inputs: 20 | filePath: 'pack\scripts\win_deps.ps1' 21 | - task: CopyFiles@2 22 | inputs: 23 | contents: | 24 | python\* 25 | targetFolder: '$(Build.ArtifactStagingDirectory)' 26 | flattenFolders: true 27 | - task: CopyFiles@2 28 | inputs: 29 | contents: deps\**\* 30 | targetFolder: '$(Build.ArtifactStagingDirectory)' 31 | - task: PublishBuildArtifacts@1 32 | inputs: 33 | pathtoPublish: '$(Build.ArtifactStagingDirectory)' 34 | artifactName: ${{ parameters.artifactName }} -------------------------------------------------------------------------------- /tests/test_loader.py: -------------------------------------------------------------------------------- 1 | from azure.functions_worker import testutils 2 | 3 | 4 | class TestLoader(testutils.WebHostTestCase): 5 | 6 | @classmethod 7 | def get_script_dir(cls): 8 | return 'load_functions' 9 | 10 | def test_loader_simple(self): 11 | r = self.webhost.request('GET', 'simple') 12 | self.assertEqual(r.status_code, 200) 13 | self.assertEqual(r.text, '__app__.simple.main') 14 | 15 | def test_loader_custom_entrypoint(self): 16 | r = self.webhost.request('GET', 'entrypoint') 17 | self.assertEqual(r.status_code, 200) 18 | self.assertEqual(r.text, '__app__.entrypoint.main') 19 | 20 | def test_loader_subdir(self): 21 | r = self.webhost.request('GET', 'subdir') 22 | self.assertEqual(r.status_code, 200) 23 | self.assertEqual(r.text, '__app__.subdir.sub.main') 24 | 25 | def test_loader_relimport(self): 26 | r = self.webhost.request('GET', 'relimport') 27 | self.assertEqual(r.status_code, 200) 28 | self.assertEqual(r.text, '__app__.relimport.relative') 29 | -------------------------------------------------------------------------------- /pack/templates/nix_env_gen.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | jobName: 'LinuxEnvGen' 3 | dependency: 'Tests' 4 | vmImage: 'ubuntu-16.04' 5 | pythonVersion: '3.6' 6 | artifactName: 'Linux' 7 | 8 | jobs: 9 | - job: ${{ parameters.jobName }} 10 | dependsOn: ${{ parameters.dependency }} 11 | pool: 12 | vmImage: ${{ parameters.vmImage }} 13 | steps: 14 | - task: UsePythonVersion@0 15 | inputs: 16 | versionSpec: ${{ parameters.pythonVersion }} 17 | addToPath: true 18 | - task: ShellScript@2 19 | inputs: 20 | disableAutoCwd: true 21 | scriptPath: 'pack/scripts/nix_deps.sh' 22 | - task: CopyFiles@2 23 | inputs: 24 | contents: | 25 | python/* 26 | targetFolder: '$(Build.ArtifactStagingDirectory)' 27 | flattenFolders: true 28 | - task: CopyFiles@2 29 | inputs: 30 | contents: deps/**/* 31 | targetFolder: '$(Build.ArtifactStagingDirectory)' 32 | - task: PublishBuildArtifacts@1 33 | inputs: 34 | pathtoPublish: '$(Build.ArtifactStagingDirectory)' 35 | artifactName: ${{ parameters.artifactName }} -------------------------------------------------------------------------------- /tests/test_servicebus_functions.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | 4 | from azure.functions_worker import testutils 5 | 6 | 7 | class TestServiceBusFunctions(testutils.WebHostTestCase): 8 | 9 | @classmethod 10 | def get_script_dir(cls): 11 | return 'servicebus_functions' 12 | 13 | def test_servicebus_basic(self): 14 | data = str(round(time.time())) 15 | r = self.webhost.request('POST', 'put_message', 16 | data=data) 17 | self.assertEqual(r.status_code, 200) 18 | self.assertEqual(r.text, 'OK') 19 | 20 | max_retries = 10 21 | 22 | for try_no in range(max_retries): 23 | # wait for trigger to process the queue item 24 | time.sleep(1) 25 | 26 | try: 27 | r = self.webhost.request('GET', 'get_servicebus_triggered') 28 | self.assertEqual(r.status_code, 200) 29 | msg = r.json() 30 | self.assertEqual(msg['body'], data) 31 | except (AssertionError, json.JSONDecodeError): 32 | if try_no == max_retries - 1: 33 | raise 34 | else: 35 | break 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /azure/functions_worker/protos/_src/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /azure/functions_worker/bindings/timer.py: -------------------------------------------------------------------------------- 1 | import json 2 | import typing 3 | 4 | from azure.functions import _abc as azf_abc 5 | 6 | from . import meta 7 | from .. import protos 8 | 9 | 10 | class TimerRequest(azf_abc.TimerRequest): 11 | 12 | def __init__(self, *, past_due: bool) -> None: 13 | self.__past_due = past_due 14 | 15 | @property 16 | def past_due(self): 17 | return self.__past_due 18 | 19 | 20 | class TimerRequestConverter(meta.InConverter, 21 | binding='timerTrigger', trigger=True): 22 | 23 | @classmethod 24 | def check_input_type_annotation( 25 | cls, pytype: type, datatype: protos.BindingInfo.DataType) -> bool: 26 | if datatype is protos.BindingInfo.undefined: 27 | return issubclass(pytype, azf_abc.TimerRequest) 28 | else: 29 | return False 30 | 31 | @classmethod 32 | def from_proto(cls, data: protos.TypedData, *, 33 | pytype: typing.Optional[type], 34 | trigger_metadata) -> typing.Any: 35 | if data.WhichOneof('data') != 'json': 36 | raise NotImplementedError 37 | 38 | info = json.loads(data.json) 39 | return TimerRequest( 40 | past_due=info.get('IsPastDue', False)) 41 | -------------------------------------------------------------------------------- /tests/test_eventhub_functions.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | 4 | from azure.functions_worker import testutils 5 | 6 | 7 | class TestEventHubFunctions(testutils.WebHostTestCase): 8 | 9 | @classmethod 10 | def get_script_dir(cls): 11 | return 'eventhub_functions' 12 | 13 | def test_eventhub_trigger(self): 14 | data = str(round(time.time())) 15 | doc = {'id': data} 16 | r = self.webhost.request('POST', 'eventhub_output', 17 | data=json.dumps(doc)) 18 | self.assertEqual(r.status_code, 200) 19 | self.assertEqual(r.text, 'OK') 20 | 21 | max_retries = 10 22 | 23 | for try_no in range(max_retries): 24 | # Allow trigger to fire. 25 | time.sleep(2) 26 | 27 | try: 28 | # Check that the trigger has fired. 29 | r = self.webhost.request('GET', 'get_eventhub_triggered') 30 | self.assertEqual(r.status_code, 200) 31 | response = r.json() 32 | 33 | self.assertEqual( 34 | response, 35 | doc 36 | ) 37 | except AssertionError as e: 38 | if try_no == max_retries - 1: 39 | raise 40 | else: 41 | break 42 | -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/helpers/helper.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | RESET='\033[0m' # No Color 4 | YELLOW='\033[1;33m' 5 | ORANGE='\033[0;33m' 6 | RED='\033[0;31m' 7 | 8 | 9 | yellow() { 10 | echo -e "${YELLOW}$1${RESET}" 11 | } 12 | get_result_of() { 13 | if [[ -z "$1" ]] 14 | then 15 | echo -e "${RED}FAILED${RESET}" 16 | elif [[ ! -f "$1" ]] 17 | then 18 | echo -e "${ORANGE}SKIPPED${RESET}" 19 | else 20 | cat "$1" 21 | fi 22 | } 23 | 24 | print_row_line() { 25 | # (32 (-10 for colors) + 4) * 6 args + 1 for beauty 26 | row=$(printf "%157s" "-") 27 | echo "${row// /-}" 28 | } 29 | 30 | print_row() { 31 | printf "%-4s" "|" 32 | for arg in "$@" 33 | do 34 | printf "%-32s %-4s" ${arg} "|" 35 | done 36 | printf "\n" 37 | } 38 | 39 | # Expects a test_script, test_log, timeout, test_result 40 | run_test() { 41 | exit_code=0 42 | set -o pipefail 43 | chmod a+x $1 44 | if [[ ${TESTS_VERBOSE} = "SILENT" ]] 45 | then 46 | timeout $3 $1 > $2 47 | exit_code=$? 48 | else 49 | timeout $3 $1 2>&1 | tee $2 50 | exit_code=$? 51 | fi 52 | # This means we had a timeout 53 | if [[ ${exit_code} = 124 ]] 54 | then 55 | echo "Test Timed Out!" 56 | echo -e "${ORANGE}TIMEOUT${RESET}" > $4 57 | fi 58 | } 59 | 60 | -------------------------------------------------------------------------------- /.ci/e2e/running-environments/Docker/test_runner.Dockerfile: -------------------------------------------------------------------------------- 1 | # When running, make sure to mount Docker sock and use the python-worker directory as the base dir 2 | # Example- 3 | # docker build -f .ci\e2e\running-environments\Docker\test_runner.Dockerfile -t my-test-image 4 | # docker run -v /var/run/docker.sock:/var/run/docker.sock my-test-image 5 | 6 | FROM mcr.microsoft.com/azure-functions/python:2.0 7 | 8 | COPY . /azure-functions-python-worker 9 | 10 | RUN apt-get update && \ 11 | apt-get install azure-functions-core-tools jq git unzip dos2unix -y 12 | 13 | # Install docker amd azure CLI 14 | RUN curl -sSL https://get.docker.com/ | sh && \ 15 | apt-get update && \ 16 | apt-get install apt-transport-https lsb-release software-properties-common dirmngr -y && \ 17 | AZ_REPO=$(lsb_release -cs) && \ 18 | echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $AZ_REPO main" | tee /etc/apt/sources.list.d/azure-cli.list && \ 19 | apt-key --keyring /etc/apt/trusted.gpg.d/Microsoft.gpg adv --keyserver packages.microsoft.com --recv-keys BC528686B50D79E339D3721CEB3E94ADBE1229CF && \ 20 | apt-get update && \ 21 | apt-get install azure-cli -y 22 | 23 | ENV ENVIRONMENT=DOCKER 24 | ENV EXIT_ON_FAIL=FALSE 25 | 26 | RUN find /azure-functions-python-worker/.ci/e2e -type f -print0 | xargs -0 dos2unix 27 | 28 | CMD [ "bash", "/azure-functions-python-worker/.ci/e2e/running-environments/Docker/start_tests_docker.sh" ] -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/func_tests_core/customer_churn_app/build_native_deps_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/helper.sh" 4 | ensure_usage "$@" 5 | 6 | mkdir -p "$1" 7 | cd "$1" 8 | git clone https://github.com/asavaritayal/customer-churn-prediction 9 | cd customer-churn-prediction 10 | FUNCTIONS_PYTHON_DOCKER_IMAGE=$4 "$3" azure functionapp publish "$2" --build-native-deps 11 | 12 | if [[ $? -eq 0 ]] 13 | then 14 | sleep 5s 15 | # https://stackoverflow.com/questions/2220301/how-to-evaluate-http-response-codes-from-bash-shell-script 16 | STATUS_CODE=$(curl --write-out %{http_code} --silent --output /dev/null https://$2.azurewebsites.net/api/predict) 17 | verify_status_code $1.result 18 | else 19 | echo -e "${RED}Publishing failed (1/2)${RESET}" 20 | # Sometimes due to flakiness with the docker daemon or an azure resource may cause it to fail- 21 | # So, we retry, but just once 22 | echo "Retrying once....." 23 | FUNCTIONS_PYTHON_DOCKER_IMAGE=$4 "$3" azure functionapp publish "$2" --build-native-deps 24 | if [[ $? -eq 0 ]] 25 | then 26 | sleep 5s 27 | STATUS_CODE=$(curl --write-out %{http_code} --silent --output /dev/null https://$2.azurewebsites.net/api/predict) 28 | verify_status_code $1.result 29 | else 30 | echo -e "${RED}Publishing failed (2/2)${RESET}" 31 | echo -e "${RED}FAILED${RESET}" > $1.result 32 | fi 33 | fi 34 | 35 | cd ../.. 36 | rm -rf "$1" -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/func_tests_core/customer_churn_app/no_bundler_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/helper.sh" 4 | ensure_usage "$@" 5 | 6 | mkdir -p "$1" 7 | cd "$1" 8 | git clone https://github.com/asavaritayal/customer-churn-prediction 9 | cd customer-churn-prediction 10 | FUNCTIONS_PYTHON_DOCKER_IMAGE=$4 "$3" azure functionapp publish "$2" --build-native-deps --no-bundler 11 | 12 | if [[ $? -eq 0 ]] 13 | then 14 | sleep 5s 15 | # https://stackoverflow.com/questions/2220301/how-to-evaluate-http-response-codes-from-bash-shell-script 16 | STATUS_CODE=$(curl --write-out %{http_code} --silent --output /dev/null https://$2.azurewebsites.net/api/predict) 17 | verify_status_code $1.result 18 | else 19 | echo -e "${RED}Publishing failed (1/2)${RESET}" 20 | # Sometimes due to flakiness with the docker daemon or an azure resource may cause it to fail- 21 | # So, we retry, but just once 22 | echo "Retrying once....." 23 | FUNCTIONS_PYTHON_DOCKER_IMAGE=$4 "$3" azure functionapp publish "$2" --build-native-deps --no-bundler 24 | if [[ $? -eq 0 ]] 25 | then 26 | sleep 5s 27 | STATUS_CODE=$(curl --write-out %{http_code} --silent --output /dev/null https://$2.azurewebsites.net/api/predict) 28 | verify_status_code $1.result 29 | else 30 | echo -e "${RED}Publishing failed (2/2)${RESET}" 31 | echo -e "${RED}FAILED${RESET}" > $1.result 32 | fi 33 | fi 34 | 35 | cd ../.. 36 | rm -rf "$1" -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/dev_docker_setup/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | USAGE="usage ./script " 5 | 6 | if [[ -z "$1" ]] 7 | then 8 | echo "Missing name of the ACR" 9 | echo ${USAGE} 10 | exit 1 11 | fi 12 | 13 | if [[ -z "$2" ]] 14 | then 15 | echo "Missing name of the dev image to use in ACR" 16 | echo ${USAGE} 17 | exit 1 18 | fi 19 | 20 | if [[ -z "$3" ]] 21 | then 22 | echo "Missing name of the working directory" 23 | echo ${USAGE} 24 | exit 1 25 | fi 26 | 27 | SCRIPT_DIR=`realpath $(dirname "${BASH_SOURCE[0]}")` 28 | 29 | mkdir -p $3 30 | cd $3 31 | az acr login --name $1 32 | git clone https://github.com/Azure/azure-functions-docker 33 | docker build -f ./azure-functions-docker/host/2.0/stretch/amd64/base.Dockerfile ./azure-functions-docker/host/2.0/stretch/amd64 -t azure-functions-base-dev 34 | # The directory to build from needs to be the amd64, because the Dockerfile copies resources 35 | docker build --build-arg BASE_IMAGE=azure-functions-base-dev -f ./azure-functions-docker/host/2.0/stretch/amd64/python.Dockerfile -t azure-functions-python-dev ./azure-functions-docker/host/2.0/stretch/amd64 36 | docker build --build-arg BASE_IMAGE=azure-functions-python-dev -f ${SCRIPT_DIR}/dev.Dockerfile ${SCRIPT_DIR}/../../../.. -t azure-functions-python-dev-updated 37 | docker tag azure-functions-python-dev-updated $1.azurecr.io/$2 38 | docker push $1.azurecr.io/$2 39 | echo "New image pushed to the ACR." 40 | echo "Starting cleanup" 41 | cd .. 42 | rm -r $3 43 | echo "Completed cleanup" -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/helpers/get_config_variables.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the configuration file 4 | GLOBAL_CONFIG="$(dirname "${BASH_SOURCE[0]}")/../../publish_config.json" 5 | if [[ -n "$1" ]] 6 | then 7 | echo "Using the provided global config" 8 | GLOBAL_CONFIG=$1 9 | fi 10 | 11 | # Docker Dev Variables 12 | DOCKER_CONFIG="$(cat "${GLOBAL_CONFIG}" | jq '.docker_setup')" 13 | ACR_NAME="$(echo "${DOCKER_CONFIG}" | jq -r '.acr_name')" 14 | DEV_IMAGE_NAME="$(echo "${DOCKER_CONFIG}" | jq -r '.dev_image_name')" 15 | DEV_DOCKER_IMAGE=${ACR_NAME}.azurecr.io/${DEV_IMAGE_NAME} 16 | DOCKER_WORKING_DIR="$(echo "${DOCKER_CONFIG}" | jq -r '.working_dir')" 17 | 18 | # Docker Prod Variables 19 | PROD_DOCKER_IMAGE="$(echo "${DOCKER_CONFIG}" | jq -r '.prod_image')" 20 | 21 | # Func Dev Variables 22 | FUNC_CONFIG="$(cat "${GLOBAL_CONFIG}" | jq '.func_setup')" 23 | FUNC_DEV_DIR="$(echo "${FUNC_CONFIG}" | jq -r '.func_dev_dir')" 24 | FUNC_DEV_URL="$(echo "${FUNC_CONFIG}" | jq -r '.func_dev_url')" 25 | 26 | # Working dir variables 27 | TESTS_CONFIG="$(cat "${GLOBAL_CONFIG}" | jq '.tests')" 28 | WORKING_DIR="$(echo "${TESTS_CONFIG}" | jq -r '.working_dir')" 29 | TESTS_LOGS="$(echo "${TESTS_CONFIG}" | jq -r '.logs')" 30 | TESTS_TIMEOUT="$(echo "${TESTS_CONFIG}" | jq -r '.timeout')" 31 | 32 | # Service Principal variables 33 | SP_CONFIG="$(cat "${GLOBAL_CONFIG}" | jq '.azure_service_principal')" 34 | SP_USER_NAME="$(echo "${SP_CONFIG}" | jq -r '.user_id')" 35 | SP_PASSWORD="$(echo "${SP_CONFIG}" | jq -r '.password')" 36 | SP_TENANT="$(echo "${SP_CONFIG}" | jq -r '.tenant')" 37 | -------------------------------------------------------------------------------- /azure/functions_worker/logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import logging.handlers 3 | import sys 4 | 5 | 6 | logger = logging.getLogger('azure.functions_worker') 7 | error_logger = logging.getLogger('azure.functions_worker_errors') 8 | 9 | 10 | def setup(log_level, log_destination): 11 | if log_level == 'TRACE': 12 | log_level = 'DEBUG' 13 | 14 | formatter = logging.Formatter( 15 | 'LanguageWorkerConsoleLog %(levelname)s: %(message)s') 16 | 17 | error_handler = None 18 | handler = None 19 | 20 | if log_destination is None: 21 | # With no explicit log destination we do split logging, 22 | # errors go into stderr, everything else -- to stdout. 23 | error_handler = logging.StreamHandler(sys.stderr) 24 | error_handler.setFormatter(formatter) 25 | error_handler.setLevel(getattr(logging, log_level)) 26 | 27 | handler = logging.StreamHandler(sys.stdout) 28 | 29 | elif log_destination in ('stdout', 'stderr'): 30 | handler = logging.StreamHandler(getattr(sys, log_destination)) 31 | 32 | elif log_destination == 'syslog': 33 | handler = logging.handlers.SysLogHandler() 34 | 35 | else: 36 | handler = logging.FileHandler(log_destination) 37 | 38 | if error_handler is None: 39 | error_handler = handler 40 | 41 | handler.setFormatter(formatter) 42 | handler.setLevel(getattr(logging, log_level)) 43 | 44 | logger.addHandler(handler) 45 | logger.setLevel(getattr(logging, log_level)) 46 | 47 | error_logger.addHandler(error_handler) 48 | error_logger.setLevel(getattr(logging, log_level)) 49 | -------------------------------------------------------------------------------- /tests/test_mock_timer_functions.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from azure.functions_worker import protos 4 | from azure.functions_worker import testutils 5 | 6 | 7 | class TestTimerFunctions(testutils.AsyncTestCase): 8 | 9 | async def test_mock_timer__return_pastdue(self): 10 | async with testutils.start_mockhost( 11 | script_root='timer_functions') as host: 12 | 13 | func_id, r = await host.load_function('return_pastdue') 14 | 15 | self.assertEqual(r.response.function_id, func_id) 16 | self.assertEqual(r.response.result.status, 17 | protos.StatusResult.Success) 18 | 19 | async def call_and_check(due: bool): 20 | _, r = await host.invoke_function( 21 | 'return_pastdue', [ 22 | protos.ParameterBinding( 23 | name='timer', 24 | data=protos.TypedData( 25 | json=json.dumps({ 26 | 'IsPastDue': due 27 | }))) 28 | ]) 29 | self.assertEqual(r.response.result.status, 30 | protos.StatusResult.Success) 31 | self.assertEqual( 32 | list(r.response.output_data), [ 33 | protos.ParameterBinding( 34 | name='pastdue', 35 | data=protos.TypedData(string=str(due))) 36 | ]) 37 | 38 | await call_and_check(True) 39 | await call_and_check(False) 40 | -------------------------------------------------------------------------------- /azure/functions_worker/bindings/eventgrid.py: -------------------------------------------------------------------------------- 1 | import json 2 | import typing 3 | 4 | from azure.functions import _eventgrid 5 | 6 | from . import meta 7 | from .. import protos 8 | 9 | 10 | class EventGridEventInConverter(meta.InConverter, 11 | binding='eventGridTrigger', trigger=True): 12 | 13 | @classmethod 14 | def check_input_type_annotation( 15 | cls, pytype: type, datatype: protos.BindingInfo.DataType) -> bool: 16 | if datatype is protos.BindingInfo.undefined: 17 | return issubclass(pytype, _eventgrid.EventGridEvent) 18 | else: 19 | return False 20 | 21 | @classmethod 22 | def from_proto(cls, data: protos.TypedData, *, 23 | pytype: typing.Optional[type], 24 | trigger_metadata) -> typing.Any: 25 | data_type = data.WhichOneof('data') 26 | 27 | if data_type == 'json': 28 | body = json.loads(data.json) 29 | else: 30 | raise NotImplementedError( 31 | f'unsupported event grid payload type: {data_type}') 32 | 33 | if trigger_metadata is None: 34 | raise NotImplementedError( 35 | f'missing trigger metadata for event grid input') 36 | 37 | return _eventgrid.EventGridEvent( 38 | id=body.get('id'), 39 | topic=body.get('topic'), 40 | subject=body.get('subject'), 41 | event_type=body.get('eventType'), 42 | event_time=cls._parse_datetime(body.get('eventTime')), 43 | data=body.get('data'), 44 | data_version=body.get('dataVersion'), 45 | ) 46 | -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/func_tests_core/helpers/helper.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | USAGE="usage ./script " 4 | # https://stackoverflow.com/questions/5947742/how-to-change-the-output-color-of-echo-in-linux 5 | RED='\033[0;31m' 6 | GREEN='\033[0;32m' 7 | RESET='\033[0m' # No Color 8 | 9 | ensure_usage() { 10 | 11 | if [[ -z "$1" ]] 12 | then 13 | echo "Missing name of the directory" 14 | echo ${USAGE} 15 | exit 1 16 | fi 17 | 18 | if [[ -z "$2" ]] 19 | then 20 | echo "Missing name of the functions app to publish" 21 | echo ${USAGE} 22 | exit 1 23 | fi 24 | 25 | if [[ -z "$3" ]] 26 | then 27 | echo "Missing name of the func executable" 28 | echo ${USAGE} 29 | exit 1 30 | fi 31 | 32 | if [[ -z "$4" ]] 33 | then 34 | echo "Missing name of the docker image to use" 35 | echo ${USAGE} 36 | exit 1 37 | fi 38 | 39 | } 40 | 41 | # Assuming Status_code is set 42 | verify_status_code() { 43 | if [[ -z "$1" ]] 44 | then 45 | echo "Missing name of the log file" 46 | exit 1 47 | fi 48 | if [[ "${STATUS_CODE}" -ne 200 ]] 49 | then 50 | MESSAGE="${RED}TEST FAILED: Expected Status Code 200. Found Status Code ${STATUS_CODE}${RESET}" 51 | (>&2 echo -e ${MESSAGE}) 52 | echo -e "${RED}FAILED${RESET}" > $1 53 | else 54 | MESSAGE="${GREEN}TEST PASSED: Status Code 200.${RESET}" 55 | echo -e ${MESSAGE} 56 | echo -e "${GREEN}PASSED${RESET}" > $1 57 | fi 58 | } 59 | 60 | fail_after_timeout() { 61 | timeout $1 ${@:2} 62 | } -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. _azure-functions-reference: 2 | 3 | ============= 4 | API Reference 5 | ============= 6 | 7 | .. module:: azure.functions 8 | :synopsis: Azure Functions bindings. 9 | 10 | .. currentmodule:: azure.functions 11 | 12 | 13 | .. _azure-functions-bindings-blob: 14 | 15 | Blob Bindings 16 | ============= 17 | 18 | .. autoclass:: azure.functions.InputStream 19 | :members: 20 | 21 | 22 | .. _azure-functions-bindings-http: 23 | 24 | HTTP Bindings 25 | ============= 26 | 27 | .. autoclass:: azure.functions.HttpRequest 28 | :members: 29 | 30 | .. autoclass:: azure.functions.HttpResponse 31 | :members: 32 | 33 | 34 | .. _azure-functions-bindings-queue: 35 | 36 | Queue Bindings 37 | ============== 38 | 39 | .. autoclass:: azure.functions.QueueMessage 40 | :members: 41 | 42 | 43 | .. _azure-functions-bindings-timer: 44 | 45 | Timer Bindings 46 | ============== 47 | 48 | .. autoclass:: azure.functions.TimerRequest 49 | :members: 50 | 51 | 52 | .. _azure-functions-bindings-cosmosdb: 53 | 54 | CosmosDB Bindings 55 | ================= 56 | 57 | .. autoclass:: azure.functions.Document 58 | :members: 59 | 60 | .. describe:: doc[field] 61 | 62 | Return the field of *doc* with field name *field*. 63 | 64 | .. describe:: doc[field] = value 65 | 66 | Set field of *doc* with field name *field* to *value*. 67 | 68 | .. autoclass:: azure.functions.DocumentList 69 | :members: 70 | 71 | 72 | .. _azure-functions-bindings-context: 73 | 74 | Function Context 75 | ================ 76 | 77 | .. autoclass:: azure.functions.Context 78 | :members: 79 | 80 | 81 | Out Parameters 82 | ============== 83 | 84 | .. autoclass:: azure.functions.Out 85 | :members: 86 | -------------------------------------------------------------------------------- /python/worker.py: -------------------------------------------------------------------------------- 1 | """Main entrypoint.""" 2 | 3 | 4 | try: 5 | from azure.functions_worker.main import main 6 | 7 | except ImportError: 8 | # Compatibility with hard-bundled pre-beta worker versions in 9 | # deployed function apps. 10 | import argparse 11 | import traceback 12 | 13 | def parse_args(): 14 | parser = argparse.ArgumentParser( 15 | description='Python Azure Functions Worker') 16 | parser.add_argument('--host') 17 | parser.add_argument('--port', type=int) 18 | parser.add_argument('--workerId', dest='worker_id') 19 | parser.add_argument('--requestId', dest='request_id') 20 | parser.add_argument('--log-level', type=str, default='INFO', 21 | choices=['TRACE', 'INFO', 'WARNING', 'ERROR'],) 22 | parser.add_argument('--log-to', type=str, default=None, 23 | help='log destination: stdout, stderr, ' 24 | 'syslog, or a file path') 25 | parser.add_argument('--grpcMaxMessageLength', type=int, 26 | dest='grpc_max_msg_len') 27 | return parser.parse_args() 28 | 29 | def main(): 30 | args = parse_args() 31 | 32 | import azure.functions # NoQA 33 | import azure.functions_worker 34 | from azure.functions_worker import aio_compat 35 | 36 | try: 37 | return aio_compat.run(azure.functions_worker.start_async( 38 | args.host, args.port, args.worker_id, args.request_id, 39 | args.grpc_max_msg_len)) 40 | except Exception: 41 | print(traceback.format_exc(), flush=True) 42 | raise 43 | 44 | 45 | if __name__ == '__main__': 46 | main() 47 | -------------------------------------------------------------------------------- /tests/test_code_quality.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import subprocess 3 | import sys 4 | import unittest 5 | 6 | 7 | ROOT_PATH = pathlib.Path(__file__).parent.parent 8 | 9 | 10 | class TestCodeQuality(unittest.TestCase): 11 | def test_mypy(self): 12 | try: 13 | import mypy # NoQA 14 | except ImportError: 15 | raise unittest.SkipTest('mypy module is missing') 16 | 17 | try: 18 | subprocess.run( 19 | [sys.executable, '-m', 'mypy', '-m', 'azure.functions_worker'], 20 | check=True, 21 | stdout=subprocess.PIPE, 22 | stderr=subprocess.PIPE, 23 | cwd=str(ROOT_PATH)) 24 | except subprocess.CalledProcessError as ex: 25 | output = ex.output.decode() 26 | raise AssertionError( 27 | f'mypy validation failed:\n{output}') from None 28 | 29 | def test_flake8(self): 30 | try: 31 | import flake8 # NoQA 32 | except ImportError: 33 | raise unittest.SkipTest('flake8 moudule is missing') 34 | 35 | config_path = ROOT_PATH / '.flake8' 36 | if not config_path.exists(): 37 | raise unittest.SkipTest('could not locate the .flake8 file') 38 | 39 | try: 40 | subprocess.run( 41 | [sys.executable, '-m', 'flake8', '--config', str(config_path)], 42 | check=True, 43 | stdout=subprocess.PIPE, 44 | stderr=subprocess.PIPE, 45 | cwd=str(ROOT_PATH)) 46 | except subprocess.CalledProcessError as ex: 47 | output = ex.output.decode() 48 | raise AssertionError( 49 | f'flake8 validation failed:\n{output}') from None 50 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | #### Investigative information 7 | 8 | Please provide the following: 9 | 10 | - Timestamp: 11 | - Function App name: 12 | - Function name(s) (as appropriate): 13 | - Core Tools version: 14 | 15 | #### Repro steps 16 | 17 | Provide the steps required to reproduce the problem: 18 | 19 | 26 | 27 | #### Expected behavior 28 | 29 | Provide a description of the expected behavior. 30 | 31 | 36 | 37 | #### Actual behavior 38 | 39 | Provide a description of the actual behavior observed. 40 | 41 | 46 | 47 | #### Known workarounds 48 | 49 | Provide a description of any known workarounds. 50 | 51 | 56 | 57 | #### Related information 58 | 59 | Provide any related information 60 | 61 | * Links to source 62 | * Contents of the requirements.txt file 63 | * Bindings used 64 | 65 | 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | # nuget 104 | *.nupkg 105 | 106 | .testconfig 107 | .pytest_cache 108 | -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/func_tests_core/new_functionapp/packapp_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/helper.sh" 4 | ensure_usage "$@" 5 | 6 | # Setup a new func app from prod func 7 | mkdir -p "$1" 8 | cd "$1" 9 | python -m venv env_new_build_native 10 | source env_new_build_native/bin/activate 11 | "$3" init . --worker-runtime python 12 | "$3" new --template httptrigger --name httptriggerTest 13 | 14 | # Change auth level to anonymous 15 | jq '.bindings[].authLevel="anonymous"' httptriggerTest/function.json > httptriggerTest/replaced.json 16 | mv httptriggerTest/replaced.json httptriggerTest/function.json 17 | 18 | # Publish and verify 19 | FUNCTIONS_PYTHON_DOCKER_IMAGE=$4 "$3" azure functionapp publish "$2" 20 | 21 | if [[ $? -eq 0 ]] 22 | then 23 | sleep 5s 24 | # https://stackoverflow.com/questions/2220301/how-to-evaluate-http-response-codes-from-bash-shell-script 25 | STATUS_CODE=$(curl --write-out %{http_code} --silent --output /dev/null https://$2.azurewebsites.net/api/httptriggerTest?name=test) 26 | verify_status_code $1.result 27 | else 28 | echo -e "${RED}Publishing failed (1/2)${RESET}" 29 | # Sometimes due to flakiness with the docker daemon or an azure resource may cause it to fail- 30 | # So, we retry, but just once 31 | echo "Retrying once....." 32 | FUNCTIONS_PYTHON_DOCKER_IMAGE=$4 "$3" azure functionapp publish "$2" 33 | if [[ $? -eq 0 ]] 34 | then 35 | sleep 5s 36 | STATUS_CODE=$(curl --write-out %{http_code} --silent --output /dev/null https://$2.azurewebsites.net/api/httptriggerTest?name=test) 37 | verify_status_code $1.result 38 | else 39 | echo -e "${RED}Publishing failed (2/2)${RESET}" 40 | echo -e "${RED}FAILED${RESET}" > $1.result 41 | fi 42 | fi 43 | 44 | deactivate 45 | cd .. 46 | rm -rf "$1" -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/func_tests_core/new_functionapp/build_native_deps_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/helper.sh" 4 | ensure_usage "$@" 5 | 6 | # Setup a new func app from prod func 7 | mkdir -p "$1" 8 | cd "$1" 9 | python -m venv env_new_build_native 10 | source env_new_build_native/bin/activate 11 | "$3" init . --worker-runtime python 12 | "$3" new --template httptrigger --name httptriggerTest 13 | 14 | # Change auth level to anonymous 15 | jq '.bindings[].authLevel="anonymous"' httptriggerTest/function.json > httptriggerTest/replaced.json 16 | mv httptriggerTest/replaced.json httptriggerTest/function.json 17 | 18 | # Publish and verify 19 | FUNCTIONS_PYTHON_DOCKER_IMAGE=$4 "$3" azure functionapp publish "$2" --build-native-deps 20 | 21 | if [[ $? -eq 0 ]] 22 | then 23 | sleep 5s 24 | # https://stackoverflow.com/questions/2220301/how-to-evaluate-http-response-codes-from-bash-shell-script 25 | STATUS_CODE=$(curl --write-out %{http_code} --silent --output /dev/null https://$2.azurewebsites.net/api/httptriggerTest?name=test) 26 | verify_status_code $1.result 27 | else 28 | echo -e "${RED}Publishing failed (1/2)${RESET}" 29 | # Sometimes due to flakiness with the docker daemon or an azure resource may cause it to fail- 30 | # So, we retry, but just once 31 | echo "Retrying once....." 32 | FUNCTIONS_PYTHON_DOCKER_IMAGE=$4 "$3" azure functionapp publish "$2" --build-native-deps 33 | if [[ $? -eq 0 ]] 34 | then 35 | sleep 5s 36 | STATUS_CODE=$(curl --write-out %{http_code} --silent --output /dev/null https://$2.azurewebsites.net/api/httptriggerTest?name=test) 37 | verify_status_code $1.result 38 | else 39 | echo -e "${RED}Publishing failed (2/2)${RESET}" 40 | echo -e "${RED}FAILED${RESET}" > $1.result 41 | fi 42 | fi 43 | 44 | deactivate 45 | cd .. 46 | rm -rf "$1" -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/func_tests_core/new_functionapp/no_bundler_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source "$(dirname "${BASH_SOURCE[0]}")/../helpers/helper.sh" 4 | ensure_usage "$@" 5 | 6 | # Setup a new func app from prod func 7 | mkdir -p "$1" 8 | cd "$1" 9 | python -m venv env_new_build_native 10 | source env_new_build_native/bin/activate 11 | "$3" init . --worker-runtime python 12 | "$3" new --template httptrigger --name httptriggerTest 13 | 14 | # Change auth level to anonymous 15 | jq '.bindings[].authLevel="anonymous"' httptriggerTest/function.json > httptriggerTest/replaced.json 16 | mv httptriggerTest/replaced.json httptriggerTest/function.json 17 | 18 | # Publish and verify 19 | FUNCTIONS_PYTHON_DOCKER_IMAGE=$4 "$3" azure functionapp publish "$2" --build-native-deps --no-bundler 20 | 21 | if [[ $? -eq 0 ]] 22 | then 23 | sleep 5s 24 | # https://stackoverflow.com/questions/2220301/how-to-evaluate-http-response-codes-from-bash-shell-script 25 | STATUS_CODE=$(curl --write-out %{http_code} --silent --output /dev/null https://$2.azurewebsites.net/api/httptriggerTest?name=test) 26 | verify_status_code $1.result 27 | else 28 | echo -e "${RED}Publishing failed (1/2)${RESET}" 29 | # Sometimes due to flakiness with the docker daemon or an azure resource may cause it to fail- 30 | # So, we retry, but just once 31 | echo "Retrying once....." 32 | FUNCTIONS_PYTHON_DOCKER_IMAGE=$4 "$3" azure functionapp publish "$2" --build-native-deps --no-bundler 33 | if [[ $? -eq 0 ]] 34 | then 35 | sleep 5s 36 | STATUS_CODE=$(curl --write-out %{http_code} --silent --output /dev/null https://$2.azurewebsites.net/api/httptriggerTest?name=test) 37 | verify_status_code $1.result 38 | else 39 | echo -e "${RED}Publishing failed (2/2)${RESET}" 40 | echo -e "${RED}FAILED${RESET}" > $1.result 41 | fi 42 | fi 43 | 44 | deactivate 45 | cd .. 46 | rm -rf "$1" -------------------------------------------------------------------------------- /azure/functions_worker/main.py: -------------------------------------------------------------------------------- 1 | """Main entrypoint.""" 2 | 3 | 4 | import argparse 5 | 6 | from . import aio_compat 7 | from . import dispatcher 8 | from . import logging 9 | from .logging import error_logger, logger 10 | 11 | 12 | def parse_args(): 13 | parser = argparse.ArgumentParser( 14 | description='Python Azure Functions Worker') 15 | parser.add_argument('--host') 16 | parser.add_argument('--port', type=int) 17 | parser.add_argument('--workerId', dest='worker_id') 18 | parser.add_argument('--requestId', dest='request_id') 19 | parser.add_argument('--log-level', type=str, default='INFO', 20 | choices=['TRACE', 'INFO', 'WARNING', 'ERROR'],) 21 | parser.add_argument('--log-to', type=str, default=None, 22 | help='log destination: stdout, stderr, ' 23 | 'syslog, or a file path') 24 | parser.add_argument('--grpcMaxMessageLength', type=int, 25 | dest='grpc_max_msg_len') 26 | return parser.parse_args() 27 | 28 | 29 | def main(): 30 | args = parse_args() 31 | logging.setup(log_level=args.log_level, log_destination=args.log_to) 32 | 33 | logger.info('Starting Azure Functions Python Worker.') 34 | logger.info('Worker ID: %s, Request ID: %s, Host Address: %s:%s', 35 | args.worker_id, args.request_id, args.host, args.port) 36 | 37 | try: 38 | return aio_compat.run(start_async( 39 | args.host, args.port, args.worker_id, args.request_id, 40 | args.grpc_max_msg_len)) 41 | except Exception: 42 | error_logger.exception('unhandled error in functions worker') 43 | raise 44 | 45 | 46 | async def start_async(host, port, worker_id, request_id, grpc_max_msg_len): 47 | disp = await dispatcher.Dispatcher.connect( 48 | host, port, worker_id, request_id, 49 | connect_timeout=5.0, max_msg_len=grpc_max_msg_len) 50 | 51 | await disp.dispatch_forever() 52 | -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/publish_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "azure_service_principal": { 3 | "user_id": "${AZURE_SERVICE_PRINCIPAL_USER}", 4 | "password": "${AZURE_SERVICE_PRINCIPAL_PASSWORD}", 5 | "tenant": "${AZURE_SERVICE_PRINCIPAL_TENANT}" 6 | }, 7 | "docker_setup": { 8 | "acr_name": "${ACR_NAME}", 9 | "working_dir": "/docker-dir", 10 | "dev_image_name": "${ACR_IMAGE_NAME}", 11 | "prod_image": "mcr.microsoft.com/azure-functions/python:2.0" 12 | }, 13 | "func_setup": { 14 | "func_dev_dir": "/func-dev-dir", 15 | "func_dev_url": "${FUNC_DEV_URL}" 16 | }, 17 | "tests": { 18 | "working_dir": "/tests-dir", 19 | "logs": "/tests-logs", 20 | "timeout": "3000s" 21 | }, 22 | "publish_function_app": { 23 | "prod_func_prod_docker": { 24 | "new_function": { 25 | "build_native_deps": "${FUNCTION_APP}", 26 | "no_bundler": "${FUNCTION_APP}", 27 | "packapp": "${FUNCTION_APP}" 28 | }, 29 | "customer_churn": { 30 | "build_native_deps": "${FUNCTION_APP}", 31 | "no_bundler": "${FUNCTION_APP}" 32 | } 33 | }, 34 | "prod_func_dev_docker": { 35 | "new_function": { 36 | "build_native_deps": "${FUNCTION_APP}", 37 | "no_bundler": "${FUNCTION_APP}", 38 | "packapp": "${FUNCTION_APP}" 39 | }, 40 | "customer_churn": { 41 | "build_native_deps": "${FUNCTION_APP}", 42 | "no_bundler": "${FUNCTION_APP}" 43 | } 44 | }, 45 | "dev_func_prod_docker": { 46 | "new_function": { 47 | "build_native_deps": "${FUNCTION_APP}", 48 | "no_bundler": "${FUNCTION_APP}", 49 | "packapp": "${FUNCTION_APP}" 50 | }, 51 | "customer_churn": { 52 | "build_native_deps": "${FUNCTION_APP}", 53 | "no_bundler": "${FUNCTION_APP}" 54 | } 55 | }, 56 | "dev_func_dev_docker": { 57 | "new_function": { 58 | "build_native_deps": "${FUNCTION_APP}", 59 | "no_bundler": "${FUNCTION_APP}", 60 | "packapp": "${FUNCTION_APP}" 61 | }, 62 | "customer_churn": { 63 | "build_native_deps": "${FUNCTION_APP}", 64 | "no_bundler": "${FUNCTION_APP}" 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /azure/functions_worker/loader.py: -------------------------------------------------------------------------------- 1 | """Python functions loader.""" 2 | 3 | 4 | import importlib 5 | import importlib.machinery 6 | import importlib.util 7 | import os 8 | import os.path 9 | import pathlib 10 | import sys 11 | import typing 12 | 13 | 14 | _AZURE_NAMESPACE = '__app__' 15 | 16 | _submodule_dirs = [] 17 | 18 | 19 | def register_function_dir(path: os.PathLike): 20 | _submodule_dirs.append(os.fspath(path)) 21 | 22 | 23 | def install(): 24 | if _AZURE_NAMESPACE not in sys.modules: 25 | # Create and register the __app__ namespace package. 26 | ns_spec = importlib.machinery.ModuleSpec(_AZURE_NAMESPACE, None) 27 | ns_spec.submodule_search_locations = _submodule_dirs 28 | ns_pkg = importlib.util.module_from_spec(ns_spec) 29 | sys.modules[_AZURE_NAMESPACE] = ns_pkg 30 | 31 | 32 | def uninstall(): 33 | pass 34 | 35 | 36 | def load_function(name: str, directory: str, script_file: str, 37 | entry_point: typing.Optional[str]): 38 | dir_path = pathlib.Path(directory) 39 | script_path = pathlib.Path(script_file) 40 | if not entry_point: 41 | entry_point = 'main' 42 | 43 | register_function_dir(dir_path.parent) 44 | 45 | try: 46 | rel_script_path = script_path.relative_to(dir_path.parent) 47 | except ValueError: 48 | raise RuntimeError( 49 | f'script path {script_file} is not relative to the specified ' 50 | f'directory {directory}' 51 | ) 52 | 53 | last_part = rel_script_path.parts[-1] 54 | modname, ext = os.path.splitext(last_part) 55 | if ext != '.py': 56 | raise RuntimeError( 57 | f'cannot load function {name}: ' 58 | f'invalid Python filename {script_file}') 59 | 60 | modname_parts = [_AZURE_NAMESPACE] 61 | modname_parts.extend(rel_script_path.parts[:-1]) 62 | modname_parts.append(modname) 63 | 64 | fullmodname = '.'.join(modname_parts) 65 | 66 | mod = importlib.import_module(fullmodname) 67 | 68 | func = getattr(mod, entry_point, None) 69 | if func is None or not callable(func): 70 | raise RuntimeError( 71 | f'cannot load function {name}: function {entry_point}() is not ' 72 | f'present in {rel_script_path}') 73 | 74 | return func 75 | -------------------------------------------------------------------------------- /azure-pipelines-nightly.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | - job: LinuxNightly 3 | timeoutInMinutes: 0 4 | pool: 5 | vmImage: 'ubuntu-16.04' 6 | steps: 7 | - task: UsePythonVersion@0 8 | inputs: 9 | versionSpec: '3.6' 10 | addToPath: true 11 | name: pythonPath 12 | displayName: Update Python Path 13 | - bash: | 14 | 15 | echo "alias python=$(pythonPath.pythonLocation)/python" > ~/.bashrc 16 | which python 17 | 18 | sudo apt-get update 19 | sudo apt-get install azure-functions-core-tools jq git unzip gettext -y 20 | 21 | # Install azure CLI 22 | sudo apt-get install apt-transport-https lsb-release software-properties-common dirmngr -y 23 | AZ_REPO=$(lsb_release -cs) 24 | sudo echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $AZ_REPO main" | tee /etc/apt/sources.list.d/azure-cli.list 25 | sudo apt-key --keyring /etc/apt/trusted.gpg.d/Microsoft.gpg adv --keyserver packages.microsoft.com --recv-keys BC528686B50D79E339D3721CEB3E94ADBE1229CF 26 | sudo apt-get update 27 | sudo apt-get install azure-cli -y 28 | displayName: Initial Setup 29 | continueOnError: false 30 | - bash: | 31 | envsubst < .ci/e2e/publish_tests/publish_config.json > .ci/e2e/publish_tests/publish_config_filled.json 32 | mv .ci/e2e/publish_tests/publish_config_filled.json .ci/e2e/publish_tests/publish_config.json 33 | displayName: Populate Settings 34 | continueOnError: false 35 | env: 36 | AZURE_SERVICE_PRINCIPAL_USER: $(AZURE_SERVICE_PRINCIPAL_USER) 37 | AZURE_SERVICE_PRINCIPAL_PASSWORD: ${AZURE_SERVICE_PRINCIPAL_PASSWORD} 38 | AZURE_SERVICE_PRINCIPAL_TENANT: $(AZURE_SERVICE_PRINCIPAL_TENANT) 39 | ACR_NAME: $(ACR_NAME) 40 | ACR_IMAGE_NAME: $(ACR_IMAGE_NAME) 41 | FUNCTION_APP: $(FUNCTION_APP) 42 | FUNC_DEV_URL: $(FUNC_DEV_URL) 43 | - bash: | 44 | chmod a+x .ci/e2e/publish_tests/test_runners/setup_test_environment.sh 45 | sudo .ci/e2e/publish_tests/test_runners/setup_test_environment.sh 46 | displayName: Setup Testing artifacts 47 | continueOnError: false 48 | - bash: | 49 | chmod a+x .ci/e2e/publish_tests/test_runners/run_all_serial.sh 50 | find .ci/e2e/publish_tests/ -type f -iname "*.sh" -exec chmod a+x {} \; 51 | sudo PATH=${PATH} .ci/e2e/publish_tests/test_runners/run_all_serial.sh 52 | displayName: E2E Tests 53 | continueOnError: false -------------------------------------------------------------------------------- /tests/test_cosmosdb_functions.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | 4 | from azure.functions_worker import testutils 5 | 6 | 7 | class TestCosmosDBFunctions(testutils.WebHostTestCase): 8 | 9 | @classmethod 10 | def get_script_dir(cls): 11 | return 'cosmosdb_functions' 12 | 13 | def test_cosmosdb_trigger(self): 14 | time.sleep(5) 15 | data = str(round(time.time())) 16 | doc = {'id': 'cosmosdb-trigger-test', 'data': data} 17 | r = self.webhost.request('POST', 'put_document', 18 | data=json.dumps(doc)) 19 | self.assertEqual(r.status_code, 200) 20 | self.assertEqual(r.text, 'OK') 21 | 22 | max_retries = 10 23 | 24 | for try_no in range(max_retries): 25 | # Allow trigger to fire 26 | time.sleep(2) 27 | 28 | try: 29 | # Check that the trigger has fired 30 | r = self.webhost.request('GET', 'get_cosmosdb_triggered') 31 | self.assertEqual(r.status_code, 200) 32 | response = r.json() 33 | response.pop('_metadata', None) 34 | 35 | self.assertEqual( 36 | response, 37 | doc 38 | ) 39 | except AssertionError as e: 40 | if try_no == max_retries - 1: 41 | raise 42 | else: 43 | break 44 | 45 | def test_cosmosdb_input(self): 46 | time.sleep(5) 47 | data = str(round(time.time())) 48 | doc = {'id': 'cosmosdb-input-test', 'data': data} 49 | r = self.webhost.request('POST', 'put_document', 50 | data=json.dumps(doc)) 51 | self.assertEqual(r.status_code, 200) 52 | self.assertEqual(r.text, 'OK') 53 | 54 | max_retries = 10 55 | 56 | for try_no in range(max_retries): 57 | # Allow trigger to fire 58 | time.sleep(2) 59 | 60 | try: 61 | # Check that the trigger has fired 62 | r = self.webhost.request('GET', 'cosmosdb_input') 63 | self.assertEqual(r.status_code, 200) 64 | response = r.json() 65 | 66 | self.assertEqual( 67 | response, 68 | doc 69 | ) 70 | except AssertionError as e: 71 | if try_no == max_retries - 1: 72 | raise 73 | else: 74 | break 75 | -------------------------------------------------------------------------------- /azure/functions_worker/bindings/cosmosdb.py: -------------------------------------------------------------------------------- 1 | import collections.abc 2 | import json 3 | import typing 4 | 5 | from azure.functions import _cosmosdb as cdb 6 | 7 | from . import meta 8 | from .. import protos 9 | 10 | 11 | class CosmosDBConverter(meta.InConverter, meta.OutConverter, 12 | binding='cosmosDB'): 13 | 14 | @classmethod 15 | def check_input_type_annotation( 16 | cls, pytype: type, datatype: protos.BindingInfo.DataType) -> bool: 17 | if datatype is protos.BindingInfo.undefined: 18 | return issubclass(pytype, cdb.DocumentList) 19 | else: 20 | return False 21 | 22 | @classmethod 23 | def check_output_type_annotation(cls, pytype: type) -> bool: 24 | return issubclass(pytype, (cdb.DocumentList, cdb.Document)) 25 | 26 | @classmethod 27 | def from_proto(cls, data: protos.TypedData, *, 28 | pytype: typing.Optional[type], 29 | trigger_metadata) -> cdb.DocumentList: 30 | data_type = data.WhichOneof('data') 31 | 32 | if data_type == 'string': 33 | body = data.string 34 | 35 | elif data_type == 'bytes': 36 | body = data.bytes.decode('utf-8') 37 | 38 | elif data_type == 'json': 39 | body = data.json 40 | 41 | else: 42 | raise NotImplementedError( 43 | f'unsupported queue payload type: {data_type}') 44 | 45 | documents = json.loads(body) 46 | if not isinstance(documents, list): 47 | documents = [documents] 48 | 49 | return cdb.DocumentList( 50 | cdb.Document.from_dict(doc) for doc in documents) 51 | 52 | @classmethod 53 | def to_proto(cls, obj: typing.Any, *, 54 | pytype: typing.Optional[type]) -> protos.TypedData: 55 | if isinstance(obj, cdb.Document): 56 | data = cdb.DocumentList([obj]) 57 | 58 | elif isinstance(obj, cdb.DocumentList): 59 | data = obj 60 | 61 | elif isinstance(obj, collections.abc.Iterable): 62 | data = cdb.DocumentList() 63 | 64 | for doc in obj: 65 | if not isinstance(doc, cdb.Document): 66 | raise NotImplementedError 67 | else: 68 | data.append(doc) 69 | 70 | else: 71 | raise NotImplementedError 72 | 73 | return protos.TypedData( 74 | json=json.dumps([dict(d) for d in data]) 75 | ) 76 | 77 | 78 | class CosmosDBTriggerConverter(CosmosDBConverter, 79 | binding='cosmosDBTrigger', trigger=True): 80 | pass 81 | -------------------------------------------------------------------------------- /tests/test_queue_functions.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from azure.functions_worker import testutils 4 | 5 | 6 | class TestQueueFunctions(testutils.WebHostTestCase): 7 | 8 | @classmethod 9 | def get_script_dir(cls): 10 | return 'queue_functions' 11 | 12 | def test_queue_basic(self): 13 | r = self.webhost.request('POST', 'put_queue', 14 | data='test-message') 15 | self.assertEqual(r.status_code, 200) 16 | self.assertEqual(r.text, 'OK') 17 | 18 | # wait for queue_trigger to process the queue item 19 | time.sleep(1) 20 | 21 | r = self.webhost.request('GET', 'get_queue_blob') 22 | self.assertEqual(r.status_code, 200) 23 | msg_info = r.json() 24 | 25 | self.assertIn('queue', msg_info) 26 | msg = msg_info['queue'] 27 | 28 | self.assertEqual(msg['body'], 'test-message') 29 | for attr in {'id', 'expiration_time', 'insertion_time', 30 | 'time_next_visible', 'pop_receipt', 'dequeue_count'}: 31 | self.assertIsNotNone(msg.get(attr)) 32 | 33 | def test_queue_return(self): 34 | r = self.webhost.request('POST', 'put_queue_return', 35 | data='test-message-return') 36 | self.assertEqual(r.status_code, 200) 37 | 38 | # wait for queue_trigger to process the queue item 39 | time.sleep(1) 40 | 41 | r = self.webhost.request('GET', 'get_queue_blob_return') 42 | self.assertEqual(r.status_code, 200) 43 | self.assertEqual(r.text, 'test-message-return') 44 | 45 | def test_queue_message_object_return(self): 46 | r = self.webhost.request('POST', 'put_queue_message_return', 47 | data='test-message-object-return') 48 | self.assertEqual(r.status_code, 200) 49 | 50 | # wait for queue_trigger to process the queue item 51 | time.sleep(1) 52 | 53 | r = self.webhost.request('GET', 'get_queue_blob_message_return') 54 | self.assertEqual(r.status_code, 200) 55 | self.assertEqual(r.text, 'test-message-object-return') 56 | 57 | def test_queue_return_multiple(self): 58 | r = self.webhost.request('POST', 'put_queue_return_multiple', 59 | data='foo') 60 | self.assertTrue(200 <= r.status_code < 300) 61 | 62 | # wait for queue_trigger to process the queue item 63 | time.sleep(1) 64 | 65 | def test_queue_return_multiple_outparam(self): 66 | r = self.webhost.request('POST', 'put_queue_multiple_out', 67 | data='foo') 68 | self.assertTrue(200 <= r.status_code < 300) 69 | self.assertEqual(r.text, 'HTTP response: foo') 70 | -------------------------------------------------------------------------------- /azure/functions_worker/aio_compat.py: -------------------------------------------------------------------------------- 1 | """Backport of asyncio.run() function from Python 3.7. 2 | 3 | Source: https://github.com/python/cpython/blob/ 4 | bd093355a6aaf2f4ca3ed153e195da57870a55eb/Lib/asyncio/runners.py 5 | """ 6 | 7 | 8 | import asyncio 9 | 10 | 11 | def get_running_loop(): 12 | """Return the running event loop. Raise a RuntimeError if there is none. 13 | 14 | This function is thread-specific. 15 | """ 16 | loop = asyncio._get_running_loop() 17 | if loop is None: 18 | raise RuntimeError('no running event loop') 19 | return loop 20 | 21 | 22 | def run(main, *, debug=False): 23 | """Run a coroutine. 24 | 25 | This function runs the passed coroutine, taking care of 26 | managing the asyncio event loop and finalizing asynchronous 27 | generators. 28 | 29 | This function cannot be called when another asyncio event loop is 30 | running in the same thread. 31 | 32 | If debug is True, the event loop will be run in debug mode. 33 | This function always creates a new event loop and closes it at the end. 34 | 35 | It should be used as a main entry point for asyncio programs, and should 36 | ideally only be called once. 37 | """ 38 | if asyncio._get_running_loop() is not None: 39 | raise RuntimeError( 40 | "asyncio.run() cannot be called from a running event loop") 41 | 42 | if not asyncio.iscoroutine(main): 43 | raise ValueError("a coroutine was expected, got {!r}".format(main)) 44 | 45 | loop = asyncio.new_event_loop() 46 | try: 47 | asyncio.set_event_loop(loop) 48 | loop.set_debug(debug) 49 | return loop.run_until_complete(main) 50 | finally: 51 | try: 52 | _cancel_all_tasks(loop) 53 | loop.run_until_complete(loop.shutdown_asyncgens()) 54 | finally: 55 | asyncio.set_event_loop(None) 56 | loop.close() 57 | 58 | 59 | def _cancel_all_tasks(loop): 60 | to_cancel = [task for task in asyncio.Task.all_tasks(loop) 61 | if not task.done()] 62 | if not to_cancel: 63 | return 64 | 65 | for task in to_cancel: 66 | task.cancel() 67 | 68 | loop.run_until_complete( 69 | asyncio.gather(*to_cancel, loop=loop, return_exceptions=True)) 70 | 71 | for task in to_cancel: 72 | if task.cancelled(): 73 | continue 74 | if task.exception() is not None: 75 | loop.call_exception_handler({ 76 | 'message': 'unhandled exception during asyncio.run() shutdown', 77 | 'exception': task.exception(), 78 | 'task': task, 79 | }) 80 | 81 | 82 | try: 83 | # Try to import the 'run' function from asyncio. 84 | from asyncio import run, get_running_loop # NoQA 85 | except ImportError: 86 | # Python <= 3.6 87 | pass 88 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | name: 1.0.0-beta$(Date:yyyyMMdd)$(Rev:.r) 2 | 3 | trigger: 4 | - dev 5 | - master 6 | 7 | variables: 8 | DOTNET_VERSION: '2.2.103' 9 | 10 | jobs: 11 | - job: Tests 12 | pool: 13 | vmImage: 'ubuntu-16.04' 14 | strategy: 15 | matrix: 16 | Python36: 17 | pythonVersion: '3.6' 18 | Python37: 19 | pythonVersion: '3.7' 20 | maxParallel: 1 21 | steps: 22 | - task: UsePythonVersion@0 23 | inputs: 24 | versionSpec: '$(pythonVersion)' 25 | addToPath: true 26 | - task: ShellScript@2 27 | inputs: 28 | disableAutoCwd: true # Execute in current directory 29 | scriptPath: .ci/linux_devops_tools.sh 30 | displayName: 'Install Core Tools' 31 | - task: DotNetCoreInstaller@0 32 | inputs: 33 | packageType: 'sdk' 34 | version: $(DOTNET_VERSION) 35 | displayName: 'Install dotnet' 36 | - task: ShellScript@2 37 | inputs: 38 | disableAutoCwd: true 39 | scriptPath: .ci/linux_devops_build.sh 40 | displayName: 'Build' 41 | - bash: | 42 | chmod +x .ci/linux_devops_tests.sh 43 | .ci/linux_devops_tests.sh 44 | env: 45 | LINUXSTORAGECONNECTIONSTRING: $(LinuxStorageConnectionString) 46 | LINUXCOSMOSDBCONNECTIONSTRING: $(LinuxCosmosDBConnectionString) 47 | LINUXEVENTHUBCONNECTIONSTRING: $(LinuxEventHubConnectionString) 48 | LINUXSERVICEBUSCONNECTIONSTRING: $(LinuxServiceBusConnectionString) 49 | displayName: 'Tests' 50 | 51 | - template: pack/templates/win_env_gen.yml 52 | parameters: 53 | jobName: 'WindowsEnvGen' 54 | dependency: 'Tests' 55 | vmImage: 'vs2017-win2016' 56 | pythonVersion: '3.6' 57 | artifactName: 'Windows' 58 | 59 | - template: pack/templates/nix_env_gen.yml 60 | parameters: 61 | jobName: 'LinuxEnvGen' 62 | dependency: 'Tests' 63 | vmImage: 'ubuntu-16.04' 64 | pythonVersion: '3.6' 65 | artifactName: 'Linux' 66 | 67 | - template: pack/templates/nix_env_gen.yml 68 | parameters: 69 | jobName: 'MacEnvGen' 70 | dependency: 'Tests' 71 | vmImage: 'macOS-10.13' 72 | pythonVersion: '3.6' 73 | artifactName: 'Mac' 74 | 75 | - job: PackageEnvironments 76 | dependsOn: ['WindowsEnvGen', 77 | 'LinuxEnvGen', 78 | 'MacEnvGen' 79 | ] 80 | pool: 81 | vmImage: 'vs2017-win2016' 82 | steps: 83 | - task: DownloadBuildArtifacts@0 84 | inputs: 85 | buildType: 'current' 86 | downloadType: 'specific' 87 | downloadPath: '$(Build.SourcesDirectory)' 88 | - task: NuGetCommand@2 89 | inputs: 90 | command: pack 91 | packagesToPack: 'pack\Microsoft.Azure.Functions.PythonWorkerRunEnvironments.nuspec' 92 | versioningScheme: 'byEnvVar' 93 | versionEnvVar: BUILD_BUILDNUMBER # Replaces version in nuspec 94 | - task: PublishBuildArtifacts@1 95 | inputs: 96 | pathtoPublish: '$(Build.ArtifactStagingDirectory)' 97 | artifactName: 'PythonWorkerRunEnvironments' 98 | -------------------------------------------------------------------------------- /tests/test_eventgrid_functions.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | import unittest 4 | import uuid 5 | 6 | from azure.functions_worker import testutils 7 | 8 | 9 | class TestEventGridFunctions(testutils.WebHostTestCase): 10 | 11 | @classmethod 12 | def get_script_dir(cls): 13 | return 'eventgrid_functions' 14 | 15 | def request(self, meth, funcname, *args, **kwargs): 16 | request_method = getattr(requests, meth.lower()) 17 | url = f'{self.webhost._addr}/runtime/webhooks/eventgrid' 18 | params = dict(kwargs.pop('params', {})) 19 | params['functionName'] = funcname 20 | if 'code' not in params: 21 | params['code'] = 'testSystemKey' 22 | headers = dict(kwargs.pop('headers', {})) 23 | headers['aeg-event-type'] = 'Notification' 24 | return request_method(url, *args, params=params, headers=headers, 25 | **kwargs) 26 | 27 | @unittest.skip("fails with 401 with recent host versions") 28 | def test_eventgrid_trigger(self): 29 | data = [{ 30 | "topic": "test-topic", 31 | "subject": "test-subject", 32 | "eventType": "Microsoft.Storage.BlobCreated", 33 | "eventTime": "2018-01-01T00:00:00.000000123Z", 34 | "id": str(uuid.uuid4()), 35 | "data": { 36 | "api": "PutBlockList", 37 | "clientRequestId": "2c169f2f-7b3b-4d99-839b-c92a2d25801b", 38 | "requestId": "44d4f022-001e-003c-466b-940cba000000", 39 | "eTag": "0x8D562831044DDD0", 40 | "contentType": "application/octet-stream", 41 | "contentLength": 2248, 42 | "blobType": "BlockBlob", 43 | "ur1": "foo", 44 | "sequencer": "000000000000272D000000000003D60F", 45 | "storageDiagnostics": { 46 | "batchId": "b4229b3a-4d50-4ff4-a9f2-039ccf26efe9" 47 | } 48 | }, 49 | "dataVersion": "", 50 | "metadataVersion": "1" 51 | }] 52 | 53 | r = self.request('POST', 'eventgrid_trigger', json=data) 54 | self.assertEqual(r.status_code, 202) 55 | 56 | max_retries = 10 57 | 58 | for try_no in range(max_retries): 59 | # Allow trigger to fire. 60 | time.sleep(2) 61 | 62 | try: 63 | # Check that the trigger has fired. 64 | r = self.webhost.request('GET', 'get_eventgrid_triggered') 65 | self.assertEqual(r.status_code, 200) 66 | response = r.json() 67 | 68 | self.assertEqual( 69 | response, 70 | { 71 | 'id': data[0]['id'], 72 | 'data': data[0]['data'], 73 | 'topic': data[0]['topic'], 74 | 'subject': data[0]['subject'], 75 | 'event_type': data[0]['eventType'], 76 | } 77 | ) 78 | except AssertionError as e: 79 | if try_no == max_retries - 1: 80 | raise 81 | else: 82 | break 83 | -------------------------------------------------------------------------------- /tests/test_mock_http_functions.py: -------------------------------------------------------------------------------- 1 | from azure.functions_worker import protos 2 | from azure.functions_worker import testutils 3 | 4 | 5 | class TestMockHost(testutils.AsyncTestCase): 6 | 7 | async def test_call_sync_function_check_logs(self): 8 | async with testutils.start_mockhost() as host: 9 | await host.load_function('sync_logging') 10 | 11 | invoke_id, r = await host.invoke_function( 12 | 'sync_logging', [ 13 | protos.ParameterBinding( 14 | name='req', 15 | data=protos.TypedData( 16 | http=protos.RpcHttp( 17 | method='GET'))) 18 | ]) 19 | 20 | self.assertEqual(r.response.result.status, 21 | protos.StatusResult.Success) 22 | 23 | self.assertEqual(len(r.logs), 1) 24 | 25 | log = r.logs[0] 26 | self.assertEqual(log.invocation_id, invoke_id) 27 | self.assertTrue(log.message.startswith( 28 | 'a gracefully handled error')) 29 | 30 | self.assertEqual(r.response.return_value.string, 'OK-sync') 31 | 32 | async def test_call_async_function_check_logs(self): 33 | async with testutils.start_mockhost() as host: 34 | await host.load_function('async_logging') 35 | 36 | invoke_id, r = await host.invoke_function( 37 | 'async_logging', [ 38 | protos.ParameterBinding( 39 | name='req', 40 | data=protos.TypedData( 41 | http=protos.RpcHttp( 42 | method='GET'))) 43 | ]) 44 | 45 | self.assertEqual(r.response.result.status, 46 | protos.StatusResult.Success) 47 | 48 | self.assertEqual(len(r.logs), 2) 49 | 50 | self.assertEqual(r.logs[0].invocation_id, invoke_id) 51 | self.assertEqual(r.logs[0].message, 'hello info') 52 | self.assertEqual(r.logs[0].level, protos.RpcLog.Information) 53 | 54 | self.assertEqual(r.logs[1].invocation_id, invoke_id) 55 | self.assertTrue(r.logs[1].message.startswith('and another error')) 56 | self.assertEqual(r.logs[1].level, protos.RpcLog.Error) 57 | 58 | self.assertEqual(r.response.return_value.string, 'OK-async') 59 | 60 | async def test_handles_unsupported_messages_gracefully(self): 61 | async with testutils.start_mockhost() as host: 62 | # Intentionally send a message to worker that isn't 63 | # going to be ever supported by it. The idea is that 64 | # workers should survive such messages and continue 65 | # their operation. If anything, the host can always 66 | # terminate the worker. 67 | await host.send( 68 | protos.StreamingMessage( 69 | worker_heartbeat=protos.WorkerHeartbeat())) 70 | 71 | _, r = await host.load_function('return_out') 72 | self.assertEqual(r.response.result.status, 73 | protos.StatusResult.Success) 74 | -------------------------------------------------------------------------------- /tests/test_blob_functions.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from azure.functions_worker import testutils 4 | 5 | 6 | class TestBlobFunctions(testutils.WebHostTestCase): 7 | 8 | @classmethod 9 | def get_script_dir(cls): 10 | return 'blob_functions' 11 | 12 | def test_blob_io_str(self): 13 | r = self.webhost.request('POST', 'put_blob_str', data='test-data') 14 | self.assertEqual(r.status_code, 200) 15 | self.assertEqual(r.text, 'OK') 16 | 17 | r = self.webhost.request('GET', 'get_blob_str') 18 | self.assertEqual(r.status_code, 200) 19 | self.assertEqual(r.text, 'test-data') 20 | 21 | r = self.webhost.request('GET', 'get_blob_as_str') 22 | self.assertEqual(r.status_code, 200) 23 | self.assertEqual(r.text, 'test-data') 24 | 25 | def test_blob_io_bytes(self): 26 | r = self.webhost.request('POST', 'put_blob_bytes', 27 | data='test-dată'.encode('utf-8')) 28 | self.assertEqual(r.status_code, 200) 29 | self.assertEqual(r.text, 'OK') 30 | 31 | r = self.webhost.request('POST', 'get_blob_bytes') 32 | self.assertEqual(r.status_code, 200) 33 | self.assertEqual(r.text, 'test-dată') 34 | 35 | r = self.webhost.request('POST', 'get_blob_as_bytes') 36 | self.assertEqual(r.status_code, 200) 37 | self.assertEqual(r.text, 'test-dată') 38 | 39 | def test_blob_io_filelike(self): 40 | r = self.webhost.request('POST', 'put_blob_filelike') 41 | self.assertEqual(r.status_code, 200) 42 | self.assertEqual(r.text, 'OK') 43 | 44 | r = self.webhost.request('POST', 'get_blob_filelike') 45 | self.assertEqual(r.status_code, 200) 46 | self.assertEqual(r.text, 'filelike') 47 | 48 | def test_blob_io_return(self): 49 | r = self.webhost.request('POST', 'put_blob_return') 50 | self.assertEqual(r.status_code, 200) 51 | 52 | r = self.webhost.request('POST', 'get_blob_return') 53 | self.assertEqual(r.status_code, 200) 54 | self.assertEqual(r.text, 'FROM RETURN') 55 | 56 | def test_blob_trigger(self): 57 | data = str(round(time.time())) 58 | 59 | r = self.webhost.request('POST', 'put_blob_trigger', 60 | data=data.encode('utf-8')) 61 | self.assertEqual(r.status_code, 200) 62 | self.assertEqual(r.text, 'OK') 63 | 64 | max_retries = 10 65 | 66 | for try_no in range(max_retries): 67 | # Allow trigger to fire 68 | time.sleep(2) 69 | 70 | try: 71 | # Check that the trigger has fired 72 | r = self.webhost.request('GET', 'get_blob_triggered') 73 | self.assertEqual(r.status_code, 200) 74 | response = r.json() 75 | 76 | self.assertEqual( 77 | response, 78 | { 79 | 'name': 'python-worker-tests/test-blob-trigger.txt', 80 | 'length': 10, 81 | 'content': data 82 | } 83 | ) 84 | except AssertionError: 85 | if try_no == max_retries - 1: 86 | raise 87 | -------------------------------------------------------------------------------- /azure/functions_worker/bindings/eventhub.py: -------------------------------------------------------------------------------- 1 | import json 2 | import typing 3 | 4 | from azure.functions import _eventhub 5 | 6 | from . import meta 7 | from .. import protos 8 | 9 | 10 | class EventHubConverter(meta.InConverter, meta.OutConverter, 11 | binding='eventHub'): 12 | 13 | @classmethod 14 | def check_input_type_annotation( 15 | cls, pytype: type, datatype: protos.BindingInfo.DataType) -> bool: 16 | if datatype is protos.BindingInfo.undefined: 17 | return issubclass(pytype, _eventhub.EventHubEvent) 18 | else: 19 | return False 20 | 21 | @classmethod 22 | def check_output_type_annotation(cls, pytype) -> bool: 23 | return ( 24 | issubclass(pytype, (str, bytes)) 25 | or (issubclass(pytype, typing.List) 26 | and issubclass(pytype.__args__[0], str)) 27 | ) 28 | 29 | @classmethod 30 | def from_proto(cls, data: protos.TypedData, *, 31 | pytype: typing.Optional[type], 32 | trigger_metadata) -> _eventhub.EventHubEvent: 33 | data_type = data.WhichOneof('data') 34 | 35 | if data_type == 'string': 36 | body = data.string.encode('utf-8') 37 | 38 | elif data_type == 'bytes': 39 | body = data.bytes 40 | 41 | elif data_type == 'json': 42 | body = data.json.encode('utf-8') 43 | 44 | else: 45 | raise NotImplementedError( 46 | f'unsupported event data payload type: {data_type}') 47 | 48 | return _eventhub.EventHubEvent(body=body) 49 | 50 | @classmethod 51 | def to_proto(cls, obj: typing.Any, *, 52 | pytype: typing.Optional[type]) -> protos.TypedData: 53 | if isinstance(obj, str): 54 | data = protos.TypedData(string=obj) 55 | 56 | elif isinstance(obj, bytes): 57 | data = protos.TypedData(bytes=obj) 58 | 59 | elif isinstance(obj, list): 60 | data = protos.TypedData(json=json.dumps(obj)) 61 | 62 | return data 63 | 64 | 65 | class EventHubTriggerConverter(EventHubConverter, 66 | binding='eventHubTrigger', trigger=True): 67 | 68 | @classmethod 69 | def from_proto(cls, data: protos.TypedData, *, 70 | pytype: typing.Optional[type], 71 | trigger_metadata) -> _eventhub.EventHubEvent: 72 | data_type = data.WhichOneof('data') 73 | 74 | if data_type == 'string': 75 | body = data.string.encode('utf-8') 76 | 77 | elif data_type == 'bytes': 78 | body = data.bytes 79 | 80 | elif data_type == 'json': 81 | body = data.json.encode('utf-8') 82 | 83 | else: 84 | raise NotImplementedError( 85 | f'unsupported event data payload type: {data_type}') 86 | 87 | return _eventhub.EventHubEvent( 88 | body=body, 89 | enqueued_time=cls._parse_datetime_metadata( 90 | trigger_metadata, 'EnqueuedTime'), 91 | partition_key=cls._decode_trigger_metadata_field( 92 | trigger_metadata, 'PartitionKey', python_type=str), 93 | sequence_number=cls._decode_trigger_metadata_field( 94 | trigger_metadata, 'SequenceNumber', python_type=int), 95 | offset=cls._decode_trigger_metadata_field( 96 | trigger_metadata, 'Offset', python_type=str) 97 | ) 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | |Branch|Status| 2 | |---|---| 3 | |master|[![Build Status](https://azfunc.visualstudio.com/Azure%20Functions%20Python/_apis/build/status/Azure%20Functions%20Python-CI?branchName=master)](https://azfunc.visualstudio.com/Azure%20Functions%20Python/_build/latest?definitionId=3&branchName=master)| 4 | |dev|[![Build Status](https://azfunc.visualstudio.com/Azure%20Functions%20Python/_apis/build/status/Azure%20Functions%20Python-CI?branchName=dev)](https://azfunc.visualstudio.com/Azure%20Functions%20Python/_build/latest?definitionId=3&branchName=dev)| 5 | 6 | This repository will host the Python language worker implementation for Azure Functions. We'll also be using it to track work items related to Python support. Please feel free to leave comments about any of the features and design patterns. 7 | 8 | > :construction: The project is currently **work in progress**. Please **do not use in production** as we expect developments over time. To receive important updates, including breaking changes announcements, watch the [Azure App Service announcements](https://github.com/Azure/app-service-announcements/issues) repository. :construction: 9 | 10 | # Overview 11 | 12 | Python support for Azure Functions is based on Python3.6, serverless hosting on Linux and the Functions 2.0 runtime. 13 | 14 | Here is the current status of Python in Azure Functions: 15 | 16 | What's available? 17 | 18 | - Build, test, debug and publish using Azure Functions Core Tools (CLI) or Visual Studio Code 19 | - Triggers / Bindings : HTTP, Blob, Queue, Timer, Cosmos DB, Event Grid, Event Hubs and Service Bus 20 | - Create a Python Function on Linux using a custom docker image 21 | 22 | What's coming? 23 | 24 | - Triggers / Bindings : Custom binding support 25 | - Python 3.7 support 26 | 27 | # Get Started 28 | 29 | - [Create your first Python function](https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-first-function-python) 30 | - [Developer guide](https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference-python) 31 | - [Binding API reference](https://docs.microsoft.com/en-us/python/api/azure-functions/azure.functions?view=azure-python) 32 | - [Develop using VS Code](https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-first-function-vs-code) 33 | - [Create a Python Function on Linux using a custom docker image](https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-function-linux-custom-image) 34 | 35 | # Give Feedback 36 | 37 | Issues and feature requests are tracked in a variety of places. To report this feedback, please file an issue to the relevant repository below: 38 | 39 | |Item|Description|Link| 40 | |----|-----|-----| 41 | | Python Worker | Programming Model, Triggers & Bindings |[File an Issue](https://github.com/Azure/azure-functions-python-worker/issues)| 42 | | Linux | Base Docker Images |[File an Issue](https://github.com/Azure/azure-functions-docker/issues)| 43 | | Runtime | Script Host & Language Extensibility |[File an Issue](https://github.com/Azure/azure-functions-host/issues)| 44 | | Core Tools | Command line interface for local development |[File an Issue](https://github.com/Azure/azure-functions-core-tools/issues)| 45 | | Portal | User Interface or Experience Issue |[File an Issue](https://github.com/azure/azure-functions-ux/issues)| 46 | | Templates | Code Issues with Creation Template |[File an Issue](https://github.com/Azure/azure-functions-templates/issues)| 47 | 48 | # Contribute 49 | 50 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 51 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 52 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 53 | 54 | Here are some pointers to get started: 55 | 56 | - [Language worker architecture](https://github.com/Azure/azure-functions-python-worker/wiki/Worker-Architecture) 57 | - [Setting up the development environment](https://github.com/Azure/azure-functions-python-worker/wiki/Contributor-Guide) 58 | - [Adding support for a new binding](https://github.com/Azure/azure-functions-python-worker/wiki/Adding-support-for-a-new-binding-type) 59 | 60 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 61 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 62 | provided by the bot. You will only need to do this once across all repos using our CLA. 63 | 64 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 65 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 66 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 67 | -------------------------------------------------------------------------------- /azure/functions_worker/protos/_src/README.md: -------------------------------------------------------------------------------- 1 | # Azure Functions Languge Worker Protobuf 2 | 3 | This repository contains the protobuf definition file which defines the gRPC service which is used between the Azure WebJobs Script host and the Azure Functions language workers. This repo is shared across many repos in many languages (for each worker) by using git commands. 4 | 5 | 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*). 6 | 7 | Learn more about Azure Function's projects on the [meta](https://github.com/azure/azure-functions) repo. 8 | 9 | ## Adding This Repo 10 | 11 | From within the Azure Functions language worker repo: 12 | 1. Define remote branch for cleaner git commands 13 | - `git remote add proto-file https://github.com/azure/azure-functions-language-worker-protobuf.git` 14 | - `git fetch proto-file` 15 | 2. Index contents of azure-functions-worker-protobuf to language worker repo 16 | - `git read-tree --prefix= -u proto-file/` 17 | 3. Add new path in language worker repo to .gitignore file 18 | - In .gitignore, add path in language worker repo 19 | 4. Finalize with commit 20 | - `git commit -m "Added subtree from https://github.com/azure/azure-functions-language-worker-protobuf. Branch: . Commit: "` 21 | - `git push` 22 | 23 | ## Pulling Updates 24 | 25 | From within the Azure Functions language worker repo: 26 | 1. Define remote branch for cleaner git commands 27 | - `git remote add proto-file https://github.com/azure/azure-functions-language-worker-protobuf.git` 28 | - `git fetch proto-file` 29 | 2. Merge updates 30 | - `git merge -s subtree proto-file/ --squash --allow-unrelated-histories` 31 | - You can also merge with an explicit path to subtree: `git merge -X subtree= --squash proto-file/ --allow-unrelated-histories` 32 | 3. Finalize with commit 33 | - `git commit -m "Updated subtree from https://github.com/azure/azure-functions-language-worker-protobuf. Branch: . Commit: "` 34 | - `git push` 35 | 36 | ## Consuming FunctionRPC.proto 37 | *Note: Update versionNumber before running following commands* 38 | 39 | ## CSharp 40 | ``` 41 | set NUGET_PATH=%UserProfile%\.nuget\packages 42 | set GRPC_TOOLS_PATH=%NUGET_PATH%\grpc.tools\\tools\windows_x86 43 | set PROTO_PATH=.\azure-functions-language-worker-protobuf\src\proto 44 | set PROTO=.\azure-functions-language-worker-protobuf\src\proto\FunctionRpc.proto 45 | set PROTOBUF_TOOLS=%NUGET_PATH%\google.protobuf.tools\\tools 46 | set MSGDIR=.\Messages 47 | 48 | if exist %MSGDIR% rmdir /s /q %MSGDIR% 49 | mkdir %MSGDIR% 50 | 51 | set OUTDIR=%MSGDIR%\DotNet 52 | mkdir %OUTDIR% 53 | %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% 54 | ``` 55 | ## JavaScript 56 | 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`. 57 | 58 | Generate JavaScript files: 59 | ``` 60 | pbjs -t json-module -w commonjs -o azure-functions-language-worker-protobuf/src/rpc.js azure-functions-language-worker-protobuf/src/proto/FunctionRpc.proto 61 | ``` 62 | Generate TypeScript files: 63 | ``` 64 | 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 65 | ``` 66 | 67 | ## Java 68 | Maven plugin : [protobuf-maven-plugin](https://www.xolstice.org/protobuf-maven-plugin/) 69 | In pom.xml add following under configuration for this plugin 70 | ${basedir}//azure-functions-language-worker-protobuf/src/proto 71 | 72 | ## Python 73 | --TODO 74 | 75 | ## Contributing 76 | 77 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 78 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 79 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 80 | 81 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 82 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 83 | provided by the bot. You will only need to do this once across all repos using our CLA. 84 | 85 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 86 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 87 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 88 | -------------------------------------------------------------------------------- /azure/functions_worker/protos/_src/src/proto/google/protobuf/duration.proto: -------------------------------------------------------------------------------- 1 | // Protocol Buffers - Google's data interchange format 2 | // Copyright 2008 Google Inc. All rights reserved. 3 | // https://developers.google.com/protocol-buffers/ 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are 7 | // met: 8 | // 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following disclaimer 13 | // in the documentation and/or other materials provided with the 14 | // distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived from 17 | // this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | syntax = "proto3"; 32 | 33 | package google.protobuf; 34 | 35 | option csharp_namespace = "Google.Protobuf.WellKnownTypes"; 36 | option cc_enable_arenas = true; 37 | option go_package = "github.com/golang/protobuf/ptypes/duration"; 38 | option java_package = "com.google.protobuf"; 39 | option java_outer_classname = "DurationProto"; 40 | option java_multiple_files = true; 41 | option objc_class_prefix = "GPB"; 42 | 43 | // A Duration represents a signed, fixed-length span of time represented 44 | // as a count of seconds and fractions of seconds at nanosecond 45 | // resolution. It is independent of any calendar and concepts like "day" 46 | // or "month". It is related to Timestamp in that the difference between 47 | // two Timestamp values is a Duration and it can be added or subtracted 48 | // from a Timestamp. Range is approximately +-10,000 years. 49 | // 50 | // # Examples 51 | // 52 | // Example 1: Compute Duration from two Timestamps in pseudo code. 53 | // 54 | // Timestamp start = ...; 55 | // Timestamp end = ...; 56 | // Duration duration = ...; 57 | // 58 | // duration.seconds = end.seconds - start.seconds; 59 | // duration.nanos = end.nanos - start.nanos; 60 | // 61 | // if (duration.seconds < 0 && duration.nanos > 0) { 62 | // duration.seconds += 1; 63 | // duration.nanos -= 1000000000; 64 | // } else if (durations.seconds > 0 && duration.nanos < 0) { 65 | // duration.seconds -= 1; 66 | // duration.nanos += 1000000000; 67 | // } 68 | // 69 | // Example 2: Compute Timestamp from Timestamp + Duration in pseudo code. 70 | // 71 | // Timestamp start = ...; 72 | // Duration duration = ...; 73 | // Timestamp end = ...; 74 | // 75 | // end.seconds = start.seconds + duration.seconds; 76 | // end.nanos = start.nanos + duration.nanos; 77 | // 78 | // if (end.nanos < 0) { 79 | // end.seconds -= 1; 80 | // end.nanos += 1000000000; 81 | // } else if (end.nanos >= 1000000000) { 82 | // end.seconds += 1; 83 | // end.nanos -= 1000000000; 84 | // } 85 | // 86 | // Example 3: Compute Duration from datetime.timedelta in Python. 87 | // 88 | // td = datetime.timedelta(days=3, minutes=10) 89 | // duration = Duration() 90 | // duration.FromTimedelta(td) 91 | // 92 | // # JSON Mapping 93 | // 94 | // In JSON format, the Duration type is encoded as a string rather than an 95 | // object, where the string ends in the suffix "s" (indicating seconds) and 96 | // is preceded by the number of seconds, with nanoseconds expressed as 97 | // fractional seconds. For example, 3 seconds with 0 nanoseconds should be 98 | // encoded in JSON format as "3s", while 3 seconds and 1 nanosecond should 99 | // be expressed in JSON format as "3.000000001s", and 3 seconds and 1 100 | // microsecond should be expressed in JSON format as "3.000001s". 101 | // 102 | // 103 | message Duration { 104 | 105 | // Signed seconds of the span of time. Must be from -315,576,000,000 106 | // to +315,576,000,000 inclusive. Note: these bounds are computed from: 107 | // 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years 108 | int64 seconds = 1; 109 | 110 | // Signed fractions of a second at nanosecond resolution of the span 111 | // of time. Durations less than one second are represented with a 0 112 | // `seconds` field and a positive or negative `nanos` field. For durations 113 | // of one second or more, a non-zero value for the `nanos` field must be 114 | // of the same sign as the `seconds` field. Must be from -999,999,999 115 | // to +999,999,999 inclusive. 116 | int32 nanos = 2; 117 | } -------------------------------------------------------------------------------- /azure/functions_worker/bindings/blob.py: -------------------------------------------------------------------------------- 1 | import io 2 | import typing 3 | 4 | from azure.functions import _abc as azf_abc 5 | 6 | from . import meta 7 | from .. import protos 8 | 9 | 10 | class InputStream(azf_abc.InputStream): 11 | def __init__(self, *, data: bytes, 12 | name: typing.Optional[str]=None, 13 | uri: typing.Optional[str]=None, 14 | length: typing.Optional[int]=None) -> None: 15 | self._io = io.BytesIO(data) 16 | self._name = name 17 | self._length = length 18 | self._uri = uri 19 | 20 | @property 21 | def name(self) -> typing.Optional[str]: 22 | return self._name 23 | 24 | @property 25 | def length(self) -> typing.Optional[int]: 26 | return self._length 27 | 28 | @property 29 | def uri(self) -> typing.Optional[str]: 30 | return self._uri 31 | 32 | def read(self, size=-1) -> bytes: 33 | return self._io.read(size) 34 | 35 | def readable(self) -> bool: 36 | return True 37 | 38 | def seekable(self) -> bool: 39 | return False 40 | 41 | def writable(self) -> bool: 42 | return False 43 | 44 | 45 | class BlobConverter(meta.InConverter, 46 | meta.OutConverter, 47 | binding='blob'): 48 | 49 | @classmethod 50 | def check_input_type_annotation( 51 | cls, pytype: type, datatype: protos.BindingInfo.DataType) -> bool: 52 | if (datatype is protos.BindingInfo.undefined 53 | or datatype is protos.BindingInfo.stream): 54 | return issubclass(pytype, azf_abc.InputStream) 55 | elif (datatype is protos.BindingInfo.binary): 56 | return issubclass(pytype, bytes) 57 | elif (datatype is protos.BindingInfo.string): 58 | return issubclass(pytype, str) 59 | else: # Unknown datatype 60 | return False 61 | 62 | @classmethod 63 | def check_output_type_annotation(cls, pytype: type) -> bool: 64 | return (issubclass(pytype, (str, bytes, bytearray, 65 | azf_abc.InputStream) or 66 | callable(getattr(pytype, 'read', None)))) 67 | 68 | @classmethod 69 | def to_proto(cls, obj: typing.Any, *, 70 | pytype: typing.Optional[type]) -> protos.TypedData: 71 | if callable(getattr(obj, 'read', None)): 72 | # file-like object 73 | obj = obj.read() 74 | 75 | if isinstance(obj, str): 76 | return protos.TypedData(string=obj) 77 | 78 | elif isinstance(obj, (bytes, bytearray)): 79 | return protos.TypedData(bytes=bytes(obj)) 80 | 81 | else: 82 | raise NotImplementedError 83 | 84 | @classmethod 85 | def from_proto(cls, data: protos.TypedData, *, 86 | pytype: typing.Optional[type], 87 | trigger_metadata) -> typing.Any: 88 | data_type = data.WhichOneof('data') 89 | 90 | if pytype is str: 91 | # Bound as dataType: string 92 | if data_type == 'string': 93 | return data.string 94 | else: 95 | raise ValueError( 96 | f'unexpected type of data received for the "blob" binding ' 97 | f'declared to receive a string: {data_type!r}' 98 | ) 99 | 100 | return data.string 101 | 102 | elif pytype is bytes: 103 | if data_type == 'bytes': 104 | return data.bytes 105 | elif data_type == 'string': 106 | # This should not happen with the correct dataType spec, 107 | # but we can be forgiving in this case. 108 | return data.string.encode('utf-8') 109 | else: 110 | raise ValueError( 111 | f'unexpected type of data received for the "blob" binding ' 112 | f'declared to receive bytes: {data_type!r}' 113 | ) 114 | 115 | if data_type == 'string': 116 | data = data.string.encode('utf-8') 117 | elif data_type == 'bytes': 118 | data = data.bytes 119 | else: 120 | raise ValueError( 121 | f'unexpected type of data received for the "blob" binding ' 122 | f': {data_type!r}' 123 | ) 124 | 125 | if trigger_metadata is None: 126 | return InputStream(data=data) 127 | else: 128 | properties = cls._decode_trigger_metadata_field( 129 | trigger_metadata, 'Properties', python_type=dict) 130 | if properties: 131 | length = properties.get('Length') 132 | if length: 133 | length = int(length) 134 | else: 135 | length = None 136 | else: 137 | length = None 138 | 139 | return InputStream( 140 | data=data, 141 | name=cls._decode_trigger_metadata_field( 142 | trigger_metadata, 'BlobTrigger', python_type=str), 143 | length=length, 144 | uri=cls._decode_trigger_metadata_field( 145 | trigger_metadata, 'Uri', python_type=str), 146 | ) 147 | 148 | 149 | class BlobTriggerConverter(BlobConverter, 150 | binding='blobTrigger', trigger=True): 151 | pass 152 | -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/run_all_parallel.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get the config file 4 | source "$(dirname "${BASH_SOURCE[0]}")/helpers/get_config_variables.sh" 5 | source "$(dirname "${BASH_SOURCE[0]}")/helpers/helper.sh" 6 | 7 | mkdir -p ${WORKING_DIR} 8 | mkdir -p ${TESTS_LOGS} 9 | 10 | BASE_DIR="$(dirname "${BASH_SOURCE[0]}")" 11 | RESET='\033[0m' # No Color 12 | BLUE='\033[1;34m' 13 | YELLOW='\033[1;33m' 14 | 15 | # This may be needed if docker image is restarted 16 | # Better to simply login again at every invoke 17 | echo "Ensuring login to Azure ACR" 18 | az acr login --name ${ACR_NAME} > /dev/null 19 | 20 | echo -e "Starting Parallel execution in background" 21 | 22 | 23 | run_test ${BASE_DIR}/dev_func_dev_docker/customer_build_native_deps.sh ${TESTS_LOGS}/dfddcbnd.log ${TESTS_TIMEOUT} ${WORKING_DIR}/dfddcbnd.result & 24 | run_test ${BASE_DIR}/dev_func_dev_docker/customer_no_bundler.sh ${TESTS_LOGS}/dfddcnb.log ${TESTS_TIMEOUT} ${WORKING_DIR}/dfddcnb.result & 25 | run_test ${BASE_DIR}/dev_func_dev_docker/new_build_native_deps.sh ${TESTS_LOGS}/dfddnbnd.log ${TESTS_TIMEOUT} ${WORKING_DIR}/dfddnbnd.result & 26 | run_test ${BASE_DIR}/dev_func_dev_docker/new_no_bundler.sh ${TESTS_LOGS}/dfddnnb.log ${TESTS_TIMEOUT} ${WORKING_DIR}/dfddnnb.result & 27 | run_test ${BASE_DIR}/dev_func_dev_docker/new_packapp.sh ${TESTS_LOGS}/dfddnpa.log ${TESTS_TIMEOUT} ${WORKING_DIR}/dfddnpa.result & 28 | 29 | run_test ${BASE_DIR}/dev_func_prod_docker/customer_build_native_deps.sh ${TESTS_LOGS}/dfpdcbnd.log ${TESTS_TIMEOUT} ${WORKING_DIR}/dfpdcbnd.result & 30 | run_test ${BASE_DIR}/dev_func_prod_docker/customer_no_bundler.sh ${TESTS_LOGS}/dfpdcnb.log ${TESTS_TIMEOUT} ${WORKING_DIR}/dfpdcnb.result & 31 | run_test ${BASE_DIR}/dev_func_prod_docker/new_build_native_deps.sh ${TESTS_LOGS}/dfpdnbnd.log ${TESTS_TIMEOUT} ${WORKING_DIR}/dfpdnbnd.result & 32 | run_test ${BASE_DIR}/dev_func_prod_docker/new_no_bundler.sh ${TESTS_LOGS}/dfpdnnb.log ${TESTS_TIMEOUT} ${WORKING_DIR}/dfpdnnb.result & 33 | run_test ${BASE_DIR}/dev_func_prod_docker/new_packapp.sh ${TESTS_LOGS}/dfpdnpa.log ${TESTS_TIMEOUT} ${WORKING_DIR}/dfpdnpa.result & 34 | 35 | run_test ${BASE_DIR}/prod_func_prod_docker/customer_build_native_deps.sh ${TESTS_LOGS}/pfpdcbnd.log ${TESTS_TIMEOUT} ${WORKING_DIR}/pfpdcbnd.result & 36 | run_test ${BASE_DIR}/prod_func_prod_docker/customer_no_bundler.sh ${TESTS_LOGS}/pfpdcnb.log ${TESTS_TIMEOUT} ${WORKING_DIR}/pfpdcnb.result & 37 | run_test ${BASE_DIR}/prod_func_prod_docker/new_build_native_deps.sh ${TESTS_LOGS}/pfpdnbnd.log ${TESTS_TIMEOUT} ${WORKING_DIR}/pfpdnbnd.result & 38 | run_test ${BASE_DIR}/prod_func_prod_docker/new_no_bundler.sh ${TESTS_LOGS}/pfpdnnb.log ${TESTS_TIMEOUT} ${WORKING_DIR}/pfpdnnb.result & 39 | run_test ${BASE_DIR}/prod_func_prod_docker/new_packapp.sh ${TESTS_LOGS}/pfpdnpa.log ${TESTS_TIMEOUT} ${WORKING_DIR}/pfpdnpa.result & 40 | 41 | run_test ${BASE_DIR}/prod_func_dev_docker/customer_build_native_deps.sh ${TESTS_LOGS}/pfddcbnd.log ${TESTS_TIMEOUT} ${WORKING_DIR}/pfddcbnd.result & 42 | run_test ${BASE_DIR}/prod_func_dev_docker/customer_no_bundler.sh ${TESTS_LOGS}/pfddcnb.log ${TESTS_TIMEOUT} ${WORKING_DIR}/pfddcnb.result & 43 | run_test ${BASE_DIR}/prod_func_dev_docker/new_build_native_deps.sh ${TESTS_LOGS}/pfddnbnd.log ${TESTS_TIMEOUT} ${WORKING_DIR}/pfddnbnd.result & 44 | run_test ${BASE_DIR}/prod_func_dev_docker/new_no_bundler.sh ${TESTS_LOGS}/pfddnnb.log ${TESTS_TIMEOUT} ${WORKING_DIR}/pfddnnb.result & 45 | run_test ${BASE_DIR}/prod_func_dev_docker/new_packapp.sh ${TESTS_LOGS}/pfddnpa.log ${TESTS_TIMEOUT} ${WORKING_DIR}/pfddnpa.result & 46 | 47 | echo "Waiting for tests to complete (this may take several minutes)..." 48 | wait 49 | 50 | echo "Test Completed!" 51 | yellow "Results-" 52 | printf "\n" 53 | 54 | print_row $(yellow "Version") $(yellow "Exist-Build-Native") \ 55 | $(yellow "Exist-No-Bundler") $(yellow "New-Build-Native") $(yellow "New-No-Bundler") $(yellow "New-Packapp") 56 | 57 | print_row_line 58 | 59 | print_row $(yellow "dev-func-dev-docker") $(get_result_of ${WORKING_DIR}/dfddcbnd.result) \ 60 | $(get_result_of ${WORKING_DIR}/dfddcnb.result) $(get_result_of ${WORKING_DIR}/dfddnbnd.result) \ 61 | $(get_result_of ${WORKING_DIR}/dfddnnb.result) $(get_result_of ${WORKING_DIR}/dfddnpa.result) 62 | 63 | print_row $(yellow "dev-func-dev-docker") $(get_result_of ${WORKING_DIR}/dfpdcbnd.result) \ 64 | $(get_result_of ${WORKING_DIR}/dfpdcnb.result) $(get_result_of ${WORKING_DIR}/dfpdnbnd.result) \ 65 | $(get_result_of ${WORKING_DIR}/dfpdnnb.result) $(get_result_of ${WORKING_DIR}/dfpdnpa.result) 66 | 67 | print_row $(yellow "dev-func-dev-docker") $(get_result_of ${WORKING_DIR}/pfddcbnd.result) \ 68 | $(get_result_of ${WORKING_DIR}/pfddcnb.result) $(get_result_of ${WORKING_DIR}/pfddnbnd.result) \ 69 | $(get_result_of ${WORKING_DIR}/pfddnnb.result) $(get_result_of ${WORKING_DIR}/pfddnpa.result) 70 | 71 | print_row $(yellow "dev-func-dev-docker") $(get_result_of ${WORKING_DIR}/pfpdcbnd.result) \ 72 | $(get_result_of ${WORKING_DIR}/pfpdcnb.result) $(get_result_of ${WORKING_DIR}/pfpdnbnd.result) \ 73 | $(get_result_of ${WORKING_DIR}/pfpdnnb.result) $(get_result_of ${WORKING_DIR}/pfpdnpa.result) 74 | 75 | 76 | # This is useful when running in a container environment 77 | printf "\n" 78 | echo "All logs are available at ${TESTS_LOGS}" 79 | echo "Sleeping for infinity, in case you need to open bash in the container" 80 | if [[ ${ENVIRONMENT} = "DOCKER" ]] 81 | then 82 | sleep infinity 83 | fi 84 | 85 | if [[ ${EXIT_ON_FAIL} -ne "FALSE" ]] 86 | then 87 | NUMBER_PASSED=$(grep -l "PASSED" ${WORKING_DIR}/*.result | wc -l) 88 | if [[ ${NUMBER_PASSED} -ne 20 ]] 89 | then 90 | echo "Not all (20) tests passed! Dieing.." 91 | exit 1 92 | else 93 | echo "All tests passed (20/20)!" 94 | fi 95 | fi -------------------------------------------------------------------------------- /.ci/e2e/publish_tests/test_runners/run_all_serial.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get the config file 4 | source "$(dirname "${BASH_SOURCE[0]}")/helpers/get_config_variables.sh" 5 | source "$(dirname "${BASH_SOURCE[0]}")/helpers/helper.sh" 6 | 7 | mkdir -p ${WORKING_DIR} 8 | mkdir -p ${TESTS_LOGS} 9 | 10 | BASE_DIR="$(dirname "${BASH_SOURCE[0]}")" 11 | RESET='\033[0m' # No Color 12 | BLUE='\033[1;34m' 13 | YELLOW='\033[1;33m' 14 | 15 | # This may be needed if docker image is restarted 16 | # Better to simply login again at every invoke 17 | echo "Ensuring login to Azure ACR" 18 | az acr login --name ${ACR_NAME} > /dev/null 19 | 20 | echo -e "Starting Serial Execution..." 21 | echo "This will take a while" 22 | 23 | 24 | run_test ${BASE_DIR}/dev_func_dev_docker/customer_build_native_deps.sh ${TESTS_LOGS}/dfddcbnd.log ${TESTS_TIMEOUT} ${WORKING_DIR}/dfddcbnd.result 25 | run_test ${BASE_DIR}/dev_func_dev_docker/customer_no_bundler.sh ${TESTS_LOGS}/dfddcnb.log ${TESTS_TIMEOUT} ${WORKING_DIR}/dfddcnb.result 26 | run_test ${BASE_DIR}/dev_func_dev_docker/new_build_native_deps.sh ${TESTS_LOGS}/dfddnbnd.log ${TESTS_TIMEOUT} ${WORKING_DIR}/dfddnbnd.result 27 | run_test ${BASE_DIR}/dev_func_dev_docker/new_no_bundler.sh ${TESTS_LOGS}/dfddnnb.log ${TESTS_TIMEOUT} ${WORKING_DIR}/dfddnnb.result 28 | run_test ${BASE_DIR}/dev_func_dev_docker/new_packapp.sh ${TESTS_LOGS}/dfddnpa.log ${TESTS_TIMEOUT} ${WORKING_DIR}/dfddnpa.result 29 | 30 | run_test ${BASE_DIR}/dev_func_prod_docker/customer_build_native_deps.sh ${TESTS_LOGS}/dfpdcbnd.log ${TESTS_TIMEOUT} ${WORKING_DIR}/dfpdcbnd.result 31 | run_test ${BASE_DIR}/dev_func_prod_docker/customer_no_bundler.sh ${TESTS_LOGS}/dfpdcnb.log ${TESTS_TIMEOUT} ${WORKING_DIR}/dfpdcnb.result 32 | run_test ${BASE_DIR}/dev_func_prod_docker/new_build_native_deps.sh ${TESTS_LOGS}/dfpdnbnd.log ${TESTS_TIMEOUT} ${WORKING_DIR}/dfpdnbnd.result 33 | run_test ${BASE_DIR}/dev_func_prod_docker/new_no_bundler.sh ${TESTS_LOGS}/dfpdnnb.log ${TESTS_TIMEOUT} ${WORKING_DIR}/dfpdnnb.result 34 | run_test ${BASE_DIR}/dev_func_prod_docker/new_packapp.sh ${TESTS_LOGS}/dfpdnpa.log ${TESTS_TIMEOUT} ${WORKING_DIR}/dfpdnpa.result 35 | 36 | run_test ${BASE_DIR}/prod_func_prod_docker/customer_build_native_deps.sh ${TESTS_LOGS}/pfpdcbnd.log ${TESTS_TIMEOUT} ${WORKING_DIR}/pfpdcbnd.result 37 | run_test ${BASE_DIR}/prod_func_prod_docker/customer_no_bundler.sh ${TESTS_LOGS}/pfpdcnb.log ${TESTS_TIMEOUT} ${WORKING_DIR}/pfpdcnb.result 38 | run_test ${BASE_DIR}/prod_func_prod_docker/new_build_native_deps.sh ${TESTS_LOGS}/pfpdnbnd.log ${TESTS_TIMEOUT} ${WORKING_DIR}/pfpdnbnd.result 39 | run_test ${BASE_DIR}/prod_func_prod_docker/new_no_bundler.sh ${TESTS_LOGS}/pfpdnnb.log ${TESTS_TIMEOUT} ${WORKING_DIR}/pfpdnnb.result 40 | run_test ${BASE_DIR}/prod_func_prod_docker/new_packapp.sh ${TESTS_LOGS}/pfpdnpa.log ${TESTS_TIMEOUT} ${WORKING_DIR}/pfpdnpa.result 41 | 42 | echo "Ensuring login to Azure ACR (again because login has been flaky)" 43 | az acr login --name ${ACR_NAME} > /dev/null 44 | run_test ${BASE_DIR}/prod_func_dev_docker/customer_build_native_deps.sh ${TESTS_LOGS}/pfddcbnd.log ${TESTS_TIMEOUT} ${WORKING_DIR}/pfddcbnd.result 45 | run_test ${BASE_DIR}/prod_func_dev_docker/customer_no_bundler.sh ${TESTS_LOGS}/pfddcnb.log ${TESTS_TIMEOUT} ${WORKING_DIR}/pfddcnb.result 46 | run_test ${BASE_DIR}/prod_func_dev_docker/new_build_native_deps.sh ${TESTS_LOGS}/pfddnbnd.log ${TESTS_TIMEOUT} ${WORKING_DIR}/pfddnbnd.result 47 | run_test ${BASE_DIR}/prod_func_dev_docker/new_no_bundler.sh ${TESTS_LOGS}/pfddnnb.log ${TESTS_TIMEOUT} ${WORKING_DIR}/pfddnnb.result 48 | run_test ${BASE_DIR}/prod_func_dev_docker/new_packapp.sh ${TESTS_LOGS}/pfddnpa.log ${TESTS_TIMEOUT} ${WORKING_DIR}/pfddnpa.result 49 | 50 | echo "Test Completed!" 51 | yellow "Results-" 52 | printf "\n" 53 | 54 | print_row $(yellow "Version") $(yellow "Exist-Build-Native") \ 55 | $(yellow "Exist-No-Bundler") $(yellow "New-Build-Native") $(yellow "New-No-Bundler") $(yellow "New-Packapp") 56 | 57 | print_row_line 58 | 59 | print_row $(yellow "dev-func-dev-docker") $(get_result_of ${WORKING_DIR}/dfddcbnd.result) \ 60 | $(get_result_of ${WORKING_DIR}/dfddcnb.result) $(get_result_of ${WORKING_DIR}/dfddnbnd.result) \ 61 | $(get_result_of ${WORKING_DIR}/dfddnnb.result) $(get_result_of ${WORKING_DIR}/dfddnpa.result) 62 | 63 | print_row $(yellow "dev-func-prod-docker") $(get_result_of ${WORKING_DIR}/dfpdcbnd.result) \ 64 | $(get_result_of ${WORKING_DIR}/dfpdcnb.result) $(get_result_of ${WORKING_DIR}/dfpdnbnd.result) \ 65 | $(get_result_of ${WORKING_DIR}/dfpdnnb.result) $(get_result_of ${WORKING_DIR}/dfpdnpa.result) 66 | 67 | print_row $(yellow "prod-func-dev-docker") $(get_result_of ${WORKING_DIR}/pfddcbnd.result) \ 68 | $(get_result_of ${WORKING_DIR}/pfddcnb.result) $(get_result_of ${WORKING_DIR}/pfddnbnd.result) \ 69 | $(get_result_of ${WORKING_DIR}/pfddnnb.result) $(get_result_of ${WORKING_DIR}/pfddnpa.result) 70 | 71 | print_row $(yellow "prod-func-prod-docker") $(get_result_of ${WORKING_DIR}/pfpdcbnd.result) \ 72 | $(get_result_of ${WORKING_DIR}/pfpdcnb.result) $(get_result_of ${WORKING_DIR}/pfpdnbnd.result) \ 73 | $(get_result_of ${WORKING_DIR}/pfpdnnb.result) $(get_result_of ${WORKING_DIR}/pfpdnpa.result) 74 | 75 | printf "\n" 76 | echo "All logs are available at ${TESTS_LOGS}" 77 | 78 | if [[ ${ENVIRONMENT} = "DOCKER" ]] 79 | then 80 | # This is useful when running in a container environment 81 | echo "Sleeping for infinity, in case you need to open bash in the container" 82 | sleep infinity 83 | fi 84 | 85 | if [[ -z ${EXIT_ON_FAIL} ]] || [[ ${EXIT_ON_FAIL} -ne "FALSE" ]] 86 | then 87 | NUMBER_PASSED=$(grep -l "PASSED" ${WORKING_DIR}/*.result | wc -l) 88 | if [[ ${NUMBER_PASSED} -ne 20 ]] 89 | then 90 | echo "Not all (20) tests passed! Dieing.." 91 | exit 1 92 | else 93 | echo "All tests passed (20/20)!" 94 | fi 95 | fi -------------------------------------------------------------------------------- /azure/functions_worker/bindings/queue.py: -------------------------------------------------------------------------------- 1 | import collections.abc 2 | import datetime 3 | import json 4 | import typing 5 | 6 | from azure.functions import _abc as azf_abc 7 | from azure.functions import _queue as azf_queue 8 | 9 | from . import meta 10 | from .. import protos 11 | 12 | 13 | class QueueMessage(azf_queue.QueueMessage): 14 | """An HTTP response object.""" 15 | 16 | def __init__(self, *, 17 | id=None, body=None, 18 | dequeue_count=None, 19 | expiration_time=None, 20 | insertion_time=None, 21 | time_next_visible=None, 22 | pop_receipt=None): 23 | super().__init__(id=id, body=body, pop_receipt=pop_receipt) 24 | self.__dequeue_count = dequeue_count 25 | self.__expiration_time = expiration_time 26 | self.__insertion_time = insertion_time 27 | self.__time_next_visible = time_next_visible 28 | 29 | @property 30 | def dequeue_count(self): 31 | return self.__dequeue_count 32 | 33 | @property 34 | def expiration_time(self): 35 | return self.__expiration_time 36 | 37 | @property 38 | def insertion_time(self): 39 | return self.__insertion_time 40 | 41 | @property 42 | def time_next_visible(self): 43 | return self.__time_next_visible 44 | 45 | def __repr__(self) -> str: 46 | return ( 47 | f'' 52 | ) 53 | 54 | 55 | class QueueMessageInConverter(meta.InConverter, 56 | binding='queueTrigger', trigger=True): 57 | 58 | @classmethod 59 | def check_input_type_annotation( 60 | cls, pytype: type, datatype: protos.BindingInfo.DataType) -> bool: 61 | if datatype is protos.BindingInfo.undefined: 62 | return issubclass(pytype, azf_abc.QueueMessage) 63 | else: 64 | return False 65 | 66 | @classmethod 67 | def from_proto(cls, data: protos.TypedData, *, 68 | pytype: typing.Optional[type], 69 | trigger_metadata) -> typing.Any: 70 | data_type = data.WhichOneof('data') 71 | 72 | if data_type == 'string': 73 | body = data.string 74 | 75 | elif data_type == 'bytes': 76 | body = data.bytes 77 | 78 | else: 79 | raise NotImplementedError( 80 | f'unsupported queue payload type: {data_type}') 81 | 82 | if trigger_metadata is None: 83 | raise NotImplementedError( 84 | f'missing trigger metadata for queue input') 85 | 86 | return QueueMessage( 87 | id=cls._decode_trigger_metadata_field( 88 | trigger_metadata, 'Id', python_type=str), 89 | body=body, 90 | dequeue_count=cls._decode_trigger_metadata_field( 91 | trigger_metadata, 'DequeueCount', python_type=int), 92 | expiration_time=cls._parse_datetime_metadata( 93 | trigger_metadata, 'ExpirationTime'), 94 | insertion_time=cls._parse_datetime_metadata( 95 | trigger_metadata, 'InsertionTime'), 96 | time_next_visible=cls._parse_datetime_metadata( 97 | trigger_metadata, 'NextVisibleTime'), 98 | pop_receipt=cls._decode_trigger_metadata_field( 99 | trigger_metadata, 'PopReceipt', python_type=str) 100 | ) 101 | 102 | 103 | class QueueMessageOutConverter(meta.OutConverter, binding='queue'): 104 | 105 | @classmethod 106 | def check_output_type_annotation(cls, pytype: type) -> bool: 107 | valid_types = (azf_abc.QueueMessage, str, bytes) 108 | return ( 109 | meta.is_iterable_type_annotation(pytype, valid_types) or 110 | (isinstance(pytype, type) and issubclass(pytype, valid_types)) 111 | ) 112 | 113 | @classmethod 114 | def to_proto(cls, obj: typing.Any, *, 115 | pytype: typing.Optional[type]) -> protos.TypedData: 116 | if isinstance(obj, str): 117 | return protos.TypedData(string=obj) 118 | 119 | elif isinstance(obj, bytes): 120 | return protos.TypedData(bytes=obj) 121 | 122 | elif isinstance(obj, azf_queue.QueueMessage): 123 | return protos.TypedData( 124 | json=json.dumps({ 125 | 'id': obj.id, 126 | 'body': obj.get_body().decode('utf-8'), 127 | }) 128 | ) 129 | 130 | elif isinstance(obj, collections.abc.Iterable): 131 | msgs = [] 132 | for item in obj: 133 | if isinstance(item, str): 134 | msgs.append(item) 135 | elif isinstance(item, azf_queue.QueueMessage): 136 | msgs.append({ 137 | 'id': item.id, 138 | 'body': item.get_body().decode('utf-8') 139 | }) 140 | else: 141 | raise NotImplementedError( 142 | 'invalid data type in output ' 143 | 'queue message list: {}'.format(type(item))) 144 | 145 | return protos.TypedData( 146 | json=json.dumps(msgs) 147 | ) 148 | 149 | raise NotImplementedError 150 | 151 | @classmethod 152 | def _format_datetime(cls, dt: typing.Optional[datetime.datetime]): 153 | if dt is None: 154 | return None 155 | else: 156 | return dt.isoformat() 157 | --------------------------------------------------------------------------------