juniper-data-client 0.4.2__tar.gz → 0.5.0__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 (46) hide show
  1. juniper_data_client-0.5.0/PKG-INFO +119 -0
  2. juniper_data_client-0.5.0/README.md +74 -0
  3. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/juniper_data_client/__init__.py +7 -2
  4. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/juniper_data_client/client.py +98 -6
  5. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/juniper_data_client/constants.py +194 -6
  6. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/juniper_data_client/contract.py +30 -20
  7. juniper_data_client-0.5.0/juniper_data_client/exceptions.py +101 -0
  8. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/juniper_data_client/testing/__init__.py +1 -1
  9. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/juniper_data_client/testing/fake_client.py +37 -18
  10. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/juniper_data_client/testing/generators.py +28 -17
  11. juniper_data_client-0.5.0/juniper_data_client.egg-info/PKG-INFO +119 -0
  12. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/juniper_data_client.egg-info/SOURCES.txt +7 -0
  13. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/pyproject.toml +2 -2
  14. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/tests/test_client.py +375 -0
  15. juniper_data_client-0.5.0/tests/test_client_coverage_gaps.py +218 -0
  16. juniper_data_client-0.5.0/tests/test_constants_all_drift.py +59 -0
  17. juniper_data_client-0.5.0/tests/test_contract.py +279 -0
  18. juniper_data_client-0.5.0/tests/test_create_dataset_signature.py +55 -0
  19. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/tests/test_fake_client.py +41 -36
  20. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/tests/test_fake_client_batch.py +3 -2
  21. juniper_data_client-0.5.0/tests/test_file_header_versions.py +57 -0
  22. juniper_data_client-0.5.0/tests/test_generator_parity.py +285 -0
  23. juniper_data_client-0.5.0/tests/test_memory_budget_check.py +462 -0
  24. juniper_data_client-0.5.0/tests/test_npz_val_partition.py +208 -0
  25. juniper_data_client-0.5.0/tests/test_retry_policy.py +110 -0
  26. juniper_data_client-0.5.0/tests/test_version_lockstep.py +108 -0
  27. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/tests/test_versioning.py +1 -1
  28. juniper_data_client-0.4.2/PKG-INFO +0 -194
  29. juniper_data_client-0.4.2/README.md +0 -149
  30. juniper_data_client-0.4.2/juniper_data_client/exceptions.py +0 -37
  31. juniper_data_client-0.4.2/juniper_data_client.egg-info/PKG-INFO +0 -194
  32. juniper_data_client-0.4.2/tests/test_contract.py +0 -116
  33. juniper_data_client-0.4.2/tests/test_generator_parity.py +0 -135
  34. juniper_data_client-0.4.2/tests/test_retry_policy.py +0 -54
  35. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/LICENSE +0 -0
  36. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/juniper_data_client/py.typed +0 -0
  37. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/juniper_data_client.egg-info/dependency_links.txt +0 -0
  38. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/juniper_data_client.egg-info/requires.txt +0 -0
  39. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/juniper_data_client.egg-info/top_level.txt +0 -0
  40. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/setup.cfg +0 -0
  41. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/tests/test_create_dataset_tags_ttl.py +0 -0
  42. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/tests/test_malformed_json_response.py +0 -0
  43. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/tests/test_npzfile_resource_lifecycle.py +0 -0
  44. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/tests/test_on_request_hook.py +0 -0
  45. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/tests/test_performance.py +0 -0
  46. {juniper_data_client-0.4.2 → juniper_data_client-0.5.0}/tests/test_x_request_id_propagation.py +0 -0
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.4
2
+ Name: juniper-data-client
3
+ Version: 0.5.0
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
+ # juniper-data-client
47
+
48
+ [![PyPI](https://img.shields.io/pypi/v/juniper-data-client)](https://pypi.org/project/juniper-data-client/)
49
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue)](https://www.python.org/)
50
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
51
+
52
+ **Python HTTP client for the juniper-data dataset-generation service.**
53
+
54
+ `juniper-data-client` is a thin, synchronous HTTP client for fetching datasets from a `juniper-data`
55
+ instance. It exposes a single `JuniperDataClient` class whose methods correspond to the REST surface:
56
+ generator schemas, dataset creation, tag-based filtering, batch operations, the named-version
57
+ registry, and download of the resulting NPZ artifacts as ready-to-train `numpy` arrays. It is the
58
+ canonical dataset-fetch interface used by `juniper-cascor` (training) and `juniper-canopy`
59
+ (visualisation), and it reads the platform's shared `X_train` / `y_train` / `X_val` / `y_val` /
60
+ `X_test` / `y_test` NPZ schema (all `float32`). `val` is
61
+ presence-conditional: `validate_npz_contract` skips a split the artifact does not
62
+ carry. Fake producers emit it (#187); live artifacts may omit it.
63
+
64
+ > **Part of the Juniper platform.** juniper-data-client is the Python HTTP client for the juniper-data
65
+ > dataset service in [Juniper](https://github.com/pcalnon/juniper-ml) — a multi-package ML research
66
+ > platform built around constructive (Cascade-Correlation) and recurrent neural networks. Point it at
67
+ > any running juniper-data instance.
68
+
69
+ ## Install
70
+
71
+ ```bash
72
+ pip install juniper-data-client
73
+ ```
74
+
75
+ Optional extras: `[observability]` adds X-Request-ID propagation via `juniper-observability`; `[test]`
76
+ and `[dev]` install the test and lint/type-check toolchains.
77
+
78
+ ## Quick start
79
+
80
+ ```python
81
+ from juniper_data_client import JuniperDataClient
82
+
83
+ with JuniperDataClient("http://localhost:8100") as client:
84
+ print(client.health_check()["status"])
85
+
86
+ result = client.create_spiral_dataset(n_spirals=2, n_points_per_spiral=100, noise=0.1, seed=42)
87
+ arrays = client.download_artifact_npz(result["dataset_id"])
88
+
89
+ print("train:", len(arrays["X_train"]), "test:", len(arrays["X_test"]))
90
+ ```
91
+
92
+ The artifacts come back as a dict of `float32` `numpy` arrays, ready for `torch.from_numpy(...)`.
93
+
94
+ ## API
95
+
96
+ | Method | Purpose |
97
+ |--------|---------|
98
+ | `health_check()` / `is_ready()` / `wait_for_ready(...)` | Liveness / readiness probes |
99
+ | `list_generators()` / `get_generator_schema(name)` | Discover available generators + their schemas |
100
+ | `create_dataset(generator, params, ...)` | Create a dataset from any generator |
101
+ | `create_spiral_dataset(...)` | Convenience wrapper for the spiral generator |
102
+ | `download_artifact_npz(dataset_id)` / `download_artifact_bytes(dataset_id)` | Download as numpy arrays / raw bytes |
103
+ | `get_dataset_metadata(id)` / `get_preview(id, n)` / `delete_dataset(id)` | Inspect / preview / delete |
104
+ | `list_datasets(limit, offset)` | Paginated dataset listing |
105
+ | `list_versions(name)` / `get_latest(name)` | Named-version registry |
106
+ | `batch_create(...)` / `batch_delete(...)` / `batch_update_tags(...)` / `batch_export(...)` | Batch operations |
107
+
108
+ ## Status
109
+
110
+ **Beta** on PyPI. The current version is shown by the badge above; see [`CHANGELOG.md`](./CHANGELOG.md).
111
+
112
+ ## Documentation
113
+
114
+ - [`docs/QUICK_START.md`](./docs/QUICK_START.md) — installation and verification guide
115
+ - [`docs/REFERENCE.md`](./docs/REFERENCE.md) — full API reference, configuration, and error model
116
+
117
+ ## License
118
+
119
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,74 @@
1
+ # juniper-data-client
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/juniper-data-client)](https://pypi.org/project/juniper-data-client/)
4
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue)](https://www.python.org/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
6
+
7
+ **Python HTTP client for the juniper-data dataset-generation service.**
8
+
9
+ `juniper-data-client` is a thin, synchronous HTTP client for fetching datasets from a `juniper-data`
10
+ instance. It exposes a single `JuniperDataClient` class whose methods correspond to the REST surface:
11
+ generator schemas, dataset creation, tag-based filtering, batch operations, the named-version
12
+ registry, and download of the resulting NPZ artifacts as ready-to-train `numpy` arrays. It is the
13
+ canonical dataset-fetch interface used by `juniper-cascor` (training) and `juniper-canopy`
14
+ (visualisation), and it reads the platform's shared `X_train` / `y_train` / `X_val` / `y_val` /
15
+ `X_test` / `y_test` NPZ schema (all `float32`). `val` is
16
+ presence-conditional: `validate_npz_contract` skips a split the artifact does not
17
+ carry. Fake producers emit it (#187); live artifacts may omit it.
18
+
19
+ > **Part of the Juniper platform.** juniper-data-client is the Python HTTP client for the juniper-data
20
+ > dataset service in [Juniper](https://github.com/pcalnon/juniper-ml) — a multi-package ML research
21
+ > platform built around constructive (Cascade-Correlation) and recurrent neural networks. Point it at
22
+ > any running juniper-data instance.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install juniper-data-client
28
+ ```
29
+
30
+ Optional extras: `[observability]` adds X-Request-ID propagation via `juniper-observability`; `[test]`
31
+ and `[dev]` install the test and lint/type-check toolchains.
32
+
33
+ ## Quick start
34
+
35
+ ```python
36
+ from juniper_data_client import JuniperDataClient
37
+
38
+ with JuniperDataClient("http://localhost:8100") as client:
39
+ print(client.health_check()["status"])
40
+
41
+ result = client.create_spiral_dataset(n_spirals=2, n_points_per_spiral=100, noise=0.1, seed=42)
42
+ arrays = client.download_artifact_npz(result["dataset_id"])
43
+
44
+ print("train:", len(arrays["X_train"]), "test:", len(arrays["X_test"]))
45
+ ```
46
+
47
+ The artifacts come back as a dict of `float32` `numpy` arrays, ready for `torch.from_numpy(...)`.
48
+
49
+ ## API
50
+
51
+ | Method | Purpose |
52
+ |--------|---------|
53
+ | `health_check()` / `is_ready()` / `wait_for_ready(...)` | Liveness / readiness probes |
54
+ | `list_generators()` / `get_generator_schema(name)` | Discover available generators + their schemas |
55
+ | `create_dataset(generator, params, ...)` | Create a dataset from any generator |
56
+ | `create_spiral_dataset(...)` | Convenience wrapper for the spiral generator |
57
+ | `download_artifact_npz(dataset_id)` / `download_artifact_bytes(dataset_id)` | Download as numpy arrays / raw bytes |
58
+ | `get_dataset_metadata(id)` / `get_preview(id, n)` / `delete_dataset(id)` | Inspect / preview / delete |
59
+ | `list_datasets(limit, offset)` | Paginated dataset listing |
60
+ | `list_versions(name)` / `get_latest(name)` | Named-version registry |
61
+ | `batch_create(...)` / `batch_delete(...)` / `batch_update_tags(...)` / `batch_export(...)` | Batch operations |
62
+
63
+ ## Status
64
+
65
+ **Beta** on PyPI. The current version is shown by the badge above; see [`CHANGELOG.md`](./CHANGELOG.md).
66
+
67
+ ## Documentation
68
+
69
+ - [`docs/QUICK_START.md`](./docs/QUICK_START.md) — installation and verification guide
70
+ - [`docs/REFERENCE.md`](./docs/REFERENCE.md) — full API reference, configuration, and error model
71
+
72
+ ## License
73
+
74
+ MIT — see [LICENSE](./LICENSE).
@@ -5,10 +5,11 @@ 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.constants import ContractKind
8
9
  from juniper_data_client.contract import validate_npz_contract
9
- from juniper_data_client.exceptions import JuniperDataClientError, JuniperDataConfigurationError, JuniperDataConnectionError, JuniperDataNotFoundError, JuniperDataTimeoutError, JuniperDataValidationError
10
+ from juniper_data_client.exceptions import JuniperDataClientError, JuniperDataConfigurationError, JuniperDataConnectionError, JuniperDataContractError, JuniperDataNotFoundError, JuniperDataTimeoutError, JuniperDataValidationError
10
11
 
11
- __version__ = "0.4.2"
12
+ __version__ = "0.5.0"
12
13
 
13
14
  __all__ = [
14
15
  "JuniperDataClient",
@@ -16,11 +17,15 @@ __all__ = [
16
17
  "JuniperDataClientError",
17
18
  "JuniperDataConfigurationError",
18
19
  "JuniperDataConnectionError",
20
+ "JuniperDataContractError",
19
21
  "JuniperDataNotFoundError",
20
22
  "JuniperDataTimeoutError",
21
23
  "JuniperDataValidationError",
22
24
  # METRICS-MON R4.3 / seed-13: instrumentation hook type alias
23
25
  # exported so consumers can type their hook closures.
24
26
  "RequestHook",
27
+ # APD-DCLIENT-006: the validate_npz_contract return Literal, exported so
28
+ # consumers can annotate and exhaustively match on the result.
29
+ "ContractKind",
25
30
  "__version__",
26
31
  ]
@@ -23,6 +23,7 @@ from juniper_data_client.constants import (
23
23
  API_KEY_HEADER_NAME,
24
24
  API_VERSION_PATH_SUFFIX,
25
25
  DEFAULT_BACKOFF_FACTOR,
26
+ DEFAULT_BACKOFF_JITTER,
26
27
  DEFAULT_BASE_URL,
27
28
  DEFAULT_LIST_LIMIT,
28
29
  DEFAULT_LIST_OFFSET,
@@ -62,7 +63,7 @@ from juniper_data_client.constants import (
62
63
  SPIRAL_TRAIN_RATIO_DEFAULT,
63
64
  URL_SCHEME_PREFIXES,
64
65
  )
65
- from juniper_data_client.exceptions import JuniperDataClientError, JuniperDataConnectionError, JuniperDataNotFoundError, JuniperDataTimeoutError, JuniperDataValidationError
66
+ from juniper_data_client.exceptions import JuniperDataClientError, JuniperDataConfigurationError, JuniperDataConnectionError, JuniperDataNotFoundError, JuniperDataTimeoutError, JuniperDataValidationError
66
67
 
67
68
  logger = logging.getLogger("juniper_data_client.client")
68
69
 
@@ -122,6 +123,42 @@ def _noop_request_hook(
122
123
  """
123
124
 
124
125
 
126
+ def _render_error_detail(detail: Any) -> str:
127
+ """Render a server ``detail`` payload as readable text for an error message.
128
+
129
+ FastAPI answers a validation failure with a *list* of error objects, e.g.::
130
+
131
+ [{"type": "missing", "loc": ["body", "seed"], "msg": "Field required"}]
132
+
133
+ Interpolating that list into an f-string produced a Python repr -- valid
134
+ text, but nothing a caller could parse and nothing a human could read at a
135
+ glance (defect-register ``APD-DCLIENT-003``). This renders the same content
136
+ as ``body.seed: Field required``, while the untouched structure stays
137
+ available on ``exc.detail``.
138
+
139
+ Anything that is not a list of mappings is returned via ``str`` unchanged,
140
+ so plain-string details and unexpected shapes both survive intact.
141
+ """
142
+ if not isinstance(detail, list):
143
+ return str(detail)
144
+
145
+ rendered: list[str] = []
146
+ for item in detail:
147
+ if not isinstance(item, dict):
148
+ rendered.append(str(item))
149
+ continue
150
+ msg = item.get("msg", "")
151
+ loc = item.get("loc")
152
+ if isinstance(loc, (list, tuple)) and loc:
153
+ location = ".".join(str(part) for part in loc)
154
+ rendered.append(f"{location}: {msg}" if msg else location)
155
+ else:
156
+ rendered.append(str(msg) if msg else str(item))
157
+ # An empty list is a degenerate but legal payload; ``str`` keeps it visible
158
+ # rather than collapsing it to an empty message.
159
+ return "; ".join(rendered) if rendered else str(detail)
160
+
161
+
125
162
  class JuniperDataClient:
126
163
  """Client for interacting with the JuniperData REST API.
127
164
 
@@ -185,13 +222,28 @@ class JuniperDataClient:
185
222
 
186
223
  Returns:
187
224
  Normalized URL with scheme, no trailing slash, no /v1 suffix
225
+
226
+ Raises:
227
+ JuniperDataConfigurationError: when the URL carries no host (an
228
+ empty string, a bare scheme, a path-only value, or a
229
+ userinfo-only authority like ``http://user:secret@``) — a
230
+ misconfiguration that would otherwise normalize to a broken,
231
+ hostless URL and fail opaquely on the first request
232
+ (``APD-DCLIENT-004``; mirrors juniper-recurrence-client).
188
233
  """
189
234
  url = url.strip()
190
235
 
191
- if not url.startswith(URL_SCHEME_PREFIXES):
236
+ # Scheme matching is case-insensitive (RFC 3986 §3.1): a case-sensitive
237
+ # startswith would re-prefix "HTTPS://host" into "http://HTTPS://host",
238
+ # silently downgrading TLS and sending the API key to hostname "https".
239
+ if not url.lower().startswith(URL_SCHEME_PREFIXES):
192
240
  url = f"{DEFAULT_URL_SCHEME_PREFIX}{url}"
193
241
 
194
242
  parsed = urlparse(url)
243
+ # hostname, not netloc: netloc accepts a userinfo-only authority
244
+ # ("user:secret@") as truthy while hostname is None for it.
245
+ if not parsed.hostname:
246
+ raise JuniperDataConfigurationError(f"base_url must include a host; got {url!r}")
195
247
  normalized = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
196
248
  normalized = normalized.rstrip("/")
197
249
 
@@ -211,6 +263,8 @@ class JuniperDataClient:
211
263
  retry_strategy = Retry(
212
264
  total=self.retries,
213
265
  backoff_factor=self.backoff_factor,
266
+ # APD-ECO-002: decorrelate retry schedules across client instances.
267
+ backoff_jitter=DEFAULT_BACKOFF_JITTER,
214
268
  status_forcelist=RETRYABLE_STATUS_CODES,
215
269
  allowed_methods=RETRY_ALLOWED_METHODS,
216
270
  )
@@ -309,14 +363,46 @@ class JuniperDataClient:
309
363
  # fall back to using the raw response text as the error detail.
310
364
  error_detail = response.text
311
365
 
366
+ # APD-DCLIENT-003: ``error_detail`` is whatever the server sent --
367
+ # a str for most handlers, but a list[dict] for FastAPI's 422. It is
368
+ # attached to the exception UNMODIFIED, and rendered separately for
369
+ # the human-readable message. Interpolating the list directly
370
+ # produced a Python repr no caller could parse, and which lost
371
+ # nothing only because there was nowhere else for the structure to
372
+ # go. Now there is: ``exc.detail``.
373
+ rendered_detail = _render_error_detail(error_detail)
374
+
375
+ # The context is passed explicitly at each site rather than unpacked
376
+ # from a shared ``**kwargs`` dict: mypy --strict infers such a dict
377
+ # as ``dict[str, object]`` and cannot match it against the typed
378
+ # keyword-only parameters, so the dict form silently gives up the
379
+ # type checking on exactly the arguments being added here.
312
380
  if response.status_code == HTTP_404_NOT_FOUND:
313
- outgoing_error = JuniperDataNotFoundError(f"Resource not found: {error_detail}")
381
+ outgoing_error = JuniperDataNotFoundError(
382
+ f"Resource not found: {rendered_detail}",
383
+ status_code=response.status_code,
384
+ detail=error_detail,
385
+ response=response,
386
+ )
314
387
  raise outgoing_error
315
388
  elif response.status_code in (HTTP_400_BAD_REQUEST, HTTP_422_UNPROCESSABLE_ENTITY):
316
- outgoing_error = JuniperDataValidationError(f"Validation error: {error_detail}")
389
+ # APD-DCLIENT-001: the status code is what separates these two.
390
+ # It rides on the exception now, so a caller no longer has to
391
+ # substring-match the message to tell them apart.
392
+ outgoing_error = JuniperDataValidationError(
393
+ f"Validation error ({response.status_code}): {rendered_detail}",
394
+ status_code=response.status_code,
395
+ detail=error_detail,
396
+ response=response,
397
+ )
317
398
  raise outgoing_error
318
399
  else:
319
- outgoing_error = JuniperDataClientError(f"Request failed ({response.status_code}): {error_detail}")
400
+ outgoing_error = JuniperDataClientError(
401
+ f"Request failed ({response.status_code}): {rendered_detail}",
402
+ status_code=response.status_code,
403
+ detail=error_detail,
404
+ response=response,
405
+ )
320
406
  raise outgoing_error
321
407
  finally:
322
408
  duration_ms = (time.monotonic() - start) * 1000.0
@@ -413,6 +499,7 @@ class JuniperDataClient:
413
499
  self,
414
500
  generator: str,
415
501
  params: Dict[str, Any],
502
+ *,
416
503
  persist: bool = True,
417
504
  name: Optional[str] = None,
418
505
  description: Optional[str] = None,
@@ -423,6 +510,12 @@ class JuniperDataClient:
423
510
  ) -> Dict[str, Any]:
424
511
  """Create a new dataset via the JuniperData API.
425
512
 
513
+ ``persist`` and everything after it are keyword-only (defect-register
514
+ ``APD-DCLIENT-008``): with nine positional-or-keyword parameters the
515
+ call ``create_dataset("spiral", p, False)`` was legal and unreadable,
516
+ and any signature reordering would silently rebind arguments. Only
517
+ ``generator`` and ``params`` — the universal pair — stay positional.
518
+
426
519
  If a dataset with the same parameters already exists, the existing
427
520
  dataset is returned (caching behavior).
428
521
 
@@ -551,7 +644,6 @@ class JuniperDataClient:
551
644
  artifacts carry the standard keys:
552
645
  - X_train, y_train: Training features and one-hot labels
553
646
  - X_test, y_test: Test features and one-hot labels
554
- - X_full, y_full: Full dataset features and one-hot labels
555
647
 
556
648
  Additional keys may be present depending on the generator (WS-1 /
557
649
  juniper-data#168):
@@ -9,18 +9,26 @@ Project: Juniper
9
9
  Sub-Project: juniper-data-client
10
10
  Application: JuniperDataClient
11
11
  Author: Paul Calnon
12
- Version: 0.4.0
12
+ Version: 0.5.0
13
13
  License: MIT License
14
14
  """
15
15
 
16
- from typing import List, Tuple
16
+ from typing import Final, List, Literal, Tuple
17
17
 
18
+ # Complete export surface: every public module-level constant, in file order,
19
+ # grouped by the section headers below. Completeness is enforced by
20
+ # tests/test_constants_all_drift.py — a new constant that is not added here
21
+ # fails that gate (and, on any module WITH __all__, CodeQL flags touched
22
+ # non-exported globals as py/unused-global-variable, blocking the merge).
18
23
  __all__ = [
24
+ # Service Configuration
19
25
  "DEFAULT_BASE_URL",
20
26
  "FAKE_BASE_URL",
27
+ # HTTP Configuration
21
28
  "DEFAULT_TIMEOUT",
22
29
  "DEFAULT_RETRIES",
23
30
  "DEFAULT_BACKOFF_FACTOR",
31
+ "DEFAULT_BACKOFF_JITTER",
24
32
  "RETRYABLE_STATUS_CODES",
25
33
  "RETRY_ALLOWED_METHODS",
26
34
  "HTTP_POOL_CONNECTIONS",
@@ -28,12 +36,15 @@ __all__ = [
28
36
  "URL_SCHEME_PREFIXES",
29
37
  "DEFAULT_URL_SCHEME_PREFIX",
30
38
  "API_VERSION_PATH_SUFFIX",
39
+ # Readiness Polling
31
40
  "DEFAULT_READY_TIMEOUT",
32
41
  "DEFAULT_READY_POLL_INTERVAL",
33
42
  "HEALTH_READY_STATUS",
43
+ # Authentication
34
44
  "API_KEY_HEADER_NAME",
35
45
  "API_KEY_ENV_VAR",
36
46
  "API_KEY_FILE_ENV_VAR",
47
+ # REST Endpoints
37
48
  "ENDPOINT_HEALTH",
38
49
  "ENDPOINT_HEALTH_READY",
39
50
  "ENDPOINT_GENERATORS",
@@ -43,6 +54,133 @@ __all__ = [
43
54
  "ENDPOINT_DATASETS_LATEST",
44
55
  "ENDPOINT_DATASET_BY_ID_TEMPLATE",
45
56
  "ENDPOINT_DATASET_ARTIFACT_TEMPLATE",
57
+ "ENDPOINT_DATASET_PREVIEW_TEMPLATE",
58
+ "ENDPOINT_BATCH_CREATE",
59
+ "ENDPOINT_BATCH_DELETE",
60
+ "ENDPOINT_BATCH_TAGS",
61
+ "ENDPOINT_BATCH_EXPORT",
62
+ # HTTP Status Codes
63
+ "HTTP_400_BAD_REQUEST",
64
+ "HTTP_404_NOT_FOUND",
65
+ "HTTP_422_UNPROCESSABLE_ENTITY",
66
+ # Listing Defaults
67
+ "DEFAULT_LIST_LIMIT",
68
+ "DEFAULT_LIST_OFFSET",
69
+ # Preview Defaults
70
+ "DEFAULT_PREVIEW_N",
71
+ "MAX_PREVIEW_N",
72
+ # Data Type Contract
73
+ "DEFAULT_ARRAY_DTYPE",
74
+ # Generator Names
75
+ "GENERATOR_SPIRAL",
76
+ "GENERATOR_XOR",
77
+ "GENERATOR_CIRCLE",
78
+ "GENERATOR_CIRCLE_LEGACY",
79
+ "GENERATOR_MOON",
80
+ "GENERATOR_GAUSSIAN",
81
+ "GENERATOR_CHECKERBOARD",
82
+ "GENERATOR_CSV_IMPORT",
83
+ "GENERATOR_MNIST",
84
+ "GENERATOR_ARC_AGI",
85
+ "GENERATOR_EQUITIES",
86
+ "GENERATOR_EQUITIES_SEQ",
87
+ "GENERATOR_MULTI_SINE",
88
+ "GENERATOR_MACKEY_GLASS",
89
+ "GENERATOR_AR_P",
90
+ "GENERATOR_IRREGULAR_SINE",
91
+ "GENERATOR_DELAY_PRODUCT",
92
+ # Generator Catalog Metadata
93
+ "GENERATOR_VERSION",
94
+ "GENERATOR_DESCRIPTION_SPIRAL",
95
+ "GENERATOR_DESCRIPTION_XOR",
96
+ "GENERATOR_DESCRIPTION_CIRCLE",
97
+ "GENERATOR_DESCRIPTION_MOON",
98
+ "GENERATOR_DESCRIPTION_GAUSSIAN",
99
+ "GENERATOR_DESCRIPTION_CHECKERBOARD",
100
+ "GENERATOR_DESCRIPTION_CSV_IMPORT",
101
+ "GENERATOR_DESCRIPTION_MNIST",
102
+ "GENERATOR_DESCRIPTION_ARC_AGI",
103
+ "GENERATOR_DESCRIPTION_EQUITIES",
104
+ "GENERATOR_DESCRIPTION_EQUITIES_SEQ",
105
+ "GENERATOR_DESCRIPTION_MULTI_SINE",
106
+ "GENERATOR_DESCRIPTION_MACKEY_GLASS",
107
+ "GENERATOR_DESCRIPTION_AR_P",
108
+ "GENERATOR_DESCRIPTION_IRREGULAR_SINE",
109
+ "GENERATOR_DESCRIPTION_DELAY_PRODUCT",
110
+ # Generator Defaults — Spiral
111
+ "SPIRAL_N_SPIRALS_DEFAULT",
112
+ "SPIRAL_N_POINTS_PER_SPIRAL_DEFAULT",
113
+ "SPIRAL_NOISE_DEFAULT",
114
+ "SPIRAL_ALGORITHM_DEFAULT",
115
+ "SPIRAL_TRAIN_RATIO_DEFAULT",
116
+ "SPIRAL_N_SPIRALS_MIN",
117
+ "SPIRAL_N_POINTS_PER_SPIRAL_MIN",
118
+ "SPIRAL_NOISE_MIN",
119
+ "SPIRAL_TRAIN_RATIO_MIN",
120
+ "SPIRAL_TRAIN_RATIO_MAX",
121
+ "SPIRAL_ALGORITHM_MODERN",
122
+ "SPIRAL_ALGORITHM_LEGACY",
123
+ "SPIRAL_ALGORITHMS",
124
+ "SPIRAL_RADIUS_SCALE",
125
+ "SPIRAL_ANGLE_TURNS",
126
+ # Generator Defaults — XOR
127
+ "XOR_N_POINTS_DEFAULT",
128
+ "XOR_NOISE_DEFAULT",
129
+ "XOR_TRAIN_RATIO_DEFAULT",
130
+ "XOR_N_POINTS_MIN",
131
+ "XOR_NOISE_MIN",
132
+ "XOR_TRAIN_RATIO_MIN",
133
+ "XOR_TRAIN_RATIO_MAX",
134
+ "XOR_CORNERS",
135
+ "XOR_CORNER_LABELS",
136
+ "XOR_NUM_CORNERS",
137
+ "XOR_NUM_CLASSES",
138
+ # Generator Defaults — Circle
139
+ "CIRCLE_N_POINTS_DEFAULT",
140
+ "CIRCLE_NOISE_DEFAULT",
141
+ "CIRCLE_FACTOR_DEFAULT",
142
+ "CIRCLE_TRAIN_RATIO_DEFAULT",
143
+ "CIRCLE_N_POINTS_MIN",
144
+ "CIRCLE_NOISE_MIN",
145
+ "CIRCLE_FACTOR_MIN",
146
+ "CIRCLE_FACTOR_MAX",
147
+ "CIRCLE_TRAIN_RATIO_MIN",
148
+ "CIRCLE_TRAIN_RATIO_MAX",
149
+ "CIRCLE_NUM_CLASSES",
150
+ # Generator Defaults — Moon
151
+ "MOON_N_POINTS_DEFAULT",
152
+ "MOON_NOISE_DEFAULT",
153
+ "MOON_TRAIN_RATIO_DEFAULT",
154
+ "MOON_N_POINTS_MIN",
155
+ "MOON_NOISE_MIN",
156
+ "MOON_TRAIN_RATIO_MIN",
157
+ "MOON_TRAIN_RATIO_MAX",
158
+ "MOON_NUM_CLASSES",
159
+ "MOON_LOWER_X_OFFSET",
160
+ "MOON_LOWER_Y_OFFSET",
161
+ "MOON_LOWER_Y_SHIFT",
162
+ # Fake Service Identity
163
+ "FAKE_SERVICE_STATUS",
164
+ "FAKE_SERVICE_NAME",
165
+ "FAKE_SERVICE_VERSION",
166
+ "FAKE_SERVICE_UPTIME_SECONDS",
167
+ # NPZ Artifact Contract (WS-1 / juniper-data#168)
168
+ "NPZ_SPLITS",
169
+ "FAKE_VAL_RATIO_DEFAULT",
170
+ "NPZ_KEY_X",
171
+ "NPZ_KEY_Y",
172
+ "NPZ_KEY_Y_REG",
173
+ "NPZ_KEY_T",
174
+ "NPZ_KEY_DT",
175
+ "NPZ_KEY_TARGET_DT",
176
+ "NPZ_KEY_OBSERVED_MASK",
177
+ "NPZ_KEY_PADDING_MASK",
178
+ "NPZ_KEY_SEQ_LENGTHS",
179
+ "ContractKind",
180
+ "CONTRACT_KIND_TABULAR",
181
+ "CONTRACT_KIND_SEQUENCE",
182
+ "TASK_TYPE_CLASSIFICATION",
183
+ "TASK_TYPE_REGRESSION",
46
184
  ]
47
185
 
48
186
  # ─── Service Configuration ───────────────────────────────────────────────────
@@ -55,6 +193,13 @@ FAKE_BASE_URL: str = "http://fake-data:8100"
55
193
  DEFAULT_TIMEOUT: int = 30
56
194
  DEFAULT_RETRIES: int = 3
57
195
  DEFAULT_BACKOFF_FACTOR: float = 0.5
196
+ # APD-ECO-002: urllib3 applies this as an ABSOLUTE additive term --
197
+ # ``backoff_value += random.random() * backoff_jitter`` -- not a proportional
198
+ # one. Without it every client that trips the same transient outage retries on
199
+ # an identical schedule, so a service that is already failing is hit by a
200
+ # synchronised herd. Matched to DEFAULT_BACKOFF_FACTOR so the spread is a full
201
+ # window on the first retry, which is the step that carries the most callers.
202
+ DEFAULT_BACKOFF_JITTER: float = 0.5
58
203
  RETRYABLE_STATUS_CODES: List[int] = [429, 500, 502, 503, 504]
59
204
  # XREPO-11 (2026-04-24): auto-retry is now restricted to idempotent
60
205
  # HTTP methods per RFC 9110 §9.2.2. POST, PATCH and DELETE were
@@ -148,6 +293,18 @@ GENERATOR_CHECKERBOARD: str = "checkerboard"
148
293
  GENERATOR_CSV_IMPORT: str = "csv_import"
149
294
  GENERATOR_MNIST: str = "mnist"
150
295
  GENERATOR_ARC_AGI: str = "arc_agi"
296
+ # W-9 (CLI experimentation plan §11, 2026-08-08): added constants for the 7
297
+ # server generators the client still lacked — the equities pair plus the five
298
+ # sequence generators (juniper-data ≥ 0.8/0.9). The parity test now also
299
+ # cross-checks the pinned mirror against the LIVE server registry whenever
300
+ # juniper-data is importable, so this list can no longer drift silently.
301
+ GENERATOR_EQUITIES: str = "equities"
302
+ GENERATOR_EQUITIES_SEQ: str = "equities_seq"
303
+ GENERATOR_MULTI_SINE: str = "multi_sine"
304
+ GENERATOR_MACKEY_GLASS: str = "mackey_glass"
305
+ GENERATOR_AR_P: str = "ar_p"
306
+ GENERATOR_IRREGULAR_SINE: str = "irregular_sine"
307
+ GENERATOR_DELAY_PRODUCT: str = "delay_product"
151
308
 
152
309
  # ─── Generator Catalog Metadata ──────────────────────────────────────────────
153
310
 
@@ -161,6 +318,13 @@ GENERATOR_DESCRIPTION_CHECKERBOARD: str = "Checkerboard pattern classification d
161
318
  GENERATOR_DESCRIPTION_CSV_IMPORT: str = "CSV/JSON import for custom datasets"
162
319
  GENERATOR_DESCRIPTION_MNIST: str = "MNIST and Fashion-MNIST digit classification dataset"
163
320
  GENERATOR_DESCRIPTION_ARC_AGI: str = "ARC-AGI visual reasoning tasks dataset"
321
+ GENERATOR_DESCRIPTION_EQUITIES: str = "Real equities OHLCV tabular dataset (next-day direction)"
322
+ GENERATOR_DESCRIPTION_EQUITIES_SEQ: str = "Real equities irregular-Δt sequence dataset (calendar-gap Δt)"
323
+ GENERATOR_DESCRIPTION_MULTI_SINE: str = "Regular-Δt superimposed-sinusoid sequence dataset"
324
+ GENERATOR_DESCRIPTION_MACKEY_GLASS: str = "Regular-Δt Mackey-Glass chaotic sequence dataset"
325
+ GENERATOR_DESCRIPTION_AR_P: str = "Regular-Δt stable AR(p) linear-stochastic sequence dataset"
326
+ GENERATOR_DESCRIPTION_IRREGULAR_SINE: str = "Irregular-Δt superimposed-sinusoid sequence dataset"
327
+ GENERATOR_DESCRIPTION_DELAY_PRODUCT: str = "Irregular-Δt bilinear delay-product capacity sequence dataset"
164
328
 
165
329
  # ─── Generator Defaults — Spiral ─────────────────────────────────────────────
166
330
 
@@ -255,7 +419,27 @@ FAKE_SERVICE_UPTIME_SECONDS: float = 0.0
255
419
 
256
420
  # Split suffixes used for every per-split NPZ key (suffix a stem with
257
421
  # f"{stem}_{split}", e.g. f"{NPZ_KEY_X}_train").
258
- NPZ_SPLITS: Tuple[str, ...] = ("train", "test", "full")
422
+ #
423
+ # "val" is the in-loop validation partition of the three-way train/val/test
424
+ # contract (design decision O-1). Membership here is presence-conditional --
425
+ # validate_npz_contract skips any split a given artifact does not carry -- so
426
+ # listing it does not require an artifact to provide it, and two-partition
427
+ # legacy artifacts keep validating unchanged.
428
+ #
429
+ # "full" is GONE (decision 11, 2026-09-05). The *_full family is no longer part of
430
+ # the contract: generators do not emit it and no consumer may require it.
431
+ #
432
+ # Removing it from this tuple does NOT stop a legacy artifact loading. Every stored
433
+ # artifact still carries X_full, and validate_npz_contract skips any split it does
434
+ # not list -- so an X_full that is present is simply not validated, which is what
435
+ # "tolerate, do not require" means in practice. Adding it back to get it validated
436
+ # would re-create the requirement this decision removed.
437
+ NPZ_SPLITS: Tuple[str, ...] = ("train", "val", "test")
438
+
439
+ # Validation share used by the synthetic generators in juniper_data_client.testing.
440
+ # Applied alongside each generator's train ratio, so the default 0.8 / 0.1 carve
441
+ # leaves the remainder to test.
442
+ FAKE_VAL_RATIO_DEFAULT: float = 0.1
259
443
 
260
444
  # Canonical per-split key stems.
261
445
  NPZ_KEY_X: str = "X" # features: (N, F) tabular or (W, L, F) sequence
@@ -268,9 +452,13 @@ NPZ_KEY_OBSERVED_MASK: str = "observed_mask" # 1=real, 0=imputed (sequence)
268
452
  NPZ_KEY_PADDING_MASK: str = "padding_mask" # 1=valid, 0=structural padding (sequence)
269
453
  NPZ_KEY_SEQ_LENGTHS: str = "seq_lengths" # valid step count per window (sequence)
270
454
 
271
- # Contract discriminators returned by ``validate_npz_contract``.
272
- CONTRACT_KIND_TABULAR: str = "tabular"
273
- CONTRACT_KIND_SEQUENCE: str = "sequence"
455
+ # Contract discriminators returned by ``validate_npz_contract``. The Literal
456
+ # lets a caller exhaustively match on the result (APD-DCLIENT-006); annotating
457
+ # the constants with Final[ContractKind] keeps their values and the Literal's
458
+ # members from drifting apart -- a mismatch fails type checking.
459
+ ContractKind = Literal["tabular", "sequence"]
460
+ CONTRACT_KIND_TABULAR: Final[ContractKind] = "tabular"
461
+ CONTRACT_KIND_SEQUENCE: Final[ContractKind] = "sequence"
274
462
 
275
463
  # ``task_type`` values carried in dataset metadata (meta.json, not the NPZ).
276
464
  TASK_TYPE_CLASSIFICATION: str = "classification"