juniper-data-client 0.4.1__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.
- juniper_data_client-0.5.0/PKG-INFO +119 -0
- juniper_data_client-0.5.0/README.md +74 -0
- {juniper_data_client-0.4.1 → juniper_data_client-0.5.0}/juniper_data_client/__init__.py +9 -2
- {juniper_data_client-0.4.1 → juniper_data_client-0.5.0}/juniper_data_client/client.py +136 -9
- juniper_data_client-0.5.0/juniper_data_client/constants.py +465 -0
- juniper_data_client-0.5.0/juniper_data_client/contract.py +135 -0
- juniper_data_client-0.5.0/juniper_data_client/exceptions.py +101 -0
- {juniper_data_client-0.4.1 → juniper_data_client-0.5.0}/juniper_data_client/testing/__init__.py +1 -1
- {juniper_data_client-0.4.1 → juniper_data_client-0.5.0}/juniper_data_client/testing/fake_client.py +54 -20
- {juniper_data_client-0.4.1 → juniper_data_client-0.5.0}/juniper_data_client/testing/generators.py +28 -17
- juniper_data_client-0.5.0/juniper_data_client.egg-info/PKG-INFO +119 -0
- {juniper_data_client-0.4.1 → juniper_data_client-0.5.0}/juniper_data_client.egg-info/SOURCES.txt +9 -0
- {juniper_data_client-0.4.1 → juniper_data_client-0.5.0}/pyproject.toml +2 -2
- {juniper_data_client-0.4.1 → juniper_data_client-0.5.0}/tests/test_client.py +403 -1
- juniper_data_client-0.5.0/tests/test_client_coverage_gaps.py +218 -0
- juniper_data_client-0.5.0/tests/test_constants_all_drift.py +59 -0
- juniper_data_client-0.5.0/tests/test_contract.py +279 -0
- juniper_data_client-0.5.0/tests/test_create_dataset_signature.py +55 -0
- {juniper_data_client-0.4.1 → juniper_data_client-0.5.0}/tests/test_fake_client.py +63 -39
- {juniper_data_client-0.4.1 → juniper_data_client-0.5.0}/tests/test_fake_client_batch.py +3 -2
- juniper_data_client-0.5.0/tests/test_file_header_versions.py +57 -0
- juniper_data_client-0.5.0/tests/test_generator_parity.py +285 -0
- juniper_data_client-0.5.0/tests/test_memory_budget_check.py +462 -0
- juniper_data_client-0.5.0/tests/test_npz_val_partition.py +208 -0
- juniper_data_client-0.5.0/tests/test_retry_policy.py +110 -0
- juniper_data_client-0.5.0/tests/test_version_lockstep.py +108 -0
- {juniper_data_client-0.4.1 → juniper_data_client-0.5.0}/tests/test_versioning.py +1 -1
- juniper_data_client-0.4.1/PKG-INFO +0 -271
- juniper_data_client-0.4.1/README.md +0 -226
- juniper_data_client-0.4.1/juniper_data_client/constants.py +0 -217
- juniper_data_client-0.4.1/juniper_data_client/exceptions.py +0 -37
- juniper_data_client-0.4.1/juniper_data_client.egg-info/PKG-INFO +0 -271
- juniper_data_client-0.4.1/tests/test_generator_parity.py +0 -135
- juniper_data_client-0.4.1/tests/test_retry_policy.py +0 -54
- {juniper_data_client-0.4.1 → juniper_data_client-0.5.0}/LICENSE +0 -0
- {juniper_data_client-0.4.1 → juniper_data_client-0.5.0}/juniper_data_client/py.typed +0 -0
- {juniper_data_client-0.4.1 → juniper_data_client-0.5.0}/juniper_data_client.egg-info/dependency_links.txt +0 -0
- {juniper_data_client-0.4.1 → juniper_data_client-0.5.0}/juniper_data_client.egg-info/requires.txt +0 -0
- {juniper_data_client-0.4.1 → juniper_data_client-0.5.0}/juniper_data_client.egg-info/top_level.txt +0 -0
- {juniper_data_client-0.4.1 → juniper_data_client-0.5.0}/setup.cfg +0 -0
- {juniper_data_client-0.4.1 → juniper_data_client-0.5.0}/tests/test_create_dataset_tags_ttl.py +0 -0
- {juniper_data_client-0.4.1 → juniper_data_client-0.5.0}/tests/test_malformed_json_response.py +0 -0
- {juniper_data_client-0.4.1 → juniper_data_client-0.5.0}/tests/test_npzfile_resource_lifecycle.py +0 -0
- {juniper_data_client-0.4.1 → juniper_data_client-0.5.0}/tests/test_on_request_hook.py +0 -0
- {juniper_data_client-0.4.1 → juniper_data_client-0.5.0}/tests/test_performance.py +0 -0
- {juniper_data_client-0.4.1 → 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
|
+
[](https://pypi.org/project/juniper-data-client/)
|
|
49
|
+
[](https://www.python.org/)
|
|
50
|
+
[](./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
|
+
[](https://pypi.org/project/juniper-data-client/)
|
|
4
|
+
[](https://www.python.org/)
|
|
5
|
+
[](./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,20 +5,27 @@ 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.
|
|
8
|
+
from juniper_data_client.constants import ContractKind
|
|
9
|
+
from juniper_data_client.contract import validate_npz_contract
|
|
10
|
+
from juniper_data_client.exceptions import JuniperDataClientError, JuniperDataConfigurationError, JuniperDataConnectionError, JuniperDataContractError, JuniperDataNotFoundError, JuniperDataTimeoutError, JuniperDataValidationError
|
|
9
11
|
|
|
10
|
-
__version__ = "0.
|
|
12
|
+
__version__ = "0.5.0"
|
|
11
13
|
|
|
12
14
|
__all__ = [
|
|
13
15
|
"JuniperDataClient",
|
|
16
|
+
"validate_npz_contract",
|
|
14
17
|
"JuniperDataClientError",
|
|
15
18
|
"JuniperDataConfigurationError",
|
|
16
19
|
"JuniperDataConnectionError",
|
|
20
|
+
"JuniperDataContractError",
|
|
17
21
|
"JuniperDataNotFoundError",
|
|
18
22
|
"JuniperDataTimeoutError",
|
|
19
23
|
"JuniperDataValidationError",
|
|
20
24
|
# METRICS-MON R4.3 / seed-13: instrumentation hook type alias
|
|
21
25
|
# exported so consumers can type their hook closures.
|
|
22
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",
|
|
23
30
|
"__version__",
|
|
24
31
|
]
|
|
@@ -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,9 +19,11 @@ 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,
|
|
26
|
+
DEFAULT_BACKOFF_JITTER,
|
|
24
27
|
DEFAULT_BASE_URL,
|
|
25
28
|
DEFAULT_LIST_LIMIT,
|
|
26
29
|
DEFAULT_LIST_OFFSET,
|
|
@@ -60,11 +63,31 @@ from juniper_data_client.constants import (
|
|
|
60
63
|
SPIRAL_TRAIN_RATIO_DEFAULT,
|
|
61
64
|
URL_SCHEME_PREFIXES,
|
|
62
65
|
)
|
|
63
|
-
from juniper_data_client.exceptions import JuniperDataClientError, JuniperDataConnectionError, JuniperDataNotFoundError, JuniperDataTimeoutError, JuniperDataValidationError
|
|
66
|
+
from juniper_data_client.exceptions import JuniperDataClientError, JuniperDataConfigurationError, JuniperDataConnectionError, JuniperDataNotFoundError, JuniperDataTimeoutError, JuniperDataValidationError
|
|
64
67
|
|
|
65
68
|
logger = logging.getLogger("juniper_data_client.client")
|
|
66
69
|
|
|
67
70
|
|
|
71
|
+
def _resolve_api_key_from_env() -> Optional[str]:
|
|
72
|
+
"""Resolve the juniper-data API key from the environment.
|
|
73
|
+
|
|
74
|
+
Honors the Docker-secret ``JUNIPER_DATA_API_KEY_FILE`` indirection (a file whose
|
|
75
|
+
stripped contents are the key, e.g. ``/run/secrets/juniper_data_api_keys``) before the
|
|
76
|
+
plain ``JUNIPER_DATA_API_KEY`` env var, so a consumer that mounts the key as a file and
|
|
77
|
+
leaves ``api_key`` unset still authenticates. Mirrors how the Juniper services resolve
|
|
78
|
+
their own secrets (e.g. cascor ``api.secrets.get_secret``).
|
|
79
|
+
"""
|
|
80
|
+
file_path = os.environ.get(API_KEY_FILE_ENV_VAR)
|
|
81
|
+
if file_path:
|
|
82
|
+
try:
|
|
83
|
+
content = Path(file_path).read_text(encoding="utf-8").strip()
|
|
84
|
+
except OSError:
|
|
85
|
+
content = ""
|
|
86
|
+
if content:
|
|
87
|
+
return content
|
|
88
|
+
return os.environ.get(API_KEY_ENV_VAR)
|
|
89
|
+
|
|
90
|
+
|
|
68
91
|
# METRICS-MON R4.3 / seed-13: type alias for the optional instrumentation
|
|
69
92
|
# hook. Consumers (canopy, cascor) pass a callable matching this shape;
|
|
70
93
|
# default in :class:`JuniperDataClient` is no-op so standalone users
|
|
@@ -100,6 +123,42 @@ def _noop_request_hook(
|
|
|
100
123
|
"""
|
|
101
124
|
|
|
102
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
|
+
|
|
103
162
|
class JuniperDataClient:
|
|
104
163
|
"""Client for interacting with the JuniperData REST API.
|
|
105
164
|
|
|
@@ -151,7 +210,7 @@ class JuniperDataClient:
|
|
|
151
210
|
# guards — the no-op call is a single attribute load + return).
|
|
152
211
|
self._on_request: RequestHook = on_request or _noop_request_hook
|
|
153
212
|
|
|
154
|
-
resolved_api_key = api_key or
|
|
213
|
+
resolved_api_key = api_key or _resolve_api_key_from_env()
|
|
155
214
|
if resolved_api_key:
|
|
156
215
|
self.session.headers[API_KEY_HEADER_NAME] = resolved_api_key
|
|
157
216
|
|
|
@@ -163,13 +222,28 @@ class JuniperDataClient:
|
|
|
163
222
|
|
|
164
223
|
Returns:
|
|
165
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).
|
|
166
233
|
"""
|
|
167
234
|
url = url.strip()
|
|
168
235
|
|
|
169
|
-
|
|
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):
|
|
170
240
|
url = f"{DEFAULT_URL_SCHEME_PREFIX}{url}"
|
|
171
241
|
|
|
172
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}")
|
|
173
247
|
normalized = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
|
|
174
248
|
normalized = normalized.rstrip("/")
|
|
175
249
|
|
|
@@ -189,6 +263,8 @@ class JuniperDataClient:
|
|
|
189
263
|
retry_strategy = Retry(
|
|
190
264
|
total=self.retries,
|
|
191
265
|
backoff_factor=self.backoff_factor,
|
|
266
|
+
# APD-ECO-002: decorrelate retry schedules across client instances.
|
|
267
|
+
backoff_jitter=DEFAULT_BACKOFF_JITTER,
|
|
192
268
|
status_forcelist=RETRYABLE_STATUS_CODES,
|
|
193
269
|
allowed_methods=RETRY_ALLOWED_METHODS,
|
|
194
270
|
)
|
|
@@ -287,14 +363,46 @@ class JuniperDataClient:
|
|
|
287
363
|
# fall back to using the raw response text as the error detail.
|
|
288
364
|
error_detail = response.text
|
|
289
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.
|
|
290
380
|
if response.status_code == HTTP_404_NOT_FOUND:
|
|
291
|
-
outgoing_error = JuniperDataNotFoundError(
|
|
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
|
+
)
|
|
292
387
|
raise outgoing_error
|
|
293
388
|
elif response.status_code in (HTTP_400_BAD_REQUEST, HTTP_422_UNPROCESSABLE_ENTITY):
|
|
294
|
-
|
|
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
|
+
)
|
|
295
398
|
raise outgoing_error
|
|
296
399
|
else:
|
|
297
|
-
outgoing_error = JuniperDataClientError(
|
|
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
|
+
)
|
|
298
406
|
raise outgoing_error
|
|
299
407
|
finally:
|
|
300
408
|
duration_ms = (time.monotonic() - start) * 1000.0
|
|
@@ -391,6 +499,7 @@ class JuniperDataClient:
|
|
|
391
499
|
self,
|
|
392
500
|
generator: str,
|
|
393
501
|
params: Dict[str, Any],
|
|
502
|
+
*,
|
|
394
503
|
persist: bool = True,
|
|
395
504
|
name: Optional[str] = None,
|
|
396
505
|
description: Optional[str] = None,
|
|
@@ -401,6 +510,12 @@ class JuniperDataClient:
|
|
|
401
510
|
) -> Dict[str, Any]:
|
|
402
511
|
"""Create a new dataset via the JuniperData API.
|
|
403
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
|
+
|
|
404
519
|
If a dataset with the same parameters already exists, the existing
|
|
405
520
|
dataset is returned (caching behavior).
|
|
406
521
|
|
|
@@ -525,12 +640,24 @@ class JuniperDataClient:
|
|
|
525
640
|
def download_artifact_npz(self, dataset_id: str) -> Dict[str, np.ndarray]:
|
|
526
641
|
"""Download and load an NPZ artifact for a dataset.
|
|
527
642
|
|
|
528
|
-
The returned dictionary contains
|
|
643
|
+
The returned dictionary contains every array in the NPZ. Classification
|
|
644
|
+
artifacts carry the standard keys:
|
|
529
645
|
- X_train, y_train: Training features and one-hot labels
|
|
530
646
|
- X_test, y_test: Test features and one-hot labels
|
|
531
|
-
- X_full, y_full: Full dataset features and one-hot labels
|
|
532
647
|
|
|
533
|
-
|
|
648
|
+
Additional keys may be present depending on the generator (WS-1 /
|
|
649
|
+
juniper-data#168):
|
|
650
|
+
- y_reg_{split}: regression target(s) (e.g. equities next-day close).
|
|
651
|
+
- 3-D sequence artifacts (X.ndim == 3, shape (W, L, F)) add a per-step
|
|
652
|
+
dt_{split} (or absolute t_{split}) elapsed-time channel plus
|
|
653
|
+
target_dt_{split} and optional observed_mask_{split} /
|
|
654
|
+
padding_mask_{split}. Pass the result to
|
|
655
|
+
:func:`juniper_data_client.validate_npz_contract` to classify
|
|
656
|
+
("tabular" / "sequence") and validate it. The matching ``task_type``
|
|
657
|
+
("classification" / "regression") and ``time_unit`` live in the
|
|
658
|
+
dataset metadata (see :meth:`get_dataset_metadata`).
|
|
659
|
+
|
|
660
|
+
Feature and label arrays are float32.
|
|
534
661
|
|
|
535
662
|
Args:
|
|
536
663
|
dataset_id: ID of the dataset whose artifact to download
|