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
- Service locator — how services are registered and resolved
- Extension points — declaring what can be overridden
- Template hooks — the hook system