Key files
Table of contents
Introduction
Every feature follows the same layout, so once you know what each file does you can find your way around any feature. This page walks through the files of notes, a typical full feature.
__init__.py
The entry point. It defines the blueprint and registers services. The framework calls init_feature when the feature loads.
# splent_feature_notes/__init__.py
from splent_framework.blueprints.base_blueprint import create_blueprint
from splent_framework.services.service_locator import register_service
from splent_io.splent_feature_notes.services import NotesService
notes_bp = create_blueprint(__name__)
def init_feature(app):
register_service(app, "NotesService", NotesService)
def inject_context_vars(app):
return {}
config.py
Puts environment variables into app.config, so a product changes what the feature reads by setting a variable.
import os
def inject_config(app):
app.config.setdefault("NOTES_PATH", os.getenv("NOTES_PATH", "notes"))
app.config.setdefault("NOTES_PAGE_SIZE", int(os.getenv("NOTES_PAGE_SIZE", "20")))
app.config.setdefault(
"NOTES_NAV",
os.getenv("NOTES_NAV", "1").strip().lower() in ("1", "true", "yes", "on"),
)
setdefault keeps whatever your product already set. Declare the same keys and defaults in the feature’s pyproject.toml, as Feature settings shows.
Without
config.py, variables read withos.getenv()are invisible to the framework.feature:inject-configgenerates it.
models.py
SQLAlchemy models. A model imports models only from features this one depends on.
from splent_framework.db import db
class Notes(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(256), nullable=False)
content = db.Column(db.Text, nullable=False)
published = db.Column(db.Boolean, default=False)
With
reports => notesin the UVL, reports may import from notes, and notes never imports from reports. See Dependency rules.
services.py
Business logic, on BaseService. It may emit signals for other features.
from splent_framework.services.BaseService import BaseService
class NotesService(BaseService):
def __init__(self, notes_repository=None):
super().__init__(notes_repository or NotesRepository())
def published(self):
return self.repository.get_published()
def publish(self, note_id):
return self.update(note_id, published=True)
See Services for the full pattern.
repositories.py
Data access, one repository per model.
from splent_framework.repositories.BaseRepository import BaseRepository
from splent_io.splent_feature_notes.models import Notes
class NotesRepository(BaseRepository):
def __init__(self):
super().__init__(Notes)
def get_published(self):
return self.model.query.filter_by(published=True).all()
See Repositories for the full pattern.
routes.py
Flask routes. They handle HTTP and delegate logic to services, resolved with service_proxy.
from splent_io.splent_feature_notes import notes_bp
from splent_framework.services.service_locator import service_proxy
notes_service = service_proxy("NotesService")
@notes_bp.route("/notes/", methods=["GET"])
def index():
...
hooks.py
Registers template hook callbacks, the HTML this feature puts into named slots.
from splent_framework.hooks.template_hooks import register_template_hook
from flask import render_template
def notes_sidebar_item():
return render_template("hooks/sidebar_notes.html")
register_template_hook("layout.sidebar.authenticated.items", notes_sidebar_item)
A callback under a name no template renders shows nowhere. See Template hooks.
commands.py
Commands run as splent feature:<name> <command>, here splent feature:notes export-notes.
import click
from splent_io.splent_feature_notes.models import Notes
@click.command("export-notes")
def export_notes():
"""Print every note of the active product as JSON lines."""
notes = Notes.query.order_by(Notes.id).all()
...
cli_commands = [export_notes, purge_drafts]
They run inside the selected product’s app, so they reach its database directly.
signals.py
Listens to signals from features this one depends on. A listener connects by the signal’s name and never imports the producer.
# splent_feature_reports/signals.py
from splent_framework.signals.signal_utils import connect_signal
@connect_signal("note-created", "splent_feature_reports")
def on_note_created(sender, note, **kwargs):
... # count the note in today's report
The framework imports signals.py when it loads the feature. If the producer is not installed, the handler never fires. See Signals.
decorators.py
Decorators that other features import.
from functools import wraps
from flask import abort
from splent_framework.params import param
def public_index_only(view):
"""Answer 404 when the product keeps its notes index private."""
@wraps(view)
def wrapper(*args, **kwargs):
if not param("notes", "NOTES_PUBLIC_INDEX"):
abort(404)
return view(*args, **kwargs)
return wrapper
# Other features: from splent_io.splent_feature_notes.decorators import public_index_only
The import makes the dependency visible to product:validate.
seeders.py
Seeds development and test data.
from splent_framework.seeders.BaseSeeder import BaseSeeder
from splent_io.splent_feature_notes.models import Notes
class NotesSeeder(BaseSeeder):
def run(self):
self.seed([Notes(title="Welcome", content="Your first note.", published=True)])
See Seeders.
migrations/
Alembic migrations of this feature only, tracked in its own alembic_<feature> version table. splent db:migrate generates them and splent db:upgrade applies them.
See also
- Directory structure, the full file tree
- Architecture layers, services, repositories and seeders
- Extensibility, hooks, refinement and the service locator