Extensions¶
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:
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)
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)