Azure / azure-functions-python-library

Azure Functions Python SDK
MIT License
151 stars 63 forks source link

Support for ASGI applications #75

Closed jordaneremieff closed 3 years ago

jordaneremieff commented 3 years ago

Hello.

I've been looking into how to use ASGI applications with Azure Functions. I maintain a library called Mangum that at one point in time supported Azure Functions, but currently only supports AWS Lambda. I've received some interest in supporting multiple serverless platforms (such as Azure) again, but noticed this library now offers support for WSGI so figured I'd raise the issue here.

Would it be desirable to include support for ASGI applications in this library? This would enable a number of ASGI application frameworks to be used in a similar way to WSGI.

I migrated the removed Azure Function support from Mangum into a different project, Bonnette, which allowed using ASGI like this:

import logging
import azure.functions as func
from bonnette import Bonnette

async def app(scope, receive, send):
    assert scope["type"] == "http"
    await send(
        {
            "type": "http.response.start",
            "status": 200,
            "headers": [[b"content-type", b"text/html; charset=utf-8"]],
        }
    )
    await send(
        {"type": "http.response.body", "body": b"<html><h1>Hello, world!</h1></html>"}
    )

def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.info("Python HTTP trigger function processed a request.")
    handler = Bonnette(app)
    return handler(req)

There is some incorrect behaviour in that project around the ASGI request/response handling (it is unmaintained), but maybe it can help serve as a reference for the Azure Functions. Additionally the implementation here might be helpful, there are a few AWS-specific things but mostly it handles only the HTTP->ASGI behaviour.

Anyway, I'm not using Azure Functions myself so beyond what I've mentioned above I don't have a lot of interest in supporting this externally, but still wanted to help find a solution for users of frameworks like FastAPI that have expressed interest.

simonw commented 3 years ago

I'd love to see ASGI support in this library - it would make it much easier for me to port https://datasette.io/ to run on Azure Functions, which would mean I could build a datasette publish azurefunctions command as seen on https://docs.datasette.io/en/stable/publish.html

tonybaloney commented 3 years ago

@simonw @jordaneremieff I've put a PoC together with an ASGI worker, I think this would handle these use cases.

https://github.com/tonybaloney/ants-azure-demos/blob/master/fastapi-functions/app/http_asgi.py

It would be used like this in your function, where app is the ASGI application instance:


def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
    return AsgiMiddleware(app).handle(req, context)
simonw commented 3 years ago

I tried a very basic hello world and it worked!

async def helloworld_app(scope, receive, send):
    await send(
        {
            "type": "http.response.start",
            "status": 200,
            "headers": [
                [b"content-type", b"text/plain"],
            ],
        }
    )
    await send(
        {
            "type": "http.response.body",
            "body": b"Hello, world from ASGI!",
        }
    )

def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
    return AsgiMiddleware(helloworld_app).handle(req, context)
simonw commented 3 years ago

I had to make a couple of changes:

    def to_asgi_http_scope(self):
        return {
            "type": "http",
            "asgi.version": self.asgi_version,
            "asgi.spec_version": self.asgi_spec_version,
            "http_version": "1.1",
            "method": self.request_method,
            "scheme": "https",
            "path": self.path_info,
            "raw_path": self.path_info.encode("utf-8"),
            "query_string": self.query_string.encode("utf-8"),
            "root_path": self.script_name,
            "headers": self._get_encoded_http_headers(),
            "server": (self.server_name, self.server_port),
        }

I added the two .encode("utf-8") calls.

simonw commented 3 years ago

Got Datasette working! https://azure-functions-datasette.azurewebsites.net/global-power-plants/global-power-plants

tonybaloney commented 3 years ago

Nice! Thanks @simonw.

I'll be writing some tests for this shim next week so I'll run back through all the arguments and validate the types (byte strings vs unicode strings) again (its different to WSGI).

simonw commented 3 years ago

Here's the source code for that demo: https://github.com/simonw/azure-functions-datasette

jordaneremieff commented 3 years ago

@tonybaloney great work! Thanks for doing this, I'll start pointing people in this direction when it lands. :)

Wealing commented 3 years ago

Hello,

Does this mean we can use FastAPI on Azure Functions now? Could you point to an example of how to do this?

Thank you!

tonybaloney commented 3 years ago

Hello,

Does this mean we can use FastAPI on Azure Functions now? Could you point to an example of how to do this?

Thank you!

Yes it will once the latest package is released on PyPi, this example will work : https://github.com/tonybaloney/ants-azure-demos/tree/master/fastapi-functions

tonybaloney commented 3 years ago

This feature is shipped in 1.7.1, released yesterday

gsaiz commented 3 years ago

If I create a brand new Azure Function App, with Python 3.8, the Runtime Version is 3.0.15885.0, and azure-functions is 1.7.0.

Therefore, I get the following error when debugging locally:

Exception: ImportError: cannot import name 'AsgiMiddleware' from 'azure.functions' (C:\Program Files\Microsoft\Azure Functions Core Tools\workers\python\3.8/WINDOWS/X64\azure\functions\__init__.py). Troubleshooting Guide: https://aka.ms/functions-modulenotfound

When I deploy it to Azure, I can't see the trace but I get an HTTP 500.

How can I make sure that my Function App runs with azure-functions 1.7.1 or higher?