juniper-recurrence
Project: Juniper — Cascade Correlation Neural Network Research Platform Application: juniper-recurrence (FastAPI + CLI service) Author: Paul Calnon License: MIT License Version: 0.2.0
FastAPI + CLI service that wraps the Δt-native Legendre Memory Unit regressor
(juniper-recurrence-model) on the
shared juniper-service-core
framework. It loads 3-D windowed sequences (equities_seq, the WS-1 irregular-Δt
contract) through juniper-data-client
and trains / serves the LMU over HTTP.
This is the application layer (WS-4b): the first real consumer of
service-core's create_app + TrainingLifecycle. The model, the data foundation,
and the service framework ship separately; this package is the glue + the HTTP/CLI
surface.
Install
pip install juniper-recurrence
pip install "juniper-recurrence[torch]" # adds the torch MLP readout (DP-3 Rung 2b)
All upstreams resolve from PyPI: juniper-service-core, juniper-model-core,
juniper-recurrence-model, juniper-data-client, plus fastapi / uvicorn.
Run
# Serve the API (single worker, in-process state). Binds 0.0.0.0:8210 by default;
# set JUNIPER_RECURRENCE_HOST=127.0.0.1 for local-only.
juniper-recurrence serve
juniper-recurrence serve --host 127.0.0.1 --port 8210
Once running, the API exposes (every /v1/* route below requires X-API-Key when API
keys are configured; health + docs are always exempt):
| Route | Method | Behavior |
|---|---|---|
/v1/health, /v1/health/ready |
GET | Liveness / readiness (exempt). |
/v1/train |
POST | Train the LMU on a dataset (synchronous); returns the TrainResult, with metrics_scope: "in_sample" labelling its final_metrics, and the fit's operation_id. |
/v1/training/status |
GET | idle / training / restoring / trained / restored / failed, the operation it describes (operation_id, busy_since, dataset_id, requested_by), model_operation_id, a failure under failed, + last metrics + training events. |
/v1/crossval |
POST | Walk-forward cross-validation over the whole dataset (the full split — the artifact's own *_full when present, otherwise rebuilt from train | val | test) (synchronous); returns aggregated per-fold metrics. |
/v1/crossval/status |
GET | Most recent cross-validation result (aggregate + per-fold). |
/v1/predict |
POST | Continuous predictions for inline X (+ dt) or a dataset ref; optional expect_operation_id (409 when it does not name the model's operation). |
/v1/model/snapshots |
GET / POST | List snapshots / save the current model; the save takes an optional expect_operation_id, like /v1/predict. |
/v1/model/snapshots/{id}/restore |
POST | Load a snapshot as the current model; the restore mints its own operation_id. |
/v1/model |
GET | Current model topology + regression metrics. |
/v1/dataset |
GET | Descriptor of the trained-on dataset. |
/v1/metrics |
GET | Prometheus metrics — IP-allowlist gated; needs the [observability] extra. |
/docs |
GET | OpenAPI / Swagger UI (exempt). |
Both POST /v1/train and POST /v1/crossval run inline (closed-form solves) and return
their result in the response — no background jobs or WebSocket streams in v1. A second
cross-validation run while one is in progress returns 409.
POST /v1/train's final_metrics are in-sample: they are scored on the split the fit was
trained on (dataset.split), and the response says so with metrics_scope: "in_sample". They
measure fit, not generalisation; the route reports no held-out metric.
Both responses carry a diagnostics block per fit -- once on POST /v1/train, once per fold
on POST /v1/crossval -- with the hyperparameters the fit resolved: theta (the data-driven
value a theta=None request became, per fold), ridge_mode / ridge_selected (the lambda
ridge="gcv" selected), gamma (the median-heuristic RFF bandwidth), and the gcv_at_edge /
null_model flags (booleans on a GCV-selected fit, which juniper-recurrence-model 0.5.0 reports;
null means not determined, as on a fixed-ridge fit). The train CLI prints the same block under Readout:.
POST /v1/train and POST /v1/crossval accept a readout selector (DP-3): "linear"
(default, closed-form least squares), "rff" (nonlinear random-Fourier-features readout, with
rff_features / rff_gamma), or "mlp" (a torch MLP with the mlp_* knobs — needs the [torch]
extra; returns 503 if torch is not installed). ridge accepts a float or "gcv" (closed-form
generalized-cross-validation selection). A linear-readout request that omits ridge gets the
service's default_ridge, "gcv" since 0.7.0 (plan W5.8 (B)): the penalty is GCV-selected on
the train-fold-standardised memory block, so on a drifting input the fit abstains (lambda at the grid
ceiling, null_model: true in diagnostics) rather than extrapolate as the former 0.0 min-norm
default did. An explicit ridge: 0.0, or JUNIPER_RECURRENCE_DEFAULT_RIDGE=0.0 on the service,
restores the old behaviour. See the juniper-recurrence-model README for the spectrum.
# Train on a juniper-data dataset, then inspect the model.
curl -sX POST localhost:8210/v1/train \
-H 'Content-Type: application/json' \
-d '{"dataset": {"dataset_id": "<id>"}, "d": 16}'
curl -s localhost:8210/v1/model
Train (headless CLI)
# Fit the LMU on a dataset and persist it — no server.
juniper-recurrence train --dataset <id> --d 16 --out model.npz
juniper-recurrence train --name equities_seq_v1 --split train
# Create the dataset from a generator with explicit generator params, then fit it.
juniper-recurrence train --generator equities_seq --params '{"symbols": ["AAPL"], "start_date": "2015-01-01", "end_date": "2022-01-01", "lookback": 64, "fundamentals_fill": "drop", "regression_target": "log_return"}'
juniper-recurrence train --generator equities_seq --params-file equities_seq.json
--params (a JSON object) and --params-file (a file holding one) carry the generator params that
POST /v1/train takes as dataset.params. They are mutually exclusive and need --generator. Pass
them explicitly for equities_seq: the generator's bare defaults are not a trainable configuration.
Either flag without --generator, malformed JSON, or JSON that is not an object exits 2. The
printed metrics are in-sample, as on the route.
One caller per service
A recurrence service is exclusively owned by one caller at a time: it has one process-wide
train_lock, one in-memory model and one snapshot directory. Two callers sharing a listener -- canopy
and a CLI experiment suite, say -- get each other's 409s and silently score each other's models.
- The operation id is how a caller proves whose model it is scoring. Each
POST /v1/trainmints anoperation_idwhen it takes the lock and returns it; a restore mints its own, which the restored model then carries. Pass it asexpect_operation_idto/v1/predictandPOST /v1/model/snapshots: a model produced by any other operation is refused with a409that names both ids. - Who holds it, and what happened. A refused
POST /v1/traingets a409whosedetailnames the holder (operation_id,busy_since,requested_by).GET /v1/training/statusreportstrainingwhile a fit runs, thentrainedorfailed(withfailure), plusmodel_operation_idfor the model/v1/predictscores. Send anX-Request-IDwith your train request; it comes back asrequested_by. - A fit is not cancellable. A caller that times out leaves the fit running under the lock; poll the status for its outcome rather than retrying.
- Keep callers apart by port. The juniper-ml experiment launcher (
util/experiment_stack.bash) starts a fresh service per run on ports 8260-8289, and that is what keeps a CLI suite and canopy apart. Canopy's service URL is outside that range (unset by default; Compose setshttp://juniper-recurrence:8210, host port 8211; a native service listens on 8210), so canopy and a suite pointed at the same native or Compose listener recreate the collision, and neither side warns.
Configuration
All settings read the JUNIPER_RECURRENCE_ environment namespace (e.g.
JUNIPER_RECURRENCE_PORT). Secrets honor the Docker _FILE indirection
(JUNIPER_RECURRENCE_API_KEYS_FILE, JUNIPER_DATA_API_KEY_FILE). When no API keys
are configured, authentication is disabled (open access — development default).
| Variable | Default | Purpose |
|---|---|---|
JUNIPER_RECURRENCE_HOST |
0.0.0.0 |
Bind host (container default; 127.0.0.1 locally). |
JUNIPER_RECURRENCE_PORT |
8210 |
Bind port (deploy maps host 8211 → container 8210). |
JUNIPER_RECURRENCE_API_KEYS |
(unset) | CSV or JSON-array of valid X-API-Key values. |
JUNIPER_DATA_URL |
http://localhost:8100 |
Upstream juniper-data base URL. |
JUNIPER_DATA_API_KEY |
(unset) | Outbound X-API-Key to juniper-data. |
JUNIPER_RECURRENCE_JUNIPER_DATA_TIMEOUT_SECONDS |
120 |
Per-request timeout of the service's juniper-data client -- dataset creation, where a cold equities_seq fetch happens, included. Keep it below your callers' own timeouts. |
Development
pip install -e ".[test]"
pytest tests/ -v
Publishing
Releases are published to PyPI via GitHub Actions
(.github/workflows/publish-recurrence-app.yml) on a juniper-recurrence-v* tag —
TestPyPI first (with a --no-deps install verification), then PyPI, via OIDC trusted
publishing (no API tokens). The model package (juniper-recurrence-model) publishes
separately on juniper-recurrence-model-v* tags.
# Cut a GitHub Release on a juniper-recurrence-v* tag (the ecosystem convention — not a
# bare `git push <tag>`); the Release drives publish-recurrence-app.yml (TestPyPI → PyPI).
gh release create juniper-recurrence-v0.2.0 --generate-notes
Ecosystem
Part of the Juniper ML research platform. See the
WS-4b build plan (notes/JUNIPER_RECURRENCE_WS4B_APP_BUILD_PLAN_2026-06-15.md in
juniper-ml) for the design of record.
Metadata
Release files for juniper-recurrence 0.7.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 | Actions |
|---|---|---|---|---|
| juniper_recurrence-0.7.0.tar.gz | 109.5 kB | With provenance | Details for juniper_recurrence-0.7.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| juniper_recurrence-0.7.0-py3-none-any.whl | Python 3 | none | any | Details for juniper_recurrence-0.7.0-py3-none-any.whl |
Total release size: 175.2 kB
Release files / juniper_recurrence-0.7.0.tar.gz
| Download URL | juniper_recurrence-0.7.0.tar.gz |
|---|---|
| Size | 109.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4ff97e926b51cbfaf0be322dec7b3989f1d33d233054862071ea9ed4dc93b9ec
|
|
BLAKE2b-256 checksum How to use checksums |
2ac38506c328c8a0bb3af5a1cfc4024949558207899f490fd0e10a9e81a603be
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logRelease files / juniper_recurrence-0.7.0-py3-none-any.whl
| Download URL | juniper_recurrence-0.7.0-py3-none-any.whl |
|---|---|
| Size | 65.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d2bcfe57998d047bc038ac328b6f013724bcfa2d056d86694584130e560d3916
|
|
BLAKE2b-256 checksum How to use checksums |
4cffcd887127eb1a771e65311590c2c17c555f5a5504b117d2cb425419f5be57
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logSecurity for juniper-recurrence 0.7.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.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for juniper_recurrence-0.7.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logA 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.