Перейти к содержанию

Extensions

Warning

The current page still doesn't have a translation for this language.

But you can help translating it: Contributing.

What are extensions in an Esmerald context? A separate and individual piece of software that can be hooked into any Esmerald application and perform specific actions individually without breaking the ecosystem.

A feature that got a lof of inspirations from the great frameworks out there but simplified for the Esmerald ecosystem.

Take Django as example. There are hundreds, if not thousands, of plugins for Django and usually, not always, the way of using them is by adding that same pluggin into the INSTALLED_APPS and go from there.

Flask on the other hand has a pattern of having those plugin objects with an init_app function.

Well, what if we could have the best of both? Esmerald as you are aware is extremely flexible and dynamic in design and therefore having an INSTALLED_APPS wouldn't make too much sense right?

Also, how could we create this pattern, like Flask, to have an init_app and allow the application to do the rest for you? Well, Esmerald now does that via its internal protocols and interfaces.

Note

Extensions only exist on an application level.

Pluggable

This object is one of a kind and does a lot of magic for you when creating an extension for your application or even for distribution.

A Pluggable is an object that receives an Extension class with parameters and hooks them into your Esmerald application and executes the extend method when starting the system.

from typing import Optional

from loguru import logger
from pydantic import BaseModel

from esmerald import Esmerald, Extension, Pluggable
from esmerald.types import DictAny


class PluggableConfig(BaseModel):
    name: str


class MyExtension(Extension):
    def __init__(
        self, app: Optional["Esmerald"] = None, config: PluggableConfig = None, **kwargs: "DictAny"
    ):
        super().__init__(app, **kwargs)
        self.app = app

    def extend(self, config: PluggableConfig) -> None:
        logger.success(f"Successfully passed a config {config.name}")


my_config = PluggableConfig(name="my extension")

pluggable = Pluggable(MyExtension, config=my_config)

# it is also possible to just pass strings instead of pluggables but this way you lose the ability to pass arguments
app = Esmerald(
    routes=[],
    extensions={"my-extension": pluggable, "my-other-extension": Pluggable("path.to.extension")},
)

It is this simple but is it the only way to add a pluggable into the system? Short answser is no.

More details about this in hooking a pluggable into the application.

Extension

This is the main class that should be extended when creating a pluggable for Esmerald.

This object internally uses the protocols to make sure you follow the patterns needed to hook a pluggable via pluggables parameter when instantiating an esmerald application.

When subclassing this object you must implement the extend function. This function is what Esmerald looks for when looking up for pluggables for your application and executes the logic.

Think of the extend as the init_app of Flask but enforced as a pattern for Esmerald.

from typing import Optional

from esmerald import Esmerald, Extension
from esmerald.types import DictAny


class MyExtension(Extension):
    def __init__(self, app: Optional["Esmerald"] = None, **kwargs: "DictAny"):
        super().__init__(app, **kwargs)
        self.app = app
        self.kwargs = kwargs

    def extend(self, **kwargs: "DictAny") -> None:
        """
        Function that should always be implemented when extending
        the Extension class or a `NotImplementedError` is raised.
        """
        # Do something here

extend()

The mandatory function that must be implemented when creating an extension to be plugged via Pluggable into Esmerald.

It is the entry-point for your extension.

The extend by default expects kwargs to be provided but you can pass your own default parameters as well as there are many ways of creating and [hooking a pluggable]

Hooking pluggables and extensions

As mentioned before, there are different ways of hooking a pluggable into your Esmerald application.

The automated and default way

When using the default and automated way, Esmerald expects the pluggable to be passed into a dict extensions upon instantiation of an Esmerald application with key-pair value entries and where the key is the name for your pluggable and the value is an instance Pluggable holding your Extension object.

When added in this way, Esmerald internally hooks your pluggable into the application and starts it by calling the extend with the provided parameters, automatically.

The app parameter is automatically injected by Esmerald and does not need to be passed as parameter if needed

from typing import Optional

from loguru import logger
from pydantic import BaseModel

from esmerald import Esmerald, Extension, Pluggable
from esmerald.types import DictAny


class PluggableConfig(BaseModel):
    name: str


class MyExtension(Extension):
    def __init__(
        self, app: Optional["Esmerald"] = None, config: PluggableConfig = None, **kwargs: "DictAny"
    ):
        super().__init__(app, **kwargs)
        self.app = app

    def extend(self, config: PluggableConfig) -> None:
        logger.success(f"Successfully passed a config {config.name}")


