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.
- juniper_data/__init__.py +88 -0
- juniper_data/__main__.py +78 -0
- juniper_data/api/__init__.py +10 -0
- juniper_data/api/app.py +111 -0
- juniper_data/api/middleware.py +95 -0
- juniper_data/api/routes/__init__.py +9 -0
- juniper_data/api/routes/datasets.py +414 -0
- juniper_data/api/routes/generators.py +125 -0
- juniper_data/api/routes/health.py +49 -0
- juniper_data/api/security.py +238 -0
- juniper_data/api/settings.py +109 -0
- juniper_data/core/__init__.py +32 -0
- juniper_data/core/artifacts.py +63 -0
- juniper_data/core/dataset_id.py +38 -0
- juniper_data/core/models.py +135 -0
- juniper_data/core/split.py +120 -0
- juniper_data/generators/__init__.py +15 -0
- juniper_data/generators/arc_agi/__init__.py +11 -0
- juniper_data/generators/arc_agi/generator.py +229 -0
- juniper_data/generators/arc_agi/params.py +56 -0
- juniper_data/generators/checkerboard/__init__.py +15 -0
- juniper_data/generators/checkerboard/generator.py +114 -0
- juniper_data/generators/checkerboard/params.py +32 -0
- juniper_data/generators/circles/__init__.py +11 -0
- juniper_data/generators/circles/generator.py +112 -0
- juniper_data/generators/circles/params.py +31 -0
- juniper_data/generators/csv_import/__init__.py +15 -0
- juniper_data/generators/csv_import/generator.py +198 -0
- juniper_data/generators/csv_import/params.py +48 -0
- juniper_data/generators/gaussian/__init__.py +11 -0
- juniper_data/generators/gaussian/generator.py +149 -0
- juniper_data/generators/gaussian/params.py +53 -0
- juniper_data/generators/mnist/__init__.py +11 -0
- juniper_data/generators/mnist/generator.py +124 -0
- juniper_data/generators/mnist/params.py +39 -0
- juniper_data/generators/spiral/__init__.py +57 -0
- juniper_data/generators/spiral/defaults.py +39 -0
- juniper_data/generators/spiral/generator.py +206 -0
- juniper_data/generators/spiral/params.py +148 -0
- juniper_data/generators/xor/__init__.py +11 -0
- juniper_data/generators/xor/generator.py +162 -0
- juniper_data/generators/xor/params.py +30 -0
- juniper_data/storage/__init__.py +120 -0
- juniper_data/storage/base.py +279 -0
- juniper_data/storage/cached.py +211 -0
- juniper_data/storage/hf_store.py +257 -0
- juniper_data/storage/kaggle_store.py +333 -0
- juniper_data/storage/local_fs.py +232 -0
- juniper_data/storage/memory.py +136 -0
- juniper_data/storage/postgres_store.py +373 -0
- juniper_data/storage/redis_store.py +264 -0
- juniper_data/tests/__init__.py +1 -0
- juniper_data/tests/conftest.py +68 -0
- juniper_data/tests/fixtures/generate_golden_datasets.py +199 -0
- juniper_data/tests/integration/__init__.py +1 -0
- juniper_data/tests/integration/test_api.py +283 -0
- juniper_data/tests/integration/test_e2e_workflow.py +378 -0
- juniper_data/tests/integration/test_lifecycle_api.py +304 -0
- juniper_data/tests/integration/test_security_integration.py +189 -0
- juniper_data/tests/integration/test_storage_workflow.py +259 -0
- juniper_data/tests/performance/__init__.py +1 -0
- juniper_data/tests/performance/test_generator_benchmarks.py +178 -0
- juniper_data/tests/performance/test_storage_benchmarks.py +257 -0
- juniper_data/tests/unit/__init__.py +1 -0
- juniper_data/tests/unit/test_api_app.py +206 -0
- juniper_data/tests/unit/test_api_routes.py +407 -0
- juniper_data/tests/unit/test_api_settings.py +100 -0
- juniper_data/tests/unit/test_arc_agi_generator.py +525 -0
- juniper_data/tests/unit/test_artifacts.py +145 -0
- juniper_data/tests/unit/test_cached_store.py +423 -0
- juniper_data/tests/unit/test_checkerboard_generator.py +232 -0
- juniper_data/tests/unit/test_circles_generator.py +256 -0
- juniper_data/tests/unit/test_csv_import_generator.py +345 -0
- juniper_data/tests/unit/test_dataset_id.py +181 -0
- juniper_data/tests/unit/test_gaussian_generator.py +333 -0
- juniper_data/tests/unit/test_hf_store.py +416 -0
- juniper_data/tests/unit/test_init.py +93 -0
- juniper_data/tests/unit/test_kaggle_store.py +469 -0
- juniper_data/tests/unit/test_lifecycle.py +394 -0
- juniper_data/tests/unit/test_main.py +127 -0
- juniper_data/tests/unit/test_middleware.py +79 -0
- juniper_data/tests/unit/test_mnist_generator.py +370 -0
- juniper_data/tests/unit/test_postgres_store.py +490 -0
- juniper_data/tests/unit/test_redis_store.py +500 -0
- juniper_data/tests/unit/test_security.py +281 -0
- juniper_data/tests/unit/test_security_boundaries.py +517 -0
- juniper_data/tests/unit/test_spiral_generator.py +566 -0
- juniper_data/tests/unit/test_split.py +245 -0
- juniper_data/tests/unit/test_storage.py +767 -0
- juniper_data/tests/unit/test_xor_generator.py +223 -0
- juniper_data-0.4.2.dist-info/METADATA +216 -0
- juniper_data-0.4.2.dist-info/RECORD +95 -0
- juniper_data-0.4.2.dist-info/WHEEL +5 -0
- juniper_data-0.4.2.dist-info/licenses/LICENSE +9 -0
- 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
|