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

Package level decoupling with FastAPI

Open ilterisYucel opened this issue 1 year ago • 7 comments
trafficstars

version: 4.41.0 fastapi version: 0.110.2

In the project I developed with Fastapi, I want to use di as decoupled example at the package level. I have OwnerContainer in owner package and i have ApplicationContainer in top level main package.

class OwnerContainer(containers.DeclarativeContainer):

    db_session = providers.Dependency()
    
    owner_service = providers.Factory(
        OwnerService,
        db_session=db_session,
    )

class ApplicationContainer(containers.DeclarativeContainer):
    config = providers.Configuration()
    database_session = providers.Resource(
        get_db_session
    )

    owner_package = providers.Container(
        OwnerContainer,
        db_session=database_session
    )

I want to use OwnerService in my owner router

@owner_router.get("/", response_model=List[GetOwnerSchema])
@inject
async def get_owners(service: OwnerService = Depends(Provide[OwnerContainer.owner_service])):
    try:
        result: List[GetOwnerSchema] = await service.get_owners()
        return result
    except Exception as e:
        print(e)
        raise HTTPException(status_code=500)

I got error 'Provide' object has no attribute 'get_owners'

According to my research, this is related to the dependency injector not being able to wire. I can't figure out whether this is related to a bug or whether I made a mistake, but I think I used it in accordance with the examples. Has anyone encountered this type of problem? I need some help.

ilterisYucel avatar May 01 '24 20:05 ilterisYucel

Have a similar issue and can confirm,

I want to make a library with some REST API routers for FastAPI application and with some DI bindings pre-configured:

Here is the LoggignContainer where I have logging-related dependencies:

class LoggingContainer(containers.DeclarativeContainer):

    logger = providers.Singleton(MyLogger)

And here is a main BaseContainer where I aggregated all smaller containers from my library:

class BaseContainer(containers.DeclarativeContainer):

    app = providers.Container(AppContainer)

    logging = providers.Container(LoggingContainer)

    web_api = providers.Container(WebApiContainer, app=app, logging=logging)

Here is my library router, very simple one and which uses DI from my lib, specifically my logger:

router = APIRouter()

tag = "Ping"

@router.get("/ping", tags=[tag], status_code=status.HTTP_200_OK)

@inject

async def ping_async(logger: Logger = Depends(Provide[LoggingContainer.logger])):

    logger.trace("Ping")

    return

In a client code a client Container was defined, it has it's own application related dependencies + inherits from my BaseContainer (alternatively base container could be added via providers.Container(...) what is up to user):

class AppContainer(BaseContainer):

    hello_world_service = providers.Factory(HelloWorldService) <- some application related registration

Now client code need to register the library router (typical registration via FastAPI methods) + wire the router :

if __name__ == "__main__":

    container = Container()

    ... here happens FastAPI code and registration of router there ....

    container.wire(modules=[ping_router])

    container.wire(modules=[__name__])

    configure_services()

    run_app()

My expectations are Logger from my library will be injected in my router but the results is: "AttributeError: 'Provide' object has no attribute logger"

I tried many combinations and changed code many ways but unless I directly use AppContainer in my library code (what is impossible to expect beyond local examples and testing since I can not know what is App level container in advance) wiring doesn't work

I also tried to make router initialization based on the factory method, this way I provided instance of the AppContainer which was inherited from BaseContainer:

def createPingRouter(baseContainer: BaseContainer):
    router = APIRouter()
    tag = "Ping"

    @router.get("/ping", tags=[tag], status_code=status.HTTP_200_OK)
    @inject
    async def get_examples_async(logger: Logger = Depends(Provide[BaseContainer.logging.logger])):
        logger.trace("Ping")
        return

    return router

So client code could provide me actual container, and in debug mode I have seen that instance is right one and it has all providers inside. However results was the same:

"AttributeError: 'Provide' object has no attribute logger"

Is there any solution for this issue?

rustam-ashurov-mcx avatar Jul 10 '24 16:07 rustam-ashurov-mcx

Try setting WiringConfiguration on your Container.

class Container(containers.DeclarativeContainer):

    wiring_config = containers.WiringConfiguration(packages=["yourapp.routers"])

https://github.com/ets-labs/python-dependency-injector/blob/master/examples/miniapps/fastapi/giphynavigator/containers.py#L10

krzysztofkusmierczyk avatar Aug 15 '24 10:08 krzysztofkusmierczyk

Try setting WiringConfiguration on your Container.

class Container(containers.DeclarativeContainer):

    wiring_config = containers.WiringConfiguration(packages=["yourapp.routers"])

https://github.com/ets-labs/python-dependency-injector/blob/master/examples/miniapps/fastapi/giphynavigator/containers.py#L10

Thank you for your response. I upgrded my python version 3.12, when library is upgraded for python 3.12, i will try your solution.

ilterisYucel avatar Aug 16 '24 08:08 ilterisYucel

when library is upgraded for python 3.12

A release for python 3.12 is now available

Jahongir-Qurbonov avatar Aug 16 '24 09:08 Jahongir-Qurbonov

@ilterisYucel You have 2 container: ApplicationContainer, OwnerContainer.

You can not have 2 global container at same time. So you can call the OwnerContainer like below, ( wiring_config also needs on ApplicationContainer)

# not working
Depends(Provide[OwnerContainer.owner_service])

# working
Depends(Provide[owner_package.owner_service])
# or
Depends(Provide["owner_package.owner_service"])
# or
Depends(Provide[ApplicationContainer.owner_package.owner_service])

rumbarum avatar Sep 14 '24 13:09 rumbarum

@rustam-ashurov-mcx

You have 2 container: BaseContainer for global scope , LoggingContainer for local scope

You can call the LoggingContainer like below, ( wiring_config including router.py also needed on BaseContainer )

# not working, LoggingContainer is not Global container .
Depends(Provide[LoggingContainer. logger])

# working
Depends(Provide[logging.logger])
# or
Depends(Provide["logging.logger"])
# or
Depends(Provide[BaseContainer.logging.logger])

rumbarum avatar Sep 14 '24 13:09 rumbarum

@ilterisYucel You have 2 container: ApplicationContainer, OwnerContainer.

You can not have 2 global container at same time. So you can call the OwnerContainer like below, ( wiring_config also needs on ApplicationContainer)

# not working
Depends(Provide[OwnerContainer.owner_service])

# working
Depends(Provide[owner_package.owner_service])
# or
Depends(Provide["owner_package.owner_service"])
# or
Depends(Provide[ApplicationContainer.owner_package.owner_service])

Thank you for your suggestion. I will try as you suggested.

ilterisYucel avatar Sep 17 '24 08:09 ilterisYucel