my_config = PluggableConfig(name="my extension")

pluggable = Pluggable(MyExtension, config=my_config)

# it is also possible to just pass strings instead of pluggables but this way you lose the ability to pass arguments
app = Esmerald(
    routes=[],
    extensions={"my-extension": pluggable, "my-other-extension": Pluggable("path.to.extension")},
)

You can access all the extensions of your application via app.extensions at any given time.

Reordering

Sometimes there are dependencies between extensions. One requires another. You can reorder the extending order by using the method ensure_extension(name) of app.extensions. It will fail if the extension doesn't exist, so only call it in extend.

from typing import Optional

from loguru import logger from pydantic import BaseModel

from esmerald import Esmerald, Extension from esmerald.types import DictAny

class MyExtension1(Extension): def extend(self) -> None: self.app.extensions.ensure_extension("extension2") logger.success(f"Extension 1")

class MyExtension2(Extension): def extend(self) -> None: logger.success(f"Extension 2")

app = Esmerald(routes=[], extensions={"extension1": MyExtension1, "extension2": MyExtension2})

The manual and independent way

Sometimes you simply don't want to start the pluggable inside an Esmerald instance automatically and you simply want to start by yourself and on your own, very much in the way Flask does with the init_app.

This way you don't need to use the Pluggable object in any way and instead you can simply just use the Extension class or even your own since you are in control of the extension.

There are two variants how to do it:

With extension class or Pluggable
from typing import Optional

from loguru import logger

from esmerald import Esmerald, Extension, Gateway, JSONResponse, Request, get
from esmerald.types import DictAny


class MyExtension(Extension):
    def __init__(self, app: Optional["Esmerald"] = None, **kwargs: "DictAny"):
        super().__init__(app, **kwargs)
        self.app = app
        self.kwargs = kwargs

    def extend(self, **kwargs: "DictAny") -> None:
        """
        Function that should always be implemented when extending
        the Extension class or a `NotImplementedError` is raised.
        """
        # Do something here like print a log or whatever you need
        logger.success("Started the extension manually")


@get("/home")
async def home(request: Request) -> JSONResponse:
    """
    Returns a list of extensions of the system.

    "extensions": ["my-extension"]
    """
    extensions = list(request.app.extensions)

    return JSONResponse({"extensions": extensions})


app = Esmerald(routes=[Gateway(handler=home)])
app.add_extension("my-extension", MyExtension)
Self registering
from typing import Optional

from loguru import logger

from esmerald import Esmerald, Extension, Gateway, JSONResponse, Request, get
from esmerald.types import DictAny


class MyExtension(Extension):
    def __init__(self, app: Optional["Esmerald"] = None, **kwargs: "DictAny"):
        super().__init__(app, **kwargs)
        self.app = app
        self.kwargs = kwargs

    def extend(self, **kwargs: "DictAny") -> None:
        """
        Function that should always be implemented when extending
        the Extension class or a `NotImplementedError` is raised.
        """
        # Do something here like print a log or whatever you need
        logger.success("Started the extension manually")

        # Add the extension to the extensions of Esmerald
        # And make it accessible
        self.app.add_extension("my-extension", self)


@get("/home")
async def home(request: Request) -> JSONResponse:
    """
    Returns a list of extensions of the system.

    "extensions": ["my-extension"]
    """
    extensions = list(request.app.extensions)

    return JSONResponse({"extensions": extensions})


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

extension = MyExtension(app=app)
extension.extend()

You can use for the late registration the methods add_extension. It will automatically initialize and call extend for you when passing a class or Pluggable, but not when passing an instance.

Standalone object

But, what if I don't want to use the Extension object for my pluggable? Is this possible? ´ Yes, it must only implement the ExtensionProtocol.

from typing import Optional

from loguru import logger

from esmerald import Esmerald, Gateway, JSONResponse, Request, get
from esmerald.types import DictAny


class Standalone:
    def __init__(self, app: Optional["Esmerald"] = None, **kwargs: "DictAny"):
        self.app = app
        self.kwargs = kwargs

    def extend(self, **kwargs: "DictAny") -> None:
        """
        Function that should always be implemented when extending
        the Extension class or a `NotImplementedError` is raised.
        """
        # Do something here like print a log or whatever you need
        logger.success("Started the extension manually")

        # Add the extension to the extensions of Esmerald
        # And make it accessible
        self.app.add_extension("standalone", self)


