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,281 @@
1
+ """Unit tests for API security: authentication and rate limiting."""
2
+
3
+ import time
4
+ from unittest.mock import MagicMock
5
+
6
+ import pytest
7
+ from fastapi import HTTPException
8
+
9
+ from juniper_data.api.security import APIKeyAuth, RateLimiter
10
+
11
+
12
+ class TestAPIKeyAuth:
13
+ """Tests for APIKeyAuth class."""
14
+
15
+ def test_disabled_when_no_keys(self) -> None:
16
+ """Auth should be disabled when no keys are configured."""
17
+ auth = APIKeyAuth(None)
18
+ assert not auth.enabled
19
+
20
+ auth = APIKeyAuth([])
21
+ assert not auth.enabled
22
+
23
+ def test_enabled_when_keys_configured(self) -> None:
24
+ """Auth should be enabled when keys are configured."""
25
+ auth = APIKeyAuth(["key1", "key2"])
26
+ assert auth.enabled
27
+
28
+ def test_validate_returns_true_when_disabled(self) -> None:
29
+ """Validate should return True when auth is disabled."""
30
+ auth = APIKeyAuth(None)
31
+ assert auth.validate(None)
32
+ assert auth.validate("any-key")
33
+
34
+ def test_validate_valid_key(self) -> None:
35
+ """Validate should return True for valid key."""
36
+ auth = APIKeyAuth(["valid-key"])
37
+ assert auth.validate("valid-key")
38
+
39
+ def test_validate_invalid_key(self) -> None:
40
+ """Validate should return False for invalid key."""
41
+ auth = APIKeyAuth(["valid-key"])
42
+ assert not auth.validate("invalid-key")
43
+ assert not auth.validate(None)
44
+
45
+ @pytest.mark.asyncio
46
+ async def test_call_returns_none_when_disabled(self) -> None:
47
+ """Dependency should return None when auth is disabled."""
48
+ auth = APIKeyAuth(None)
49
+ request = MagicMock()
50
+ request.headers.get.return_value = None
51
+
52
+ result = await auth(request)
53
+ assert result is None
54
+
55
+ @pytest.mark.asyncio
56
+ async def test_call_raises_401_when_missing_key(self) -> None:
57
+ """Dependency should raise 401 when key is missing."""
58
+ auth = APIKeyAuth(["valid-key"])
59
+ request = MagicMock()
60
+ request.headers.get.return_value = None
61
+
62
+ with pytest.raises(HTTPException) as exc_info:
63
+ await auth(request)
64
+ assert exc_info.value.status_code == 401
65
+ assert "Missing API key" in exc_info.value.detail
66
+
67
+ @pytest.mark.asyncio
68
+ async def test_call_raises_401_when_invalid_key(self) -> None:
69
+ """Dependency should raise 401 when key is invalid."""
70
+ auth = APIKeyAuth(["valid-key"])
71
+ request = MagicMock()
72
+ request.headers.get.return_value = "invalid-key"
73
+
74
+ with pytest.raises(HTTPException) as exc_info:
75
+ await auth(request)
76
+ assert exc_info.value.status_code == 401
77
+ assert "Invalid API key" in exc_info.value.detail
78
+
79
+ @pytest.mark.asyncio
80
+ async def test_call_returns_key_when_valid(self) -> None:
81
+ """Dependency should return the key when valid."""
82
+ auth = APIKeyAuth(["valid-key"])
83
+ request = MagicMock()
84
+ request.headers.get.return_value = "valid-key"
85
+
86
+ result = await auth(request)
87
+ assert result == "valid-key"
88
+
89
+
90
+ class TestRateLimiter:
91
+ """Tests for RateLimiter class."""
92
+
93
+ def test_disabled_allows_all(self) -> None:
94
+ """Disabled limiter should allow all requests."""
95
+ limiter = RateLimiter(requests_per_minute=5, enabled=False)
96
+
97
+ for _ in range(100):
98
+ allowed, remaining, _ = limiter.check("key")
99
+ assert allowed
100
+
101
+ def test_allows_within_limit(self) -> None:
102
+ """Limiter should allow requests within limit."""
103
+ limiter = RateLimiter(requests_per_minute=5, enabled=True)
104
+
105
+ for i in range(5):
106
+ allowed, remaining, _ = limiter.check("key")
107
+ assert allowed
108
+ assert remaining == 5 - i - 1
109
+
110
+ def test_blocks_over_limit(self) -> None:
111
+ """Limiter should block requests over limit."""
112
+ limiter = RateLimiter(requests_per_minute=5, enabled=True)
113
+
114
+ for _ in range(5):
115
+ limiter.check("key")
116
+
117
+ allowed, remaining, reset_in = limiter.check("key")
118
+ assert not allowed
119
+ assert remaining == 0
120
+ assert reset_in > 0
121
+
122
+ def test_different_keys_tracked_separately(self) -> None:
123
+ """Different keys should have separate limits."""
124
+ limiter = RateLimiter(requests_per_minute=5, enabled=True)
125
+
126
+ for _ in range(5):
127
+ limiter.check("key1")
128
+
129
+ allowed1, _, _ = limiter.check("key1")
130
+ allowed2, _, _ = limiter.check("key2")
131
+
132
+ assert not allowed1
133
+ assert allowed2
134
+
135
+ def test_window_reset(self) -> None:
136
+ """Window should reset after time expires."""
137
+ limiter = RateLimiter(requests_per_minute=5, window_seconds=1, enabled=True)
138
+
139
+ for _ in range(5):
140
+ limiter.check("key")
141
+
142
+ allowed, _, _ = limiter.check("key")
143
+ assert not allowed
144
+
145
+ time.sleep(1.1)
146
+
147
+ allowed, remaining, _ = limiter.check("key")
148
+ assert allowed
149
+ assert remaining == 4
150
+
151
+ def test_reset_clears_counters(self) -> None:
152
+ """Reset should clear all counters."""
153
+ limiter = RateLimiter(requests_per_minute=5, enabled=True)
154
+
155
+ for _ in range(5):
156
+ limiter.check("key")
157
+
158
+ allowed, _, _ = limiter.check("key")
159
+ assert not allowed
160
+
161
+ limiter.reset()
162
+
163
+ allowed, _, _ = limiter.check("key")
164
+ assert allowed
165
+
166
+ @pytest.mark.asyncio
167
+ async def test_call_allows_when_within_limit(self) -> None:
168
+ """Dependency should allow requests within limit."""
169
+ limiter = RateLimiter(requests_per_minute=5, enabled=True)
170
+ request = MagicMock()
171
+ request.client.host = "127.0.0.1"
172
+ request.state = MagicMock()
173
+
174
+ for _ in range(5):
175
+ await limiter(request, api_key=None)
176
+
177
+ @pytest.mark.asyncio
178
+ async def test_call_raises_429_when_over_limit(self) -> None:
179
+ """Dependency should raise 429 when over limit."""
180
+ limiter = RateLimiter(requests_per_minute=5, enabled=True)
181
+ request = MagicMock()
182
+ request.client.host = "127.0.0.1"
183
+ request.state = MagicMock()
184
+
185
+ for _ in range(5):
186
+ await limiter(request, api_key=None)
187
+
188
+ with pytest.raises(HTTPException) as exc_info:
189
+ await limiter(request, api_key=None)
190
+ assert exc_info.value.status_code == 429
191
+ assert "Rate limit exceeded" in exc_info.value.detail
192
+
193
+ @pytest.mark.asyncio
194
+ async def test_call_uses_api_key_for_limiting(self) -> None:
195
+ """Dependency should use API key for rate limiting when provided."""
196
+ limiter = RateLimiter(requests_per_minute=5, enabled=True)
197
+ request = MagicMock()
198
+ request.client.host = "127.0.0.1"
199
+ request.state = MagicMock()
200
+
201
+ for _ in range(5):
202
+ await limiter(request, api_key="key1")
203
+
204
+ with pytest.raises(HTTPException):
205
+ await limiter(request, api_key="key1")
206
+
207
+ await limiter(request, api_key="key2")
208
+
209
+ def test_rate_limiter_window_property(self) -> None:
210
+ """Window property should return configured window seconds."""
211
+ limiter = RateLimiter(requests_per_minute=10, window_seconds=30)
212
+ assert limiter.window == 30
213
+
214
+ def test_get_key_with_no_client(self) -> None:
215
+ """_get_key should return 'ip:unknown' when request has no client."""
216
+ limiter = RateLimiter()
217
+ request = MagicMock()
218
+ request.client = None
219
+ key = limiter._get_key(request, None)
220
+ assert key == "ip:unknown"
221
+
222
+ @pytest.mark.asyncio
223
+ async def test_call_noop_when_disabled(self) -> None:
224
+ """Dependency should do nothing when disabled."""
225
+ limiter = RateLimiter(requests_per_minute=5, enabled=False)
226
+ request = MagicMock()
227
+ request.client.host = "127.0.0.1"
228
+
229
+ for _ in range(100):
230
+ await limiter(request, api_key=None)
231
+
232
+
233
+ class TestSecurityModuleFunctions:
234
+ """Tests for module-level security functions."""
235
+
236
+ def test_get_api_key_auth_returns_instance(self) -> None:
237
+ """get_api_key_auth should return an APIKeyAuth instance."""
238
+ from juniper_data.api.security import get_api_key_auth, reset_security_state
239
+
240
+ reset_security_state()
241
+ auth = get_api_key_auth()
242
+ assert isinstance(auth, APIKeyAuth)
243
+
244
+ def test_get_api_key_auth_returns_same_instance(self) -> None:
245
+ """get_api_key_auth should return same instance on second call."""
246
+ from juniper_data.api.security import get_api_key_auth, reset_security_state
247
+
248
+ reset_security_state()
249
+ auth1 = get_api_key_auth()
250
+ auth2 = get_api_key_auth()
251
+ assert auth1 is auth2
252
+
253
+ def test_get_rate_limiter_returns_instance(self) -> None:
254
+ """get_rate_limiter should return a RateLimiter instance."""
255
+ from juniper_data.api.security import get_rate_limiter, reset_security_state
256
+
257
+ reset_security_state()
258
+ limiter = get_rate_limiter()
259
+ assert isinstance(limiter, RateLimiter)
260
+
261
+ def test_get_rate_limiter_returns_same_instance(self) -> None:
262
+ """get_rate_limiter should return same instance on second call."""
263
+ from juniper_data.api.security import get_rate_limiter, reset_security_state
264
+
265
+ reset_security_state()
266
+ limiter1 = get_rate_limiter()
267
+ limiter2 = get_rate_limiter()
268
+ assert limiter1 is limiter2
269
+
270
+ def test_reset_security_state(self) -> None:
271
+ """reset_security_state should clear cached instances."""
272
+ from juniper_data.api.security import get_api_key_auth, get_rate_limiter, reset_security_state
273
+
274
+ reset_security_state()
275
+ auth1 = get_api_key_auth()
276
+ limiter1 = get_rate_limiter()
277
+ reset_security_state()
278
+ auth2 = get_api_key_auth()
279
+ limiter2 = get_rate_limiter()
280
+ assert auth1 is not auth2
281
+ assert limiter1 is not limiter2