RDFLib / prez

Prez is a data-configurable Linked Data API framework that delivers profiles of Knowledge Graph data according to the Content Negotiation by Profile standard.
BSD 3-Clause "New" or "Revised" License
18 stars 7 forks source link

Change to app factory pattern #224

Closed ashleysommer closed 2 months ago

ashleysommer commented 2 months ago

See this thread for discussion of the benefits of the factory pattern in Uvicorn. Support for this feature was merged to Uvicorn in Oct 2020, and is commonly used. It is also supported in other ASGI runners.

This change set was motivated initially for my need to pass a base prefix (aka, root_path) to the FastAPI() constructor in prez.app (for deploying in a container on Azure). After modifying the app creation code to allow parameterising the construction, it was immediately obvious I could allow other variables to parameterize the creation of the FastAPI instance. This led to even being able to optionally use a whole different Settings instance if the user wants to manually create their own and pass it in rather than using the one auto-generated in by Pydantic in prez.config.

We use this pattern in a couple of FastAPI applications we've written recently at CSIRO, and it seems robust.

I have two questions that I came across while editing the app.py file: 1) It looks like the app is using two different middlewares to add CORS support. There is the bundled starlette CORSMiddleware and the local add_cors_headers middleware. Both do approximately the same thing. I've left both in use in this change, but I feel like only one is needed. 2) The function _get_sparql_service_description() is in app.py, it looks important but it doesn't seem to be used anywhere. I've left it intact in this change, but perhaps it needs to be removed, or maybe it is supposed to be used somewhere else?

recalcitrantsupplant commented 2 months ago
1. It looks like the app is using two different middlewares to add CORS support. There is the bundled `starlette CORSMiddleware` and the local `add_cors_headers` middleware. Both do approximately the same thing. I've left both in use in this change, but I feel like only one is needed.

There should only be one and I don't think they should be as open as they are @lalewis1 could you please look into this next week - it's lower priority & I've added it here: https://github.com/RDFLib/prez/issues/227

2. The function `_get_sparql_service_description()` is in `app.py`, it looks important but it doesn't seem to be used anywhere. I've left it intact in this change, but perhaps it needs to be removed, or maybe it is supposed to be used somewhere else?

Nick added this one and I believe it was for OGC Records compliance, we'll need to revisit it.

ashleysommer commented 2 months ago

would this imply they're using Prez as a python import/library Yeah that was approximately my thought process around that.

If my understanding is correct, in the current form Prez is more of an application template, where the intention is the user would clone or fork it, modify the source files to add their own routers, new APIs, set their own settings.py file in prez.config.

However I'm thinking of a situation where you could take take a built prez module (like in the docker container), import it into a main.py file, build your own Settings() object, feed that into the App factory, get the Prez FastAPI app back out, then add any additional routers you want, and pass it to uvicorn.

I have a branch in my local codebase that does exactly that to build out the Prez-as-a-function implementation for use in the Azure Function Apps environment.

ashleysommer commented 2 months ago

Have not got my head around the pattern yet - will need to read up on it

Its not very different to the old pattern. Running the app from the commandline in uvicorn is basically the same as before except instead of giving uvicorn the module path of the app instance, you give it the module path of a function, (and add the --factory flag) then uvicorn calls that function to get the app instance.

In non-parameterised use cases, the result is exactly the same as before. But for someone importing Prez as a module, they can call the app factory with different parameters to get out different versions of the app, and modify the app themselves, before passing it to uvicorn.

It means you can do things like this:

# main.py
import uvicorn
from prez.app import assemble_app
from prez.config import Settings

my_settings = Settings()
prez_app = assemble_app(title="My Custom Prez", version="1.0", settings=my_settings)
# add app customisations
my_port = 8001

if __name__ == "__main__":
    uvicorn.run(prez_app, port=my_port, reload=True)

or

# main.py
from fastapi import FastAPI
import uvicorn
from prez.app import assemble_app
from prez.config import Settings

def my_custom_factory() -> FastAPI:
    my_settings = Settings()
    my_app = assemble_app(title="My Custom Prez", version="1.1", settings=my_settings)
    # customize my_app here
    return my_app

my_port = 8001

if __name__ == "__main__":
    uvicorn.run(my_custom_factory, factory=True, port=my_port, reload=True)

or for Azure Functions:

# function_app.py
import azure.functions as func
from prez.app import assemble_app
from prez.config import Settings

my_settings = Settings()
prez_app = assemble_app(title="Azure Prez function", version="1.0", settings=my_settings)
# add extra app routes required

app = func.AsgiFunctionApp(app=prez_app, http_auth_level=func.AuthLevel.FUNCTION)
recalcitrantsupplant commented 2 months ago

Thanks for the explanation Ashley - merging this in now