diff --git a/azure/functions/_http_asgi.py b/azure/functions/_http_asgi.py index cdae357e..de382b1d 100644 --- a/azure/functions/_http_asgi.py +++ b/azure/functions/_http_asgi.py @@ -1,9 +1,9 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import asyncio -from typing import Callable, Dict, List, Tuple, Optional, Any, Union +from typing import Dict, List, Tuple, Optional, Any, Union import logging +import asyncio from wsgiref.headers import Headers from ._abc import Context @@ -104,32 +104,52 @@ async def _send(self, message): class AsgiMiddleware: + """This middleware is to adapt an ASGI supported Python server + framework into Azure Functions. It can be used by either calling the + .handle() function or exposing the .main property in a HttpTrigger. + """ + _logger = logging.getLogger('azure.functions.AsgiMiddleware') + _usage_reported = False + def __init__(self, app): - logging.debug("Instantiating ASGI middleware.") + """Instantiate an ASGI middleware to convert Azure Functions HTTP + request into ASGI Python object. Example on handling ASGI app in a HTTP + trigger by overwriting the .main() method: + + import azure.functions as func + + from FastapiApp import app + + main = func.AsgiMiddleware(app).main + """ + if not self._usage_reported: + self._logger.info("Instantiating Azure Functions ASGI middleware.") + self._usage_reported = True + self._app = app - self.loop = asyncio.new_event_loop() - logging.debug("asyncio event loop initialized.") - - # Usage - # main = func.AsgiMiddleware(app).main - @property - def main(self) -> Callable[[HttpRequest, Context], HttpResponse]: - return self._handle - - # Usage - # return func.AsgiMiddleware(app).handle(req, context) - def handle( - self, req: HttpRequest, context: Optional[Context] = None - ) -> HttpResponse: - logging.info(f"Handling {req.url} as ASGI request.") + self._loop = asyncio.new_event_loop() + self.main = self._handle + + def handle(self, req: HttpRequest, context: Optional[Context] = None): + """Method to convert an Azure Functions HTTP request into a ASGI + Python object. Example on handling ASGI app in a HTTP trigger by + calling .handle() in .main() method: + + import azure.functions as func + + from FastapiApp import app + + def main(req, context): + return func.AsgiMiddleware(app).handle(req, context) + """ + self._logger.debug(f"Handling {req.url} as an ASGI request.") return self._handle(req, context) - def _handle(self, req: HttpRequest, - context: Optional[Context]) -> HttpResponse: + def _handle(self, req, context): asgi_request = AsgiRequest(req, context) - asyncio.set_event_loop(self.loop) + asyncio.set_event_loop(self._loop) scope = asgi_request.to_asgi_http_scope() - asgi_response = self.loop.run_until_complete( + asgi_response = self._loop.run_until_complete( AsgiResponse.from_app(self._app, scope, req.get_body()) ) diff --git a/azure/functions/_http_wsgi.py b/azure/functions/_http_wsgi.py index 734d2411..7923b02d 100644 --- a/azure/functions/_http_wsgi.py +++ b/azure/functions/_http_wsgi.py @@ -1,7 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from typing import Callable, Dict, List, Optional, Any +from typing import Dict, List, Optional, Any +import logging from io import BytesIO, StringIO from os import linesep from urllib.parse import urlparse @@ -142,26 +143,47 @@ def _start_response(self, status: str, response_headers: List[Any]): class WsgiMiddleware: + """This middleware is to adapt a WSGI supported Python server + framework into Azure Functions. It can be used by either calling the + .handle() function or exposing the .main property in a HttpTrigger. + """ + _logger = logging.getLogger('azure.functions.WsgiMiddleware') + _usage_reported = False + def __init__(self, app): + """Instantiate a WSGI middleware to convert Azure Functions HTTP + request into WSGI Python object. Example on handling WSGI app in a HTTP + trigger by overwriting the .main() method: + + import azure.functions as func + + from FlaskApp import app + + main = func.WsgiMiddleware(app.wsgi_app).main + """ + if not self._usage_reported: + self._logger.info("Instantiating Azure Functions WSGI middleware.") + self._usage_reported = True + self._app = app self._wsgi_error_buffer = StringIO() + self.main = self._handle + + def handle(self, req: HttpRequest, context: Optional[Context] = None): + """Method to convert an Azure Functions HTTP request into a WSGI + Python object. Example on handling WSGI app in a HTTP trigger by + calling .handle() in .main() method: + + import azure.functions as func + + from FlaskApp import app - # Usage - # main = func.WsgiMiddleware(app).main - @property - def main(self) -> Callable[[HttpRequest, Context], HttpResponse]: - return self._handle - - # Usage - # return func.WsgiMiddleware(app).handle(req, context) - def handle(self, - req: HttpRequest, - context: Optional[Context] = None) -> HttpResponse: + def main(req, context): + return func.WsgiMiddleware(app.wsgi_app).handle(req, context) + """ return self._handle(req, context) - def _handle(self, - req: HttpRequest, - context: Optional[Context]) -> HttpResponse: + def _handle(self, req, context): wsgi_request = WsgiRequest(req, context) environ = wsgi_request.to_environ(self._wsgi_error_buffer) wsgi_response = WsgiResponse.from_app(self._app, environ) diff --git a/tests/test_http_asgi.py b/tests/test_http_asgi.py index b90097e5..1df7c243 100644 --- a/tests/test_http_asgi.py +++ b/tests/test_http_asgi.py @@ -142,6 +142,11 @@ def test_middleware_calls_app(self): self.assertEqual(response.get_body(), test_body) def test_middleware_calls_app_with_context(self): + """Test if the middleware can be used by exposing the .handle method, + specifically when the middleware is used as + def main(req, context): + return AsgiMiddleware(app).handle(req, context) + """ app = MockAsgiApplication() test_body = b'Hello world!' app.response_body = test_body @@ -153,3 +158,22 @@ def test_middleware_calls_app_with_context(self): # Verify asserted self.assertEqual(response.status_code, 200) self.assertEqual(response.get_body(), test_body) + + def test_middleware_wrapper(self): + """Test if the middleware can be used by exposing the .main property, + specifically when the middleware is used as + main = AsgiMiddleware(app).main + """ + app = MockAsgiApplication() + test_body = b'Hello world!' + app.response_body = test_body + app.response_code = 200 + req = self._generate_func_request() + ctx = self._generate_func_context() + + main = AsgiMiddleware(app).main + response = main(req, ctx) + + # Verify asserted + self.assertEqual(response.status_code, 200) + self.assertEqual(response.get_body(), test_body) diff --git a/tests/test_http_wsgi.py b/tests/test_http_wsgi.py index 3addf518..e70aef29 100644 --- a/tests/test_http_wsgi.py +++ b/tests/test_http_wsgi.py @@ -164,10 +164,29 @@ def test_response_with_exception(self): self.assertEqual(e.exception.message, 'wsgi excpt') def test_middleware_handle(self): + """Test if the middleware can be used by exposing the .handle method, + specifically when the middleware is used as + def main(req, context): + return WsgiMiddleware(app).handle(req, context) + """ app = self._generate_wsgi_app() func_request = self._generate_func_request() func_response = WsgiMiddleware(app).handle(func_request) self.assertEqual(func_response.status_code, 200) + self.assertEqual(func_response.get_body(), b'sample string') + + def test_middleware_wrapper(self): + """Test if the middleware can be used by exposing the .main property, + specifically when the middleware is used as + main = WsgiMiddleware(app).main + """ + app = self._generate_wsgi_app() + main = WsgiMiddleware(app).main + func_request = self._generate_func_request() + func_context = self._generate_func_context() + func_response = main(func_request, func_context) + self.assertEqual(func_response.status_code, 200) + self.assertEqual(func_response.get_body(), b'sample string') def _generate_func_request( self,