Skip to content

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:

  1. Esmerald - The application instance.
  2. Include - The second level.
  3. Gateway - The third level, inside an include.
  4. 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:

  1. Esmerald - The application instance. First Level.
  2. Gateway - The second level, inside the app routes.
    1. Handler - The third level, inside the Gateway.
  3. WebSocketGateway - The second level, inside the app routes.
    1. Handler - The third level, inside the WebSocketGateway.
  4. Include - The second level. Inside the app routes.
    1. ChildEsmerald - The third level inside the include and also first level as independent instance.
      1. Gateway - The second level, inside the ChildEsmerald.
        1. Handler - The second level, inside the Gateway.

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.