Skip to content

Pluggables

What are pluggables 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.

In Esmerald world, this is called pluggable.

Note

Pluggables only exist on an application level.

Pluggable

This object is one of a kind and does a lot of magic for you when creating a pluggble 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)

app = Esmerald(routes=[], pluggables={"my-extension": pluggable})

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.

Danger

If another object but the Extension is provided to the Pluggable, it will raise an ImproperlyConfigured. Pluggables are always expecting an Extension to be provided.

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

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 pluggables 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)

app = Esmerald(routes=[], pluggables={"my-extension": pluggable})

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

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.

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 pluggables of Esmerald
        # And make it accessible
        self.app.add_pluggable("my-extension", self)


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

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

    return JSONResponse({"pluggables": pluggables})


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

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

Standalone object

But, what if I don't want to use the Extension object for my pluggable? Is this possible?

Short answer, yes, but this comes with limitations:

  • You cannot hook the class within a Pluggable and use the automated way.
  • You will always need to start it manually.
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"):
        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 pluggables of Esmerald
        # And make it accessible
        self.app.add_pluggable("standalone", self)


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

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

    return JSONResponse({"pluggables": pluggables})


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

extension = Standalone(app=app)
extension.extend()

Important notes

As you can see, pluggables 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

A Pluggable 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=[], pluggables={"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 __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 pluggables(self) -> Dict[str, "Pluggable"]:
        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 pluggables(self) -> Dict[str, "Pluggable"]:
        return {"my-extension": Pluggable(MyExtension, config=my_config)}


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