cledar-sdk 2.0.2__py3-none-any.whl → 2.0.3__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 (85) hide show
  1. cledar/__init__.py +0 -0
  2. cledar/kafka/README.md +239 -0
  3. cledar/kafka/__init__.py +40 -0
  4. cledar/kafka/clients/base.py +98 -0
  5. cledar/kafka/clients/consumer.py +110 -0
  6. cledar/kafka/clients/producer.py +80 -0
  7. cledar/kafka/config/schemas.py +178 -0
  8. cledar/kafka/exceptions.py +22 -0
  9. cledar/kafka/handlers/dead_letter.py +82 -0
  10. cledar/kafka/handlers/parser.py +49 -0
  11. cledar/kafka/logger.py +3 -0
  12. cledar/kafka/models/input.py +13 -0
  13. cledar/kafka/models/message.py +10 -0
  14. cledar/kafka/models/output.py +8 -0
  15. cledar/kafka/tests/.env.test.kafka +3 -0
  16. cledar/kafka/tests/README.md +216 -0
  17. cledar/kafka/tests/conftest.py +104 -0
  18. cledar/kafka/tests/integration/__init__.py +1 -0
  19. cledar/kafka/tests/integration/conftest.py +78 -0
  20. cledar/kafka/tests/integration/helpers.py +47 -0
  21. cledar/kafka/tests/integration/test_consumer_integration.py +375 -0
  22. cledar/kafka/tests/integration/test_integration.py +394 -0
  23. cledar/kafka/tests/integration/test_producer_consumer_interaction.py +388 -0
  24. cledar/kafka/tests/integration/test_producer_integration.py +217 -0
  25. cledar/kafka/tests/unit/__init__.py +1 -0
  26. cledar/kafka/tests/unit/test_base_kafka_client.py +391 -0
  27. cledar/kafka/tests/unit/test_config_validation.py +609 -0
  28. cledar/kafka/tests/unit/test_dead_letter_handler.py +443 -0
  29. cledar/kafka/tests/unit/test_error_handling.py +674 -0
  30. cledar/kafka/tests/unit/test_input_parser.py +310 -0
  31. cledar/kafka/tests/unit/test_input_parser_comprehensive.py +489 -0
  32. cledar/kafka/tests/unit/test_utils.py +25 -0
  33. cledar/kafka/tests/unit/test_utils_comprehensive.py +408 -0
  34. cledar/kafka/utils/callbacks.py +19 -0
  35. cledar/kafka/utils/messages.py +28 -0
  36. cledar/kafka/utils/topics.py +2 -0
  37. cledar/kserve/README.md +352 -0
  38. cledar/kserve/__init__.py +3 -0
  39. cledar/kserve/tests/__init__.py +0 -0
  40. cledar/kserve/tests/test_utils.py +64 -0
  41. cledar/kserve/utils.py +27 -0
  42. cledar/logging/README.md +53 -0
  43. cledar/logging/__init__.py +3 -0
  44. cledar/logging/tests/test_universal_plaintext_formatter.py +249 -0
  45. cledar/logging/universal_plaintext_formatter.py +94 -0
  46. cledar/monitoring/README.md +71 -0
  47. cledar/monitoring/__init__.py +3 -0
  48. cledar/monitoring/monitoring_server.py +112 -0
  49. cledar/monitoring/tests/integration/test_monitoring_server_int.py +162 -0
  50. cledar/monitoring/tests/test_monitoring_server.py +59 -0
  51. cledar/nonce/README.md +99 -0
  52. cledar/nonce/__init__.py +3 -0
  53. cledar/nonce/nonce_service.py +36 -0
  54. cledar/nonce/tests/__init__.py +0 -0
  55. cledar/nonce/tests/test_nonce_service.py +136 -0
  56. cledar/redis/README.md +536 -0
  57. cledar/redis/__init__.py +15 -0
  58. cledar/redis/async_example.py +111 -0
  59. cledar/redis/example.py +37 -0
  60. cledar/redis/exceptions.py +22 -0
  61. cledar/redis/logger.py +3 -0
  62. cledar/redis/model.py +10 -0
  63. cledar/redis/redis.py +525 -0
  64. cledar/redis/redis_config_store.py +252 -0
  65. cledar/redis/tests/test_async_integration_redis.py +158 -0
  66. cledar/redis/tests/test_async_redis_service.py +380 -0
  67. cledar/redis/tests/test_integration_redis.py +119 -0
  68. cledar/redis/tests/test_redis_service.py +319 -0
  69. cledar/storage/README.md +529 -0
  70. cledar/storage/__init__.py +4 -0
  71. cledar/storage/constants.py +3 -0
  72. cledar/storage/exceptions.py +50 -0
  73. cledar/storage/models.py +19 -0
  74. cledar/storage/object_storage.py +955 -0
  75. cledar/storage/tests/conftest.py +18 -0
  76. cledar/storage/tests/test_abfs.py +164 -0
  77. cledar/storage/tests/test_integration_filesystem.py +359 -0
  78. cledar/storage/tests/test_integration_s3.py +453 -0
  79. cledar/storage/tests/test_local.py +384 -0
  80. cledar/storage/tests/test_s3.py +521 -0
  81. {cledar_sdk-2.0.2.dist-info → cledar_sdk-2.0.3.dist-info}/METADATA +1 -1
  82. cledar_sdk-2.0.3.dist-info/RECORD +84 -0
  83. cledar_sdk-2.0.2.dist-info/RECORD +0 -4
  84. {cledar_sdk-2.0.2.dist-info → cledar_sdk-2.0.3.dist-info}/WHEEL +0 -0
  85. {cledar_sdk-2.0.2.dist-info → cledar_sdk-2.0.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,380 @@
1
+ # mypy: disable-error-code=no-untyped-def
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from datetime import datetime
6
+ from enum import Enum
7
+ from unittest.mock import AsyncMock, patch
8
+
9
+ import pytest
10
+ import redis.asyncio as aioredis
11
+ from pydantic import BaseModel
12
+
13
+ from cledar.redis import AsyncRedisService, FailedValue, RedisServiceConfig
14
+
15
+
16
+ class UserModel(BaseModel):
17
+ user_id: int
18
+ name: str
19
+
20
+
21
+ class Color(Enum):
22
+ RED = 1
23
+ BLUE = 2
24
+
25
+
26
+ @pytest.fixture(name="config")
27
+ def fixture_config() -> RedisServiceConfig:
28
+ return RedisServiceConfig(redis_host="localhost", redis_port=6379, redis_db=0)
29
+
30
+
31
+ @pytest.fixture(name="async_redis_client")
32
+ def fixture_async_redis_client() -> AsyncMock:
33
+ client = AsyncMock()
34
+ client.ping = AsyncMock(return_value=True)
35
+ client.aclose = AsyncMock()
36
+ return client
37
+
38
+
39
+ @pytest.fixture(name="service")
40
+ def fixture_service(
41
+ config: RedisServiceConfig, async_redis_client: AsyncMock
42
+ ) -> AsyncRedisService:
43
+ with patch("cledar.redis.redis.aioredis.Redis", return_value=async_redis_client):
44
+ service = AsyncRedisService(config)
45
+ service._client = async_redis_client
46
+ return service
47
+
48
+
49
+ @pytest.mark.asyncio
50
+ async def test_connect_success_initializes_client(config: RedisServiceConfig) -> None:
51
+ with patch("cledar.redis.redis.aioredis.Redis") as redis_instance:
52
+ mock_client = AsyncMock()
53
+ mock_client.ping = AsyncMock(return_value=True)
54
+ redis_instance.return_value = mock_client
55
+
56
+ service = AsyncRedisService(config)
57
+ await service.connect()
58
+
59
+ redis_instance.assert_called_once_with(
60
+ host=config.redis_host,
61
+ port=config.redis_port,
62
+ db=config.redis_db,
63
+ password=config.redis_password,
64
+ decode_responses=True,
65
+ )
66
+
67
+
68
+ @pytest.mark.asyncio
69
+ async def test_connect_failure_raises_connection_error(
70
+ config: RedisServiceConfig,
71
+ ) -> None:
72
+ with patch("cledar.redis.redis.aioredis.Redis") as redis_instance:
73
+ mock_client = AsyncMock()
74
+ mock_client.ping = AsyncMock(side_effect=aioredis.ConnectionError())
75
+ redis_instance.return_value = mock_client
76
+
77
+ service = AsyncRedisService(config)
78
+ with pytest.raises(Exception) as exc:
79
+ await service.connect()
80
+ assert "Could not initialize Redis client" in str(exc.value)
81
+
82
+
83
+ @pytest.mark.asyncio
84
+ async def test_is_alive_true(
85
+ service: AsyncRedisService, async_redis_client: AsyncMock
86
+ ) -> None:
87
+ async_redis_client.ping.return_value = True
88
+ assert await service.is_alive() is True
89
+
90
+
91
+ @pytest.mark.asyncio
92
+ async def test_is_alive_false_on_exception(
93
+ service: AsyncRedisService, async_redis_client: AsyncMock
94
+ ) -> None:
95
+ async_redis_client.ping.side_effect = aioredis.ConnectionError()
96
+ assert await service.is_alive() is False
97
+
98
+
99
+ @pytest.mark.asyncio
100
+ async def test_set_with_pydantic_model_serializes_and_sets(
101
+ service: AsyncRedisService, async_redis_client: AsyncMock
102
+ ) -> None:
103
+ model = UserModel(user_id=1, name="Alice")
104
+ async_redis_client.set = AsyncMock(return_value=True)
105
+
106
+ result = await service.set("user:1", model)
107
+ assert result is True
108
+ value = async_redis_client.set.call_args.args[1]
109
+
110
+ as_dict = json.loads(value)
111
+ assert as_dict == model.model_dump()
112
+
113
+
114
+ @pytest.mark.asyncio
115
+ async def test_set_with_dict_enum_datetime_uses_custom_encoder(
116
+ service: AsyncRedisService, async_redis_client: AsyncMock
117
+ ) -> None:
118
+ now = datetime(2024, 1, 2, 3, 4, 5)
119
+ payload = {"color": Color.RED, "when": now}
120
+ async_redis_client.set = AsyncMock(return_value=True)
121
+
122
+ assert await service.set("meta", payload) is True
123
+ value = async_redis_client.set.call_args.args[1]
124
+ as_dict = json.loads(value)
125
+ assert as_dict["color"] == "red"
126
+ assert as_dict["when"] == now.isoformat()
127
+
128
+
129
+ @pytest.mark.asyncio
130
+ async def test_set_serialization_error_raises(service: AsyncRedisService) -> None:
131
+ bad = {"x": {1}}
132
+ with pytest.raises(Exception) as exc:
133
+ await service.set("k", bad)
134
+ assert "Failed to serialize value" in str(exc.value)
135
+
136
+
137
+ @pytest.mark.asyncio
138
+ async def test_set_connection_error_maps(
139
+ service: AsyncRedisService, async_redis_client: AsyncMock
140
+ ) -> None:
141
+ async_redis_client.set = AsyncMock(side_effect=aioredis.ConnectionError("conn"))
142
+ with pytest.raises(Exception) as exc:
143
+ await service.set("k", {"a": 1})
144
+ assert "Error connecting to Redis host" in str(exc.value)
145
+
146
+
147
+ @pytest.mark.asyncio
148
+ async def test_set_redis_error_maps(
149
+ service: AsyncRedisService, async_redis_client: AsyncMock
150
+ ) -> None:
151
+ async_redis_client.set = AsyncMock(side_effect=aioredis.RedisError("oops"))
152
+ with pytest.raises(Exception) as exc:
153
+ await service.set("k", {"a": 1})
154
+ assert "Failed to set key" in str(exc.value)
155
+
156
+
157
+ @pytest.mark.asyncio
158
+ async def test_get_returns_none_for_missing(
159
+ service: AsyncRedisService, async_redis_client: AsyncMock
160
+ ) -> None:
161
+ async_redis_client.get = AsyncMock(return_value=None)
162
+ assert await service.get("missing", UserModel) is None
163
+
164
+
165
+ @pytest.mark.asyncio
166
+ async def test_get_success_deserializes_to_model(
167
+ service: AsyncRedisService, async_redis_client: AsyncMock
168
+ ) -> None:
169
+ model = UserModel(user_id=2, name="Bob")
170
+ async_redis_client.get = AsyncMock(return_value=json.dumps(model.model_dump()))
171
+ got = await service.get("user:2", UserModel)
172
+ assert isinstance(got, UserModel)
173
+ assert got == model
174
+
175
+
176
+ @pytest.mark.asyncio
177
+ async def test_get_json_decode_error_maps(
178
+ service: AsyncRedisService, async_redis_client: AsyncMock
179
+ ) -> None:
180
+ async_redis_client.get = AsyncMock(return_value="not-json")
181
+ with pytest.raises(Exception) as exc:
182
+ await service.get("k", UserModel)
183
+ assert "Failed to decode JSON" in str(exc.value)
184
+
185
+
186
+ @pytest.mark.asyncio
187
+ async def test_get_validation_error_maps(
188
+ service: AsyncRedisService, async_redis_client: AsyncMock
189
+ ) -> None:
190
+ async_redis_client.get = AsyncMock(return_value=json.dumps({"user_id": 3}))
191
+ with pytest.raises(Exception) as exc:
192
+ await service.get("k", UserModel)
193
+ assert "Validation failed" in str(exc.value)
194
+
195
+
196
+ @pytest.mark.asyncio
197
+ async def test_get_connection_error_maps(
198
+ service: AsyncRedisService, async_redis_client: AsyncMock
199
+ ) -> None:
200
+ async_redis_client.get = AsyncMock(side_effect=aioredis.ConnectionError("down"))
201
+ with pytest.raises(Exception) as exc:
202
+ await service.get("k", UserModel)
203
+ assert "Error connecting to Redis host" in str(exc.value)
204
+
205
+
206
+ @pytest.mark.asyncio
207
+ async def test_get_redis_error_maps(
208
+ service: AsyncRedisService, async_redis_client: AsyncMock
209
+ ) -> None:
210
+ async_redis_client.get = AsyncMock(side_effect=aioredis.RedisError("nope"))
211
+ with pytest.raises(Exception) as exc:
212
+ await service.get("k", UserModel)
213
+ assert "Failed to get key" in str(exc.value)
214
+
215
+
216
+ @pytest.mark.asyncio
217
+ async def test_get_raw_returns_value(
218
+ service: AsyncRedisService, async_redis_client: AsyncMock
219
+ ) -> None:
220
+ async_redis_client.get = AsyncMock(return_value="raw")
221
+ assert await service.get_raw("k") == "raw"
222
+
223
+
224
+ @pytest.mark.asyncio
225
+ async def test_get_raw_errors_map(
226
+ service: AsyncRedisService, async_redis_client: AsyncMock
227
+ ) -> None:
228
+ async_redis_client.get = AsyncMock(side_effect=aioredis.RedisError("err"))
229
+ with pytest.raises(Exception) as exc:
230
+ await service.get_raw("k")
231
+ assert "Failed to get key" in str(exc.value)
232
+
233
+
234
+ @pytest.mark.asyncio
235
+ async def test_list_keys_success(
236
+ service: AsyncRedisService, async_redis_client: AsyncMock
237
+ ) -> None:
238
+ async_redis_client.keys = AsyncMock(return_value=["a", "b"])
239
+ assert await service.list_keys("*") == ["a", "b"]
240
+
241
+
242
+ @pytest.mark.asyncio
243
+ async def test_list_keys_connection_error(
244
+ service: AsyncRedisService, async_redis_client: AsyncMock
245
+ ) -> None:
246
+ async_redis_client.keys = AsyncMock(side_effect=aioredis.ConnectionError("err"))
247
+ with pytest.raises(Exception) as exc:
248
+ await service.list_keys("*")
249
+ assert "Error connecting to Redis host" in str(exc.value)
250
+
251
+
252
+ @pytest.mark.asyncio
253
+ async def test_list_keys_redis_error(
254
+ service: AsyncRedisService, async_redis_client: AsyncMock
255
+ ) -> None:
256
+ async_redis_client.keys = AsyncMock(side_effect=aioredis.RedisError("err"))
257
+ with pytest.raises(Exception) as exc:
258
+ await service.list_keys("*")
259
+ assert "Failed to list keys" in str(exc.value)
260
+
261
+
262
+ @pytest.mark.asyncio
263
+ async def test_mget_empty_returns_empty(service: AsyncRedisService) -> None:
264
+ assert await service.mget([], UserModel) == []
265
+
266
+
267
+ @pytest.mark.asyncio
268
+ async def test_mget_success_and_failures(
269
+ service: AsyncRedisService, async_redis_client: AsyncMock
270
+ ) -> None:
271
+ good = UserModel(user_id=1, name="A").model_dump()
272
+ bad_json = "{not-json}"
273
+ bad_validation = json.dumps({"user_id": 2})
274
+ none_value = None
275
+ async_redis_client.mget = AsyncMock(
276
+ return_value=[
277
+ json.dumps(good),
278
+ bad_json,
279
+ bad_validation,
280
+ none_value,
281
+ ]
282
+ )
283
+ keys = ["k1", "k2", "k3", "k4"]
284
+ results = await service.mget(keys, UserModel)
285
+
286
+ assert isinstance(results[0], UserModel)
287
+ assert isinstance(results[1], FailedValue)
288
+ assert results[1].key == "k2"
289
+ assert isinstance(results[2], FailedValue)
290
+ assert results[2].key == "k3"
291
+ assert results[3] is None
292
+
293
+
294
+ @pytest.mark.asyncio
295
+ async def test_mget_connection_error_maps(
296
+ service: AsyncRedisService, async_redis_client: AsyncMock
297
+ ) -> None:
298
+ async_redis_client.mget = AsyncMock(side_effect=aioredis.ConnectionError("down"))
299
+ with pytest.raises(Exception) as exc:
300
+ await service.mget(["a"], UserModel)
301
+ assert "Error connecting to Redis host" in str(exc.value)
302
+
303
+
304
+ @pytest.mark.asyncio
305
+ async def test_mget_redis_error_maps(
306
+ service: AsyncRedisService, async_redis_client: AsyncMock
307
+ ) -> None:
308
+ async_redis_client.mget = AsyncMock(side_effect=aioredis.RedisError("err"))
309
+ with pytest.raises(Exception) as exc:
310
+ await service.mget(["a"], UserModel)
311
+ assert "Failed to mget keys" in str(exc.value)
312
+
313
+
314
+ @pytest.mark.asyncio
315
+ async def test_delete_success(
316
+ service: AsyncRedisService, async_redis_client: AsyncMock
317
+ ) -> None:
318
+ async_redis_client.delete = AsyncMock(return_value=1)
319
+ assert await service.delete("k") is True
320
+ async_redis_client.delete.assert_called_once_with("k")
321
+
322
+
323
+ @pytest.mark.asyncio
324
+ async def test_delete_connection_error_maps(
325
+ service: AsyncRedisService, async_redis_client: AsyncMock
326
+ ) -> None:
327
+ async_redis_client.delete = AsyncMock(side_effect=aioredis.ConnectionError("down"))
328
+ with pytest.raises(Exception) as exc:
329
+ await service.delete("k")
330
+ assert "Error connecting to Redis host" in str(exc.value)
331
+
332
+
333
+ @pytest.mark.asyncio
334
+ async def test_delete_redis_error_maps(
335
+ service: AsyncRedisService, async_redis_client: AsyncMock
336
+ ) -> None:
337
+ async_redis_client.delete = AsyncMock(side_effect=aioredis.RedisError("err"))
338
+ with pytest.raises(Exception) as exc:
339
+ await service.delete("k")
340
+ assert "Failed to delete key" in str(exc.value)
341
+
342
+
343
+ @pytest.mark.asyncio
344
+ async def test_set_plain_string_value(
345
+ service: AsyncRedisService, async_redis_client: AsyncMock
346
+ ) -> None:
347
+ async_redis_client.set = AsyncMock(return_value=True)
348
+ assert await service.set("greeting", "hello") is True
349
+ value = (
350
+ async_redis_client.set.call_args.args[1]
351
+ if async_redis_client.set.call_args.args
352
+ else async_redis_client.set.call_args[0][1]
353
+ )
354
+ assert value == "hello"
355
+
356
+
357
+ @pytest.mark.asyncio
358
+ async def test_type_validation_errors(service: AsyncRedisService) -> None:
359
+ with pytest.raises(ValueError, match="Key must be a string"):
360
+ await service.set(123, "x") # type: ignore[arg-type]
361
+
362
+ with pytest.raises(ValueError, match="Key must be a string"):
363
+ await service.get(123, UserModel) # type: ignore[arg-type]
364
+
365
+ with pytest.raises(ValueError, match="Key must be a string"):
366
+ await service.get_raw(123) # type: ignore[arg-type]
367
+
368
+ with pytest.raises(ValueError, match="Pattern must be a string"):
369
+ await service.list_keys(123) # type: ignore[arg-type]
370
+
371
+ with pytest.raises(ValueError, match="Keys must be a list"):
372
+ await service.mget("not-a-list", UserModel) # type: ignore[arg-type]
373
+
374
+
375
+ @pytest.mark.asyncio
376
+ async def test_close_connection(
377
+ service: AsyncRedisService, async_redis_client: AsyncMock
378
+ ) -> None:
379
+ await service.close()
380
+ async_redis_client.aclose.assert_called_once()
@@ -0,0 +1,119 @@
1
+ # mypy: disable-error-code=no-untyped-def
2
+ import json
3
+ from datetime import datetime
4
+ from enum import Enum
5
+
6
+ import pytest
7
+ from pydantic import BaseModel
8
+ from testcontainers.redis import RedisContainer
9
+
10
+ from cledar.redis.redis import (
11
+ CustomEncoder,
12
+ FailedValue,
13
+ RedisService,
14
+ RedisServiceConfig,
15
+ )
16
+
17
+
18
+ class UserModel(BaseModel):
19
+ user_id: int
20
+ name: str
21
+
22
+
23
+ class Color(Enum):
24
+ RED = 1
25
+ BLUE = 2
26
+
27
+
28
+ @pytest.fixture(scope="module")
29
+ def redis_container():
30
+ """Start a Redis container for testing."""
31
+ with RedisContainer("redis:7.2-alpine") as redis_db:
32
+ yield redis_db
33
+
34
+
35
+ @pytest.fixture(scope="module")
36
+ def redis_service(redis_container: RedisContainer) -> RedisService:
37
+ host = redis_container.get_container_host_ip()
38
+ port = int(redis_container.get_exposed_port(6379))
39
+
40
+ config = RedisServiceConfig(redis_host=host, redis_port=port, redis_db=0)
41
+ return RedisService(config)
42
+
43
+
44
+ def test_is_alive(redis_service: RedisService) -> None:
45
+ assert redis_service.is_alive() is True
46
+
47
+
48
+ def test_set_and_get_pydantic_model(redis_service: RedisService) -> None:
49
+ key = "user:1"
50
+ model = UserModel(user_id=1, name="Alice")
51
+ assert redis_service.set(key, model) is True
52
+ got = redis_service.get(key, UserModel)
53
+ assert isinstance(got, UserModel)
54
+ assert got == model
55
+
56
+
57
+ def test_set_plain_string_and_get_raw(redis_service: RedisService) -> None:
58
+ key = "greeting"
59
+ assert redis_service.set(key, "hello") is True
60
+ assert redis_service.get_raw(key) == "hello"
61
+
62
+
63
+ def test_set_with_enum_and_datetime_uses_custom_encoder(
64
+ redis_service: RedisService,
65
+ ) -> None:
66
+ key = "meta"
67
+ now = datetime(2024, 1, 2, 3, 4, 5)
68
+ payload = {"color": Color.RED, "when": now}
69
+ assert redis_service.set(key, payload) is True
70
+
71
+ raw = redis_service.get_raw(key)
72
+ data = json.loads(raw) # type: ignore
73
+ assert data["color"] == "red"
74
+ assert data["when"] == now.isoformat()
75
+
76
+
77
+ def test_list_keys(redis_service: RedisService) -> None:
78
+ prefix = "listkeys:test:"
79
+ keys = [f"{prefix}{i}" for i in range(3)]
80
+ for k in keys:
81
+ assert redis_service.set(k, {"i": 1}) is True
82
+
83
+ listed = redis_service.list_keys(f"{prefix}*")
84
+ for k in keys:
85
+ assert k in listed
86
+
87
+
88
+ def test_mget_mixed_results(redis_service: RedisService) -> None:
89
+ ok = UserModel(user_id=2, name="Bob")
90
+ k1 = "mget:ok"
91
+ k2 = "mget:not_json"
92
+ k3 = "mget:bad_validation"
93
+ k4 = "mget:none"
94
+
95
+ assert redis_service.set(k1, ok) is True
96
+ assert redis_service.set(k2, "{not-json}") is True
97
+ assert redis_service.set(k3, json.dumps({"user_id": 3})) is True
98
+
99
+ results = redis_service.mget([k1, k2, k3, k4], UserModel)
100
+
101
+ assert isinstance(results[0], UserModel)
102
+ assert isinstance(results[1], FailedValue)
103
+ assert isinstance(results[2], FailedValue)
104
+ assert results[3] is None
105
+
106
+
107
+ def test_delete(redis_service: RedisService) -> None:
108
+ key = "delete:test"
109
+ assert redis_service.set(key, {"x": 1}) is True
110
+ assert redis_service.delete(key) is True
111
+ assert redis_service.get_raw(key) is None
112
+
113
+
114
+ def test_custom_encoder_direct_usage() -> None:
115
+ payload = {"c": Color.BLUE, "d": datetime(2025, 1, 1, 0, 0, 0)}
116
+ s = json.dumps(payload, cls=CustomEncoder)
117
+ data = json.loads(s)
118
+ assert data["c"] == "blue"
119
+ assert data["d"] == "2025-01-01T00:00:00"