pyproject.toml
Every moving part of SPLENT reads from pyproject.toml. It is the only place where you declare what something is, what version it is, and what it depends on.
Table of contents
Why it matters
SPLENT does not use separate config files for versioning, feature declarations, deployment metadata, UVL configuration, or feature contracts. Everything lives in pyproject.toml. This means the following.
- There is no version drift between what is declared and what is installed.
- The CLI, the framework, and the cache all read from the same file.
- A product’s entire composition (which features, at which version, for which environment) is readable in one place.
- A feature’s public interface (routes, blueprints, models, dependencies) is declared alongside its version.
In a product
A product’s pyproject.toml is the authoritative definition of what that product is and what it is made of.
[project]
name = "sample_splent_app"
version = "0.0.1"
requires-python = ">=3.13"
[project.optional-dependencies]
dev = ["watchdog"]
core = ["splent_framework"]
[tool.splent]
cli_version = "1.4.5"
spl = "sample_splent_spl"
# Features active in ALL environments
features = [
"splent-io/splent_feature_auth@v1.5.8",
"splent-io/splent_feature_public@v1.6.0",
"splent-io/splent_feature_redis@v1.5.6",
"splent-io/splent_feature_mail@v1.3.6",
"splent-io/splent_feature_confirmemail@v1.2.15",
"splent-io/splent_feature_profile@v1.5.7",
"splent-io/splent_feature_reset@v1.3.3",
"splent-io/splent_feature_session_redis@v1.0.7",
"splent-io/splent_feature_nginx@v1.0.0",
]
# Features active only in dev
features_dev = [
"splent-io/splent_feature_admin@v1.0.0",
"splent-io/splent_feature_phpmyadmin@v1.0.0",
"splent-io/splent_feature_mailhog@v1.0.7",
]
# Features active only in prod
features_prod = []
# Where the SPL model itself comes from. The name above is not enough to
# download anything, so the DOI travels with the product.
[tool.splent.spl_model]
mirror = "uvlhub.io"
doi = "10.5281/zenodo.19219696"
concept_doi = "10.5281/zenodo.19219695"
version = "v2"
The SPL model pin
spl names the product line. [tool.splent.spl_model] is what makes that
name resolvable, since a name on its own cannot be turned into a download.
With the DOI recorded here, any clone of this repository fetches the model
from UVLHub with nothing else in the workspace, no catalog repository and
no marketplace.
| Key | Meaning |
|---|---|
mirror |
Where the model is published. Only uvlhub.io is implemented |
doi |
The version DOI. This is what gets downloaded, and it pins the exact bytes |
concept_doi |
The DOI of the line, stable across versions. Used by spl:outdated to report that a newer model exists |
version |
A label for humans reading the file, and what the cache directory is keyed on when it is known |
file |
The model’s filename on UVLHub, when it is not <name>.uvl |
The block is machine managed. Write it with spl:pin
rather than by hand, and it is replaced wholesale rather than merged.
Feature groups
Features are declared under [tool.splent], not in [project.optional-dependencies], because SPLENT feature entries use the org/name@version format which is not PEP 508 compliant.
| Key | When loaded | Example |
|---|---|---|
features |
Always (all environments) | Core features needed everywhere |
features_dev |
Only when SPLENT_ENV=dev or --dev |
Debug panels, fake data generators, test seeders |
features_prod |
Only when SPLENT_ENV=prod or --prod |
Monitoring, caching, analytics |
When running with --dev, the effective feature list is features + features_dev.
When running with --prod, the effective feature list is features + features_prod.
Features present in both base and env-specific lists are deduplicated automatically.
Who reads this
| Consumer | What it reads | Why |
|---|---|---|
splent_framework |
[tool.splent].features + env-specific |
Discovers and loads features at runtime (in topological order) |
product:resolve |
[tool.splent].features + env-specific |
Clones and installs all declared features |
product:up / product:down |
[tool.splent].features + env-specific |
Starts/stops Docker containers for features |
feature:compile |
[tool.splent].features + env-specific |
Compiles webpack assets for active features |
feature:order |
[tool.splent].features + UVL |
Resolves and displays the topological load order |
db:upgrade / db:migrate |
[tool.splent].features + env-specific |
Applies migrations in dependency order |
db:seed |
[tool.splent].features + UVL |
Runs seeders in topological order |
feature:status |
[tool.splent].features + env-specific |
Shows which features from pyproject are tracked in manifest |
spl:* / product:validate / product:missing / product:auto-require |
[tool.splent].spl plus [tool.splent.spl_model], or [tool.splent.uvl] |
Resolves the UVL model from the working copy, the cache, then UVLHub by DOI (preferred), or from the legacy per-product location |
doctor |
Multiple sections | Validates the full product configuration |
Legacy support. Products that still declare features under
[project.optional-dependencies].featurescontinue to work. The framework and CLI read from[tool.splent.features]first, then fall back to the legacy location.
In a feature
Each feature is its own Python package with its own pyproject.toml. It contains the package metadata and the feature contract, a machine-readable declaration of what the feature provides and requires.
[project]
name = "splent_feature_auth"
version = "1.2.2"
requires-python = ">=3.13"
[tool.setuptools]
package-dir = { "" = "src" }
[tool.setuptools.packages.find]
where = ["src"]
# ── Feature Contract (auto-generated) ─────────────────────────────────────────
[tool.splent.contract]
description = "Authentication feature: registration, login, logout, and session management"
archetype = "full"
category = "infrastructure"
tags = ["auth", "session"]
[tool.splent.contract.provides]
routes = ["/login", "/logout", "/signup/"]
blueprints = ["auth_bp"]
models = ["User"]
commands = []
hooks = ["layout.anonymous_sidebar", "layout.authenticated_sidebar"]
services = ["AuthenticationService"]
signals = ["user-registered"]
translations = ["es"]
docker = []
[tool.splent.contract.requires]
features = []
env_vars = ["SECRET_KEY"]
signals = []
[tool.splent.contract.extensible]
services = ["AuthenticationService"]
models = ["User"]
templates = ["auth/login_form.html", "auth/signup_form.html"]
hooks = ["layout.anonymous_sidebar", "layout.authenticated_sidebar"]
routes = true
The feature contract
The [tool.splent.contract] section is auto-generated by splent feature:contract --write and kept up to date by splent feature:release. You do not need to maintain it manually.
| Field | Inferred from |
|---|---|
archetype |
Source-file analysis (models → full, routes only → light, implemented services → service, otherwise config) |
provides.routes |
@<bp>.route(...) decorators in routes.py |
provides.blueprints |
Blueprint variable names in __init__.py |
provides.models |
class <Name>(db.Model) in models.py |
provides.hooks |
register_template_hook("slot", func) calls in hooks.py |
provides.services |
class <Name>(BaseService) in services.py plus register_service(app, "Name", …) calls in __init__.py |
provides.signals |
signal("name") definitions in services or signals.py |
provides.translations |
Language directories under translations/ |
provides.commands |
@click.command() definitions in commands.py |
provides.docker |
docker-compose*.yml / docker-compose*.yaml files at feature root |
requires.features |
Imports of other splent_feature_* packages plus bare service_proxy("XService") usages resolved against the other features’ registered services |
requires.features_optional |
service_proxy(...) usages wrapped in try:, the codebase idiom for graceful degradation (e.g. public showing events only when installed). Soft, shown by the marketplace but never enforced by feature:install |
requires.env_vars |
os.getenv(...) and os.environ[...] calls |
requires.signals |
@<signal>.connect subscriptions in signals.py |
Preserved fields. description, category, tags, env and
requires.features_manual survive regeneration. Set them once and they
stay. features_manual declares real dependencies that static analysis
cannot see (e.g. team storing photos through the media admin UI); the
generator writes requires.features as the union of what it infers plus
features_manual, so auto-inferred entries expire when the code stops
using them while manual ones persist. check:contracts tells you when a
manual entry is missing. category and tags are presentation metadata
for the marketplace (feature:search --category content, the web catalog).
env field
Features that only belong in a specific environment declare env in their contract.
[tool.splent.contract]
description = "phpMyAdmin database management UI for development"
env = "dev"
When env = "dev" is set, the following happens.
feature:install,feature:add, andfeature:attachautomatically place the feature infeatures_dev(no need for--devflag)product:configureclassifies it correctlyproduct:buildexcludes it from production artifacts
Refinement section
Features that refine another feature declare it in [tool.splent.refinement].
[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" }]
See Refinement for the full explanation.
Docker contract
Features that ship Docker infrastructure declare it in [tool.splent.contract.docker]. This section is auto-inferred from the feature’s docker/docker-compose.yml by feature:contract --write and updated during feature:release.
[tool.splent.contract.docker]
services = ["splent_feature_redis"]
ports = ["${REDIS_HOST_PORT}:6379"]
volumes = []
networks = ["splent_network"]
build = false # true if the feature includes a Dockerfile
healthcheck = false # true if the service defines a health check
[tool.splent.contract.docker.depends_on]
services = [] # other feature Docker services this one requires
Port variables use the _HOST_PORT suffix so the port offset mechanism adjusts them automatically.
| Field | Description |
|---|---|
services |
Docker Compose service names defined by this feature |
ports |
Host-to-container port mappings ("host:container") |
volumes |
Named volumes declared by this feature |
networks |
Docker networks used or created |
build |
Whether the feature includes a custom Dockerfile |
healthcheck |
Whether the service defines a Docker health check |
depends_on.services |
Service names from other features that must be running first |
Here is an example for nginx (with custom Dockerfile for prod).
[tool.splent.contract.docker]
services = ["splent_feature_nginx"]
ports = ["${NGINX_HTTP_HOST_PORT}:80", "${NGINX_HTTPS_HOST_PORT}:443"]
volumes = ["letsencrypt", "certbot_webroot"]
networks = ["splent_network"]
build = true
healthcheck = false
[tool.splent.contract.docker.depends_on]
services = []
check:infra reads these declarations to detect port conflicts, volume collisions, and missing health checks across the entire product.
The following consumers read the contract.
| Consumer | What it reads | Why |
|---|---|---|
feature:install |
requires.features, env |
Checks dependencies before installing; auto-detects dev/prod scope |
feature:add / feature:attach |
env |
Auto-classifies into features_dev or features_prod |
feature:remove / feature:detach |
requires.features |
Blocks removal if another feature depends on this one |
product:validate |
All contract fields | Detects conflicts between features |
product:build |
docker, env |
Merges compose files, excludes dev-only features |
export:puml |
All contract fields | Generates PlantUML diagrams |
In splent_cli and splent_framework
The CLI and the framework are themselves Python packages with their own pyproject.toml files inside the workspace.
[project]
name = "splent_cli"
version = "1.7.0"
The following consumers read this.
| Consumer | What it reads | Why |
|---|---|---|
version |
[project].version |
Reads directly from source, never from stale installed metadata |
doctor |
[project].version |
Cross-checks CLI and framework major versions for compatibility |
release:cli, release:framework |
[project].version |
Bumps the version before tagging and publishing |
The version command reads CLI and framework versions directly from their pyproject.toml files in the workspace, not from importlib.metadata. This ensures the version shown is always current, even if the editable install has not been refreshed since the last version bump.
Key sections reference
| Section | Used by | Purpose |
|---|---|---|
[project].name |
pip, framework, CLI | Package identity |
[project].version |
version, doctor, release commands |
Current version |
[project].requires-python |
pip | Python version constraint |
[project].dependencies |
doctor |
Runtime dependency validation |
[project.optional-dependencies].dev |
pip | Development dependencies |
[project.optional-dependencies].core |
pip | Core framework dependency |
[tool.splent].features |
framework, CLI, startup scripts | Base feature composition (all envs) |
[tool.splent].features_dev |
framework, CLI, startup scripts | Dev-only features |
[tool.splent].features_prod |
framework, CLI, startup scripts | Prod-only features |
[tool.splent].spl |
product:validate, product:missing, product:auto-require, doctor, feature:order |
SPL name. Resolved to a model through the working copy, the cache, then UVLHub |
[tool.splent.spl_model] |
Everything that resolves the SPL above | The DOI pointer that makes the name resolvable. Written by spl:pin |
[tool.splent.uvl] |
product:validate, product:missing, product:auto-require, doctor, feature:order |
(Legacy) UVL model location and metadata; still supported, but spl is preferred |
[tool.splent.contract] |
feature:remove, product:validate, export:puml |
Feature public interface declaration |
[tool.setuptools] |
pip | Package discovery (src/ layout) |