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

  1. Why it matters
  2. In a product
    1. The SPL model pin
    2. Feature groups
    3. Who reads this
  3. In a feature
    1. The feature contract
    2. env field
    3. Refinement section
    4. Docker contract
  4. In splent_cli and splent_framework
  5. Key sections reference

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].features continue 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, and feature:attach automatically place the feature in features_dev (no need for --dev flag)
  • product:configure classifies it correctly
  • product:build excludes 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)

Back to top

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