Skip to content

Dependencies

Dependencies are a piece of great functionality now common in a lot of the frameworks out there and allows the concept of dependency injection to take place.

Esmerald uses the Inject object to manage those dependencies in every application level

Dependencies and the application levels

In every level the dependencies parameter (among others) are available to be used and handle specific dependencies raised on each level.

The dependencies are read from top-down in a python dictionary format, which means the last one takes the priority.

How to use

Assuming we have a User model using Edgy.

from myapp.accounts.models import User
from edgy.exceptions import ObjectNotFound

from esmerald import Esmerald, Gateway, Inject, Injects, get


async def get_user_model() -> User:
    try:
        return await User.get(pk=1)
    except ObjectNotFound:
        return None


@get("/me", dependencies={"user": Inject(get_user_model)})
async def me(user: User = Injects()) -> str:
    return user.email


app = Esmerald(routes=[Gateway(handler=me)])

The example above is very simple and of course a user can be obtained in a slighly and safer way but for it serves only for example purposes.

Using dependencies is quite simple, it needs:

  1. Uses Inject object.
  2. Uses the Injects object to, well, inject the dependency into the handler.

Some complexity

Dependencies can be injected in many levels as previously referred and that also means, you can implement the levels of complexity you desire.

from esmerald import Esmerald, Gateway, Include, Inject, get


def first_dependency() -> int:
    return 20


def second_dependency(number: int) -> bool:
    return number >= 5


@get("/validate")
async def me(is_valid: bool) -> bool:
    return is_valid


app = Esmerald(
    routes=[
        Include(
            routes=[Gateway(handler=me)],
            dependencies={
                "is_valid": Inject(second_dependency),
            },
        )
    ],
    dependencies={"number": Inject(first_dependency)},
)

What is happening

The number is obtained from the first_dependency and passed to the second_dependency as a result and validates and checks if the value is bigger or equal than 5 and that result is_valid is than passed to the main handler /validate returning a bool.

Exceptions

All the levels are managed in a simple top-down approach where one takes priority over another as previously mentioned but.

Pior to version 1.0.0, a ChildEsmerald was an independent instance that is plugged into a main Esmerald application but since it is like another Esmerald instance that also means the ChildEsmerald didn't take priority over the top-level application.

In other words, a ChildEsmerald did not take priority over the main instance but the rules of prioritization of the levels inside a ChildEsmerald prevailed the same as for a normal Esmerald instance.

Some exceptions are still applied. For example, for dependencies and exception handlers, the rule of isolation and priority is still applied.

The same is applied also to exception handlers.

More real world examples

Now let us imagine that we have a web application with one of the views. Something like this:

from typing import List

from esmerald import get
from esmerald.openapi.datastructures import OpenAPIResponse


@get(
    "/users",
    tags=["User"],
    description="List of all the users in the system",
    summary="Lists all users",
    responses={
        200: OpenAPIResponse(model=[UserOut]),
        400: OpenAPIResponse(model=Error, description="Bad response"),
    },
)
async def users(user_dao: UserDAO) -> List[UserOut]:
    """
    Lists all the users in the system.
    """
    return await user_dao.get_all()

