python-dependency-injector icon indicating copy to clipboard operation
python-dependency-injector copied to clipboard

how to inject FastAPI bearer instances into Depends

Open mxab opened this issue 4 years ago • 14 comments
trafficstars

Hi, I'm currently struggeling on how to get this to work.

I have a third party "factory method" that provides me with a method I can use in FastAPI's Depends resolver

from third_party import auth_factory

@app.get("/foo")
def foo(user = Depends(auth_factory(scope="read", extractor = some_token_extractor ))):
    doSomething()

the oauth_factory produces as callable with fastapi know signature

def user_checker(oauth_header:HTTPAuthorizationCredentials = Security(bearer))
   ...
   return user

My problem now is when I want the some_token_extractor be provided by python-dependency-injector

something like this does not work:

user_detail_extractor = Provide[Container.user_detail_extractor]

@app.get("/with_di")
def with_di(user = Depends(auth_factory(scope="read", extractor = user_detail_extractor ))):

    return {
        "user": user["username"]
    }

class Container(DeclarativeContainer):

    user_detail_extractor = providers.Callable(some_di_extractor, some_config="bla")

I have a full demo setup here https://github.com/mxab/fastapi-di-depends-issue/tree/main/fastapi_di_depends_issue

but I cannot get this test work: https://github.com/mxab/fastapi-di-depends-issue/blob/main/tests/test_fastapi_di_depends_issue.py#L27

Does this in general not work or what is it I'm missing

Thanks very much in advance

mxab avatar Jul 12 '21 10:07 mxab

Hi @mxab , thanks for providing an example. I'll take a look.

Does this in general not work or what is it I'm missing

Dependency Injector can not introspect arguments of Depends(auth_factory(...)). It expects Depends to contain Provide marker. Something you could try is to make injection directly to the auth_factory here https://github.com/mxab/fastapi-di-depends-issue/blob/main/fastapi_di_depends_issue/third_party.py#L20

rmk135 avatar Jul 12 '21 11:07 rmk135

I think for now I was able to realise to do the DI part via a class that implments __call__ and using an instance of this in the Depends call.

Thanks!

mxab avatar Jul 12 '21 15:07 mxab

Hi @rmk135, I think I'm still facing the problem on how to marry this. I have a simpler show case now just with FastAPI's Bearers classes

Some container:

class Container(DeclarativeContainer):
    config = providers.Configuration()
    bearer  = providers.Singleton(HTTPBasic, auto_error=config.secured.as_(bool))

The application:

app = FastAPI()

#### working
fixed_bearer = HTTPBasic()
@app.get("/fixed")
def fixed(user: HTTPBasicCredentials = Depends(fixed_bearer)):

    return {"username" : user.username}

#### not working
di_bearer = Provide[Container.bearer]

@app.get("/with_di")
@inject
def with_di(user: HTTPBasicCredentials = Depends(di_bearer)):

    return {"username" : user.username if user else None}

the second handler does inject the bearer itself and not the the bearer's __call__ method provides

This makes partially sense for me, but I'm still wondering if there is a way to get this to work

This is the failing test https://github.com/mxab/fastapi-di-depends-issue/blob/main/tests/test_fastapi_di_depends_issue.py#L31

mxab avatar Jul 13 '21 10:07 mxab

As a workarround I can use it like this:



@inject
async def workarround(
    request: Request, di_bearer=Provide[Container.bearer]
) -> HTTPBasicCredentials:

    return await di_bearer(request)


@app.get("/with_di_workarround")
def with_di_workarround(user: HTTPBasicCredentials = Depends(workarround)):

    return {"username": user.username if user else None}

But it's not pretty :)

mxab avatar Jul 13 '21 11:07 mxab

As long as I don't type the di_bearer in the signature, otherwise FastAPI goes mad :)

async def workarround(
    request: Request, di_bearer:HttpBasic=Provide[Container.bearer]): ... # di_bearer:HttpBasic -> boom

mxab avatar Jul 14 '21 09:07 mxab

ok the problem with the workarround is also that it breaks the open api endpoin :/