@get("/home")
async def home(request: Request) -> JSONResponse:
    """
    Returns a list of extensions of the system.

    "extensions": ["standalone"]
    """
    extensions = list(request.app.extensions)

    return JSONResponse({"extensions": extensions})


app = Esmerald(routes=[Gateway(handler=home)], extensions=[Standalone])

Important notes

As you can see, extensions in Esmerald can be a powerful tool that isolates common functionality from the main Esmerald application and can be used to leverage the creation of plugins to be used across your applications and/or to create opensource packages for any need.

ChildEsmerald and pluggables

An Extension is not the same as a ChildEsmerald.

These are two completely independent pieces of functionality with completely different purposes, be careful when considering one and the other.

Can a ChildEsmerald be added as a pluggable? Of course.

You can do whatever you want with a pluggable, that is the beauty of this system.

Let us see how it would look like if you had a pluggable where the goal was to add a ChildEsmerald into the current applications being plugged.

from typing import Optional

from loguru import logger

from esmerald import ChildEsmerald, Esmerald, Extension, Gateway, JSONResponse, Pluggable, get
from esmerald.types import DictAny


@get("/home")
async def home() -> JSONResponse:
    return JSONResponse({"detail": "Welcome"})


class ChildEsmeraldPluggable(Extension):
    def __init__(self, app: Optional["Esmerald"] = None, **kwargs: "DictAny"):
        super().__init__(app, **kwargs)
        self.app = app
        self.kwargs = kwargs

    def extend(self, **kwargs: "DictAny") -> None:
        """
        Add a child Esmerald into the main application.
        """
        # Do something here like print a log or whatever you need
        logger.info("Adding the ChildEsmerald via pluggable...")

        child = ChildEsmerald(routes=[Gateway(handler=home, name="child-esmerald-home")])
        self.app.add_child_esmerald(path="/pluggable", child=child)

        logger.success("Added the ChildEsmerald via pluggable.")


app = Esmerald(routes=[], extensions={"child-esmerald": Pluggable(ChildEsmeraldPluggable)})

Crazy dynamic, isn't it? So clean and so simple that you can do whatever you desire with Esmerald.

Pluggables and the application settings

Like almost everything in Esmerald, you can also add the Pluggables via settings instead of adding when you instantiate the application.

from typing import Dict, Optional

from loguru import logger
from pydantic import BaseModel

from esmerald import Esmerald, EsmeraldAPISettings, Extension, Pluggable
from esmerald.types import DictAny


class PluggableConfig(BaseModel):
    name: str


my_config = PluggableConfig(name="my extension")


class MyExtension(Extension):
    def extend(self, config: PluggableConfig) -> None:
        logger.success(f"Successfully passed a config {config.name}")


class AppSettings(EsmeraldAPISettings):
    @property
    def extensions(self) -> Dict[str, Union["Extension", "Pluggable", type["Extension"]]]:
        return {"my-extension": Pluggable(MyExtension, config=my_config)}


app = Esmerald(routes=[])

And simply start the application.

ESMERALD_SETTINGS_MODULE=AppSettings uvicorn src:app --reload

INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [28720]
INFO:     Started server process [28722]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
$env:ESMERALD_SETTINGS_MODULE="AppSettings"; uvicorn src:app --reload

INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [28720]
INFO:     Started server process [28722]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

If you prefer, you can also use the settings_module.

from typing import Dict, Optional

from loguru import logger
from pydantic import BaseModel

from esmerald import Esmerald, EsmeraldAPISettings, Extension, Pluggable
from esmerald.types import DictAny


class PluggableConfig(BaseModel):
    name: str


my_config = PluggableConfig(name="my extension")


class MyExtension(Extension):
    def __init__(
        self, app: Optional["Esmerald"] = None, config: PluggableConfig = None, **kwargs: "DictAny"
    ):
        super().__init__(app, **kwargs)
        self.app = app

    def extend(self, config: PluggableConfig) -> None:
        logger.success(f"Successfully passed a config {config.name}")


class AppSettings(EsmeraldAPISettings):
    @property
    def extensions(self) -> Dict[str, Union["Extension", "Pluggable", type["Extension"]]]:
        return {"my-extension": Pluggable(MyExtension, config=my_config)}


app = Esmerald(routes=[], settings_module=AppSettings)