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 Saffier.
from myapp.accounts.models import User
from saffier.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:
- Uses
Inject
object. - 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:
- The
UserDAO
is located somewhere in the codebase likemyapp.accounts.daos
. - The
ArticleDAO
is located somewhere in the codebase likemyapp.articles.daos
. - The
PostDAO
is located somewhere in the codebase likemyapp.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.