juniper-data-client 0.4.1__tar.gz → 0.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. juniper_data_client-0.4.2/PKG-INFO +194 -0
  2. juniper_data_client-0.4.2/README.md +149 -0
  3. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/juniper_data_client/__init__.py +3 -1
  4. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/juniper_data_client/client.py +38 -3
  5. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/juniper_data_client/constants.py +60 -0
  6. juniper_data_client-0.4.2/juniper_data_client/contract.py +125 -0
  7. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/juniper_data_client/testing/fake_client.py +17 -2
  8. juniper_data_client-0.4.2/juniper_data_client.egg-info/PKG-INFO +194 -0
  9. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/juniper_data_client.egg-info/SOURCES.txt +2 -0
  10. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/pyproject.toml +1 -1
  11. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/tests/test_client.py +28 -1
  12. juniper_data_client-0.4.2/tests/test_contract.py +116 -0
  13. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/tests/test_fake_client.py +22 -3
  14. juniper_data_client-0.4.1/PKG-INFO +0 -271
  15. juniper_data_client-0.4.1/README.md +0 -226
  16. juniper_data_client-0.4.1/juniper_data_client.egg-info/PKG-INFO +0 -271
  17. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/LICENSE +0 -0
  18. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/juniper_data_client/exceptions.py +0 -0
  19. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/juniper_data_client/py.typed +0 -0
  20. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/juniper_data_client/testing/__init__.py +0 -0
  21. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/juniper_data_client/testing/generators.py +0 -0
  22. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/juniper_data_client.egg-info/dependency_links.txt +0 -0
  23. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/juniper_data_client.egg-info/requires.txt +0 -0
  24. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/juniper_data_client.egg-info/top_level.txt +0 -0
  25. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/setup.cfg +0 -0
  26. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/tests/test_create_dataset_tags_ttl.py +0 -0
  27. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/tests/test_fake_client_batch.py +0 -0
  28. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/tests/test_generator_parity.py +0 -0
  29. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/tests/test_malformed_json_response.py +0 -0
  30. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/tests/test_npzfile_resource_lifecycle.py +0 -0
  31. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/tests/test_on_request_hook.py +0 -0
  32. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/tests/test_performance.py +0 -0
  33. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/tests/test_retry_policy.py +0 -0
  34. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/tests/test_versioning.py +0 -0
  35. {juniper_data_client-0.4.1 → juniper_data_client-0.4.2}/tests/test_x_request_id_propagation.py +0 -0
