Refinement

A refinement feature modifies another feature — overriding services, templates, hooks, extending models, or adding routes — without forking.

Table of contents

How it works

A refinement feature declares [tool.splent.refinement] in its pyproject.toml, specifying exactly what it overrides, extends, or adds. The framework applies these changes at startup in UVL order.


Real example: notes_tags refining notes

splent_feature_notes_tags adds a tags column to Notes and overrides NotesService:

# splent_feature_notes_tags/pyproject.toml
[tool.splent.refinement]
refines = "splent_feature_notes"

[tool.splent.refinement.extends]
models = [{ target = "Notes", mixin = "NotesTagsMixin" }]

[tool.splent.refinement.overrides]
services = [{ target = "NotesService", replacement = "NotesServiceWithTags" }]

The mixin

# splent_feature_notes_tags/models.py
from splent_framework.db import db

class NotesTagsMixin:
    tags = db.Column(db.String(500), nullable=True, default="")

    def get_tags_list(self):
        if not self.tags:
            return []
        return [t.strip() for t in self.tags.split(",") if t.strip()]

    def has_tag(self, tag):
        return tag.lower() in [t.lower() for t in self.get_tags_list()]

At startup, FeatureIntegrator._apply_model_extensions() injects tags, get_tags_list(), and has_tag() into the Notes model. No changes to splent_feature_notes needed.

The service override

# splent_feature_notes_tags/__init__.py
from splent_framework.refinement import refine_model, refine_service
from .models import NotesTagsMixin
from .services import NotesServiceWithTags

def init_feature(app):
    refine_model("Notes", NotesTagsMixin)
    refine_service(app, "NotesService", NotesServiceWithTags)

Routes using service_proxy("NotesService") automatically get the merged class.


Complete example: auth_2fa refining auth

# splent_feature_auth_2fa/pyproject.toml
[tool.splent.refinement]
refines = "splent_feature_auth"

[tool.splent.refinement.overrides]
services  = [{ target = "AuthenticationService", replacement = "AuthenticationService2FA" }]
templates = [{ target = "auth/login_form.html" }]
hooks     = [{ target = "layout.navbar.authenticated" }]

[tool.splent.refinement.extends]
models = [{ target = "User", mixin = "User2FAMixin" }]
routes = [{ blueprint = "auth", module = "routes_2fa" }]

Override service

def init_feature(app):
    refine_model("User", User2FAMixin)
    refine_service(app, "AuthenticationService", AuthenticationService2FA)

refine_service builds a merged class inheriting from both, so super() works.

Override template

Place the replacement at the same path:

splent_feature_auth_2fa/templates/auth/login_form.html

Flask searches blueprints in reverse registration order. The refiner registers after the base, so its template wins.

Extend model

# splent_feature_auth_2fa/models.py
class User2FAMixin:
    totp_secret = db.Column(db.String(32), nullable=True)
    totp_enabled = db.Column(db.Boolean, default=False)

    def verify_totp(self, token):
        import pyotp
        return pyotp.TOTP(self.totp_secret).verify(token)

Migrations for extended models

The refiner owns its own migrations with manual ALTER TABLE operations:

# migrations/versions/001_add_totp.py
def upgrade():
    op.add_column("user", sa.Column("totp_secret", sa.String(32), nullable=True))
    op.add_column("user", sa.Column("totp_enabled", sa.Boolean, default=False))

FEATURE_TABLES = set() in env.py — autogenerate ignores shared tables.

UVL constraint

splent_feature_auth_2fa => splent_feature_auth

The refiner always loads after the base.


See also


Back to top

splent. Distributed by an LGPL license v3. Contact us: drorganvidez@us.es