ets-labs / python-dependency-injector

Dependency injection framework for Python
https://python-dependency-injector.ets-labs.org/
BSD 3-Clause "New" or "Revised" License
3.73k stars 300 forks source link

how to inject FastAPI bearer instances into Depends #470

Open mxab opened 3 years ago

mxab commented 3 years ago

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

rmk135 commented 3 years ago

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

mxab commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

ok forgot the Depends


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

    return await di_bearer(request)
mxab commented 3 years ago

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

adriangb commented 2 years ago

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.

mxab commented 2 years ago

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

EdwardBlair commented 2 years ago

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

billcrook commented 2 years ago

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.

Digoya commented 2 years ago

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)
thatguysimon commented 1 year ago

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?