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
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_PLACEHOLDERin 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 |