Azure / azure-functions-python-library

Azure Functions Python SDK
MIT License
147 stars 61 forks source link

WsgiFunctionApp with DispatcherMiddleware and registering new HTTP triggers blueprints #213

Closed mycaule closed 2 months ago

mycaule commented 2 months ago

Using a technique called DispatcherMiddleware it is possible to mount a Flask app only at an arbitrary prefix path.

Would it be possible for the http_app_func to manage only this mount path so that the app can register other HTTP endpoints (using durable functions and orchestrators for example)

import azure.functions as func
from werkzeug.serving import run_simple
from werkzeug.middleware.dispatcher import DispatcherMiddleware

flask_app = Flask(__name__)
@flask_app.get("/")
def hello():
    return Response(f"<h1>Hello World</h1>", mimetype="text/html")

flask_app.wsgi_app = DispatcherMiddleware(run_simple, {'/abc/123': flask_app.wsgi_app})
# see https://stackoverflow.com/a/18967744/1360476

app = func.WsgiFunctionApp(app=flask_app.wsgi_app, http_auth_level=func.AuthLevel.ANONYMOUS)
# app.register_blueprint(other_http_bp_here)

It appears every routes become managed by http_app_func because of the constant route template below.

https://github.com/Azure/azure-functions-python-library/blob/b85eb7a7909198de93fd974dd8225aa9d9cbdbc5/azure/functions/decorators/function_app.py#L2842-L2847

Moreover using the host.json file it looks like it is only possible to set the $.extensions.http.routePrefix globally

https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook?tabs=isolated-process%2Cfunctionsv2&pivots=programming-language-python#hostjson-settings

mycaule commented 2 months ago

Adding this class in my project solved the problem.

prefix = "v1"

class FlaskFunctionApp(ExternalHttpFunctionApp):
    """A class to create a WsgiFunctionApp object."""

    def __init__(self, app, http_auth_level: AuthLevel | str = AuthLevel.FUNCTION):
        """Constructor of WsgiFunctionApp object."""

        super().__init__(auth_level=http_auth_level)
        self._add_http_app(WsgiMiddleware(app))

    def _add_http_app(self, http_middleware: AsgiMiddleware | WsgiMiddleware) -> None:
        """Add a Wsgi app integrated http function."""

        if not isinstance(http_middleware, WsgiMiddleware):
            raise TypeError("Please pass WsgiMiddleware instance" " as parameter.")

        wsgi_middleware: WsgiMiddleware = http_middleware

        @self.http_type(http_type="wsgi")
        @self.route(methods=(method for method in HttpMethod), auth_level=self.auth_level, route=prefix + "/{*route}")
        def http_app_func(req: HttpRequest, context: Context):
            return wsgi_middleware.handle(req, context)

I then just needed to mount using the same prefix.

from werkzeug.exceptions import NotFound

middleware = DispatcherMiddleware(NotFound(), {f"/{prefix}": flask_app.wsgi_app})
app = FlaskFunctionApp(app=middleware, http_auth_level=func.AuthLevel.ANONYMOUS)