juniper-data 0.4.2__py3-none-any.whl

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 (95) hide show
  1. juniper_data/__init__.py +88 -0
  2. juniper_data/__main__.py +78 -0
  3. juniper_data/api/__init__.py +10 -0
  4. juniper_data/api/app.py +111 -0
  5. juniper_data/api/middleware.py +95 -0
  6. juniper_data/api/routes/__init__.py +9 -0
  7. juniper_data/api/routes/datasets.py +414 -0
  8. juniper_data/api/routes/generators.py +125 -0
  9. juniper_data/api/routes/health.py +49 -0
  10. juniper_data/api/security.py +238 -0
  11. juniper_data/api/settings.py +109 -0
  12. juniper_data/core/__init__.py +32 -0
  13. juniper_data/core/artifacts.py +63 -0
  14. juniper_data/core/dataset_id.py +38 -0
  15. juniper_data/core/models.py +135 -0
  16. juniper_data/core/split.py +120 -0
  17. juniper_data/generators/__init__.py +15 -0
  18. juniper_data/generators/arc_agi/__init__.py +11 -0
  19. juniper_data/generators/arc_agi/generator.py +229 -0
  20. juniper_data/generators/arc_agi/params.py +56 -0
  21. juniper_data/generators/checkerboard/__init__.py +15 -0
  22. juniper_data/generators/checkerboard/generator.py +114 -0
  23. juniper_data/generators/checkerboard/params.py +32 -0
  24. juniper_data/generators/circles/__init__.py +11 -0
  25. juniper_data/generators/circles/generator.py +112 -0
  26. juniper_data/generators/circles/params.py +31 -0
  27. juniper_data/generators/csv_import/__init__.py +15 -0
  28. juniper_data/generators/csv_import/generator.py +198 -0
  29. juniper_data/generators/csv_import/params.py +48 -0
  30. juniper_data/generators/gaussian/__init__.py +11 -0
  31. juniper_data/generators/gaussian/generator.py +149 -0
  32. juniper_data/generators/gaussian/params.py +53 -0
  33. juniper_data/generators/mnist/__init__.py +11 -0
  34. juniper_data/generators/mnist/generator.py +124 -0
  35. juniper_data/generators/mnist/params.py +39 -0
  36. juniper_data/generators/spiral/__init__.py +57 -0
  37. juniper_data/generators/spiral/defaults.py +39 -0
  38. juniper_data/generators/spiral/generator.py +206 -0
  39. juniper_data/generators/spiral/params.py +148 -0
  40. juniper_data/generators/xor/__init__.py +11 -0
  41. juniper_data/generators/xor/generator.py +162 -0
  42. juniper_data/generators/xor/params.py +30 -0
  43. juniper_data/storage/__init__.py +120 -0
  44. juniper_data/storage/base.py +279 -0
  45. juniper_data/storage/cached.py +211 -0
  46. juniper_data/storage/hf_store.py +257 -0
  47. juniper_data/storage/kaggle_store.py +333 -0
  48. juniper_data/storage/local_fs.py +232 -0
  49. juniper_data/storage/memory.py +136 -0
  50. juniper_data/storage/postgres_store.py +373 -0
  51. juniper_data/storage/redis_store.py +264 -0
  52. juniper_data/tests/__init__.py +1 -0
  53. juniper_data/tests/conftest.py +68 -0
  54. juniper_data/tests/fixtures/generate_golden_datasets.py +199 -0
  55. juniper_data/tests/integration/__init__.py +1 -0
  56. juniper_data/tests/integration/test_api.py +283 -0
  57. juniper_data/tests/integration/test_e2e_workflow.py +378 -0
  58. juniper_data/tests/integration/test_lifecycle_api.py +304 -0
  59. juniper_data/tests/integration/test_security_integration.py +189 -0
  60. juniper_data/tests/integration/test_storage_workflow.py +259 -0
  61. juniper_data/tests/performance/__init__.py +1 -0
  62. juniper_data/tests/performance/test_generator_benchmarks.py +178 -0
  63. juniper_data/tests/performance/test_storage_benchmarks.py +257 -0
  64. juniper_data/tests/unit/__init__.py +1 -0
  65. juniper_data/tests/unit/test_api_app.py +206 -0
  66. juniper_data/tests/unit/test_api_routes.py +407 -0
  67. juniper_data/tests/unit/test_api_settings.py +100 -0
  68. juniper_data/tests/unit/test_arc_agi_generator.py +525 -0
  69. juniper_data/tests/unit/test_artifacts.py +145 -0
  70. juniper_data/tests/unit/test_cached_store.py +423 -0
  71. juniper_data/tests/unit/test_checkerboard_generator.py +232 -0
  72. juniper_data/tests/unit/test_circles_generator.py +256 -0
  73. juniper_data/tests/unit/test_csv_import_generator.py +345 -0
  74. juniper_data/tests/unit/test_dataset_id.py +181 -0
  75. juniper_data/tests/unit/test_gaussian_generator.py +333 -0
  76. juniper_data/tests/unit/test_hf_store.py +416 -0
  77. juniper_data/tests/unit/test_init.py +93 -0
  78. juniper_data/tests/unit/test_kaggle_store.py +469 -0
  79. juniper_data/tests/unit/test_lifecycle.py +394 -0
  80. juniper_data/tests/unit/test_main.py +127 -0
  81. juniper_data/tests/unit/test_middleware.py +79 -0
  82. juniper_data/tests/unit/test_mnist_generator.py +370 -0
  83. juniper_data/tests/unit/test_postgres_store.py +490 -0
  84. juniper_data/tests/unit/test_redis_store.py +500 -0
  85. juniper_data/tests/unit/test_security.py +281 -0
  86. juniper_data/tests/unit/test_security_boundaries.py +517 -0
  87. juniper_data/tests/unit/test_spiral_generator.py +566 -0
  88. juniper_data/tests/unit/test_split.py +245 -0
  89. juniper_data/tests/unit/test_storage.py +767 -0
  90. juniper_data/tests/unit/test_xor_generator.py +223 -0
  91. juniper_data-0.4.2.dist-info/METADATA +216 -0
  92. juniper_data-0.4.2.dist-info/RECORD +95 -0
  93. juniper_data-0.4.2.dist-info/WHEEL +5 -0
  94. juniper_data-0.4.2.dist-info/licenses/LICENSE +9 -0
  95. juniper_data-0.4.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,500 @@
