Application levels¶
An Esmerald application is composed by levels and those levels can be Gateway, WebSocketGateway, Include, handlers or even another Esmerald or ChildEsmerald.
There are some levels in the application, let's use an example.
from esmerald import Esmerald, Gateway, Include, Request, get
@get("/me")
async def me(request: Request) -> str:
return "Hello, world!"
app = Esmerald(routes=[Include("/", routes=[Gateway(handler=me)])])
Levels:
- Esmerald - The application instance.
- Include - The second level.
- Gateway - The third level, inside an include.
- Handler - The forth level, the Gateway handler.
You can create as many levels as you desire. From nested includes to ChildEsmerald and create your own design.
With a ChildEsmerald¶
from pydantic import BaseModel, EmailStr
from esmerald import (
APIView,
ChildEsmerald,
Esmerald,
Gateway,
Include,
Request,
WebSocket,
WebSocketGateway,
get,
post,
websocket,
)
@get("/me")
async def me(request: Request) -> str:
return "Hello, world!"
@websocket(path="/ws")
async def websocket_endpoint_include(socket: WebSocket) -> None:
await socket.accept()
await socket.send_text("Hello, new world!")
await socket.close()
class User(BaseModel):
name: str
email: EmailStr
password: str
class UserApiView(APIView):
path = "/users"
@post("/create")
async def create_user(self, data: User, request: Request) -> None: ...
@websocket(path="/ws")
async def websocket_endpoint(self, socket: WebSocket) -> None:
await socket.accept()
await socket.send_text("Hello, world!")
await socket.close()
child_esmerald = ChildEsmerald(routes=[Gateway(handler=UserApiView)])
app = Esmerald(
routes=[
Include(
"/",
routes=[
Gateway(handler=me),
WebSocketGateway(handler=websocket_endpoint_include),
Include("/child", child_esmerald),
],
)
]
)
Levels:
- Esmerald - The application instance. First Level.
- Gateway - The second level, inside the app routes.
- Handler - The third level, inside the Gateway.
- WebSocketGateway - The second level, inside the app routes.
- Handler - The third level, inside the WebSocketGateway.
- Include - The second level. Inside the app routes.
- ChildEsmerald - The third level inside the include and also first level as independent instance.
Warning
A ChildEsmerald
is an independent instance that is plugged into a main Esmerald
application, but since
it is like another Esmerald
object, that also means the ChildEsmerald
does not take precedence over the top-level
application, instead, treats its own Gateway,
WebSocketGateway, Include,
handlers or even another Esmerald
or
ChildEsmerald and parameters in isolation.
Exceptions¶
ChildEsmerald
, as per warning above, has its own rules, but there are always exceptions to any almost every rule.
Although it is an independent instance with its own rules, this is not applied to every parameter.
Middlewares and Permissions are actually global and the rules of
precedence can be applied between an Esmerald
instance and the corresponding ChildEsmerald
apps.
In other words, you don't need to create/repeat the same permissions and middlewares (common to both) across
every instance. They can be applied globally from the top main Esmerald
object.
from pydantic import BaseModel, EmailStr
from esmerald import (
APIView,
ChildEsmerald,
Esmerald,
Gateway,
Include,
Request,
WebSocket,
WebSocketGateway,
get,
post,
settings,
websocket,
)
from esmerald.config.jwt import JWTConfig
from esmerald.contrib.auth.saffier.base_user import User
from esmerald.exceptions import NotAuthorized
from esmerald.middleware.authentication import AuthResult, BaseAuthMiddleware
from esmerald.permissions import IsAdminUser
from esmerald.security.jwt.token import Token
from lilya._internal._connection import Connection
from lilya.middleware import DefineMiddleware as LilyaMiddleware
from lilya.types import ASGIApp
from saffier.exceptions import ObjectNotFound
class JWTAuthMiddleware(BaseAuthMiddleware):
def __init__(self, app: "ASGIApp", config: "JWTConfig"):
super().__init__(app)
self.app = app
self.config = config
async def retrieve_user(self, user_id) -> User:
try:
return await User.get(pk=user_id)
except ObjectNotFound:
raise NotAuthorized()
async def authenticate(self, request: Connection) -> AuthResult:
token = request.headers.get(self.config.api_key_header)
if not token:
raise NotAuthorized("JWT token not found.")
token = Token.decode(
token=token, key=self.config.signing_key, algorithm=self.config.algorithm
)
user = await self.retrieve_user(token.sub)
return AuthResult(user=user)
class IsAdmin(IsAdminUser):
def is_user_staff(self, request: "Request") -> bool:
"""
Add logic to verify if a user is staff
"""
@get()
async def home() -> None: ...
@get("/me")
async def me(request: Request) -> str:
return "Hello, world!"
@websocket(path="/ws")
async def websocket_endpoint_include(socket: WebSocket) -> None:
await socket.accept()
await socket.send_text("Hello, new world!")
await socket.close()
class User(BaseModel):
name: str
email: EmailStr
password: str
class UserApiView(APIView):
path = "/users"
@post("/create")
async def create_user(self, data: User, request: Request) -> None: ...
@websocket(path="/ws")
async def websocket_endpoint(self, socket: WebSocket) -> None:
await socket.accept()
await socket.send_text("Hello, world!")
await socket.close()
child_esmerald = ChildEsmerald(
routes=[Gateway("/home", handler=home), Gateway(handler=UserApiView)]
)
jwt_config = JWTConfig(
signing_key=settings.secret_key,
)
app = Esmerald(
routes=[
Include(
"/",
routes=[
Gateway(handler=me),
WebSocketGateway(handler=websocket_endpoint_include),
Include("/admin", child_esmerald),
],
)
],
permissions=[IsAdmin],
middleware=[LilyaMiddleware(JWTAuthMiddleware, config=jwt_config)],
)
Notes¶
The example given is intentionally big and "complex" simply to show that even with that complexity in place,
the middleware
and permissions
remained global to the whole application without the need to implement
on both Esmerald
and ChildEsmerald
.