app/features.py
Rama main · réplica de lectura del repositorio original.
"""Optional features: the ones an install can switch off.
A feature is one entry here — key, name, description, what it hides — and the
nav entry, route guard, wizard tickbox and settings row all read this list.
Four places that must agree on what a feature is called is how the fourth gets
missed.
Off means hidden, not deleted: `/schedule` says so rather than 404, and nothing
stored is touched, so switching it back on restores what was there
(decisions.md #87). Trades recorded through a feature are ordinary trades and
are never affected.
"""
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class Feature:
key: str # the settings field name under `features:`
name: str # what the wizard and the settings page call it
blurb: str # what it does, for somebody deciding
doc: str = "" # docs path for "More information", relative to docs_url
# Nav entries this feature owns. Hidden when it is off, so the bar never
# offers a way to a page that will only tell you it is unavailable.
nav_keys: tuple[str, ...] = ()
# Route prefixes it owns, for the guard. Checked with `startswith`, so
# "/schedule" covers "/schedule/complete" and "/schedule/skip" without listing them —
# a list that has to be kept in step with the routes would fall behind.
routes: tuple[str, ...] = ()
FEATURES: tuple[Feature, ...] = (
Feature(
key="dca_schedule",
name="DCA Schedule",
blurb="An optional DCA (Dollar Cost Averaging) scheduling tool.",
doc="guides/dca-schedule/",
nav_keys=("plan",),
routes=("/schedule",),
),
)
BY_KEY = {f.key: f for f in FEATURES}
def enabled(settings, key: str) -> bool:
"""Whether a feature is on. Unknown keys are on, because a feature nobody
has declared cannot have been switched off."""
return bool(getattr(settings.features, key, True))
def hidden_nav_keys(settings) -> set[str]:
"""Nav entries to leave out, because the feature that owns them is off."""
return {
nav_key
for feature in FEATURES
if not enabled(settings, feature.key)
for nav_key in feature.nav_keys
}
def owning(path: str) -> Feature | None:
"""The feature that owns a request path, if any.
Prefix matching with a boundary check: `/schedule` owns `/schedule/complete` but
must not own a future `/planning` — a bare `startswith` would swallow it
and the bug would show up as a page that is mysteriously switched off.
"""
for feature in FEATURES:
for route in feature.routes:
if path == route or path.startswith(route + "/"):
return feature
return None