Feature settings

Table of contents

Introduction

A feature declares the settings it reads, with their defaults, in its own pyproject.toml. Your product writes only the ones it decides differently. Neither goes in a .env, so your env file stays about infrastructure.

# splent_feature_notes/pyproject.toml

[tool.splent.config]
# Notes listed on each page.
NOTES_PAGE_SIZE = 20
# my_product/pyproject.toml

[tool.splent.config]
NOTES_PAGE_SIZE = 50

Your product now lists fifty notes a page. Every other product of the line keeps twenty.


How a value reaches your app

Where the value of a feature setting comes from DECLARED MERGED READ, IF EDITABLE FEATURE [tool.splent.config] every setting and its default PRODUCT [tool.splent.config] only what it decides The environment app.config written by product:env and product:build 1 Stored in the panel typed after the app started 2 app.config set before the app started 3 Schema default nobody decided
The merge turns declarations into the environment. A setting the admin panel can edit is then read in the order on the right.

product:env --merge turns the declarations into your docker/.env, and product:build writes the same values into deploy/.env.example. Both follow the same rules, so a deployment never disagrees with development.


Declaring a setting in a feature

# splent_feature_notes/pyproject.toml

[tool.splent.config]
# The word the notes are served under.
NOTES_PATH = "notes"
# What the empty editor says. Empty means the feature's own default.
NOTES_PLACEHOLDER = ""

The value is the default, so the block also lists what the feature reads. Declare a key even when its default is empty, and keep the block in step with the feature’s config.py.

The comment right above a key is its help. marketplace:index copies it to the marketplace, so keep it touching the key. A value of "<SET>" means only the deployment can supply it.


Deciding a value in your product

# my_product/pyproject.toml

[tool.splent.config]
NOTES_PLACEHOLDER = "es:Escribe una nota|en:Write a note"
NOTES_PAGE_SIZE = 50

Write only what differs. A product that agrees with a default says nothing, so the table reads as a list of your decisions.


Which value wins

Weakest first.

Source Owned by
Feature docker/.env.example The feature, for settings next to its compose file
Feature [tool.splent.config] The feature
Product [tool.splent.config] The product
Product .env.<env>.example The deployment

When two features declare the same key, the later one wins within each layer. Your product’s env template has the last word, since product:env --merge rebuilds .env from it on every run. See Merge flow.


How values look in the environment

An env file has no types, so every value becomes a string.

TOML In the environment
20, true 20, true
["Draft", "Review"] Draft,Review

A table, or a list item with a comma, stops the merge, since no variable can carry it. A list or map the feature types in its schema travels as JSON instead.

A key ending in _HOST_PORT gets your product’s port offset.


Checking what your product decided

splent product:env --merge --dev
  declared 19 default(s) from feature pyprojects
  product  9 value(s) this product decides
  ports    adjusted 1 feature port(s) (+281)
  merged   4 feature .env file(s) into product .env

For the keys a feature types in its schema, product:params shows each value and the layer that decided it, without building the app.


Typing a setting

[tool.splent.config] says what a feature reads, not what a value may be. Add a sibling table, [tool.splent.config_schema], and a wrong value stops the app with a clear message instead of being ignored.

# splent_feature_notes/pyproject.toml

[tool.splent.config]
NOTES_PUBLIC_INDEX = true
NOTES_MAX_ATTACHMENT_SIZE = "100M"

[tool.splent.config_schema]
NOTES_PUBLIC_INDEX = { type = "bool", bind = "runtime", label = "Public index" }
NOTES_MAX_ATTACHMENT_SIZE = { type = "size", bind = "deploy" }

The values stay where they were. The framework checks them at boot, and product:validate, product:env, product:build and feature:release check them too. The reference lists every field and type.

What the app does at boot

Right after a feature’s inject_config, the framework writes each typed key to app.config. Weakest first, it takes the default, a value another feature set earlier, your product’s [tool.splent.config] (else its config.py), and the environment, unless that only repeats what product:env --merge copied. A value the type refuses stops the start.

splent_feature_notes cannot start with these values:
  - NOTES_MAX_ATTACHMENT_SIZE: the environment: expected a size (100M, 1G or a number of bytes), got '20Q'

A required key nobody sets stops it too. A runtime key only logs, since an administrator can still fix it in the panel. product:config marks drift when the environment changed a derivation key.

Reading a parameter

from splent_framework.params import param

if param("notes", "NOTES_PUBLIC_INDEX"):
    ...
{% if param('notes', 'NOTES_PUBLIC_INDEX') %}…{% endif %}

The first argument is the feature’s short name or its package name. You get the typed value, and for a runtime key whatever valid value an administrator stored. A key the feature does not declare raises LookupError, so a typo fails where you read it. app.config holds the same value, without the panel.

Keeping a secret

A key with type = "secret" is a credential, whatever its name.

Its default is empty, and it cannot be bound at runtime. A product that writes one in [tool.splent.config] fails product:validate and does not start, so set it in the deployment’s .env. product:config masks it.

Letting a value select features

