Flask
Requires Flask 2.3.3+, Python 3.11+. The range states API compatibility, not a patch level, so run a supported Flask release with current security patches. See the compatibility matrix for the full pin string.
Install + wire
Section titled “Install + wire”Z4J() and Z4J.init_app(app) read configuration from Flask's app.config or from the environment; they do not take a config dict argument.
Factory pattern with deferred init:
from flask import Flaskfrom z4j_flask import Z4J
z4j = Z4J()
def create_app(): app = Flask(__name__) app.config.from_object("myapp.config.Config") # Required: Z4J_BRAIN_URL, Z4J_TOKEN, Z4J_HMAC_SECRET, # Z4J_PROJECT_ID. Optional: Z4J_AGENT_NAME and Z4J_AGENT_ID. z4j.init_app(app) return appOr the one-shot form, with config already on app.config or in env:
Z4J(app)Configuration sources
Section titled “Configuration sources”The extension accepts three configuration shapes. Environment variables have highest precedence, then flat Flask keys, then the nested Flask dict.
| Source | Example |
|---|---|
| Flat Flask keys | app.config["Z4J_BRAIN_URL"] = "...", app.config["Z4J_TOKEN"] = "...", app.config["Z4J_HMAC_SECRET"] = "..." |
| Nested Flask dict | app.config["Z4J"] = {"brain_url": "...", "token": "...", "hmac_secret": "...", "project_id": "billing-prod"} |
| Environment variables | Z4J_BRAIN_URL, Z4J_TOKEN, Z4J_HMAC_SECRET, Z4J_PROJECT_ID, Z4J_AGENT_NAME |
Z4J_HMAC_SECRET is required; the agent refuses to start without it. To skip the agent entirely (CI, local dev), set Z4J_DISABLED=1 (env) or app.config["Z4J_DISABLED"] = True. Leave the key unset to enable: app.config["Z4J_DISABLED"] = False is forwarded to the runtime config as an unknown field, validation rejects it, and the agent never starts.
Lifecycle
Section titled “Lifecycle”Flask's application context does not have a natural startup hook. z4j-flask starts the agent during init_app itself; the WebSocket connection lives in a background asyncio runtime that the extension manages. Request teardown hooks are registered automatically.
When TASKIQ_BROKER is configured, Flask discovery attaches z4j's TaskIQ
middleware but does not guess a loop during synchronous app initialization.
Initialize Z4J(app) before the component that starts TaskIQ; the broker's real
startup hook then binds its owner loop. Until it is bound, TaskIQ submit and
result probes from z4j fail closed. A separate TaskIQ worker needs its own agent.
For long-running workers that don't run under Flask's request loop (RQ worker, Celery beat scheduler):
from myapp import create_appapp = create_app() # Z4J.init_app already ran inside; the agent is up.# ... RQ worker loop ...Multi-process
Section titled “Multi-process”Each gunicorn / uWSGI worker process registers separately. z4j's worker-first protocol identifies each one by (agent_id, worker_id), so a gunicorn -w 4 deployment shows up as four workers under one agent in the dashboard.
Z4J_AGENT_NAME is host metadata on the shared agent row, not a per-worker identity. Keep it stable for all workers that share one token. If hosts need separate labels, mint a separate agent token for each host instead of varying the name by PID.
Blueprints
Section titled “Blueprints”z4j does not register any Flask blueprints. It attaches as pure background behavior; your URL space is untouched.
CLI (flask ...)
Section titled “CLI (flask ...)”Management commands run under the same init_app path, so the agent attempts to start when they boot. Set Z4J_DISABLED=1 for one-off commands you do not want surfaced as agent connects.
init_app also registers flask z4j-reconcile (--dry-run, --json), which runs the declarative schedule reconciler. It exits 0 on success, on no-op, and when brain_url, token or project_id is missing (that case logs a warning and skips), and 1 when any schedule fails to reconcile.
Verify with doctor
Section titled “Verify with doctor”python -m z4j_flask doctor checks the buffer directory and brain DNS / TCP / TLS path, then starts a temporary WebSocket runtime using the Z4J_* env vars your service is configured with. It does not import the Flask app or read app.config.
# Always run as the same user the service runs under.sudo -u www-data /srv/app/venv/bin/python -m z4j_flask doctor
# Skip the WS round-trip when z4j is intentionally offline:python -m z4j_flask doctor --no-websocket
# Machine-readable for scripting:python -m z4j_flask doctor --jsonExit 0 on all-green, 1 on any failure. It catches service-user startup failures and DNS, firewall, or certificate issues. The WebSocket probe returns when the background runtime starts and does not wait for authenticated hello_ack, so a bad token, project value, or HMAC secret can still pass. Confirm authentication in the agent and brain logs. See service-user deployments.
Troubleshooting
Section titled “Troubleshooting”First, run python -m z4j_flask doctor -- it surfaces the most common failures with a specific reason.
-
Gunicorn / uWSGI preload mode --
--preloadimports your app (and installs the agent) ONCE in the arbiter, thenfork()s the workers. Threads do not survive a fork, so without extra wiring the workers inherit a dead agent and capture nothing. Add a post-fork hook so each worker re-establishes its own agent:gunicorn.conf.py def post_fork(server, worker):from z4j_bare import post_fork as z4j_post_forkz4j_post_fork()For uWSGI use the
@postforkdecorator to callz4j_bare.post_fork(). If you do NOT preload (the gunicorn default), the app is imported in each worker after the fork and no hook is needed. -
PermissionError: ... /var/www/.z4jin service log -- the agent auto-relocates the buffer to$TMPDIR/z4j-{uid}. See service-user deployments.
Integration scope
Section titled “Integration scope”This package connects the web process and its configured queue handles. Task execution still happens in the queue's worker process, which needs its own engine adapter and agent configuration. It does not provide HTTP request tracing or automatically capture arbitrary threads started by Flask handlers. Use a supported queue engine for the task lifecycle and controls shown in z4j.