1
+ """Unit tests for RedisDatasetStore."""
2
+
3
+ from datetime import UTC, datetime
4
+ from unittest.mock import MagicMock, patch
5
+
6
+ import numpy as np
7
+ import pytest
8
+
9
+ from juniper_data.core.models import DatasetMeta
10
+
11
+
12
+ @pytest.fixture
13
+ def sample_meta() -> DatasetMeta:
14
+ """Create sample metadata."""
15
+ return DatasetMeta(
16
+ dataset_id="test-dataset",
17
+ generator="test",
18
+ generator_version="1.0.0",
19
+ params={"seed": 42},
20
+ n_samples=100,
21
+ n_features=2,
22
+ n_classes=2,
23
+ n_train=80,
24
+ n_test=20,
25
+ class_distribution={"0": 50, "1": 50},
26
+ created_at=datetime.now(UTC),
27
+ )
28
+
29
+
30
+ @pytest.fixture
31
+ def sample_meta_with_ttl() -> DatasetMeta:
32
+ """Create sample metadata with TTL."""
33
+ return DatasetMeta(
34
+ dataset_id="test-ttl",
35
+ generator="test",
36
+ generator_version="1.0.0",
37
+ params={"seed": 42},
38
+ n_samples=100,
39
+ n_features=2,
40
+ n_classes=2,
41
+ n_train=80,
42
+ n_test=20,
43
+ class_distribution={"0": 50, "1": 50},
44
+ created_at=datetime.now(UTC),
45
+ ttl_seconds=3600,
46
+ )
47
+
48
+
49
+ @pytest.fixture
50
+ def sample_arrays() -> dict[str, np.ndarray]:
51
+ """Create sample arrays."""
52
+ return {
53
+ "X_train": np.random.randn(80, 2).astype(np.float32),
54
+ "y_train": np.random.randn(80, 2).astype(np.float32),
55
+ "X_test": np.random.randn(20, 2).astype(np.float32),
56
+ "y_test": np.random.randn(20, 2).astype(np.float32),
57
+ }
58
+
59
+
60
+ @pytest.fixture
61
+ def mock_redis_module():
62
+ """Create a mock redis module and patch it into redis_store."""
63
+ mock_redis = MagicMock()
64
+ mock_client = MagicMock()
65
+ mock_redis.Redis.return_value = mock_client
66
+ mock_pipeline = MagicMock()
67
+ mock_client.pipeline.return_value = mock_pipeline
68
+
69
+ with patch.dict("sys.modules", {"redis": mock_redis}):
70
+ with patch("juniper_data.storage.redis_store.REDIS_AVAILABLE", True):
71
+ with patch("juniper_data.storage.redis_store.redis", mock_redis):
72
+ yield mock_redis, mock_client, mock_pipeline
73
+
74
+
75
+ @pytest.mark.unit
76
+ @pytest.mark.storage
77
+ class TestRedisDatasetStoreInit:
78
+ """Tests for RedisDatasetStore initialization."""
79
+
80
+ def test_init_default_params(self, mock_redis_module) -> None:
81
+ """Initialize with default parameters."""
82
+ mock_redis, mock_client, _ = mock_redis_module
83
+ from juniper_data.storage.redis_store import RedisDatasetStore
84
+
85
+ store = RedisDatasetStore()
86
+ mock_redis.Redis.assert_called_once_with(
87
+ host="localhost", port=6379, db=0, password=None, decode_responses=False
88
+ )
89
+ assert store._key_prefix == "juniper:dataset:"
90
+ assert store._default_ttl is None
91
+
92
+ def test_init_custom_params(self, mock_redis_module) -> None:
93
+ """Initialize with custom parameters."""
94
+ mock_redis, _, _ = mock_redis_module
95
+ from juniper_data.storage.redis_store import RedisDatasetStore
96
+
97
+ store = RedisDatasetStore(
98
+ host="redis.example.com", port=6380, db=1, password="secret", key_prefix="myapp:", default_ttl=600
99
+ )
100
+ mock_redis.Redis.assert_called_once_with(
101
+ host="redis.example.com", port=6380, db=1, password="secret", decode_responses=False
102
+ )
103
+ assert store._key_prefix == "myapp:"
104
+ assert store._default_ttl == 600
105
+
106
+ def test_init_with_connection_pool(self, mock_redis_module) -> None:
107
+ """Initialize with an existing connection pool."""
108
+ mock_redis, _, _ = mock_redis_module
109
+ from juniper_data.storage.redis_store import RedisDatasetStore
110
+
111
+ mock_pool = MagicMock()
112
+ _store = RedisDatasetStore(connection_pool=mock_pool)
113
+ mock_redis.Redis.assert_called_once_with(connection_pool=mock_pool)
114
+
115
+ def test_init_raises_without_redis(self) -> None:
116
+ """Raises ImportError when redis is not available."""
117
+ with patch("juniper_data.storage.redis_store.REDIS_AVAILABLE", False):
118
+ from juniper_data.storage.redis_store import RedisDatasetStore
119
+
120
+ with pytest.raises(ImportError, match="Redis package not installed"):
121
+ RedisDatasetStore()
122
+
123
+
124
+ @pytest.mark.unit
125
+ @pytest.mark.storage
126
+ class TestRedisDatasetStoreKeys:
127
+ """Tests for Redis key generation."""
128
+
129
+ def test_meta_key(self, mock_redis_module) -> None:
130
+ """Meta key includes prefix and :meta suffix."""
131
+ from juniper_data.storage.redis_store import RedisDatasetStore
132
+
133
+ store = RedisDatasetStore(key_prefix="test:")
134
+ assert store._meta_key("ds-123") == "test:ds-123:meta"
135
+
136
+ def test_artifact_key(self, mock_redis_module) -> None:
137
+ """Artifact key includes prefix and :artifact suffix."""
138
+ from juniper_data.storage.redis_store import RedisDatasetStore
139
+
140
+ store = RedisDatasetStore(key_prefix="test:")
141
+ assert store._artifact_key("ds-123") == "test:ds-123:artifact"
142
+
143
+
144
+ @pytest.mark.unit
145
+ @pytest.mark.storage
146
+ class TestRedisDatasetStoreEncoding:
147
+ """Tests for metadata and array encoding/decoding."""
148
+
149
+ def test_encode_decode_meta_roundtrip(self, mock_redis_module, sample_meta) -> None:
150
+ """Encoding then decoding metadata preserves data."""
151
+ from juniper_data.storage.redis_store import RedisDatasetStore
152
+
153
+ store = RedisDatasetStore()
154
+ encoded = store._encode_meta(sample_meta)
155
+ assert isinstance(encoded, bytes)
156
+ decoded = store._decode_meta(encoded)
157
+ assert decoded.dataset_id == sample_meta.dataset_id
158
+ assert decoded.generator == sample_meta.generator
159
+ assert decoded.n_samples == sample_meta.n_samples
160
+
161
+ def test_encode_arrays_returns_bytes(self, mock_redis_module, sample_arrays) -> None:
162
+ """Encoding arrays returns bytes (NPZ format)."""
163
+ from juniper_data.storage.redis_store import RedisDatasetStore
164
+
165
+ store = RedisDatasetStore()
166
+ encoded = store._encode_arrays(sample_arrays)
167
+ assert isinstance(encoded, bytes)
168
+ assert len(encoded) > 0
169
+
170
+
171
+ @pytest.mark.unit
172
+ @pytest.mark.storage
173
+ class TestRedisDatasetStoreSave:
174
+ """Tests for save operation."""
175
+
176
+ def test_save_without_ttl(self, mock_redis_module, sample_meta, sample_arrays) -> None:
177
+ """Save without TTL uses SET."""
178
+ _, mock_client, mock_pipeline = mock_redis_module
179
+ from juniper_data.storage.redis_store import RedisDatasetStore
180
+
181
+ store = RedisDatasetStore()
182
+ store.save("ds-1", sample_meta, sample_arrays)
183
+
184
+ mock_client.pipeline.assert_called_once()
185
+ assert mock_pipeline.set.call_count == 2
186
+ assert mock_pipeline.setex.call_count == 0
187
+ mock_pipeline.execute.assert_called_once()
188
+
189
+ def test_save_with_meta_ttl(self, mock_redis_module, sample_meta_with_ttl, sample_arrays) -> None:
190
+ """Save with metadata TTL uses SETEX."""
191
+ _, mock_client, mock_pipeline = mock_redis_module
192
+ from juniper_data.storage.redis_store import RedisDatasetStore
193
+
194
+ store = RedisDatasetStore()
195
+ store.save("ds-1", sample_meta_with_ttl, sample_arrays)
196
+
197
+ assert mock_pipeline.setex.call_count == 2
198
+ assert mock_pipeline.set.call_count == 0
199
+ mock_pipeline.execute.assert_called_once()
200
+
201
+ def test_save_with_default_ttl(self, mock_redis_module, sample_meta, sample_arrays) -> None:
202
+ """Save with store default TTL uses SETEX."""
203
+ _, mock_client, mock_pipeline = mock_redis_module
204
+ from juniper_data.storage.redis_store import RedisDatasetStore
205
+
206
+ store = RedisDatasetStore(default_ttl=300)
207
+ store.save("ds-1", sample_meta, sample_arrays)
208
+
209
+ assert mock_pipeline.setex.call_count == 2
210
+ mock_pipeline.execute.assert_called_once()
211
+
212
+
213
+ @pytest.mark.unit
214
+ @pytest.mark.storage
215
+ class TestRedisDatasetStoreGetMeta:
216
+ """Tests for get_meta operation."""
217
+
218
+ def test_get_meta_found(self, mock_redis_module, sample_meta) -> None:
219
+ """get_meta returns metadata when found."""
220
+ _, mock_client, _ = mock_redis_module
221
+ from juniper_data.storage.redis_store import RedisDatasetStore
222
+
223
+ store = RedisDatasetStore()
224
+ encoded = store._encode_meta(sample_meta)
225
+ mock_client.get.return_value = encoded
226
+
227
+ result = store.get_meta("ds-1")
228
+ assert result is not None
229
+ assert result.dataset_id == sample_meta.dataset_id
230
+
231
+ def test_get_meta_not_found(self, mock_redis_module) -> None:
232
+ """get_meta returns None when not found."""
233
+ _, mock_client, _ = mock_redis_module
234
+ from juniper_data.storage.redis_store import RedisDatasetStore
235
+
236
+ store = RedisDatasetStore()
237
+ mock_client.get.return_value = None
238
+
239
+ result = store.get_meta("nonexistent")
240
+ assert result is None
241
+
242
+
243
+ @pytest.mark.unit
244
+ @pytest.mark.storage
245
+ class TestRedisDatasetStoreGetArtifact:
246
+ """Tests for get_artifact_bytes operation."""
247
+
248
+ def test_get_artifact_bytes_found(self, mock_redis_module) -> None:
249
+ """get_artifact_bytes returns bytes when found."""
250
+ _, mock_client, _ = mock_redis_module
251
+ from juniper_data.storage.redis_store import RedisDatasetStore
252
+
253
+ store = RedisDatasetStore()
254
+ mock_client.get.return_value = b"npz-data-bytes"
255
+
256
+ result = store.get_artifact_bytes("ds-1")
257
+ assert result == b"npz-data-bytes"
258
+
259
+ def test_get_artifact_bytes_not_found(self, mock_redis_module) -> None:
260
+ """get_artifact_bytes returns None when not found."""
261
+ _, mock_client, _ = mock_redis_module
262
+ from juniper_data.storage.redis_store import RedisDatasetStore
263
+
264
+ store = RedisDatasetStore()
265
+ mock_client.get.return_value = None
266
+
267
+ result = store.get_artifact_bytes("nonexistent")
268
+ assert result is None
269
+
270
+
271
+ @pytest.mark.unit
272
+ @pytest.mark.storage
273
+ class TestRedisDatasetStoreExists:
274
+ """Tests for exists operation."""
275
+
276
+ def test_exists_true(self, mock_redis_module) -> None:
277
+ """exists returns True when key exists."""
278
+ _, mock_client, _ = mock_redis_module
279
+ from juniper_data.storage.redis_store import RedisDatasetStore
280
+
281
+ store = RedisDatasetStore()
282
+ mock_client.exists.return_value = 1
283
+
284
+ assert store.exists("ds-1") is True
285
+
286
+ def test_exists_false(self, mock_redis_module) -> None:
287
+ """exists returns False when key doesn't exist."""
288
+ _, mock_client, _ = mock_redis_module
289
+ from juniper_data.storage.redis_store import RedisDatasetStore
290
+
291
+ store = RedisDatasetStore()
292
+ mock_client.exists.return_value = 0
293
+
294
+ assert store.exists("ds-1") is False
295
+
296
+
297
+ @pytest.mark.unit
298
+ @pytest.mark.storage
299
+ class TestRedisDatasetStoreDelete:
300
+ """Tests for delete operation."""
301
+
302
+ def test_delete_existing(self, mock_redis_module) -> None:
303
+ """delete returns True when keys were deleted."""
304
+ _, mock_client, _ = mock_redis_module
305
+ from juniper_data.storage.redis_store import RedisDatasetStore
306
+
307
+ store = RedisDatasetStore()
308
+ mock_client.delete.return_value = 2
309
+
310
+ assert store.delete("ds-1") is True
311
+
312
+ def test_delete_nonexistent(self, mock_redis_module) -> None:
313
+ """delete returns False when no keys were deleted."""
314
+ _, mock_client, _ = mock_redis_module
315
+ from juniper_data.storage.redis_store import RedisDatasetStore
316
+
317
+ store = RedisDatasetStore()
318
+ mock_client.delete.return_value = 0
319
+
320
+ assert store.delete("nonexistent") is False
321
+
322
+
323
+ @pytest.mark.unit
324
+ @pytest.mark.storage
325
+ class TestRedisDatasetStoreListDatasets:
326
+ """Tests for list_datasets operation."""
327
+
328
+ def test_list_datasets(self, mock_redis_module) -> None:
329
+ """list_datasets returns sorted dataset IDs."""
330
+ _, mock_client, _ = mock_redis_module
331
+ from juniper_data.storage.redis_store import RedisDatasetStore
332
+
333
+ store = RedisDatasetStore()
334
+ mock_client.scan_iter.return_value = [
335
+ b"juniper:dataset:ds-b:meta",
336
+ b"juniper:dataset:ds-a:meta",
337
+ b"juniper:dataset:ds-c:meta",
338
+ ]
339
+
340
+ result = store.list_datasets()
341
+ assert result == ["ds-a", "ds-b", "ds-c"]
342
+
343
+ def test_list_datasets_with_limit_and_offset(self, mock_redis_module) -> None:
344
+ """list_datasets respects limit and offset."""
345
+ _, mock_client, _ = mock_redis_module
346
+ from juniper_data.storage.redis_store import RedisDatasetStore
347
+
348
+ store = RedisDatasetStore()
349
+ mock_client.scan_iter.return_value = [
350
+ b"juniper:dataset:ds-a:meta",
351
+ b"juniper:dataset:ds-b:meta",
352
+ b"juniper:dataset:ds-c:meta",
353
+ ]
354
+
355
+ result = store.list_datasets(limit=1, offset=1)
356
+ assert result == ["ds-b"]
357
+
358
+ def test_list_datasets_with_string_keys(self, mock_redis_module) -> None:
359
+ """list_datasets handles string keys (non-bytes)."""
360
+ _, mock_client, _ = mock_redis_module
361
+ from juniper_data.storage.redis_store import RedisDatasetStore
362
+
363
+ store = RedisDatasetStore()
364
+ mock_client.scan_iter.return_value = [
365
+ "juniper:dataset:ds-a:meta",
366
+ ]
367
+
368
+ result = store.list_datasets()
369
+ assert result == ["ds-a"]
370
+
371
+
372
+ @pytest.mark.unit
373
+ @pytest.mark.storage
374
+ class TestRedisDatasetStoreUpdateMeta:
375
+ """Tests for update_meta operation."""
376
+
377
+ def test_update_meta_found_with_ttl(self, mock_redis_module, sample_meta) -> None:
378
+ """update_meta updates and preserves TTL when positive."""
379
+ _, mock_client, _ = mock_redis_module
380
+ from juniper_data.storage.redis_store import RedisDatasetStore
381
+
382
+ store = RedisDatasetStore()
383
+ mock_client.exists.return_value = True
384
+ mock_client.ttl.return_value = 300
385
+
386
+ result = store.update_meta("ds-1", sample_meta)
387
+ assert result is True
388
+ mock_client.setex.assert_called_once()
389
+
390
+ def test_update_meta_found_no_ttl(self, mock_redis_module, sample_meta) -> None:
391
+ """update_meta uses SET when no TTL."""
392
+ _, mock_client, _ = mock_redis_module
393
+ from juniper_data.storage.redis_store import RedisDatasetStore
394
+
395
+ store = RedisDatasetStore()
396
+ mock_client.exists.return_value = True
397
+ mock_client.ttl.return_value = -1
398
+
399
+ result = store.update_meta("ds-1", sample_meta)
400
+ assert result is True
401
+ mock_client.set.assert_called_once()
402
+
403
+ def test_update_meta_not_found(self, mock_redis_module, sample_meta) -> None:
404
+ """update_meta returns False when dataset doesn't exist."""
405
+ _, mock_client, _ = mock_redis_module
406
+ from juniper_data.storage.redis_store import RedisDatasetStore
407
+
408
+ store = RedisDatasetStore()
409
+ mock_client.exists.return_value = False
410
+
411
+ result = store.update_meta("nonexistent", sample_meta)
412
+ assert result is False
413
+
414
+
415
+ @pytest.mark.unit
416
+ @pytest.mark.storage
417
+ class TestRedisDatasetStoreListAllMetadata:
418
+ """Tests for list_all_metadata operation."""
419
+
420
+ def test_list_all_metadata(self, mock_redis_module, sample_meta) -> None:
421
+ """list_all_metadata returns all metadata objects."""
422
+ _, mock_client, _ = mock_redis_module
423
+ from juniper_data.storage.redis_store import RedisDatasetStore
424
+
425
+ store = RedisDatasetStore()
426
+ encoded = store._encode_meta(sample_meta)
427
+ mock_client.scan_iter.return_value = [b"juniper:dataset:ds-1:meta"]
428
+ mock_client.get.return_value = encoded
429
+
430
+ result = store.list_all_metadata()
431
+ assert len(result) == 1
432
+ assert result[0].dataset_id == sample_meta.dataset_id
433
+
434
+ def test_list_all_metadata_skips_none(self, mock_redis_module) -> None:
435
+ """list_all_metadata skips keys with None data."""
436
+ _, mock_client, _ = mock_redis_module
437
+ from juniper_data.storage.redis_store import RedisDatasetStore
438
+
439
+ store = RedisDatasetStore()
440
+ mock_client.scan_iter.return_value = [b"juniper:dataset:ds-1:meta"]
441
+ mock_client.get.return_value = None
442
+
443
+ result = store.list_all_metadata()
444
+ assert len(result) == 0
445
+
446
+
447
+ @pytest.mark.unit
448
+ @pytest.mark.storage
449
+ class TestRedisDatasetStorePing:
450
+ """Tests for ping operation."""
451
+
452
+ def test_ping_success(self, mock_redis_module) -> None:
453
+ """ping returns True when Redis is connected."""
454
+ _, mock_client, _ = mock_redis_module
455
+ from juniper_data.storage.redis_store import RedisDatasetStore
456
+
457
+ store = RedisDatasetStore()
458
+ mock_client.ping.return_value = True
459
+
460
+ assert store.ping() is True
461
+
462
+ def test_ping_failure(self, mock_redis_module) -> None:
463
+ """ping returns False when connection fails."""
464
+ _, mock_client, _ = mock_redis_module
465
+ from juniper_data.storage.redis_store import RedisDatasetStore
466
+
467
+ store = RedisDatasetStore()
468
+ mock_client.ping.side_effect = ConnectionError("Connection refused")
469
+
470
+ assert store.ping() is False
471
+
472
+
473
+ @pytest.mark.unit
474
+ @pytest.mark.storage
475
+ class TestRedisDatasetStoreFlush:
476
+ """Tests for flush_prefix operation."""
477
+
478
+ def test_flush_prefix_with_keys(self, mock_redis_module) -> None:
479
+ """flush_prefix deletes matching keys."""
480
+ _, mock_client, _ = mock_redis_module
481
+ from juniper_data.storage.redis_store import RedisDatasetStore
482
+
483
+ store = RedisDatasetStore()
484
+ mock_client.scan_iter.return_value = [b"key1", b"key2"]
485
+ mock_client.delete.return_value = 2
486
+
487
+ result = store.flush_prefix()
488
+ assert result == 2
489
+ mock_client.delete.assert_called_once_with(b"key1", b"key2")
490
+
491
+ def test_flush_prefix_no_keys(self, mock_redis_module) -> None:
492
+ """flush_prefix returns 0 when no matching keys."""
493
+ _, mock_client, _ = mock_redis_module
494
+ from juniper_data.storage.redis_store import RedisDatasetStore
495
+
496
+ store = RedisDatasetStore()
497
+ mock_client.scan_iter.return_value = []
498
+
499
+ result = store.flush_prefix()
500
+ assert result == 0