Extensions

Introduction

Okay, let us introduce you to Fluid Extensions. They are inspired by Flask extensions and meant to extend the functionality of WebFluid. (Who would have guessed :O)

Extensions should not provide endpoints, serve files or frontend and other things like that. Instead, they can hook into the framework CLI or provide bundled functionality to improve the developers' experience.

The base extension

All extensions of WebFluid should be children of the frameworks FluidExtension. This is the base extension that provides the default structure of Fluid Extensions. So let's take a look at how to create your own extensions:

Creating your first extension

At first, we need to set up your extension as a PyProject. This is necessary because the wf CLI uses the entry-points API to load your extensions' command line interface. WebFluid uses Typer as its command line tooling. If you are not familiar with Typer, you can read more about it here. We won't go into detail about it.

Like we already said, we are building up on the current state of this documentation. So we are assuming you do already have your project directory in place and continue working from inside it.

Setting up your PyProject

terminal bash
mkdir -p myext/src/extension
cd myext
myext/pyproject.toml toml
[project]
name = "extension"
version = "1.0"

[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[tool.setuptools.packages.find]
where = ["src"]

[project.entry-points."webfluid.extensions"]
myext = "extension.main:MyExtension"

This is a minimal example of what your PyProject could look like. There are two things to take note of:

  • We are using the entrypoint "webfluid.extensions" to register your extension. The frameworks' CLI uses the name of your entrypoint to register your extensions' CLI as a sub-cli of itself.
  • If you want to offer your own CLI, your extension must be a child of the FluidExtension class.

A minimal extension

Okay, let's start building your first extension. At first, we will just create a quick hello world example for your CLI:

myext/src/extension/main.py python
from webfluid.extensions import FluidExtension
import typer


class MyExtension(FluidExtension):
    _cli = typer.Typer(help="My first 'Hello World!' example.")

    @staticmethod
    @_cli.command()
    def hello(name: str):
        typer.secho(
            f"Hello {name} :)",
            fg=typer.colors.GREEN, bold=True
        )

This is already it. Now you can install your extension as an editable package and test your first command. You can do this by running:

terminal bash
pip install -e .
wf myext hello "my friend"

Expanding your fluid

Now that you have created your first cli command, we can start expanding your fluid. Therefore, you can override the FluidExtension.expand_fluid method. There you get passed in the fluid instance, and since extensions are meant to be registered before fluid.mix(), you can do whatever you want with it. This includes registering lifecycle hooks, which we will talk about in the "Framework utility" chapter.

Anyway, more important is that you will have access to the fluids' config dictionary that is parsed when your app initializes (like we explained in the previous chapter). So now we can combine what we've learned so far and extend our current setup:

myext/src/extension/main.py python
from webfluid.extensions import FluidExtension
from typing import TYPE_CHECKING, Optional
import typer

if TYPE_CHECKING:
    from webfluid import Fluid


class MyExtension(FluidExtension):
    _cli = typer.Typer(help="My first 'Hello World!' example.")

    # Stick with the parents' signature so your IDE is happy
    def __init__(self, fluid: Optional["Fluid"] = None, *_, **__):
        self._foo = "default"
        self._bar = "default"

        super().__init__(fluid)

    def expand_fluid(self, fluid: "Fluid", *_, **__):
        self._foo = fluid.config.get("MYEXT_FOO", self._foo)
        self._bar = fluid.config.get("MYEXT_BAR", self._bar)

        fluid.jinja_env.globals["my_ext"] = self._my_util

    def _my_util(self) -> str:
        return f"Foo is {self._foo} but bar is {self._bar}!"

    @staticmethod
    @_cli.command()
    def hello(name: str):
        typer.secho(
            f"Hello {name} :)",
            fg=typer.colors.GREEN, bold=True
        )
main.py python
from webfluid import Fluid
from webfluid.core.config import register_config
from fastapi.responses import HTMLResponse

from extension.main import MyExtension


@register_config(10)
class MyConfig:
    SESSION_COOKIE_SECURE = True

    MYEXT_BAR = "crazy"

fluid = Fluid(__name__)

# FluidExtension.__init__(fluid) calls expand_fluid on itself
# automatically if fluid is not None.
my_ext = MyExtension(fluid)


@fluid.get("/", response_class=HTMLResponse)
async def home():
    return await fluid.render(
        "index.html",
        title="Hello World!",
        name="my friend"
    )


@fluid.get("/health")
async def health():
    return {"status": "ok"}


if __name__ == "__main__":
    fluid.mix()
fluid/templates/index.html html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{ title }}</title>

    <script>
        function healthCheck() {
            const result = document.getElementById('result')
            fetch('/health').then(res => {
                if (res.ok) result.style.color = 'green'
                else result.style.color = 'red'
                return res.json()
            }).then(data => {
                result.innerText = JSON.stringify(data)
                setTimeout(() => result.innerText = '', 2000)
            })
        }
    </script>
</head>
<body>
    <p>Hello <b>{{ name }}</b>!</p>

    <button onclick="healthCheck()">Health Check</button>
    <p id="result"></p>

    <p>{{ my_ext() }}</p>
</body>
</html>

That's it for now. This is all you need to know about shipping your own extensions.

Continue reading

From here you can continue straight with Scheduling.