Extensions

Migrate

Alright, now that we are familiar with SQLAlchemy, the logical next step is keeping your database up to date. And that's the point where we should talk about migrating your database. Of course, we do already provide an extension for that as well.

Working with Migrate

Migrate is a CLI-only extension for WebFluid that provides single- and multi-db Alembic templates, enrolls the matching one based on your app configuration and manages your migrations for each of your projects' apps. In general, it is a wrapper for alembic commands with improved compatibility for WebFluid.

Anyway, since your main.py file is not executed when working with Migrate, it is time to outsource our config class into the apps' config.py file. Migrate uses the frameworks' magic we talked about in Config classes to parse the config and evaluate which template to use when it gets initialized:

fluid/config.py python
from webfluid.core.config import register_config
import os


@register_config(10)
class MyConfig:
    SESSION_COOKIE_SECURE = True

    MYEXT_BAR = "crazy"

    # To test both templates we will create a second app later on
    # and uncomment the SQLALCHEMY_BINDS only for the second one:
    # SQLALCHEMY_BINDS = {
    #     "test": os.getenv("TEST_DB_URI", "sqlite:///test.db")
    # }


# You do not need to import this file manually, the framework
# initializes it automatically if it exists.

Preparing for migration

Okay, let's update your models so we've got something to migrate:

fluid/models.py python
from webfluid.core.ext import db
from sqlalchemy import func
from sqlalchemy.orm import Mapped, mapped_column
from datetime import datetime


class MyModel(db.Model):
    id: Mapped[int] = mapped_column(primary_key=True)
    value: Mapped[str]
    created_at: Mapped[datetime] = mapped_column(
        server_default=func.now()
    )

    def __init__(self, value: str):
        self.value = value


class SecondModel(db.Model):
    __bind_key__ = "test"

    id: Mapped[int] = mapped_column(primary_key=True)
    value: Mapped[str]
    created_at: Mapped[datetime] = mapped_column(
        server_default=func.now()
    )

    def __init__(self, value: str):
        self.value = value

And integrate the update into your app. It is important to switch to the app factory setup, because Migrate assumes you are working with an app factory in your main.py file:

main.py python
from webfluid import Fluid
from webfluid.core.ext import scheduler, db
from fastapi import Request
from fastapi.responses import HTMLResponse
from fastapi.exceptions import HTTPException
from apscheduler.triggers.interval import IntervalTrigger
from sqlalchemy import select

from extension.main import MyExtension
from fluid.jobs import heart
from fluid.models import MyModel

my_ext = MyExtension()
scheduler.add_job(heart, IntervalTrigger(seconds=5))


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


async def health():
    return {"status": "ok"}


async def get_model(request: Request):
    model_id = request.query_params.get("id")
    if not model_id:
        raise HTTPException(status_code=400, detail="Bad Request")

    async with db.async_executor(model=MyModel) as e:
        results = await e.exec(
            select(MyModel).where(MyModel.id == model_id)
        )
        result = results.first()
        if not result:
            raise HTTPException(status_code=404, detail="Model not found")

        timestamp = result.created_at
        return {
            "model_id": model_id,
            "value": result.value,
            "created_at": timestamp.isoformat() if timestamp else None
        }


async def add_model(request: Request):
    data = await request.json()
    value = data.get("value")
    if not value:
        raise HTTPException(status_code=400, detail="Bad Request")

    async with db.async_executor(model=MyModel) as e:
        model = await e.insert(
            MyModel(value),
            flush=True
        )
        return {
            "model_id": model.id,
            "value": model.value,
            "created_at": model.created_at.isoformat()
        }


def create_app() -> Fluid:
    app = Fluid(__name__)

    app.get("/", response_class=HTMLResponse)(home)
    app.get("/health")(health)
    app.get("/my-model")(get_model)
    app.post("/my-model")(add_model)

    my_ext.expand_fluid(app)

    return app


if __name__ == "__main__":
    fluid = create_app()
    fluid.mix()

Single-DB

Now everything is set in place, and we can start migrating your databases using the Migrate CLI. This is done in three commands:

terminal bash
# Initialize Migrate to pick the correct alembic template
wf migrate init app

# The generated env.py scans the additives directory,
# so make sure it exists (wf create project makes it for you)
mkdir additives

# Perform revision with autogenerate
wf migrate revision app -a

# Upgrade your database
wf migrate upgrade app

# And test it (do not forget to set
# EXT_SQLALCHEMY = 1 in your app.ini before):
wf run app

Multi-DB

Alright, now let's test the same thing with multiple binds:

terminal bash
# Copy your app config:
cp app_configs/app.ini app_configs/app2.ini

# Now set your DATABASE_URI to sqlite:///app2.db
# and optionally add a TEST_DB_URI if you want.
# Then uncomment the SQLALCHEMY_BINDS in your config.

# The procedure is exactly the same:
wf migrate init app2
wf migrate revision app2 -a
wf migrate upgrade app2

wf run app2
 

wf migrate init looks at your config to decide which template to enroll: it uses the multi-db template as soon as your config defines SQLALCHEMY_BINDS, and the single-db template otherwise. Keep the following current limitations in mind while working with Migrate:

 

Autogenerated revisions collect the models of every Additive under your additives directory, without skipping the ones that are disabled for the app you are migrating. Review your revisions before applying them, so you do not migrate a foreign Additive's tables into your app.

 

The migration environment does not resolve *_FILE config values the way wf run does. If your secrets are file backed, point Migrate at a config that carries the real values, otherwise it may not connect with the correct credentials.

 

Migrate decides between the single- and multi-db template from the SQLALCHEMY_BINDS in your config. Binds you attach at runtime through Model.set_bind are not visible at init time, so add them to your config before running wf migrate init.

Continue reading

From here you can continue straight with Mail.