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:
- 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.
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
andInjects()
respectively. - Using the
Factory()
within andInject()
andInjects()
. - Using
Requires()
within anInject()
andInjects()
. - Using
Security()
within anInject()
andInjects()
or within aRequires()
. - Using
Requires()
without using anInject()
andInjects()
limiting it to the handler and not application layer dependency. *