def test_openapi(client: TestClient):

    resp = client.get("/openapi.json")
    resp.raise_for_status()
    assert resp.status_code == 200

results in error:

TypeError: Object of type 'Provide' is not JSON serializable

mxab avatar Jul 14 '21 11:07 mxab

ok forgot the Depends


@inject
async def workarround(
    request: Request, di_bearer: HTTPBasic = Depends(Provide[Container.bearer])
) -> HTTPBasicCredentials:

    return await di_bearer(request)

mxab avatar Jul 14 '21 12:07 mxab

So the only question left is if there is a nice way as the workarround function ?

mxab avatar Jul 14 '21 12:07 mxab

I think part of the trouble stems from the fact that Depends looks for an instance of Security at compile time, before anything is injected. I believe it would get an instance of whatever Provide[Container.bearer] returns, which is not going to be an instance of fastapi.security.base.SecurityBase.

adriangb avatar Aug 01 '21 18:08 adriangb

Yeah also assumed that this is kind of the case. Thank you for pointing out where this check is happening

mxab avatar Aug 08 '21 10:08 mxab

I have yet to work out how to inject a callable dependency

class MyDep:
    def __call__(self, request: Request) -> str:
        return "Hello"

@app.get()
@inject
def my_route(dep_val: str = Depends(Provide[container.my_dep])):
    return dep_val # should return "Hello"

I believe this is a similar issue. Using the workaround method will work, I'm sure but it is rather messy. FastAPI leaves much to be desired when you want something more than a very rudamentary CRUD app. Unfortunately I don't think this can be fixed without significant rework of FastAPI's "dependency" system. Maybe I'll just go back to .net 😝

Some ideas... If you override the signature of the method to be dep_val: str = Depends(resolved_instance) i.e. after dependency-injectors resolution but before FastAPI's dependency resolution it may work? Super hacky... And may have its own problems if e.g. you don't want the call method to be invoked automatically... If FastAPI had a dependency resolve hook that would be neat. Maybe it is possible, I'm still learning the internals of both libraries.

How does dependency_overrides_provider https://github.com/tiangolo/fastapi/blob/b8c4149e89d1c97d204ac3f965e6144e3dc126a9/fastapi/routing.py#L442 work? Seems undocumented

EdwardBlair avatar Aug 21 '21 12:08 EdwardBlair

FastAPI leaves much to be desired when you want something more than a very rudamentary CRUD app. Unfortunately I don't think this can be fixed without significant rework of FastAPI's "dependency" system.

I agree 100%. So much so that I created this discussion in FastAPI project. If there was a better story with starlette+pydantic I would probably just use that and try to figure out the apidocs. My ideal combo would be starlette+pydantic+python-dependency-injector+openapi docs.

Edit: Removed mention of my experiment which doesn't actually hook in any deeper than already outlined in the docs.

billcrook avatar Oct 08 '21 18:10 billcrook

I ended up with something like this:

class ApplicationContainer(containers.DeclarativeContainer):
    partial_service = providers.Factory(Service, kw="foo")

@inject
def service(fastapi_dep=Depends(FastAPI_dependency), service=Depends(Provider[ApplicationContainer.partial_service])):
    return service(fastapi_dep)

ApplicationContainer.service = service

Later can be used as:

@router.post("/route")
@inject
async def handler(service=Depends(ApplicationContainer.service)):
    print(service)

Digoya avatar Nov 01 '21 22:11 Digoya

I think part of the trouble stems from the fact that Depends looks for an instance of Security at compile time, before anything is injected. I believe it would get an instance of whatever Provide[Container.bearer] returns, which is not going to be an instance of fastapi.security.base.SecurityBase.

@adriangb You are absolutely right. Provide[Container.bearer] returns an instance of dependency_injector.wiring.Provide so FastAPI doesn't consider this dependency as a "Security" dependency and doesn't do its magic of creating the relevant OpenAPI definitions, etc.

@rmk135 It seems Dependency Injector is mostly geared towards injecting at runtime, while FastAPI requires security dependencies to be present at compile/startup time in order for it to work properly. Or is there a feature/workaround I'm missing?

thatguysimon avatar Aug 04 '22 21:08 thatguysimon