SPLENT Framework
Core library for building modular, feature-driven Flask applications using Software Product Line principles.
Quick start
from splent_framework import create_splent_app
app = create_splent_app(__name__, config_name="development")
Configuration order
A key can be set by four layers, and the stronger one wins whatever order
the code runs in: the framework's defaults, then a feature's
inject_config, then the keys the product's config.py sets itself, then
the environment (a variable named like the key, and the operator settings
below). Before, a feature's default replaced what the product's config.py
chose, because the features load after it. Every key is traced in
app.extensions["splent_config_trace"] with its layer, and a value that
held against a weaker one carries "action": "kept" and the value it
overruled. The environment winning over the product is accepted but marked
as drift: the entry carries "drift": true and, in overruled_source and
overruled_value, what the product had chosen. A key the framework's
config reads from a variable of the same name (TIMEZONE, the BABEL_
locales, the TASKS_ settings and the operator settings below) is traced
as the environment when that variable chose it, and otherwise keeps the
value the product's config.py gives it. product:config shows the
trace.
In development product:env --merge writes every feature's declared
default into the product's .env, so there the environment wins over
config.py for any key a feature declares. A product decides a feature's
key in its own [tool.splent.config], which that merge puts above the
feature's; config.py is for keys no feature reads from the environment.
Production defaults
A product built with create_splent_app gets these without any code. Each
one is a variable in the deployment's .env. The header and asset settings
can also be fixed in the product's config.py, which then wins over the
.env. For SPLENT_COOKIE_SECURE, the cookie variables and
MAX_CONTENT_LENGTH the .env wins over the product and over any
feature's inject_config, because only the deployment knows whether it
runs over HTTPS and how large its uploads are. The factory applies them
again after the features load.
| Variable | Default | What it does |
|---|---|---|
SPLENT_TRUSTED_PROXIES |
0 |
Number of reverse proxies in front of the app. Set it to 1 behind a TLS proxy (nginx, Caddy, Traefik) so the app sees the client address and the https the proxy was reached on, builds https:// links and sends HSTS. Leave it at 0 when the app port is reachable without the proxy. |
SPLENT_COOKIE_SECURE |
auto |
Session and remember-me cookies are always HttpOnly. Left unset, every cookie is Secure when the request arrived over HTTPS, directly or through a trusted proxy, so behind TLS set SPLENT_TRUSTED_PROXIES. true forces Secure, false never sets it. |
SESSION_COOKIE_SAMESITE |
Lax |
Lax, Strict or None, in any case. None is for a client on another site that signs in with the cookie, and it needs Secure: the app warns at start when the cookie may go out without it. None also lets any site post with the user's session, so every view exempt from CSRF that takes the session needs a defence of its own first; the app names each one in the log, at start and when a feature exempts it later. Anything else stops the app at start. REMEMBER_COOKIE_SAMESITE follows it unless set. |
SESSION_COOKIE_SECURE |
unset | true marks the session cookie Secure on every request, even under auto. REMEMBER_COOKIE_SECURE follows it unless set. SPLENT_COOKIE_SECURE=false still wins for the session cookie. |
MAX_CONTENT_LENGTH |
16M |
Largest request body. Bigger ones get 413, as JSON for a client that sent or asked for JSON. Accepts bytes or K, M, G, and 0 for no limit. |
SPLENT_SECURITY_HEADERS |
true |
nosniff, X-Frame-Options, Referrer-Policy, Permissions-Policy and Cache-Control: no-store for signed-in users. false leaves them all to the proxy. |
SPLENT_FRAME_OPTIONS |
SAMEORIGIN |
DENY, or off to send nothing. |
SPLENT_REFERRER_POLICY |
strict-origin-when-cross-origin |
Any Referrer-Policy value. |
SPLENT_PERMISSIONS_POLICY |
camera, microphone, geolocation, payment and USB off | The whole Permissions-Policy value. Embedded maps lose their location button under the default; allow geolocation here if a product needs it. |
SPLENT_NO_STORE_SIGNED_IN |
true |
Cache-Control: no-store on pages for a signed-in user. |
SPLENT_HSTS_MAX_AGE |
31536000 |
Sent only over HTTPS. 0 turns HSTS off. |
SPLENT_HSTS_INCLUDE_SUBDOMAINS, SPLENT_HSTS_PRELOAD |
false |
The optional HSTS directives. |
SPLENT_CONTENT_SECURITY_POLICY |
none | A Content-Security-Policy to send. Opt in, since existing templates use inline scripts. |
SPLENT_CSP_REPORT_ONLY |
false |
Send that policy as Content-Security-Policy-Report-Only to try it first. |
SPLENT_ASSET_MAX_AGE |
31536000 |
How long a versioned asset (?v=<mtime>) may be cached. 0 turns it off. |
SPLENT_ASSET_VERSIONING |
true |
Add ?v=<mtime> to the asset URLs url_for builds. |
Database connections are pinged when they leave the pool and recycled after
an hour (SQLALCHEMY_ENGINE_OPTIONS), so a database restart or MariaDB's
eight hour wait_timeout never reaches a visitor as a 500. A product changes
them in its config.py.
Health probes
GET /healthz answers {"status": "ok"} while the process is alive, and
GET /readyz answers the same once every readiness check passes, or 503
with {"status": "unavailable"}. Both are answered by a middleware around
the whole app, so a probe opens no session, sets no cookie, runs no
feature's hook and is never subject to CSRF. Which check failed, and why,
goes to the log and not into the response.
The framework checks the database (SELECT 1), the session store when
it answers a ping (the Redis of splent_feature_session_redis: with it down
every page fails), and the task executor checks its Redis in rq mode. A
feature adds its own service from init_feature:
from splent_framework.health import register_readiness_check
register_readiness_check("search", lambda: client.ping(), app)
| Variable | Default | What it does |
|---|---|---|
SPLENT_HEALTH_PATH |
/healthz |
Where the liveness probe answers. off turns it off. |
SPLENT_READY_PATH |
/readyz |
Where the readiness probe answers. off turns it off. |
SPLENT_READY_TIMEOUT |
3 |
Seconds the checks may take before the probe answers 503. While a slow check is still running, the next probes answer 503 at once. |
Background tasks
splent_framework.tasks runs work outside the request: a feature marks a
function at the top level of a module with @task and enqueues it with
the ids it needs.
from splent_framework.tasks import enqueue, get_run, task
@task(timeout=60, retries=2)
def transform_uvl(hubfile_id):
...
run = enqueue(transform_uvl, {"hubfile_id": hubfile.id}, key=hubfile.id)
get_run(run.id).status # waiting, queued, running, succeeded, failed, timed_out, dropped
A task enqueued after a write that is not committed yet waits for the
commit, and is dropped if the transaction, or the savepoint it was
enqueued in, is rolled back; with nothing pending it starts at once
(after_commit=True or False decides it instead). Each run gets an app context and a database session of its
own. A key merges the run into a run of the same task and key that has
not started yet. A failed attempt is retried up to the task's retries
(at most 10), waiting backoff seconds (10) and twice as long each time
after. Arguments must be JSON values in every mode, and a task must do no
harm when it runs twice. The mode is read once, when the app is built: a
command or a seeder that needs the work done before it exits calls the
task, which is still a plain function, or wait() in thread mode.
| Variable | Default | What it does |
|---|---|---|
TASKS_MODE |
thread |
sync runs the task in the caller before enqueue returns, which the testing configuration always uses. thread runs it in a bounded pool of the web process: lost if the process stops, and a thread cannot be stopped at its timeout, so a slow run is only logged and marked overdue. It does not protect against a task that hangs: that task holds its thread for good and what comes after waits behind it, in memory, with a warning in the log at the timeout and every minute after. A product whose tasks read untrusted input runs rq in production. rq queues it in Redis for python -m splent_framework.tasks.worker, which stops an attempt at its timeout and survives a restart of the web; it needs splent_framework[rq]. Anything else stops the app at start. |
TASKS_THREAD_WORKERS |
2 |
Tasks that run at once in thread mode. |
TASKS_DEFAULT_TIMEOUT |
180 |
Seconds an attempt may take when its task names none. |
TASKS_REDIS_URL |
REDIS_TASKS_URL, then REDIS_URL |
The Redis of the rq queue. Empty takes the tasks database splent_feature_redis sets apart (REDIS_TASKS_URL), so the queue is not flushed with a cache or the sessions. /readyz depends on it in that mode. |
TASKS_QUEUE |
the product's name | The queue, so two products on one Redis keep their work apart. |
TASKS_RUN_TTL |
86400 |
Seconds Redis keeps the record of an ended run. |
The worker builds the product's app from SPLENT_APP in the profile
SPLENT_ENV names. python -m splent_framework.tasks.worker --check is
for the container's healthcheck: it builds no app and opens no connection,
and exits 0 while the worker's last heartbeat holds. The worker writes
when the next one is due to TASKS_WORKER_HEARTBEAT_FILE (by default
splent-tasks-worker.heartbeat in the temporary directory) and removes
it when it stops. --burst stops when the queue is empty.
Requirements
- Python 3.13+
- Flask 3.1+
- SQLAlchemy 2.0+
Documentation
Full docs, tutorials, and architecture reference at docs.splent.io
License
splent_framework is free software under the GNU Lesser General Public
License, version 3 or any later version (LGPL-3.0-or-later). See
COPYING.LESSER, and COPYING for the GNU General Public License it builds
on. A product built on the framework can carry any license of its own choice.
Metadata
Release files for splent-framework 1.21.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| splent_framework-1.21.0.tar.gz | 368.2 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| splent_framework-1.21.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 596.9 kB
Release files / splent_framework-1.21.0.tar.gz
| Download URL | splent_framework-1.21.0.tar.gz |
|---|---|
| Size | 368.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
507b6361a3f9248b1d9f90b0af68a845b86651ec99e65b89224fd4200a9c354b
|
|
BLAKE2b-256 checksum How to use checksums |
b2ef9b4b85f78c7c4fdb215fa1d3b6c7655d918c7091a8bd805d23562d51aa29
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.16
|
Release files / splent_framework-1.21.0-py3-none-any.whl
| Download URL | splent_framework-1.21.0-py3-none-any.whl |
|---|---|
| Size | 228.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0c34bece69e6763a88bfcb9c286c51738d9fb1cb46e95cb8de5f7dd6f9b92c5a
|
|
BLAKE2b-256 checksum How to use checksums |
97f08e6c669ea5e20b5514f30dc611766c4723e6acf083a5deee4cabcb365db9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.16
|
Security for splent-framework 1.21.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.