A few values constrain the rest of the line. A shop that sells subscriptions needs billing, for example. Mark such a bool, enum or list with uvl = true, and give the SPL’s model nodes without a package that stand for its values.

shop {org 'splent-io', package 'splent_feature_shop'}
    mandatory
        shop_sells {param 'SHOP_SELLS'}
            alternative
                sells_items {value 'items'}
                sells_subscriptions {value 'subscriptions'}

constraints
    sells_subscriptions => billing

Nodes are matched by their param and value attributes, never by name. A node with param and no value holds the choices of an enum or a list. A bool’s node stands for true, and the node of a list without choices for a non-empty list. Write an enum as an alternative with a node per choice, a list as an or or optional group, and a bool as an optional child.

product:validate, product:derive and product:build select the nodes your values select before solving. product:missing, product:auto-require and product:configure tell you which value to set instead of offering such a node as a feature. Keep strings, numbers and sizes out of the model, since the solver drops arithmetic.

Changing a schema

A product that sets a parameter breaks when a release removes the key, changes its type or items, narrows its choices or range, adds or changes a pattern, or makes it required. feature:release refuses such a release unless it bumps the major version, or the minor below 1.0.0. Adding a key, widening a range and adding choices are safe.


Making a setting editable in the admin

Some settings are editorial, such as what the empty editor says. Whoever runs the site should change them without a developer. Register a schema in init_feature, and the feature that provides the settings panel renders a form for it.

from splent_framework.settings.settings_schema import register_settings

register_settings("notes", "Notes", [
    {"key": "placeholder", "env": "NOTES_PLACEHOLDER", "type": "text",
     "default": "", "label": "Placeholder", "help": "What the empty editor says."},
], icon="folder")

env names the variable the field shadows. Left out, it is <FEATURE>_<KEY> in capitals, so state it whenever the variable is named otherwise. If the feature already types its settings, leave the fields out. register_settings("notes", "Notes", icon="folder") builds one field per bind = "runtime" parameter.

Which value the panel shows

Source When it was decided
The stored setting Somebody typed it into the panel
app.config The product set it before the app started
The schema default Nobody decided

A field saved unchanged is stored empty, meaning “follow the environment”, so pressing Save never freezes today’s values.

Reading it back

from splent_framework.settings.settings_schema import setting_value, get_config

setting_value("notes", "placeholder")   # one field
get_config("notes")                     # the whole schema, typed
{{ feature_setting('notes', 'placeholder') }}

config.NOTES_PLACEHOLDER in a template reads the environment alone, so it misses what an administrator typed.

What not to make editable

A URL segment, a path, a port, or anything read while the app starts. Routes are built from NOTES_PATH when they register, so a new value would change nothing until a restart. Never put a credential in a form either.


What stays in the env file

Secrets, hostnames, ports, paths tied to a volume, and anything that differs between machines. pyproject.toml is committed, and docker/.env is not.

A path that is also the mount target of a production volume stays in .env.prod.example, next to the volume it must match.


Advanced

Adding a schema to a feature that already has settings

The boot overwrites app.config after inject_config, so work out derived values (such as MAX_CONTENT_LENGTH from NOTES_MAX_ATTACHMENT_SIZE) in init_feature. Every typed key is in app.config, so config.get("KEY", "both") gets the declared default, not "both". Values arrive as the product wrote them, so normalise them where you read them, or refuse them with a pattern.

A key whose unset value means something no default can say goes in the schema alone, and app.config holds None until someone sets it.

Values a feature’s own editor stores

param() also reads a runtime value a feature’s own editor stored through SettingsService under the key in small letters (SHOP_NAME as shop_name). The panel stores under <feature>_<field>.

In a built image, the schema comes from splent.contracts.json.


Reference

Fields

Field Means
type Required. One of the types
default Left out, the key’s value in [tool.splent.config]
choices The words an enum may be, or the items a list may hold
min, max The range of an int, float or size (max = "1G")
pattern A regular expression a string, or each string of a list, must match whole
required true when a value must be supplied. It has no default then
bind When the value is decided. See Bind
items A list’s items or a map’s values, str (default), int, float, bool or url
uvl true when the value selects features
label, help What the settings panel shows

An unknown field or type, choices on an int, or a default the type refuses stops the boot and fails product:validate, naming the key.

Types

Type A value
bool true, false. The environment also takes yes, on, 1 and their opposites
int, float A number
str Any string
text A string a reader sees, may be per language (es:Hola\|en:Hello)
enum One of choices
list A list. The environment writes it comma separated or as a JSON array
map A table. The environment writes it as a JSON object
url http or https with a host, or a path from /
template A path under templates/, without ... product:validate checks it exists
endpoint blueprint.view
size 100M, 1G, 524288. 0, none or unlimited mean no limit
secret A credential. See Keeping a secret

In the panel a bool is a switch, an int a number, an enum a select and the rest a line of text. Lists and maps get no field.

Bind

bind Decided by When the environment changes it
derivation (default) The product, in [tool.splent.config] Accepted, and marked as drift
deploy (default of a secret) The deployment, in its .env Normal
runtime An administrator, in the settings panel Normal, and the panel wins

Back to top

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