juniper-data-client 0.3.1__tar.gz → 0.4.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 (22) hide show
  1. {juniper_data_client-0.3.1 → juniper_data_client-0.4.0}/PKG-INFO +8 -2
  2. {juniper_data_client-0.3.1 → juniper_data_client-0.4.0}/README.md +7 -1
  3. {juniper_data_client-0.3.1 → juniper_data_client-0.4.0}/juniper_data_client/__init__.py +2 -9
  4. {juniper_data_client-0.3.1 → juniper_data_client-0.4.0}/juniper_data_client/client.py +121 -10
  5. juniper_data_client-0.4.0/juniper_data_client/testing/__init__.py +45 -0
  6. juniper_data_client-0.4.0/juniper_data_client/testing/fake_client.py +715 -0
  7. juniper_data_client-0.4.0/juniper_data_client/testing/generators.py +284 -0
  8. {juniper_data_client-0.3.1 → juniper_data_client-0.4.0}/juniper_data_client.egg-info/PKG-INFO +8 -2
  9. {juniper_data_client-0.3.1 → juniper_data_client-0.4.0}/juniper_data_client.egg-info/SOURCES.txt +8 -1
  10. {juniper_data_client-0.3.1 → juniper_data_client-0.4.0}/pyproject.toml +28 -5
  11. {juniper_data_client-0.3.1 → juniper_data_client-0.4.0}/tests/test_client.py +200 -9
  12. juniper_data_client-0.4.0/tests/test_fake_client.py +568 -0
  13. juniper_data_client-0.4.0/tests/test_fake_client_batch.py +312 -0
  14. juniper_data_client-0.4.0/tests/test_performance.py +402 -0
  15. juniper_data_client-0.4.0/tests/test_versioning.py +215 -0
  16. {juniper_data_client-0.3.1 → juniper_data_client-0.4.0}/LICENSE +0 -0
  17. {juniper_data_client-0.3.1 → juniper_data_client-0.4.0}/juniper_data_client/exceptions.py +0 -0
  18. {juniper_data_client-0.3.1 → juniper_data_client-0.4.0}/juniper_data_client/py.typed +0 -0
  19. {juniper_data_client-0.3.1 → juniper_data_client-0.4.0}/juniper_data_client.egg-info/dependency_links.txt +0 -0
  20. {juniper_data_client-0.3.1 → juniper_data_client-0.4.0}/juniper_data_client.egg-info/requires.txt +0 -0
  21. {juniper_data_client-0.3.1 → juniper_data_client-0.4.0}/juniper_data_client.egg-info/top_level.txt +0 -0
  22. {juniper_data_client-0.3.1 → juniper_data_client-0.4.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: juniper-data-client
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: HTTP client for the JuniperData dataset generation service
5
5
  Author: Paul Calnon
6
6
  License-Expression: MIT
@@ -206,6 +206,12 @@ y_train = torch.from_numpy(arrays["y_train"]) # torch.float32
206
206
  | `download_artifact_bytes(id)` | Download raw NPZ bytes |
207
207
  | `get_preview(id, n)` | Get JSON preview of samples |
208
208
  | `delete_dataset(id)` | Delete a dataset |
209
+ | `list_versions(name)` | List version history for a dataset |
210
+ | `get_latest(name)` | Get latest version by name |
211
+ | `batch_delete(dataset_ids)` | Delete multiple datasets |
212
+ | `batch_create(datasets)` | Create multiple datasets |
213
+ | `batch_update_tags(dataset_ids, add_tags, remove_tags)` | Update tags on multiple datasets |
214
+ | `batch_export(dataset_ids)` | Export multiple datasets as ZIP |
209
215
  | `close()` | Close the client session |
210
216
 
211
217
  ### Exceptions
@@ -242,7 +248,7 @@ Downloaded artifacts contain the following numpy arrays (all `float32`):
242
248
 
243
249
  ## Requirements
244
250
 
245
- - Python >=3.11
251
+ - Python >=3.12
246
252
  - numpy >=1.24.0
247
253
  - requests >=2.28.0
248
254
  - urllib3 >=2.0.0
@@ -164,6 +164,12 @@ y_train = torch.from_numpy(arrays["y_train"]) # torch.float32
164
164
  | `download_artifact_bytes(id)` | Download raw NPZ bytes |
165
165
  | `get_preview(id, n)` | Get JSON preview of samples |
166
166
  | `delete_dataset(id)` | Delete a dataset |
167
+ | `list_versions(name)` | List version history for a dataset |
168
+ | `get_latest(name)` | Get latest version by name |
169
+ | `batch_delete(dataset_ids)` | Delete multiple datasets |
170
+ | `batch_create(datasets)` | Create multiple datasets |
171
+ | `batch_update_tags(dataset_ids, add_tags, remove_tags)` | Update tags on multiple datasets |
172
+ | `batch_export(dataset_ids)` | Export multiple datasets as ZIP |
167
173
  | `close()` | Close the client session |
168
174
 
169
175
  ### Exceptions
@@ -200,7 +206,7 @@ Downloaded artifacts contain the following numpy arrays (all `float32`):
200
206
 
201
207
  ## Requirements
202
208
 
203
- - Python >=3.11
209
+ - Python >=3.12
204
210
  - numpy >=1.24.0
205
211
  - requests >=2.28.0
206
212
  - urllib3 >=2.0.0
@@ -5,16 +5,9 @@ dataset generation service, used by both JuniperCascor and juniper-canopy.
5
5
  """
6
6
 
7
7
  from juniper_data_client.client import JuniperDataClient
8
- from juniper_data_client.exceptions import (
9
- JuniperDataClientError,
10
- JuniperDataConfigurationError,
11
- JuniperDataConnectionError,
12
- JuniperDataNotFoundError,
13
- JuniperDataTimeoutError,
14
- JuniperDataValidationError,
15
- )
8
+ from juniper_data_client.exceptions import JuniperDataClientError, JuniperDataConfigurationError, JuniperDataConnectionError, JuniperDataNotFoundError, JuniperDataTimeoutError, JuniperDataValidationError
16
9
 
17
- __version__ = "0.3.1"
10
+ __version__ = "0.4.0"
18
11
 
19
12
  __all__ = [
20
13
  "JuniperDataClient",
@@ -15,13 +15,7 @@ import requests
15
15
  from requests.adapters import HTTPAdapter
16
16
  from urllib3.util.retry import Retry
17
17
 
18
- from juniper_data_client.exceptions import (
19
- JuniperDataClientError,
20
- JuniperDataConnectionError,
21
- JuniperDataNotFoundError,
22
- JuniperDataTimeoutError,
23
- JuniperDataValidationError,
24
- )
18
+ from juniper_data_client.exceptions import JuniperDataClientError, JuniperDataConnectionError, JuniperDataNotFoundError, JuniperDataTimeoutError, JuniperDataValidationError
25
19
 
26
20
 
27
21
  class JuniperDataClient:
@@ -104,7 +98,7 @@ class JuniperDataClient:
104
98
  total=self.retries,
105
99
  backoff_factor=self.backoff_factor,
106
100
  status_forcelist=[429, 500, 502, 503, 504],
107
- allowed_methods=["HEAD", "GET", "POST", "DELETE"],
101
+ allowed_methods=["HEAD", "GET", "POST", "PATCH", "DELETE"],
108
102
  )
109
103
 
110
104
  adapter = HTTPAdapter(
@@ -233,7 +227,16 @@ class JuniperDataClient:
233
227
  response = self._request("GET", f"/v1/generators/{name}/schema")
234
228
  return response.json()
235
229
 
236
- def create_dataset(self, generator: str, params: Dict[str, Any], persist: bool = True) -> Dict[str, Any]:
230
+ def create_dataset(
231
+ self,
232
+ generator: str,
233
+ params: Dict[str, Any],
234
+ persist: bool = True,
235
+ name: Optional[str] = None,
236
+ description: Optional[str] = None,
237
+ created_by: Optional[str] = None,
238
+ parent_dataset_id: Optional[str] = None,
239
+ ) -> Dict[str, Any]:
237
240
  """Create a new dataset via the JuniperData API.
238
241
 
239
242
  If a dataset with the same parameters already exists, the existing
@@ -243,6 +246,11 @@ class JuniperDataClient:
243
246
  generator: Name of the dataset generator to use (e.g., "spiral")
244
247
  params: Parameters to pass to the generator
245
248
  persist: Whether to persist the dataset (default: True)
249
+ name: Optional dataset name for versioning. When provided, the service
250
+ automatically assigns an incrementing version number.
251
+ description: Optional human-readable description of the dataset.
252
+ created_by: Optional identifier for the creator (user or system).
253
+ parent_dataset_id: Optional ID of the parent dataset this was derived from.
246
254
 
247
255
  Returns:
248
256
  Parsed JSON response containing dataset_id, generator, meta, and artifact_url
@@ -251,15 +259,50 @@ class JuniperDataClient:
251
259
  JuniperDataValidationError: If parameters are invalid
252
260
  JuniperDataNotFoundError: If generator not found
253
261
  """
254
- payload = {
262
+ payload: Dict[str, Any] = {
255
263
  "generator": generator,
256
264
  "params": params,
257
265
  "persist": persist,
258
266
  }
267
+ if name is not None:
268
+ payload["name"] = name
269
+ if description is not None:
270
+ payload["description"] = description
271
+ if created_by is not None:
272
+ payload["created_by"] = created_by
273
+ if parent_dataset_id is not None:
274
+ payload["parent_dataset_id"] = parent_dataset_id
259
275
 
260
276
  response = self._request("POST", "/v1/datasets", json=payload)
261
277
  return response.json()
262
278
 
279
+ def list_versions(self, name: str) -> Dict[str, Any]:
280
+ """List all versions of a named dataset.
281
+
282
+ Args:
283
+ name: Dataset name to list versions for.
284
+
285
+ Returns:
286
+ Dict with dataset_name, versions list, total count, and latest_version.
287
+ """
288
+ response = self._request("GET", "/v1/datasets/versions", params={"name": name})
289
+ return response.json()
290
+
291
+ def get_latest(self, name: str) -> Dict[str, Any]:
292
+ """Get the latest version of a named dataset.
293
+
294
+ Args:
295
+ name: Dataset name to get latest version of.
296
+
297
+ Returns:
298
+ Dataset metadata for the latest version.
299
+
300
+ Raises:
301
+ JuniperDataNotFoundError: If no versions exist for the given name.
302
+ """
303
+ response = self._request("GET", "/v1/datasets/latest", params={"name": name})
304
+ return response.json()
305
+
263
306
  def list_datasets(self, limit: int = 100, offset: int = 0) -> List[str]:
264
307
  """List dataset IDs.
265
308
 
@@ -394,6 +437,74 @@ class JuniperDataClient:
394
437
 
395
438
  return self.create_dataset("spiral", params)
396
439
 
440
+ # ─── Batch Operations ────────────────────────────────────────────────
441
+
442
+ def batch_delete(self, dataset_ids: List[str]) -> Dict[str, Any]:
443
+ """Delete multiple datasets in a single request.
444
+
445
+ Args:
446
+ dataset_ids: List of dataset IDs to delete (1-100).
447
+
448
+ Returns:
449
+ Dictionary with deleted, not_found, and total_deleted.
450
+ """
451
+ response = self._request("POST", "/v1/datasets/batch-delete", json={"dataset_ids": dataset_ids})
452
+ return response.json()
453
+
454
+ def batch_create(self, datasets: List[Dict[str, Any]]) -> Dict[str, Any]:
455
+ """Create multiple datasets in a single request.
456
+
457
+ Each item should have 'generator' and 'params' keys, and optionally
458
+ 'persist', 'tags', and 'ttl_seconds'.
459
+
460
+ Args:
461
+ datasets: List of dataset specifications (1-50).
462
+
463
+ Returns:
464
+ Dictionary with results, total_created, and total_failed.
465
+ """
466
+ response = self._request("POST", "/v1/datasets/batch-create", json={"datasets": datasets})
467
+ return response.json()
468
+
469
+ def batch_update_tags(
470
+ self,
471
+ dataset_ids: List[str],
472
+ add_tags: Optional[List[str]] = None,
473
+ remove_tags: Optional[List[str]] = None,
474
+ ) -> Dict[str, Any]:
475
+ """Add or remove tags from multiple datasets.
476
+
477
+ Args:
478
+ dataset_ids: List of dataset IDs to update (1-100).
479
+ add_tags: Tags to add to all specified datasets.
480
+ remove_tags: Tags to remove from all specified datasets.
481
+
482
+ Returns:
483
+ Dictionary with updated, not_found, and total_updated.
484
+ """
485
+ payload: Dict[str, Any] = {"dataset_ids": dataset_ids}
486
+ if add_tags:
487
+ payload["add_tags"] = add_tags
488
+ if remove_tags:
489
+ payload["remove_tags"] = remove_tags
490
+ response = self._request("PATCH", "/v1/datasets/batch-tags", json=payload)
491
+ return response.json()
492
+
493
+ def batch_export(self, dataset_ids: List[str]) -> bytes:
494
+ """Export multiple datasets as a ZIP archive of NPZ files.
495
+
496
+ Args:
497
+ dataset_ids: List of dataset IDs to export (1-50).
498
+
499
+ Returns:
500
+ Raw bytes of the ZIP archive.
501
+
502
+ Raises:
503
+ JuniperDataNotFoundError: If none of the datasets exist.
504
+ """
505
+ response = self._request("POST", "/v1/datasets/batch-export", json={"dataset_ids": dataset_ids})
506
+ return response.content
507
+
397
508
  def close(self) -> None:
398
509
  """Close the client session and release resources."""
399
510
  self.session.close()
@@ -0,0 +1,45 @@
1
+ #####################################################################################################################################################################################################
2
+ # Project: Juniper
3
+ # Sub-Project: juniper-data-client
4
+ # Application: juniper_data_client
5
+ # File Name: __init__.py
6
+ # Author: Paul Calnon
7
+ # Version: 0.4.0
8
+ #
9
+ # Date Created: 2026-02-27
10
+ # Last Modified: 2026-02-27
11
+ #
12
+ # License: MIT License
13
+ # Copyright: Copyright (c) 2024-2026 Paul Calnon
14
+ #
15
+ # Description:
16
+ # Testing submodule for juniper-data-client. Provides FakeDataClient,
17
+ # a drop-in replacement for JuniperDataClient that generates synthetic
18
+ # datasets in-memory without requiring a running JuniperData service.
19
+ #####################################################################################################################################################################################################
20
+
21
+ """Testing utilities for juniper-data-client.
22
+
23
+ Provides :class:`FakeDataClient` for use in unit tests of consumers
24
+ like JuniperCascor and juniper-canopy, as well as standalone synthetic
25
+ dataset generators.
26
+
27
+ Usage::
28
+
29
+ from juniper_data_client.testing import FakeDataClient
30
+
31
+ with FakeDataClient() as client:
32
+ result = client.create_spiral_dataset(n_spirals=2, seed=42)
33
+ arrays = client.download_artifact_npz(result["dataset_id"])
34
+ """
35
+
36
+ from juniper_data_client.testing.fake_client import FakeDataClient
37
+ from juniper_data_client.testing.generators import generate_circle, generate_moon, generate_spiral, generate_xor
38
+
39
+ __all__ = [
40
+ "FakeDataClient",
41
+ "generate_circle",
42
+ "generate_moon",
43
+ "generate_spiral",
44
+ "generate_xor",
45
+ ]