As you can notice, the `user_dao`` is injected automatically using the appropriate level of dependency injection.

Let us see the urls.py and understand from where we got the user_dao:

from esmerald import Factory, Include, Inject

# Using lambdas
route_patterns = [
    Include(
        "/api/v1",
        routes=[
            Include("/accounts", namespace="accounts.v1.urls"),
            Include("/articles", namespace="articles.v1.urls"),
            Include("/posts", namespace="posts.v1.urls"),
        ],
        interceptors=[LoggingInterceptor],  # Custom interceptor
        dependencies={
            "user_dao": Inject(lambda: UserDAO()),
            "article_dao": Inject(lambda: ArticleDAO()),
            "post_dao": Inject(lambda: PostDAO()),
        },
    )
]

# Using the Factory
route_patterns = [
    Include(
        "/api/v1",
        routes=[
            Include("/accounts", namespace="accounts.v1.urls"),
            Include("/articles", namespace="articles.v1.urls"),
            Include("/posts", namespace="posts.v1.urls"),
        ],
        interceptors=[LoggingInterceptor],  # Custom interceptor
        dependencies={
            "user_dao": Inject(Factory(UserDAO)),
            "article_dao": Inject(Factory(ArticleDAO)),
            "post_dao": Inject(Factory(PostDAO)),
        },
    )
]

In the previous example we use lambdas to create a callable from DAO instances and we refactor it to use the Factory object instead. It is cleaner and more pleasant to work with.

The cleaner version of lambdas using Esmerald it is called Factory.

Note

You can see the Python lambdas as the equivalent of the anonymous functions in JavaScript. If you are still not sure, see more details about it.

Tip

Learn more about Esmerald DAOs and how to take advantage of those.

The Factory is a clean wrapper around any callable (classes usually are callables as well, even without instantiating the object itself).

Tip

No need to explicitly instantiate the class, just pass the class definition to the Factory and Esmerald takes care of the rest for you.

Importing using strings

Like everything is Esmerald, there are different ways of achieving the same results and the Factory is no exception.

In the previous examples we were passing the UserDAO, ArticleDAO and PostDAO classes directly into the Factory object and that also means that you will need to import the objects to then be passed.

What can happen with this process? Majority of the times nothing but you can also have the classic partially imported ... annoying error, right?

Well, the good news is that Esmerald got you covered, as usual.

The Factory also allows import via string without the need of importing directly the object to the place where it is needed.

Let us then see how it would look like and let us then assume:

  1. The UserDAO is located somewhere in the codebase like myapp.accounts.daos.
  2. The ArticleDAO is located somewhere in the codebase like myapp.articles.daos.
  3. The PostDAO is located somewhere in the codebase like myapp.posts.daos.

Ok, now that we know this, let us see how it would look like in the codebase importing it inside the Factory.

from esmerald import Factory, Include, Inject

route_patterns = [
    Include(
        "/api/v1",
        routes=[
            Include("/accounts", namespace="accounts.v1.urls"),
            Include("/articles", namespace="articles.v1.urls"),
            Include("/posts", namespace="posts.v1.urls"),
        ],
        interceptors=[LoggingInterceptor],  # Custom interceptor
        dependencies={
            "user_dao": Inject(Factory("myapp.accounts.daos.UserDAO")),
            "article_dao": Inject(Factory("myapp.articles.daos.ArticleDAO")),
            "post_dao": Inject(Factory("myapp.posts.daos.PostDAO")),
        },
    )
]

Now, this is a beauty is it not? This way, the codebase is cleaner and without all of those imported objects from the top.

Tip

Both cases work well within Esmerald, this is simply an alternative in case the complexity of the codebase increases and you would like to tidy it up a bit more.

In conclusion, if your views/routes expect dependencies, you can define them in the upper level as described and Esmerald will make sure that they will be automatically injected.

Requires and Security

From the version 3.6.3+, Esmerald allows also to use what we call a "simpler" dependency injection. This dependency injection system does not aim replace the current sytem but aims to provide another way of using some dependencies in a simpler fashion.

The Security object is used, as the name suggests, to implement the out of the box security provided by Esmerald and in that section, that is explained how to apply whereas te Requires implements a more high level dependency system.

You can import directly from esmerald:

Requires

from esmerald import Requires

Security

from esmerald import Requires

Warning

Neither Requires() or Security() are designed to work on an application level as is. For application layers and dependencies, you must still use the normal dependency injection system to make it work or use the Requires within the application layers.

Requires

This is what we describe a simple dependency.

An example how to use Requires would be something like this:

from typing import Any, Dict

from esmerald import Gateway, Requires, get, Esmerald


async def query_params(q: str | None = None, skip: int = 0, limit: int = 20):
    return {"q": q, "skip": skip, "limit": limit}


@get("/items")
async def get_params(params: Dict[str, Any] = Requires(query_params)) -> Any:
    return params


app = Esmerald(
    routes=[Gateway(handler=get_params)],
)

This example is very simple but you can extend to whatever you want and need. The Requires is not a Pydantic model but a pure Python class. You can apply to any other complex example and having a Requires inside more Requires.

from typing import Dict, Any
from esmerald import Gateway, Requires, get, Esmerald


async def query_params(q: str | None = None, skip: int = 0, limit: int = 20):
    return {"q": q, "skip": skip, "limit": limit}


async def get_user() -> Dict[str, Any]:
    return {"username": "admin"}


async def get_user(
    user: Dict[str, Any] = Requires(get_user), params: Dict[str, Any] = Requires(query_params)
):
    return {"user": user, "params": params}


@get("/info")
async def get_info(info: Dict[str, Any] = Requires(get_user)) -> Any:
    return info


app = Esmerald(
    routes=[Gateway(handler=get_info)],
)

Requires within the application layers

Now this is where things start to get interesting. Esmerald operates in layers and almost everything works like that.

What if you want to use the requires to operate on a layer level? Can you do it? Yes.

It works as we normally declare dependencies, for example, a Factory object.

from typing import Any

from esmerald import Gateway, Inject, Injects, JSONResponse, Requires, get, Esmerald


async def get_user():
    return {"id": 1, "name": "Alice"}


async def get_current_user(user: Any = Requires(get_user)):
    return user


@get(
    "/items",
    dependencies={"current_user": Inject(get_current_user)},
)
async def get_items(current_user: Any = Injects()) -> JSONResponse:
    return JSONResponse({"message": "Hello", "user": current_user})


app = Esmerald(
    routes=[
        Gateway(handler=get_items),
    ]
)

Security within the Requires

You can mix Security() and Requires() without any issues as both subclass the same base but there are nuances compared to the direct application of the Security without using the Requires object.

For more details how to directly use the Security without using the Requires, please check the security provided by Esmerald section where it goes in into detail how to use it.

from lilya.middleware.request_context import RequestContextMiddleware
from lilya.middleware import DefineMiddleware


app = Esmerald(
    routes=[...],
    middleware=[
        middleware=[DefineMiddleware(RequestContextMiddleware)],
    ]
)

Warning

You can mix both Requires() and Security() (Security inside Requires) but for this to work properly, you will need to add the RequestContextMiddleware from Lilya or an exception will be raised.

Now, how can we make this simple example work? Like this:

from typing import Any

from lilya.middleware import DefineMiddleware
from lilya.middleware.request_context import RequestContextMiddleware
from pydantic import BaseModel

from esmerald import Gateway, Requires, Security, get, Esmerald
from esmerald.security.api_key import APIKeyInCookie

api_key = APIKeyInCookie(name="key")


class User(BaseModel):
    username: str


def get_current_user(oauth_header: str = Security(api_key)):
    user = User(username=oauth_header)
    return user


@get("/users/me", security=[api_key])
def read_current_user(current_user: User = Requires(get_current_user)) -> Any:
    return current_user


app = Esmerald(
    routes=[Gateway(handler=read_current_user)],
    middleware=[DefineMiddleware(RequestContextMiddleware)],
)

This example is an short adaptation of security using jwt where we update the dependency to add a Requires that also depends on a Security.

The Security() object is used only when you want to apply the niceties of Esmerald security in your application.

It is also a wrapper that does some magic for you by adding some extras automatically. The Security object expects you to have an instance that implements an async __call__(self, connection: Request) -> Any: in order to operate.

Let us see a quick example:

from esmerald import Request, Security, HTTPException, get, Inject, Injects, Esmerald, Gateway
from lilya import status
from typing import cast, Any
from pydantic import BaseModel


class MyCustomSecurity:
    def __init__(self, name: str, **kwargs: Any) -> None:
        self.name = name
        self.__auto_error__ = kwargs.pop("auto_error", True)

    async def __call__(self, request: Request) -> dict[str, None]:
        api_key = request.query_params.get(self.name, None)
        if api_key:
            return cast(str, api_key)

        if self.__auto_error__:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Not authenticated",
            )
        return None


# Instantiate the custom security scheme
api_key = MyCustomSecurity(name="key")

# Use the custom security scheme
security = Security(api_key)


class User(BaseModel):
    username: str


def get_current_user(oauth_header: str = Security(api_key)):
    user = User(username=oauth_header)
    return user


@get(
    "/users/me",
    security=[api_key],
    dependencies={"current_user": Inject(get_current_user)},
)
def read_current_user(current_user: User = Injects()) -> Any:
    return current_user


# Start the application
app = Esmerald(
    routes=[Gateway(handler=read_current_user)],
)

Application layer

But what about you using the application layer architecture? Is it possible? Also yes. Let us update the previous example to make sure we reflect that.

from typing import Any

from lilya.middleware import DefineMiddleware
from lilya.middleware.request_context import RequestContextMiddleware
from pydantic import BaseModel

from esmerald import Gateway, Requires, Security, get, Esmerald, Inject, Injects
from esmerald.security.api_key import APIKeyInCookie

api_key = APIKeyInCookie(name="key")


class User(BaseModel):
    username: str


def get_current_user(oauth_header: str = Security(api_key)):
    user = User(username=oauth_header)
    return user


def get_user(user: User = Requires(get_current_user)) -> User:
    return user


@get(
    "/users/me",
    security=[api_key],
    dependencies={"current_user": Inject(get_user)},
)
def read_current_user(current_user: User = Injects()) -> Any:
    return current_user


app = Esmerald(
    routes=[Gateway(handler=read_current_user)],
    middleware=[DefineMiddleware(RequestContextMiddleware)],
)

Recap

There many ways of implementing the dependency injection in Esmerald:

  • Using the layers with Inject and Injects() respectively.
  • Using the Factory() within and Inject() and Injects().
  • Using Requires() within an Inject() and Injects().
  • Using Security() within an Inject() and Injects() or within a Requires().
  • Using Requires() without using an Inject() and Injects() limiting it to the handler and not application layer dependency. *