Replies: 2 comments 1 reply
-
This is an excellent writeup and I feel like I need some time to chew on this to get my thoughts clear but (at the risk of sounding a bit naive) I wonder whether we can't build out |
Beta Was this translation helpful? Give feedback.
-
I don't think there was ever an intent to not support DI. It's really nice for all the reasons you describe and should be supported.
This sounds like a good idea to me.
FastAPI does support non-coroutine controllers, the function converts non-coroutine controllers into coroutines by running them in a threadpool because this makes the code simpler; the controller it creates isn't conditionally FastAPI does let you use I agree with you that the
One of the challenges is request model validation is different for POST vs. GET requests. POST requests are using a pydantic model declared as a type hint of the controller. While GET requests are using a dataclass as the type hint with a default value of
Another challenge is the business logic defined by the backend client may need to be aware of this dependency. I'm imagining something like an oauth dependency that validates a user's request based on an API key and returns a list of collections that user is allowed to search. In order to add a dependency, the user will have to:
Users who are adding dependencies are probably extending stac-fastapi internals anyways so maybe this isn't a big deal? One solution is to require dependencies to pass data to the backend through the |
Beta Was this translation helpful? Give feedback.
-
I'm working on adding a feature to our
stac-fastapi
+pgstac
catalog and have been experimenting with customizing the Transactions extension. I quickly found that the current stac-fastapi architecture does not support FastAPI's Dependency Injection (DI) feature. I figured that I would start this discussion to track the following:DI in FastAPI
Why
I think FastAPI's use of DI is its killer feature. It provides an inversion of control wherein our controllers/route-handlers can declare their required input parameters and FastAPI will accommodate. This encourages developers to encapsulate common needs (eg auth requirements, obtaining db connections) into reusable dependencies and thereby cluttering their controller logic with these utility steps. Additionally, it makes testing controllers very simple as FastAPI ships with first-class support for overriding dependencies.
How it works
A quick exploration into FastAPI's code shows how they are doing the DI magic:
router.add_api_route(...)
, we create anAPIRoute
object.APIRoute
, we create aDependant
object of our controller.dependant
involves using theinspect
module to retrieve the signature of the controller. It then iterates through each input parameter and if those are instances of theDepends
class, turns those intoDependant
objects (this involves some recursion where we do the same for each dependency's dependencies viaget_param_sub_dependant(...)
->get_sub_depndant(...)
->get_dependant(...)
->get_param_sub_dependant(...)
and so on).app
property onto the route instance. This is done by first creating a callable request handler which takes in a request, resolves the dependencies of the controller, calls the controller, and finally marshall's the response to the expected response class (link). That callable request handler is then converted intoASGIApp
instance via Starlette'srequest_response(...)
.After all of this, FastAPI has a router with individual ASGIApp route handlers to process requests. (☝️ was all of this necessary for this discussion? maybe not, but understanding this helped me figure out how DI could fit in with stac-fastapi and I wanted to log it somewhere 😅)
Dependency Injection in STAC FastAPI
Why doesn't it work?
Prior to adding any controller to our router, we first pass it through
create_async_endpoint(...)
:stac-fastapi/stac_fastapi/api/stac_fastapi/api/app.py
Lines 125 to 127 in 7ba0a40
This seems to have three functions:
stac-fastapi/stac_fastapi/api/stac_fastapi/api/routes.py
Lines 46 to 47 in 7ba0a40
stac-fastapi/stac_fastapi/api/stac_fastapi/api/routes.py
Lines 73 to 82 in 7ba0a40
stac-fastapi/stac_fastapi/api/stac_fastapi/api/routes.py
Lines 56 to 58 in 7ba0a40
Step 2 is where DI really falls down, as FastAPI's use of the
inspect
module now no longer sees the controller that we generate, rather it sees the wrapped controller that expects only the request and request coerced into our request model.How to support Dependency Injection
Support for DI is pretty simple: stop using
create_async_endpoint(...)
. FastAPI has its own mechanisms to handle non-coroutine controllers (our step 1) and coerce the response into our expected response class (step 3). It seems like the only possibly challenging part is converting a request object into an expected request model (step 2).Normally within FastAPI, you would declare the request model as a dependency on the controller itself. However, I assume stac-fastapi wants to do this outside of the controller as we want to limit controller's abilities to dictate the request bodies as those come from the STAC Spec (ie we are enforcing that extensions comply with the STAC Spec). It seems like this is the hard part about supporting DI within stac-fastapi.
Ideas for enforcing the expected request model while supporting Dependency Injection
I haven't thought too deeply about this (and this is what I'd like to sort out in this discussion), but it feels like there is likely a path forward to get both top-down enforcement of the shape of the request and response objects for each controller within our extensions (ie enforcing the STAC Spec) while also granting controllers the inversion of control offered by DI.
One quick idea would be to make use
route_class_override
parameter ofrouter.add_api_route(...)
. We could make our ourAPIRoute
class that descends from FastAPI'sAPIRoute
but also uses theinspect
module to review the controller and ensure that it expects our specified request model and returns our expected response model, throwing an error otherwise.I'd love to hear others' thoughts. Am I missing something regarding the purpose of
create_async_endpoint(...)
? Any obvious paths forward to supporting DI within the project?Beta Was this translation helpful? Give feedback.
All reactions