cledar-sdk 2.0.1__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.1.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.1.dist-info/RECORD +0 -4
  84. {cledar_sdk-2.0.1.dist-info → cledar_sdk-2.0.3.dist-info}/WHEEL +0 -0
  85. {cledar_sdk-2.0.1.dist-info → cledar_sdk-2.0.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,22 @@
1
+ class RedisServiceError(Exception):
2
+ """Base exception for RedisService errors."""
3
+
4
+
5
+ class RedisConnectionError(RedisServiceError):
6
+ """Raised when the Redis connection cannot be established or used."""
7
+
8
+
9
+ class RedisClientNotInitializedError(RedisServiceError):
10
+ """Raised when a Redis operation is attempted without an initialized client."""
11
+
12
+
13
+ class RedisSerializationError(RedisServiceError):
14
+ """Raised when serialization of a value fails before sending to Redis."""
15
+
16
+
17
+ class RedisDeserializationError(RedisServiceError):
18
+ """Raised when deserialization of a value fetched from Redis fails."""
19
+
20
+
21
+ class RedisOperationError(RedisServiceError):
22
+ """Raised for generic Redis operation errors (e.g., command failures)."""
cledar/redis/logger.py ADDED
@@ -0,0 +1,3 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger("redis_service")
cledar/redis/model.py ADDED
@@ -0,0 +1,10 @@
1
+ from dataclasses import dataclass
2
+ from typing import TypeVar
3
+
4
+
5
+ @dataclass
6
+ class BaseConfigClass:
7
+ pass
8
+
9
+
10
+ ConfigAbstract = TypeVar("ConfigAbstract", bound=BaseConfigClass)
cledar/redis/redis.py ADDED
@@ -0,0 +1,525 @@
1
+ import json
2
+ import logging
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from enum import Enum
6
+ from typing import Any, TypeVar, cast
7
+
8
+ import redis
9
+ import redis.asyncio as aioredis
10
+ from pydantic import BaseModel, ValidationError
11
+
12
+ from .exceptions import (
13
+ RedisConnectionError,
14
+ RedisDeserializationError,
15
+ RedisOperationError,
16
+ RedisSerializationError,
17
+ )
18
+
19
+ logger = logging.getLogger("redis_service")
20
+
21
+
22
+ class CustomEncoder(json.JSONEncoder):
23
+ """
24
+ Custom JSON encoder that can handle Enum objects and datetime objects.
25
+ """
26
+
27
+ def default(self, o: Any) -> Any:
28
+ if isinstance(o, Enum):
29
+ return o.name.lower()
30
+ if isinstance(o, datetime):
31
+ return o.isoformat()
32
+ return super().default(o)
33
+
34
+
35
+ T = TypeVar("T", bound=BaseModel)
36
+
37
+
38
+ @dataclass
39
+ class FailedValue:
40
+ key: str
41
+ error: Exception
42
+
43
+
44
+ @dataclass
45
+ class RedisServiceConfig:
46
+ redis_host: str
47
+ redis_port: int
48
+ redis_db: int = 0
49
+ redis_password: str | None = None
50
+
51
+
52
+ class RedisService:
53
+ def __init__(self, config: RedisServiceConfig):
54
+ self.config = config
55
+ self._client: redis.Redis
56
+ self.connect()
57
+
58
+ def connect(self) -> None:
59
+ try:
60
+ self._client = redis.Redis(
61
+ host=self.config.redis_host,
62
+ port=self.config.redis_port,
63
+ db=self.config.redis_db,
64
+ password=self.config.redis_password,
65
+ decode_responses=True,
66
+ )
67
+ logger.info(
68
+ "Redis client initialized.",
69
+ extra={
70
+ "host": self.config.redis_host,
71
+ "port": self.config.redis_port,
72
+ "db": self.config.redis_db,
73
+ },
74
+ )
75
+ self._client.ping()
76
+ logger.info(
77
+ "Redis client pinged successfully.",
78
+ extra={"host": self.config.redis_host},
79
+ )
80
+ except redis.ConnectionError as exc:
81
+ logger.exception("Failed to initialize Redis client.")
82
+ raise RedisConnectionError("Could not initialize Redis client") from exc
83
+
84
+ def is_alive(self) -> bool:
85
+ try:
86
+ return bool(self._client.ping())
87
+ except redis.ConnectionError:
88
+ logger.exception(
89
+ "Redis connection error during health check. Can't ping Redis host %s",
90
+ self.config.redis_host,
91
+ )
92
+ return False
93
+
94
+ def _prepare_for_serialization(self, value: Any) -> Any:
95
+ """
96
+ Recursively process data structures, converting BaseModel instances to
97
+ serializable dicts.
98
+ """
99
+ if isinstance(value, BaseModel):
100
+ return value.model_dump()
101
+ if isinstance(value, list):
102
+ return [self._prepare_for_serialization(item) for item in value]
103
+ if isinstance(value, dict):
104
+ return {k: self._prepare_for_serialization(v) for k, v in value.items()}
105
+ return value
106
+
107
+ def set(self, key: str, value: Any) -> bool:
108
+ if not isinstance(key, str):
109
+ raise ValueError(f"Key must be a string, got {type(key)}")
110
+ if value is None:
111
+ logger.debug("Value is none", extra={"key": key})
112
+ try:
113
+ processed_value = self._prepare_for_serialization(value)
114
+ if isinstance(processed_value, (dict, list)):
115
+ try:
116
+ final_value = json.dumps(processed_value, cls=CustomEncoder)
117
+
118
+ except (TypeError, ValueError) as exc:
119
+ logger.exception(
120
+ "Serialization error before setting Redis key.",
121
+ extra={"key": key},
122
+ )
123
+ raise RedisSerializationError(
124
+ "Failed to serialize value for Redis"
125
+ ) from exc
126
+
127
+ else:
128
+ final_value = processed_value
129
+ return bool(self._client.set(key, final_value))
130
+
131
+ except redis.ConnectionError as exc:
132
+ logger.exception("Redis connection error.", extra={"key": key})
133
+ raise RedisConnectionError(
134
+ f"Error connecting to Redis host {self.config.redis_host}"
135
+ ) from exc
136
+
137
+ except redis.RedisError as exc:
138
+ logger.exception("Error setting Redis key.", extra={"key": key})
139
+ raise RedisOperationError(f"Failed to set key '{key}'") from exc
140
+
141
+ def get(self, key: str, model: type[T]) -> T | None:
142
+ if not isinstance(key, str):
143
+ raise ValueError(f"Key must be a string, got {type(key)}")
144
+
145
+ try:
146
+ value = self._client.get(key)
147
+ if value is None:
148
+ logger.debug("Value is none", extra={"key": key})
149
+ return None
150
+ try:
151
+ return model.model_validate(json.loads(str(value)))
152
+
153
+ except json.JSONDecodeError as exc:
154
+ logger.exception("JSON Decode error.", extra={"key": key})
155
+ raise RedisDeserializationError(
156
+ f"Failed to decode JSON for key '{key}'"
157
+ ) from exc
158
+
159
+ except ValidationError as exc:
160
+ logger.exception(
161
+ "Validation error.", extra={"key": key, "model": model}
162
+ )
163
+ raise RedisDeserializationError(
164
+ f"Validation failed for key '{key}' and model '{model.__name__}'"
165
+ ) from exc
166
+
167
+ except redis.ConnectionError as exc:
168
+ logger.exception("Redis connection error.", extra={"key": key})
169
+ raise RedisConnectionError(
170
+ f"Error connecting to Redis host {self.config.redis_host}"
171
+ ) from exc
172
+
173
+ except redis.RedisError as exc:
174
+ logger.exception("Error getting Redis key.", extra={"key": key})
175
+ raise RedisOperationError(f"Failed to get key '{key}'") from exc
176
+
177
+ def get_raw(self, key: str) -> Any | None:
178
+ if not isinstance(key, str):
179
+ raise ValueError(f"Key must be a string, got {type(key)}")
180
+
181
+ try:
182
+ value = self._client.get(key)
183
+ if value is None:
184
+ logger.debug("Value is none", extra={"key": key})
185
+ return value
186
+
187
+ except redis.ConnectionError as exc:
188
+ logger.exception("Redis connection error.", extra={"key": key})
189
+ raise RedisConnectionError(
190
+ f"Error connecting to Redis host {self.config.redis_host}"
191
+ ) from exc
192
+
193
+ except redis.RedisError as exc:
194
+ logger.exception("Error getting Redis key.", extra={"key": key})
195
+ raise RedisOperationError(f"Failed to get key '{key}'") from exc
196
+
197
+ def list_keys(self, pattern: str) -> list[str]:
198
+ if not isinstance(pattern, str):
199
+ raise ValueError(f"Pattern must be a string, got {type(pattern)}")
200
+
201
+ try:
202
+ keys_result = self._client.keys(pattern)
203
+ return cast(list[str], keys_result)
204
+
205
+ except redis.ConnectionError as exc:
206
+ logger.exception("Redis connection error.", extra={"pattern": pattern})
207
+ raise RedisConnectionError(
208
+ f"Error connecting to Redis host {self.config.redis_host}"
209
+ ) from exc
210
+
211
+ except redis.RedisError as exc:
212
+ logger.exception("Error listing Redis keys.", extra={"pattern": pattern})
213
+ raise RedisOperationError(
214
+ f"Failed to list keys for pattern '{pattern}'"
215
+ ) from exc
216
+
217
+ def mget(self, keys: list[str], model: type[T]) -> list[T | None | FailedValue]:
218
+ if not isinstance(keys, list):
219
+ raise ValueError(f"Keys must be a list, got {type(keys)}")
220
+
221
+ if not keys:
222
+ return []
223
+
224
+ try:
225
+ values = cast(list[Any], self._client.mget(keys))
226
+ results: list[T | None | FailedValue] = []
227
+
228
+ for value, key in zip(values, keys, strict=False):
229
+ if value is None:
230
+ results.append(None)
231
+ continue
232
+
233
+ try:
234
+ validated_data = model.model_validate(json.loads(str(value)))
235
+ results.append(validated_data)
236
+
237
+ except json.JSONDecodeError as exc:
238
+ logger.exception("JSON Decode error.", extra={"key": key})
239
+ results.append(FailedValue(key=key, error=exc))
240
+ continue
241
+
242
+ except ValidationError as exc:
243
+ logger.exception(
244
+ "Validation error.",
245
+ extra={"key": key, "model": model.__name__},
246
+ )
247
+ results.append(FailedValue(key=key, error=exc))
248
+ continue
249
+
250
+ return results
251
+
252
+ except redis.ConnectionError as exc:
253
+ logger.exception("Redis connection error.", extra={"keys": keys})
254
+ raise RedisConnectionError(
255
+ f"Error connecting to Redis host {self.config.redis_host}"
256
+ ) from exc
257
+
258
+ except redis.RedisError as exc:
259
+ logger.exception("Error getting multiple Redis keys.")
260
+ raise RedisOperationError("Failed to mget keys") from exc
261
+
262
+ def delete(self, key: str) -> bool:
263
+ if not isinstance(key, str):
264
+ raise ValueError(f"Key must be a string, got {type(key)}")
265
+
266
+ try:
267
+ result = self._client.delete(key)
268
+ logger.info("Key deleted successfully", extra={"key": key})
269
+ return bool(result)
270
+
271
+ except redis.ConnectionError as exc:
272
+ logger.exception("Redis connection error.", extra={"key": key})
273
+ raise RedisConnectionError(
274
+ f"Error connecting to Redis host {self.config.redis_host}"
275
+ ) from exc
276
+
277
+ except redis.RedisError as exc:
278
+ logger.exception("Error deleting Redis key.", extra={"key": key})
279
+ raise RedisOperationError(f"Failed to delete key '{key}'") from exc
280
+
281
+
282
+ class AsyncRedisService:
283
+ """Asynchronous Redis service with async/await support."""
284
+
285
+ def __init__(self, config: RedisServiceConfig):
286
+ self.config = config
287
+ self._client: aioredis.Redis
288
+
289
+ async def connect(self) -> None:
290
+ """Establish connection to Redis asynchronously."""
291
+ try:
292
+ self._client = aioredis.Redis(
293
+ host=self.config.redis_host,
294
+ port=self.config.redis_port,
295
+ db=self.config.redis_db,
296
+ password=self.config.redis_password,
297
+ decode_responses=True,
298
+ )
299
+ logger.info(
300
+ "Async Redis client initialized.",
301
+ extra={
302
+ "host": self.config.redis_host,
303
+ "port": self.config.redis_port,
304
+ "db": self.config.redis_db,
305
+ },
306
+ )
307
+ await self._client.ping()
308
+ logger.info(
309
+ "Async Redis client pinged successfully.",
310
+ extra={"host": self.config.redis_host},
311
+ )
312
+ except aioredis.ConnectionError as exc:
313
+ logger.exception("Failed to initialize async Redis client.")
314
+ raise RedisConnectionError("Could not initialize Redis client") from exc
315
+
316
+ async def close(self) -> None:
317
+ """Close the Redis connection."""
318
+ await self._client.aclose()
319
+ logger.info("Async Redis client closed.")
320
+
321
+ async def is_alive(self) -> bool:
322
+ """Check if Redis connection is alive."""
323
+ try:
324
+ return bool(await self._client.ping())
325
+ except aioredis.ConnectionError:
326
+ logger.exception(
327
+ "Redis connection error during health check. Can't ping Redis host %s",
328
+ self.config.redis_host,
329
+ )
330
+ return False
331
+
332
+ def _prepare_for_serialization(self, value: Any) -> Any:
333
+ """
334
+ Recursively process data structures, converting BaseModel instances to
335
+ serializable dicts.
336
+ """
337
+ if isinstance(value, BaseModel):
338
+ return value.model_dump()
339
+ if isinstance(value, list):
340
+ return [self._prepare_for_serialization(item) for item in value]
341
+ if isinstance(value, dict):
342
+ return {k: self._prepare_for_serialization(v) for k, v in value.items()}
343
+ return value
344
+
345
+ async def set(self, key: str, value: Any) -> bool:
346
+ """Set a key-value pair in Redis."""
347
+ if not isinstance(key, str):
348
+ raise ValueError(f"Key must be a string, got {type(key)}")
349
+ if value is None:
350
+ logger.debug("Value is none", extra={"key": key})
351
+ try:
352
+ processed_value = self._prepare_for_serialization(value)
353
+ if isinstance(processed_value, (dict, list)):
354
+ try:
355
+ final_value = json.dumps(processed_value, cls=CustomEncoder)
356
+
357
+ except (TypeError, ValueError) as exc:
358
+ logger.exception(
359
+ "Serialization error before setting Redis key.",
360
+ extra={"key": key},
361
+ )
362
+ raise RedisSerializationError(
363
+ "Failed to serialize value for Redis"
364
+ ) from exc
365
+
366
+ else:
367
+ final_value = processed_value
368
+ return bool(await self._client.set(key, final_value))
369
+
370
+ except aioredis.ConnectionError as exc:
371
+ logger.exception("Redis connection error.", extra={"key": key})
372
+ raise RedisConnectionError(
373
+ f"Error connecting to Redis host {self.config.redis_host}"
374
+ ) from exc
375
+
376
+ except aioredis.RedisError as exc:
377
+ logger.exception("Error setting Redis key.", extra={"key": key})
378
+ raise RedisOperationError(f"Failed to set key '{key}'") from exc
379
+
380
+ async def get(self, key: str, model: type[T]) -> T | None:
381
+ """Get a value from Redis and validate it against a Pydantic model."""
382
+ if not isinstance(key, str):
383
+ raise ValueError(f"Key must be a string, got {type(key)}")
384
+
385
+ try:
386
+ value = await self._client.get(key)
387
+ if value is None:
388
+ logger.debug("Value is none", extra={"key": key})
389
+ return None
390
+ try:
391
+ return model.model_validate(json.loads(str(value)))
392
+
393
+ except json.JSONDecodeError as exc:
394
+ logger.exception("JSON Decode error.", extra={"key": key})
395
+ raise RedisDeserializationError(
396
+ f"Failed to decode JSON for key '{key}'"
397
+ ) from exc
398
+
399
+ except ValidationError as exc:
400
+ logger.exception(
401
+ "Validation error.", extra={"key": key, "model": model}
402
+ )
403
+ raise RedisDeserializationError(
404
+ f"Validation failed for key '{key}' and model '{model.__name__}'"
405
+ ) from exc
406
+
407
+ except aioredis.ConnectionError as exc:
408
+ logger.exception("Redis connection error.", extra={"key": key})
409
+ raise RedisConnectionError(
410
+ f"Error connecting to Redis host {self.config.redis_host}"
411
+ ) from exc
412
+
413
+ except aioredis.RedisError as exc:
414
+ logger.exception("Error getting Redis key.", extra={"key": key})
415
+ raise RedisOperationError(f"Failed to get key '{key}'") from exc
416
+
417
+ async def get_raw(self, key: str) -> Any | None:
418
+ """Get a raw value from Redis without deserialization."""
419
+ if not isinstance(key, str):
420
+ raise ValueError(f"Key must be a string, got {type(key)}")
421
+
422
+ try:
423
+ value = await self._client.get(key)
424
+ if value is None:
425
+ logger.debug("Value is none", extra={"key": key})
426
+ return value
427
+
428
+ except aioredis.ConnectionError as exc:
429
+ logger.exception("Redis connection error.", extra={"key": key})
430
+ raise RedisConnectionError(
431
+ f"Error connecting to Redis host {self.config.redis_host}"
432
+ ) from exc
433
+
434
+ except aioredis.RedisError as exc:
435
+ logger.exception("Error getting Redis key.", extra={"key": key})
436
+ raise RedisOperationError(f"Failed to get key '{key}'") from exc
437
+
438
+ async def list_keys(self, pattern: str) -> list[str]:
439
+ """List keys matching a pattern."""
440
+ if not isinstance(pattern, str):
441
+ raise ValueError(f"Pattern must be a string, got {type(pattern)}")
442
+
443
+ try:
444
+ keys_result = await self._client.keys(pattern)
445
+ return cast(list[str], keys_result)
446
+
447
+ except aioredis.ConnectionError as exc:
448
+ logger.exception("Redis connection error.", extra={"pattern": pattern})
449
+ raise RedisConnectionError(
450
+ f"Error connecting to Redis host {self.config.redis_host}"
451
+ ) from exc
452
+
453
+ except aioredis.RedisError as exc:
454
+ logger.exception("Error listing Redis keys.", extra={"pattern": pattern})
455
+ raise RedisOperationError(
456
+ f"Failed to list keys for pattern '{pattern}'"
457
+ ) from exc
458
+
459
+ async def mget(
460
+ self, keys: list[str], model: type[T]
461
+ ) -> list[T | None | FailedValue]:
462
+ """Get multiple values from Redis."""
463
+ if not isinstance(keys, list):
464
+ raise ValueError(f"Keys must be a list, got {type(keys)}")
465
+
466
+ if not keys:
467
+ return []
468
+
469
+ try:
470
+ values = cast(list[Any], await self._client.mget(keys))
471
+ results: list[T | None | FailedValue] = []
472
+
473
+ for value, key in zip(values, keys, strict=False):
474
+ if value is None:
475
+ results.append(None)
476
+ continue
477
+
478
+ try:
479
+ validated_data = model.model_validate(json.loads(str(value)))
480
+ results.append(validated_data)
481
+
482
+ except json.JSONDecodeError as exc:
483
+ logger.exception("JSON Decode error.", extra={"key": key})
484
+ results.append(FailedValue(key=key, error=exc))
485
+ continue
486
+
487
+ except ValidationError as exc:
488
+ logger.exception(
489
+ "Validation error.",
490
+ extra={"key": key, "model": model.__name__},
491
+ )
492
+ results.append(FailedValue(key=key, error=exc))
493
+ continue
494
+
495
+ return results
496
+
497
+ except aioredis.ConnectionError as exc:
498
+ logger.exception("Redis connection error.", extra={"keys": keys})
499
+ raise RedisConnectionError(
500
+ f"Error connecting to Redis host {self.config.redis_host}"
501
+ ) from exc
502
+
503
+ except aioredis.RedisError as exc:
504
+ logger.exception("Error getting multiple Redis keys.")
505
+ raise RedisOperationError("Failed to mget keys") from exc
506
+
507
+ async def delete(self, key: str) -> bool:
508
+ """Delete a key from Redis."""
509
+ if not isinstance(key, str):
510
+ raise ValueError(f"Key must be a string, got {type(key)}")
511
+
512
+ try:
513
+ result = await self._client.delete(key)
514
+ logger.info("Key deleted successfully", extra={"key": key})
515
+ return bool(result)
516
+
517
+ except aioredis.ConnectionError as exc:
518
+ logger.exception("Redis connection error.", extra={"key": key})
519
+ raise RedisConnectionError(
520
+ f"Error connecting to Redis host {self.config.redis_host}"
521
+ ) from exc
522
+
523
+ except aioredis.RedisError as exc:
524
+ logger.exception("Error deleting Redis key.", extra={"key": key})
525
+ raise RedisOperationError(f"Failed to delete key '{key}'") from exc