@@ -0,0 +1,194 @@
1
+ Metadata-Version: 2.4
2
+ Name: juniper-data-client
3
+ Version: 0.4.2
4
+ Summary: HTTP client for the JuniperData dataset generation service
5
+ Author: Paul Calnon
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/pcalnon/juniper-data-client
8
+ Project-URL: Repository, https://github.com/pcalnon/juniper-data-client
9
+ Project-URL: Documentation, https://github.com/pcalnon/juniper-data-client#readme
10
+ Project-URL: Issues, https://github.com/pcalnon/juniper-data-client/issues
11
+ Keywords: juniper,juniper-ml,juniper-ai,artificial-intelligence,dataset,machine-learning,api-client
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.12
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: numpy>=1.24.0
27
+ Requires-Dist: requests>=2.28.0
28
+ Requires-Dist: urllib3>=2.0.0
29
+ Provides-Extra: test
30
+ Requires-Dist: pytest>=7.0.0; extra == "test"
31
+ Requires-Dist: pytest-cov>=4.0.0; extra == "test"
32
+ Requires-Dist: pytest-timeout>=2.2.0; extra == "test"
33
+ Requires-Dist: responses>=0.23.0; extra == "test"
34
+ Requires-Dist: juniper-observability>=0.1.1; extra == "test"
35
+ Provides-Extra: observability
36
+ Requires-Dist: juniper-observability>=0.1.1; extra == "observability"
37
+ Provides-Extra: dev
38
+ Requires-Dist: juniper-data-client[test]; extra == "dev"
39
+ Requires-Dist: black>=23.0.0; extra == "dev"
40
+ Requires-Dist: isort>=5.12.0; extra == "dev"
41
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
42
+ Requires-Dist: flake8>=7.0.0; extra == "dev"
43
+ Requires-Dist: types-requests>=2.28.0; extra == "dev"
44
+ Dynamic: license-file
45
+
46
+ <!-- markdownlint-disable MD013 MD033 MD041 -->
47
+ <!--
48
+ MD013 (line-length): README contains prose paragraphs that intentionally
49
+ exceed the 512-char ecosystem limit (canonical
50
+ §4 layout). Wrapping harms PyPI rendering.
51
+ MD033 (no-inline-html): The right-aligned logo + spacing rely on HTML.
52
+ MD041 (first-line-heading): The HTML logo is the first line by design.
53
+ Mirrors juniper-ml #283, juniper-cascor #276, juniper-cascor-worker #71.
54
+ -->
55
+ <div align="right" width="150px" height="150px" align="right" valign="top"> <img src="images/Juniper_Logo_150px.png" alt="Juniper" align="right" valign="top" width="150px" /></div>
56
+ <br /> <br /> <br /> <br />
57
+
58
+ # Juniper: Dynamic Neural Network Research Platform
59
+
60
+ Juniper is an AI/ML research platform for investigating dynamic neural network architectures and novel learning paradigms. The project emphasizes ground-up implementations from primary literature, enabling a more transparent exploration of fundamental algorithms.
61
+
62
+ ## Juniper Data Client
63
+
64
+ `juniper-data-client` is the **Python HTTP client library** for the `juniper-data` dataset-generation service. The package exposes a single `JuniperDataClient` class whose methods correspond to the REST surface of `juniper-data`: generator schemas, dataset creation, tag-based filtering, batch operations, the named-version registry, and download of the resulting NPZ artifacts as ready-to-train `numpy` arrays. It is the canonical dataset-fetch interface used by both `juniper-cascor` (for training) and `juniper-canopy` (for visualisation), and it serializes the platform's shared `X_train` / `y_train` / `X_test` / `y_test` / `X_full` / `y_full` NPZ schema (all `float32`).
65
+
66
+ ## Distribution
67
+
68
+ `juniper-data-client` is published on PyPI as **[`juniper-data-client`](https://pypi.org/project/juniper-data-client/)**.
69
+ The package is also surfaced through the platform meta-distribution
70
+ **[`juniper-ml`](https://pypi.org/project/juniper-ml/)**, which installs
71
+ the full client stack via `pip install juniper-ml[all]`.
72
+
73
+ ```bash
74
+ pip install juniper-data-client
75
+ ```
76
+
77
+ ## Ecosystem Compatibility
78
+
79
+ This client library is part of the [Juniper](https://github.com/pcalnon/juniper-ml) ecosystem.
80
+ Verified compatible versions:
81
+
82
+ | juniper-data | juniper-cascor | juniper-canopy | data-client | cascor-client | cascor-worker |
83
+ |--------------|----------------|----------------|-------------|---------------|---------------|
84
+ | 0.6.x | 0.5.x | 0.5.x | >=0.4.1 | >=0.4.0 | >=0.4.0 |
85
+
86
+ For full-stack Docker deployment and integration tests, see [`juniper-deploy`](https://github.com/pcalnon/juniper-deploy).
87
+
88
+ ## Architecture
89
+
90
+ `juniper-data-client` is a thin synchronous HTTP client. It does not embed any dataset-generation logic of its own: every call resolves to an HTTP request against a `juniper-data` instance, and every NPZ artifact returned has been produced server-side.
91
+
92
+ ```text
93
+ ┌────────────────────────┐ ┌──────────────────┐
94
+ │ Caller (e.g. │ HTTP (requests + │ juniper-data │
95
+ │ juniper-cascor / │ urllib3 Retry) │ REST service │
96
+ │ juniper-canopy / │ ──────────────────► │ Port 8100 │
97
+ │ research notebook) │ ◄────────────────── │ /v1/... │
98
+ └──────────┬─────────────┘ JSON + NPZ └──────────────────┘
99
+ │ uses
100
+ ▼
101
+ ┌────────────────────────┐
102
+ │ juniper-data-client │
103
+ │ JuniperDataClient │
104
+ │ (this package) │
105
+ └────────────────────────┘
106
+ ```
107
+
108
+ The client retries idempotent verbs (GET) on transient failures via `urllib3.Retry` with exponential backoff; mutating verbs (POST, PATCH, DELETE) are **not** auto-retried, to avoid duplicate-side-effect bugs against the dataset registry. Optional `X-Request-ID` propagation and a `RequestHook` instrumentation surface are available for callers that integrate with the platform's observability stack.
109
+
110
+ ## Related Services
111
+
112
+ | Service | Relationship | Notes |
113
+ |---------|-------------|-------|
114
+ | [juniper-data](https://github.com/pcalnon/juniper-data) | The HTTP service this client targets | Set `base_url` to the service's URL (default `http://localhost:8100`) |
115
+ | [juniper-cascor](https://github.com/pcalnon/juniper-cascor) | Primary consumer; uses this client to fetch training datasets | Reads `JUNIPER_DATA_URL` |
116
+ | [juniper-canopy](https://github.com/pcalnon/juniper-canopy) | Secondary consumer; uses this client to fetch visualisation data | Reads `JUNIPER_DATA_URL` |
117
+
118
+ ## Active Research Components
119
+
120
+ `juniper-data-client` does not host research components of its own; it is the surface through which other components of the Juniper platform reach the research components hosted by `juniper-data`. Through this client, callers access the **ARC-AGI dataset families** (ARC-AGI-1 and ARC-AGI-2), the **named-version dataset registry** (`list_versions`, `get_latest`), the **batch dataset operations** (`batch_create`, `batch_delete`, `batch_update_tags`, `batch_export`), and the **NPZ artifact contract** that the rest of the platform consumes. Treating these as research artifacts is appropriate: each is a stable interface around which comparative experiments are composed.
121
+
122
+ ## Quick Start Guide
123
+
124
+ ### Prerequisites
125
+
126
+ - Python ≥ 3.12
127
+ - A running `juniper-data` instance reachable at the URL passed as `base_url` (typically `http://localhost:8100`)
128
+
129
+ ### Installation
130
+
131
+ ```bash
132
+ pip install juniper-data-client
133
+ ```
134
+
135
+ Optional extras: `[observability]` enables `X-Request-ID` propagation through `juniper-observability`; `[test]` installs the testing dependencies; `[dev]` adds linting and type-checking tools.
136
+
137
+ ### Verification
138
+
139
+ ```python
140
+ from juniper_data_client import JuniperDataClient
141
+
142
+ with JuniperDataClient("http://localhost:8100") as client:
143
+ health = client.health_check()
144
+ print(f"Service status: {health['status']}")
145
+
146
+ result = client.create_spiral_dataset(
147
+ n_spirals=2,
148
+ n_points_per_spiral=100,
149
+ noise=0.1,
150
+ seed=42,
151
+ )
152
+ arrays = client.download_artifact_npz(result["dataset_id"])
153
+
154
+ print(f"Training samples: {len(arrays['X_train'])}")
155
+ print(f"Test samples: {len(arrays['X_test'])}")
156
+ ```
157
+
158
+ For PyTorch consumers, the returned arrays convert directly with `torch.from_numpy`:
159
+
160
+ ```python
161
+ import torch
162
+ X_train = torch.from_numpy(arrays["X_train"]) # torch.float32
163
+ y_train = torch.from_numpy(arrays["y_train"]) # torch.float32
164
+ ```
165
+
166
+ ### Next Steps
167
+
168
+ - [`docs/QUICK_START.md`](docs/QUICK_START.md) — complete installation and verification guide
169
+ - [`docs/REFERENCE.md`](docs/REFERENCE.md) — full API reference, configuration, and error model
170
+ - [`docs/DEVELOPER_CHEATSHEET.md`](docs/DEVELOPER_CHEATSHEET.md) — quick-reference card for development tasks
171
+ - [`juniper-data`](https://github.com/pcalnon/juniper-data) — the upstream dataset service
172
+ - [`juniper-ml`](https://pypi.org/project/juniper-ml/) — platform meta-package on PyPI
173
+
174
+ ## Research Philosophy
175
+
176
+ The Juniper platform exists to study learning algorithms whose network architecture is not fixed in advance. Its initial anchor is the Cascade-Correlation algorithm of Fahlman and Lebiere (1990), implemented from the primary literature without recourse to higher-level abstractions that elide the algorithm's operational detail. The organising commitment is that algorithm implementations remain inspectable at the level at which they were originally specified: candidate units, correlation objectives, weight-freezing semantics, and the structural events that grow the network are first-class artifacts of the codebase rather than internal details of a library wrapper. This permits comparative work — across algorithms, datasets, and hyperparameter regimes — to be conducted on a known and reproducible substrate.
177
+
178
+ The current platform comprises a Cascade-Correlation training service exposing a REST and WebSocket interface, a dataset-generation service with a named-version registry that includes the ARC-AGI families, a real-time monitoring dashboard for inspecting training dynamics as they occur, and a distributed worker that parallelises candidate-unit training across hosts. Near-term work extends the architectural-growth catalogue beyond Cascade-Correlation, introduces multi-network orchestration for comparative experiments at the level of network populations rather than individual runs, and tightens the dataset–training–monitoring loop into a reproducible research workbench. The longer-term direction is the systematic empirical study of constructive and architecture-growing learning algorithms, with first-class infrastructure for the ablation, comparison, and replication that such a study requires.
179
+
180
+ Within this programme, `juniper-data-client` is the canonical integration boundary between the training and data services. Its contract is the dataset-fetch interface used by every training-side consumer; changes to its surface are therefore changes to the platform's shared dataset semantics.
181
+
182
+ ## Documentation
183
+
184
+ | Document | Purpose |
185
+ |----------|---------|
186
+ | [`docs/DOCUMENTATION_OVERVIEW.md`](docs/DOCUMENTATION_OVERVIEW.md) | Navigation index for all `juniper-data-client` documentation |
187
+ | [`docs/QUICK_START.md`](docs/QUICK_START.md) | Complete installation and verification guide |
188
+ | [`docs/REFERENCE.md`](docs/REFERENCE.md) | Full API reference, configuration, error model, and NPZ schema |
189
+ | [`docs/DEVELOPER_CHEATSHEET.md`](docs/DEVELOPER_CHEATSHEET.md) | Quick-reference card for development tasks |
190
+ | [`CHANGELOG.md`](CHANGELOG.md) | Version history |
191
+
192
+ ## License
193
+
194
+ MIT License — see [`LICENSE`](LICENSE) for details.
@@ -0,0 +1,149 @@
1
+ <!-- markdownlint-disable MD013 MD033 MD041 -->
2
+ <!--
3
+ MD013 (line-length): README contains prose paragraphs that intentionally
4
+ exceed the 512-char ecosystem limit (canonical
5
+ §4 layout). Wrapping harms PyPI rendering.
6
+ MD033 (no-inline-html): The right-aligned logo + spacing rely on HTML.
7
+ MD041 (first-line-heading): The HTML logo is the first line by design.
8
+ Mirrors juniper-ml #283, juniper-cascor #276, juniper-cascor-worker #71.
9
+ -->
10
+ <div align="right" width="150px" height="150px" align="right" valign="top"> <img src="images/Juniper_Logo_150px.png" alt="Juniper" align="right" valign="top" width="150px" /></div>
11
+ <br /> <br /> <br /> <br />
12
+
13
+ # Juniper: Dynamic Neural Network Research Platform
14
+
15
+ Juniper is an AI/ML research platform for investigating dynamic neural network architectures and novel learning paradigms. The project emphasizes ground-up implementations from primary literature, enabling a more transparent exploration of fundamental algorithms.
16
+
17
+ ## Juniper Data Client
18
+
19
+ `juniper-data-client` is the **Python HTTP client library** for the `juniper-data` dataset-generation service. The package exposes a single `JuniperDataClient` class whose methods correspond to the REST surface of `juniper-data`: generator schemas, dataset creation, tag-based filtering, batch operations, the named-version registry, and download of the resulting NPZ artifacts as ready-to-train `numpy` arrays. It is the canonical dataset-fetch interface used by both `juniper-cascor` (for training) and `juniper-canopy` (for visualisation), and it serializes the platform's shared `X_train` / `y_train` / `X_test` / `y_test` / `X_full` / `y_full` NPZ schema (all `float32`).
20
+
21
+ ## Distribution
22
+
23
+ `juniper-data-client` is published on PyPI as **[`juniper-data-client`](https://pypi.org/project/juniper-data-client/)**.
24
+ The package is also surfaced through the platform meta-distribution
25
+ **[`juniper-ml`](https://pypi.org/project/juniper-ml/)**, which installs
26
+ the full client stack via `pip install juniper-ml[all]`.
27
+
28
+ ```bash
29
+ pip install juniper-data-client
30
+ ```
31
+
32
+ ## Ecosystem Compatibility
33
+
34
+ This client library is part of the [Juniper](https://github.com/pcalnon/juniper-ml) ecosystem.
35
+ Verified compatible versions:
36
+
37
+ | juniper-data | juniper-cascor | juniper-canopy | data-client | cascor-client | cascor-worker |
38
+ |--------------|----------------|----------------|-------------|---------------|---------------|
39
+ | 0.6.x | 0.5.x | 0.5.x | >=0.4.1 | >=0.4.0 | >=0.4.0 |
40
+
41
+ For full-stack Docker deployment and integration tests, see [`juniper-deploy`](https://github.com/pcalnon/juniper-deploy).
42
+
43
+ ## Architecture
44
+
45
+ `juniper-data-client` is a thin synchronous HTTP client. It does not embed any dataset-generation logic of its own: every call resolves to an HTTP request against a `juniper-data` instance, and every NPZ artifact returned has been produced server-side.
46
+
47
+ ```text
48
+ ┌────────────────────────┐ ┌──────────────────┐
49
+ │ Caller (e.g. │ HTTP (requests + │ juniper-data │
50
+ │ juniper-cascor / │ urllib3 Retry) │ REST service │
51
+ │ juniper-canopy / │ ──────────────────► │ Port 8100 │
52
+ │ research notebook) │ ◄────────────────── │ /v1/... │
53
+ └──────────┬─────────────┘ JSON + NPZ └──────────────────┘
54
+ │ uses
55
+ ▼
56
+ ┌────────────────────────┐
57
+ │ juniper-data-client │
58
+ │ JuniperDataClient │
59
+ │ (this package) │
60
+ └────────────────────────┘
61
+ ```
62
+
63
+ The client retries idempotent verbs (GET) on transient failures via `urllib3.Retry` with exponential backoff; mutating verbs (POST, PATCH, DELETE) are **not** auto-retried, to avoid duplicate-side-effect bugs against the dataset registry. Optional `X-Request-ID` propagation and a `RequestHook` instrumentation surface are available for callers that integrate with the platform's observability stack.
64
+
65
+ ## Related Services
66
+
67
+ | Service | Relationship | Notes |
68
+ |---------|-------------|-------|
69
+ | [juniper-data](https://github.com/pcalnon/juniper-data) | The HTTP service this client targets | Set `base_url` to the service's URL (default `http://localhost:8100`) |
70
+ | [juniper-cascor](https://github.com/pcalnon/juniper-cascor) | Primary consumer; uses this client to fetch training datasets | Reads `JUNIPER_DATA_URL` |
71
+ | [juniper-canopy](https://github.com/pcalnon/juniper-canopy) | Secondary consumer; uses this client to fetch visualisation data | Reads `JUNIPER_DATA_URL` |
72
+
73
+ ## Active Research Components
74
+
75
+ `juniper-data-client` does not host research components of its own; it is the surface through which other components of the Juniper platform reach the research components hosted by `juniper-data`. Through this client, callers access the **ARC-AGI dataset families** (ARC-AGI-1 and ARC-AGI-2), the **named-version dataset registry** (`list_versions`, `get_latest`), the **batch dataset operations** (`batch_create`, `batch_delete`, `batch_update_tags`, `batch_export`), and the **NPZ artifact contract** that the rest of the platform consumes. Treating these as research artifacts is appropriate: each is a stable interface around which comparative experiments are composed.
76
+
77
+ ## Quick Start Guide
78
+
79
+ ### Prerequisites
80
+
81
+ - Python ≥ 3.12
82
+ - A running `juniper-data` instance reachable at the URL passed as `base_url` (typically `http://localhost:8100`)
83
+
84
+ ### Installation
85
+
86
+ ```bash
87
+ pip install juniper-data-client
88
+ ```
89
+
90
+ Optional extras: `[observability]` enables `X-Request-ID` propagation through `juniper-observability`; `[test]` installs the testing dependencies; `[dev]` adds linting and type-checking tools.
91
+
92
+ ### Verification
93
+
94
+ ```python
95
+ from juniper_data_client import JuniperDataClient
96
+
97
+ with JuniperDataClient("http://localhost:8100") as client:
98
+ health = client.health_check()
99
+ print(f"Service status: {health['status']}")
100
+
101
+ result = client.create_spiral_dataset(
102
+ n_spirals=2,
103
+ n_points_per_spiral=100,
104
+ noise=0.1,
105
+ seed=42,
106
+ )
107
+ arrays = client.download_artifact_npz(result["dataset_id"])
108
+
109
+ print(f"Training samples: {len(arrays['X_train'])}")
110
+ print(f"Test samples: {len(arrays['X_test'])}")
111
+ ```
112
+
113
+ For PyTorch consumers, the returned arrays convert directly with `torch.from_numpy`:
114
+
115
+ ```python
116
+ import torch
117
+ X_train = torch.from_numpy(arrays["X_train"]) # torch.float32
118
+ y_train = torch.from_numpy(arrays["y_train"]) # torch.float32
119
+ ```
120
+
121
+ ### Next Steps
122
+
123
+ - [`docs/QUICK_START.md`](docs/QUICK_START.md) — complete installation and verification guide
124
+ - [`docs/REFERENCE.md`](docs/REFERENCE.md) — full API reference, configuration, and error model
125
+ - [`docs/DEVELOPER_CHEATSHEET.md`](docs/DEVELOPER_CHEATSHEET.md) — quick-reference card for development tasks
126
+ - [`juniper-data`](https://github.com/pcalnon/juniper-data) — the upstream dataset service
127
+ - [`juniper-ml`](https://pypi.org/project/juniper-ml/) — platform meta-package on PyPI
128
+
129
+ ## Research Philosophy
130
+
131
+ The Juniper platform exists to study learning algorithms whose network architecture is not fixed in advance. Its initial anchor is the Cascade-Correlation algorithm of Fahlman and Lebiere (1990), implemented from the primary literature without recourse to higher-level abstractions that elide the algorithm's operational detail. The organising commitment is that algorithm implementations remain inspectable at the level at which they were originally specified: candidate units, correlation objectives, weight-freezing semantics, and the structural events that grow the network are first-class artifacts of the codebase rather than internal details of a library wrapper. This permits comparative work — across algorithms, datasets, and hyperparameter regimes — to be conducted on a known and reproducible substrate.
132
+
133
+ The current platform comprises a Cascade-Correlation training service exposing a REST and WebSocket interface, a dataset-generation service with a named-version registry that includes the ARC-AGI families, a real-time monitoring dashboard for inspecting training dynamics as they occur, and a distributed worker that parallelises candidate-unit training across hosts. Near-term work extends the architectural-growth catalogue beyond Cascade-Correlation, introduces multi-network orchestration for comparative experiments at the level of network populations rather than individual runs, and tightens the dataset–training–monitoring loop into a reproducible research workbench. The longer-term direction is the systematic empirical study of constructive and architecture-growing learning algorithms, with first-class infrastructure for the ablation, comparison, and replication that such a study requires.
134
+
135
+ Within this programme, `juniper-data-client` is the canonical integration boundary between the training and data services. Its contract is the dataset-fetch interface used by every training-side consumer; changes to its surface are therefore changes to the platform's shared dataset semantics.
136
+
137
+ ## Documentation
138
+
139
+ | Document | Purpose |
140
+ |----------|---------|
141
+ | [`docs/DOCUMENTATION_OVERVIEW.md`](docs/DOCUMENTATION_OVERVIEW.md) | Navigation index for all `juniper-data-client` documentation |
142
+ | [`docs/QUICK_START.md`](docs/QUICK_START.md) | Complete installation and verification guide |
143
+ | [`docs/REFERENCE.md`](docs/REFERENCE.md) | Full API reference, configuration, error model, and NPZ schema |
144
+ | [`docs/DEVELOPER_CHEATSHEET.md`](docs/DEVELOPER_CHEATSHEET.md) | Quick-reference card for development tasks |
145
+ | [`CHANGELOG.md`](CHANGELOG.md) | Version history |
146
+
147
+ ## License
148
+
149
+ MIT License — see [`LICENSE`](LICENSE) for details.
@@ -5,12 +5,14 @@ dataset generation service, used by both JuniperCascor and juniper-canopy.
5
5
  """
6
6
 
7
7
  from juniper_data_client.client import JuniperDataClient, RequestHook
8
+ from juniper_data_client.contract import validate_npz_contract
8
9
  from juniper_data_client.exceptions import JuniperDataClientError, JuniperDataConfigurationError, JuniperDataConnectionError, JuniperDataNotFoundError, JuniperDataTimeoutError, JuniperDataValidationError
9
10
 
10
- __version__ = "0.4.1"
11
+ __version__ = "0.4.2"
11
12
 
12
13
  __all__ = [
13
14
  "JuniperDataClient",
15
+ "validate_npz_contract",
14
16
  "JuniperDataClientError",
15
17
  "JuniperDataConfigurationError",
16
18
  "JuniperDataConnectionError",
@@ -8,6 +8,7 @@ import io
8
8
  import logging
9
9
  import os
10
10
  import time
11
+ from pathlib import Path
11
12
  from typing import Any, Callable, Dict, List, Optional
12
13
  from urllib.parse import urlparse
13
14
 
@@ -18,6 +19,7 @@ from urllib3.util.retry import Retry
18
19
 
19
20
  from juniper_data_client.constants import (
20
21
  API_KEY_ENV_VAR,
22
+ API_KEY_FILE_ENV_VAR,
21
23
  API_KEY_HEADER_NAME,
22
24
  API_VERSION_PATH_SUFFIX,
23
25
  DEFAULT_BACKOFF_FACTOR,
@@ -65,6 +67,26 @@ from juniper_data_client.exceptions import JuniperDataClientError, JuniperDataCo
65
67
  logger = logging.getLogger("juniper_data_client.client")
66
68
 
67
69
 
70
+ def _resolve_api_key_from_env() -> Optional[str]:
71
+ """Resolve the juniper-data API key from the environment.
72
+
73
+ Honors the Docker-secret ``JUNIPER_DATA_API_KEY_FILE`` indirection (a file whose
74
+ stripped contents are the key, e.g. ``/run/secrets/juniper_data_api_keys``) before the
75
+ plain ``JUNIPER_DATA_API_KEY`` env var, so a consumer that mounts the key as a file and
76
+ leaves ``api_key`` unset still authenticates. Mirrors how the Juniper services resolve
77
+ their own secrets (e.g. cascor ``api.secrets.get_secret``).
78
+ """
79
+ file_path = os.environ.get(API_KEY_FILE_ENV_VAR)
80
+ if file_path:
81
+ try:
82
+ content = Path(file_path).read_text(encoding="utf-8").strip()
83
+ except OSError:
84
+ content = ""
85
+ if content:
86
+ return content
87
+ return os.environ.get(API_KEY_ENV_VAR)
88
+
89
+
68
90
  # METRICS-MON R4.3 / seed-13: type alias for the optional instrumentation
69
91
  # hook. Consumers (canopy, cascor) pass a callable matching this shape;
70
92
  # default in :class:`JuniperDataClient` is no-op so standalone users
@@ -151,7 +173,7 @@ class JuniperDataClient:
151
173
  # guards — the no-op call is a single attribute load + return).
152
174
  self._on_request: RequestHook = on_request or _noop_request_hook
153
175
 
154
- resolved_api_key = api_key or os.environ.get(API_KEY_ENV_VAR)
176
+ resolved_api_key = api_key or _resolve_api_key_from_env()
155
177
  if resolved_api_key:
156
178
  self.session.headers[API_KEY_HEADER_NAME] = resolved_api_key
157
179
 
@@ -525,12 +547,25 @@ class JuniperDataClient:
525
547
  def download_artifact_npz(self, dataset_id: str) -> Dict[str, np.ndarray]:
526
548
  """Download and load an NPZ artifact for a dataset.
527
549
 
528
- The returned dictionary contains numpy arrays with the standard keys:
550
+ The returned dictionary contains every array in the NPZ. Classification
551
+ artifacts carry the standard keys:
529
552
  - X_train, y_train: Training features and one-hot labels
530
553
  - X_test, y_test: Test features and one-hot labels
531
554
  - X_full, y_full: Full dataset features and one-hot labels
532
555
 
533
- All arrays are float32 dtype.
556
+ Additional keys may be present depending on the generator (WS-1 /
557
+ juniper-data#168):
558
+ - y_reg_{split}: regression target(s) (e.g. equities next-day close).
559
+ - 3-D sequence artifacts (X.ndim == 3, shape (W, L, F)) add a per-step
560
+ dt_{split} (or absolute t_{split}) elapsed-time channel plus
561
+ target_dt_{split} and optional observed_mask_{split} /
562
+ padding_mask_{split}. Pass the result to
563
+ :func:`juniper_data_client.validate_npz_contract` to classify
564
+ ("tabular" / "sequence") and validate it. The matching ``task_type``
565
+ ("classification" / "regression") and ``time_unit`` live in the
566
+ dataset metadata (see :meth:`get_dataset_metadata`).
567
+
568
+ Feature and label arrays are float32.
534
569
 
535
570
  Args:
536
571
  dataset_id: ID of the dataset whose artifact to download
@@ -15,6 +15,36 @@ License: MIT License
15
15
 
16
16
  from typing import List, Tuple
17
17
 
18
+ __all__ = [
19
+ "DEFAULT_BASE_URL",
20
+ "FAKE_BASE_URL",
21
+ "DEFAULT_TIMEOUT",
22
+ "DEFAULT_RETRIES",
23
+ "DEFAULT_BACKOFF_FACTOR",
24
+ "RETRYABLE_STATUS_CODES",
25
+ "RETRY_ALLOWED_METHODS",
26
+ "HTTP_POOL_CONNECTIONS",
27
+ "HTTP_POOL_MAXSIZE",
28
+ "URL_SCHEME_PREFIXES",
29
+ "DEFAULT_URL_SCHEME_PREFIX",
30
+ "API_VERSION_PATH_SUFFIX",
31
+ "DEFAULT_READY_TIMEOUT",
32
+ "DEFAULT_READY_POLL_INTERVAL",
33
+ "HEALTH_READY_STATUS",
34
+ "API_KEY_HEADER_NAME",
35
+ "API_KEY_ENV_VAR",
36
+ "API_KEY_FILE_ENV_VAR",
37
+ "ENDPOINT_HEALTH",
38
+ "ENDPOINT_HEALTH_READY",
39
+ "ENDPOINT_GENERATORS",
40
+ "ENDPOINT_GENERATOR_SCHEMA_TEMPLATE",
41
+ "ENDPOINT_DATASETS",
42
+ "ENDPOINT_DATASETS_VERSIONS",
43
+ "ENDPOINT_DATASETS_LATEST",
44
+ "ENDPOINT_DATASET_BY_ID_TEMPLATE",
45
+ "ENDPOINT_DATASET_ARTIFACT_TEMPLATE",
46
+ ]
47
+
18
48
  # ─── Service Configuration ───────────────────────────────────────────────────
19
49
 
20
50
  DEFAULT_BASE_URL: str = "http://localhost:8100"
@@ -53,6 +83,11 @@ HEALTH_READY_STATUS: str = "ready"
53
83
 
54
84
  API_KEY_HEADER_NAME: str = "X-API-Key"
55
85
  API_KEY_ENV_VAR: str = "JUNIPER_DATA_API_KEY"
86
+ # Docker-secret indirection: when set, points at a file whose stripped contents are the
87
+ # API key (e.g. /run/secrets/juniper_data_api_keys). The client honors this before the
88
+ # plain API_KEY_ENV_VAR, so a deployment that mounts the key as a file (and sets only the
89
+ # _FILE form) still authenticates -- mirrors how the Juniper services read their secrets.
90
+ API_KEY_FILE_ENV_VAR: str = f"{API_KEY_ENV_VAR}_FILE"
56
91
 
57
92
  # ─── REST Endpoints ──────────────────────────────────────────────────────────
58
93
 
@@ -215,3 +250,28 @@ FAKE_SERVICE_STATUS: str = "ok"
215
250
  FAKE_SERVICE_NAME: str = "juniper-data"
216
251
  FAKE_SERVICE_VERSION: str = "fake"
217
252
  FAKE_SERVICE_UPTIME_SECONDS: float = 0.0
253
+
254
+ # ─── NPZ Artifact Contract (WS-1 / juniper-data#168) ─────────────────────────
255
+
256
+ # Split suffixes used for every per-split NPZ key (suffix a stem with
257
+ # f"{stem}_{split}", e.g. f"{NPZ_KEY_X}_train").
258
+ NPZ_SPLITS: Tuple[str, ...] = ("train", "test", "full")
259
+
260
+ # Canonical per-split key stems.
261
+ NPZ_KEY_X: str = "X" # features: (N, F) tabular or (W, L, F) sequence
262
+ NPZ_KEY_Y: str = "y" # one-hot classification target
263
+ NPZ_KEY_Y_REG: str = "y_reg" # regression target (e.g. next-day close)
264
+ NPZ_KEY_T: str = "t" # absolute per-step time (sequence)
265
+ NPZ_KEY_DT: str = "dt" # per-step elapsed time / Δt (sequence)
266
+ NPZ_KEY_TARGET_DT: str = "target_dt" # irregular forecast horizon (sequence)
267
+ NPZ_KEY_OBSERVED_MASK: str = "observed_mask" # 1=real, 0=imputed (sequence)
268
+ NPZ_KEY_PADDING_MASK: str = "padding_mask" # 1=valid, 0=structural padding (sequence)
269
+ NPZ_KEY_SEQ_LENGTHS: str = "seq_lengths" # valid step count per window (sequence)
270
+
271
+ # Contract discriminators returned by ``validate_npz_contract``.
272
+ CONTRACT_KIND_TABULAR: str = "tabular"
273
+ CONTRACT_KIND_SEQUENCE: str = "sequence"
274
+
275
+ # ``task_type`` values carried in dataset metadata (meta.json, not the NPZ).
276
+ TASK_TYPE_CLASSIFICATION: str = "classification"
277
+ TASK_TYPE_REGRESSION: str = "regression"
@@ -0,0 +1,125 @@
1
+ """NPZ contract validation for tabular and 3-D sequence dataset artifacts.
2
+
3
+ WS-1 (juniper-data#168) adds an additive, ``X.ndim``-dispatched NPZ contract: a
4
+ 2-D ``X`` is the legacy tabular artifact (unchanged), while a 3-D ``X``
5
+ ``(W, L, F)`` is a time-series / irregular-Δt sequence artifact carrying a
6
+ per-step ``dt`` (or absolute ``t``) channel plus optional ``observed_mask`` /
7
+ ``padding_mask``.
8
+
9
+ ``validate_npz_contract`` classifies a loaded artifact as ``"tabular"`` or
10
+ ``"sequence"`` and enforces the sequence rules: at least one of ``t`` / ``dt``;
11
+ ``dt >= 0`` with ``dt[:, 0] == 0``; consistent ``t`` / ``dt``; binary masks of the
12
+ right shape; and ``observed_mask`` only meaningful where ``padding_mask == 1``.
13
+ The 2-D path is untouched and returns immediately.
14
+
15
+ Reference: ``juniper-ml/notes/JUNIPER_RECURSE_DELTA_T_HANDLING_2026-06-05.md`` §6.
16
+
17
+ Project: Juniper
18
+ Sub-Project: juniper-data-client
19
+ Application: JuniperDataClient
20
+ Author: Paul Calnon
21
+ Version: 0.4.1
22
+ License: MIT License
23
+ """
24
+
25
+ from typing import Dict
26
+
27
+ import numpy as np
28
+
29
+ from juniper_data_client.constants import (
30
+ CONTRACT_KIND_SEQUENCE,
31
+ CONTRACT_KIND_TABULAR,
32
+ NPZ_KEY_DT,
33
+ NPZ_KEY_OBSERVED_MASK,
34
+ NPZ_KEY_PADDING_MASK,
35
+ NPZ_KEY_T,
36
+ NPZ_KEY_X,
37
+ NPZ_SPLITS,
38
+ )
39
+
40
+
41
+ def validate_npz_contract(arrays: Dict[str, np.ndarray], *, dt_atol: float = 1e-6) -> str:
42
+ """Classify and validate a loaded NPZ artifact's contract.
43
+
44
+ Args:
45
+ arrays: NPZ array mapping (e.g. the dict returned by
46
+ :meth:`JuniperDataClient.download_artifact_npz`). An
47
+ ``np.lib.npyio.NpzFile`` also works (it supports ``in`` / ``[]``).
48
+ dt_atol: absolute tolerance for the ``t`` / ``dt`` consistency check.
49
+
50
+ Returns:
51
+ ``"tabular"`` for a 2-D ``X`` (legacy path, no further checks), or
52
+ ``"sequence"`` for a validated 3-D artifact.
53
+
54
+ Raises:
55
+ ValueError: if ``X`` is neither 2-D nor 3-D, or any 3-D sequence rule is
56
+ violated (missing ``t`` / ``dt``; bad ``dt`` shape, sign, or
57
+ ``dt[:, 0]``; inconsistent ``t`` / ``dt``; a non-binary or mis-shaped
58
+ mask; or ``observed_mask`` set on a padded step).
59
+ """
60
+ x_full_key = f"{NPZ_KEY_X}_full"
61
+ x_train_key = f"{NPZ_KEY_X}_train"
62
+ x = arrays[x_full_key] if x_full_key in arrays else arrays[x_train_key]
63
+ if x.ndim == 2:
64
+ return CONTRACT_KIND_TABULAR
65
+ if x.ndim != 3:
66
+ raise ValueError(f"X must be 2-D (tabular) or 3-D (sequence), got {x.ndim}-D")
67
+
68
+ for split in NPZ_SPLITS:
69
+ x_key = f"{NPZ_KEY_X}_{split}"
70
+ if x_key in arrays:
71
+ xs = arrays[x_key]
72
+ _validate_sequence_split(arrays, split, int(xs.shape[0]), int(xs.shape[1]), dt_atol)
73
+ return CONTRACT_KIND_SEQUENCE
74
+
75
+
76
+ def _validate_sequence_split(arrays: Dict[str, np.ndarray], split: str, n_windows: int, lookback: int, dt_atol: float) -> None:
77
+ """Enforce the 3-D sequence rules for one split's keys."""
78
+ t_key = f"{NPZ_KEY_T}_{split}"
79
+ dt_key = f"{NPZ_KEY_DT}_{split}"
80
+ has_t = t_key in arrays
81
+ has_dt = dt_key in arrays
82
+ if not (has_t or has_dt):
83
+ raise ValueError(f"{split}: a 3-D artifact needs at least one of {t_key!r} / {dt_key!r}")
84
+ if has_dt:
85
+ _validate_dt(arrays[dt_key], n_windows, lookback, dt_key)
86
+ if has_t and has_dt:
87
+ _validate_t_dt_consistency(arrays[t_key], arrays[dt_key], split, dt_atol)
88
+ _validate_masks(arrays, split, n_windows, lookback)
89
+
90
+
91
+ def _validate_dt(dt: np.ndarray, n_windows: int, lookback: int, dt_key: str) -> None:
92
+ """``dt`` must be ``(W, L)``, non-negative, with a zero first column."""
93
+ if dt.shape != (n_windows, lookback):
94
+ raise ValueError(f"{dt_key} shape {dt.shape} != {(n_windows, lookback)}")
95
+ if np.any(dt < 0):
96
+ raise ValueError(f"{dt_key} has negative gaps")
97
+ if n_windows and np.any(dt[:, 0] != 0):
98
+ raise ValueError(f"{dt_key}[:, 0] must be 0 by convention")
99
+
100
+
101
+ def _validate_t_dt_consistency(t: np.ndarray, dt: np.ndarray, split: str, dt_atol: float) -> None:
102
+ """When both ``t`` and ``dt`` are present they must agree to tolerance."""
103
+ recon = np.zeros_like(t)
104
+ recon[:, 1:] = np.diff(t, axis=1)
105
+ if not np.allclose(recon, dt, atol=dt_atol):
106
+ raise ValueError(f"{split}: t_ and dt_ are inconsistent")
107
+
108
+
109
+ def _validate_masks(arrays: Dict[str, np.ndarray], split: str, n_windows: int, lookback: int) -> None:
110
+ """Masks (if present) must be binary, correctly shaped, and consistent."""
111
+ for mask_key in (f"{NPZ_KEY_OBSERVED_MASK}_{split}", f"{NPZ_KEY_PADDING_MASK}_{split}"):
112
+ if mask_key in arrays:
113
+ mask = arrays[mask_key]
114
+ if mask.shape != (n_windows, lookback):
115
+ raise ValueError(f"{mask_key} shape {mask.shape} != {(n_windows, lookback)}")
116
+ if not np.isin(mask, (0, 1)).all():
117
+ raise ValueError(f"{mask_key} must be binary (0/1)")
118
+
119
+ observed_key = f"{NPZ_KEY_OBSERVED_MASK}_{split}"
120
+ padding_key = f"{NPZ_KEY_PADDING_MASK}_{split}"
121
+ if observed_key in arrays and padding_key in arrays:
122
+ observed = arrays[observed_key]
123
+ padding = arrays[padding_key]
124
+ if np.any((padding == 0) & (observed == 1)):
125
+ raise ValueError(f"{split}: observed_mask=1 on a padded (padding_mask=0) step")