Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable WSGI for Python Function App #45

Merged
merged 10 commits into from
Feb 14, 2020
Merged

Enable WSGI for Python Function App #45

merged 10 commits into from
Feb 14, 2020

Conversation

Hazhzeng
Copy link
Contributor

@Hazhzeng Hazhzeng commented Feb 12, 2020

Acknowledgement

This feature is migrated from @vtbassmatt 's azf-wsgi project which enables wsgi feature for Azure Functions Python app. We've contacted @vtbassmatt and agreed to implement this feature on azure-functions-python-library. We greatly appreciate his contribution, helping us expand the Python user base in his own time.

Usage

  1. Override the main entrypoint
import logging

import azure.functions as func
from ..FlaskApp.wsgi import application

main = func.WsgiMiddleware(application).main
  1. Alternatively, use the old syntax as demonstrated in azf-wsgi
import logging

import azure.functions as func
from ..FlaskApp.wsgi import application

def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
    return func.WsgiMiddleware(application).handle(req, context)

Fixes

This PR also includes some fixes from the original repository
Thanks

Limitation

  • The azure functions architecture does not support data streaming since it requires http persistent connection.
  • The Python function app is running on a single process, to spawn multiple processes from the same host, set FUNCTIONS_WORKER_PROCESS_COUNT app setting to a higher value (10).

Todos

Since the feature is not officially released, feature-wise this PR is ready for review, and of course, we need to verify the following features before we release this WSGI change.

  1. Client app logging ✔
  2. Client app routing (query_params) ✔
  3. Environment variable propagating (from host to client app) ✔
  4. Content-Length (binary, application/json, text/plain) ✔
  5. Chunked data transfer ✔
  6. Response headers ✔
  7. Client app error emissions ✔
  8. Client app exception handling ✔

resolves: Azure/azure-functions-python-worker#165

Copy link

@vtbassmatt vtbassmatt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome, thanks folks! I'll deprecate the separate project in favor of this 😁

@rolexsanches
Copy link

Hello

Its aready able to run Django Applications too?

Thanks!

@Hazhzeng
Copy link
Contributor Author

Hey @rolexsanches,

Yes, Django also accept wsgi request via WSGI interface.
The following code should do the trick.

import logging

import azure.functions as func
from ..Django.wsgi import DjangoApplication

main = func.WsgiMiddleware(DjangoApplication).main

Please wait for the next Python release to enable this feature.

self.content_type = self._lowercased_headers.get('content-type')
self.content_length = str(len(func_req_body))
self.server_name = getattr(url, 'hostname', None)
self.server_port = str(self._get_port(url, self._lowercased_headers))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getattr might be slightly slower (due to reflection) than regular get, but may not be a big deal. Just a note.

@elasticdotventures
Copy link

elasticdotventures commented Feb 16, 2021

The following code should do the trick. ?? (Ha! not exactly)

import logging

import azure.functions as func
from ..Django.wsgi import DjangoApplication

main = func.WsgiMiddleware(DjangoApplication).main

So I expended a few neurons trying to wrap my head around Azure Functions & Django and WSGI at the same time and this helpful snippet makes little/no sense to me.
(Perhaps I'm being too literal) @Hazhzeng do you mean to use "..Django.wsgi" or is that just a lazy notation? I'm not sure what the ".." operator does in Python, so I'm going to assume it's a typo/invalid and it's not equivalent to the javascript spread operator, maybe your python-fu is stronger than mine.

I'm going to assume that code snippet belongs in __init__.py (or whatever file is referenced by the function.json), and that it's bindings should be of "type": "httpTrigger" and probably "authLevel":"anonymous"

It's not manage.py is the command line utility for admin tasks, but for some reason my Azure Function deployment script wants to deploy that. I've added manage.py to the .funcignore

I think the __init__.py should probably look something more like:

import os
import azure.functions as func
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myapp.settings')
application = get_wsgi_application()
 
def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
    return func.WsgiMiddleware(application).handle(req, context)

^^^ note: YMMV .. I haven't actually gotten this working YET (so the lines above may be totally wrong), though I'm pretty confident the earlier sample above doesn't work either, so hoping somebody might see this.

I'm also going to assume that on Azure functions the host.json needs to have the routePrefix disabled such as:

    "extensions": {
        "http": {
            "routePrefix": ""
        }
    }

as described in the "Customize the http endpoint" section of the Azure Functions reference
https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger?tabs=python#customize-the-http-endpoint

In functions.json

{
  {
  "bindings":[
     "route":"myapp/{*route}",
  ]
  }
}

And that sorta works, but not really. Because for my purpose the django urls.py is still pretty upset about running within an Azure Function.

The *route is a kludge, maybe better answers here: https://docs.microsoft.com/en-us/aspnet/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2#constraints

So I can't figure out how to get Django to load from an Azure function but I've put in sufficient time in this fools errand so I'm giving up and heading back to a Docker instance.

@DharakOne
Copy link

How do create wsgi object in flask?

Could someone to give me example?

from ..FlaskApp.wsgi import application

import logging

import azure.functions as func
from ..FlaskApp.wsgi import application

@stefanushinardi
Copy link

@DharakOne this example might clear out some things: https://github.com/Hazhzeng/functions-wsgi-demo. Let me know if it is not clear

@echennells
Copy link

@DharakOne this example might clear out some things: https://github.com/Hazhzeng/functions-wsgi-demo. Let me know if it is not clear

Isn't the example using the azf-wsgi library and the old style?

https://github.com/Hazhzeng/functions-wsgi-demo/blob/master/HttpTrigger/__init__.py

@eelwk
Copy link

eelwk commented Jun 15, 2021

For anyone wishing for a very simple starting example implementation, try this:

app.py file (this is your simple hello world flask app)
path to file ..function-flask/api/app.py

import json

application = Flask(__name__)

@application.route("/api/httpTrigger")
def home():
    return "Hello, from Flask!"

I used an http Trigger function, cleverly named httpTrigger, thus the routing above.
My function app's __init.py__
path to file ..function-flask/httpTrigger/init.py

import logging

import azure.functions as func
from api.app import application

def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
    return func.WsgiMiddleware(application).handle(req, context)

Running the function app locally using the function runtime core tools, if I hit this URL:
http://localhost:7071/api/httpTrigger

The response I get back is a 200 OK and "Hello, from Flask!"

@ivan-ferrante
Copy link

Hi guys! 👋
For those looking for an easy implementation example with flask_restx and API registration: https://github.com/IvanFerrante92/azfunc-wsgi-example/tree/main/Azfn-Wsgi/AzfnWSGI

@wanghaisheng
Copy link

wanghaisheng commented Mar 16, 2022

@eelwk
hey there i got my app.py like this, how to adjust this



import json

application = Flask(__name__)

def tiktoka():
    return "Hello, from Flask!"


if __name__ == "__main__":

    app.add_url_rule('/', 'webio_view', webio_view(partial(tiktoka, lang='')),
                     methods=['GET', 'POST', 'OPTIONS'])
   app.run()

then init.py it always start a random port instead of func 7071 default port

from .app  import tiktoka
app = Flask(__name__)

app.add_url_rule('/', 'webio_view', webio_view(partial(tiktoka, lang='')()),
                    methods=['GET', 'POST', 'OPTIONS'])

def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:

    return func.WsgiMiddleware(app).handle(req, context)


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Consume Flask routes for HTTP triggered Azure Functions