cledar-sdk 2.0.3__py3-none-any.whl → 2.1.0__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.
- cledar/__init__.py +1 -0
- cledar/kafka/__init__.py +2 -0
- cledar/kafka/clients/base.py +24 -5
- cledar/kafka/clients/consumer.py +28 -0
- cledar/kafka/clients/producer.py +17 -0
- cledar/kafka/config/schemas.py +91 -7
- cledar/kafka/exceptions.py +7 -12
- cledar/kafka/handlers/dead_letter.py +26 -20
- cledar/kafka/handlers/parser.py +36 -2
- cledar/kafka/logger.py +2 -0
- cledar/kafka/models/input.py +4 -0
- cledar/kafka/models/message.py +4 -0
- cledar/kafka/models/output.py +4 -0
- cledar/kafka/utils/callbacks.py +9 -0
- cledar/kafka/utils/messages.py +11 -0
- cledar/kafka/utils/topics.py +13 -0
- cledar/kserve/__init__.py +2 -0
- cledar/kserve/utils.py +3 -0
- cledar/logging/__init__.py +2 -0
- cledar/logging/universal_plaintext_formatter.py +17 -12
- cledar/monitoring/__init__.py +2 -0
- cledar/monitoring/monitoring_server.py +45 -1
- cledar/nonce/__init__.py +2 -0
- cledar/nonce/nonce_service.py +30 -4
- cledar/redis/__init__.py +2 -0
- cledar/redis/async_example.py +4 -3
- cledar/redis/example.py +30 -0
- cledar/redis/exceptions.py +3 -0
- cledar/redis/logger.py +2 -0
- cledar/redis/model.py +4 -0
- cledar/redis/redis.py +252 -13
- cledar/redis/redis_config_store.py +81 -0
- cledar/storage/__init__.py +2 -0
- cledar/storage/constants.py +2 -0
- cledar/storage/exceptions.py +29 -0
- cledar/storage/models.py +22 -0
- cledar/storage/object_storage.py +342 -23
- {cledar_sdk-2.0.3.dist-info → cledar_sdk-2.1.0.dist-info}/METADATA +1 -1
- {cledar_sdk-2.0.3.dist-info → cledar_sdk-2.1.0.dist-info}/RECORD +41 -41
- {cledar_sdk-2.0.3.dist-info → cledar_sdk-2.1.0.dist-info}/WHEEL +0 -0
- {cledar_sdk-2.0.3.dist-info → cledar_sdk-2.1.0.dist-info}/licenses/LICENSE +0 -0
cledar/redis/redis.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Redis service implementations for the Cledar SDK."""
|
|
2
|
+
|
|
1
3
|
import json
|
|
2
4
|
import logging
|
|
3
5
|
from dataclasses import dataclass
|
|
@@ -20,11 +22,18 @@ logger = logging.getLogger("redis_service")
|
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
class CustomEncoder(json.JSONEncoder):
|
|
23
|
-
"""
|
|
24
|
-
Custom JSON encoder that can handle Enum objects and datetime objects.
|
|
25
|
-
"""
|
|
25
|
+
"""Custom JSON encoder that can handle Enum objects and datetime objects."""
|
|
26
26
|
|
|
27
27
|
def default(self, o: Any) -> Any:
|
|
28
|
+
"""Process objects for JSON encoding.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
o: The object to encode.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Any: The encoded object.
|
|
35
|
+
|
|
36
|
+
"""
|
|
28
37
|
if isinstance(o, Enum):
|
|
29
38
|
return o.name.lower()
|
|
30
39
|
if isinstance(o, datetime):
|
|
@@ -37,12 +46,16 @@ T = TypeVar("T", bound=BaseModel)
|
|
|
37
46
|
|
|
38
47
|
@dataclass
|
|
39
48
|
class FailedValue:
|
|
49
|
+
"""Represents a failed Redis operation for a specific key."""
|
|
50
|
+
|
|
40
51
|
key: str
|
|
41
52
|
error: Exception
|
|
42
53
|
|
|
43
54
|
|
|
44
55
|
@dataclass
|
|
45
56
|
class RedisServiceConfig:
|
|
57
|
+
"""Configuration for Redis services."""
|
|
58
|
+
|
|
46
59
|
redis_host: str
|
|
47
60
|
redis_port: int
|
|
48
61
|
redis_db: int = 0
|
|
@@ -50,12 +63,26 @@ class RedisServiceConfig:
|
|
|
50
63
|
|
|
51
64
|
|
|
52
65
|
class RedisService:
|
|
66
|
+
"""Synchronous Redis service with Pydantic model support."""
|
|
67
|
+
|
|
53
68
|
def __init__(self, config: RedisServiceConfig):
|
|
69
|
+
"""Initialize the synchronous Redis service.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
config: The service configuration.
|
|
73
|
+
|
|
74
|
+
"""
|
|
54
75
|
self.config = config
|
|
55
76
|
self._client: redis.Redis
|
|
56
77
|
self.connect()
|
|
57
78
|
|
|
58
79
|
def connect(self) -> None:
|
|
80
|
+
"""Establish a connection to Redis.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
RedisConnectionError: If the connection fails.
|
|
84
|
+
|
|
85
|
+
"""
|
|
59
86
|
try:
|
|
60
87
|
self._client = redis.Redis(
|
|
61
88
|
host=self.config.redis_host,
|
|
@@ -82,6 +109,12 @@ class RedisService:
|
|
|
82
109
|
raise RedisConnectionError("Could not initialize Redis client") from exc
|
|
83
110
|
|
|
84
111
|
def is_alive(self) -> bool:
|
|
112
|
+
"""Check if the Redis connection is alive.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
bool: True if connection is alive, False otherwise.
|
|
116
|
+
|
|
117
|
+
"""
|
|
85
118
|
try:
|
|
86
119
|
return bool(self._client.ping())
|
|
87
120
|
except redis.ConnectionError:
|
|
@@ -92,9 +125,17 @@ class RedisService:
|
|
|
92
125
|
return False
|
|
93
126
|
|
|
94
127
|
def _prepare_for_serialization(self, value: Any) -> Any:
|
|
95
|
-
"""
|
|
128
|
+
"""Process data structures for serialization.
|
|
129
|
+
|
|
96
130
|
Recursively process data structures, converting BaseModel instances to
|
|
97
131
|
serializable dicts.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
value: The value to process.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Any: The processed value.
|
|
138
|
+
|
|
98
139
|
"""
|
|
99
140
|
if isinstance(value, BaseModel):
|
|
100
141
|
return value.model_dump()
|
|
@@ -105,6 +146,22 @@ class RedisService:
|
|
|
105
146
|
return value
|
|
106
147
|
|
|
107
148
|
def set(self, key: str, value: Any) -> bool:
|
|
149
|
+
"""Set a value in Redis.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
key: The key to set.
|
|
153
|
+
value: The value to set.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
bool: True if successful, False otherwise.
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
ValueError: If the key is not a string.
|
|
160
|
+
RedisSerializationError: If serialization fails.
|
|
161
|
+
RedisConnectionError: If the connection fails.
|
|
162
|
+
RedisOperationError: If the operation fails.
|
|
163
|
+
|
|
164
|
+
"""
|
|
108
165
|
if not isinstance(key, str):
|
|
109
166
|
raise ValueError(f"Key must be a string, got {type(key)}")
|
|
110
167
|
if value is None:
|
|
@@ -139,6 +196,22 @@ class RedisService:
|
|
|
139
196
|
raise RedisOperationError(f"Failed to set key '{key}'") from exc
|
|
140
197
|
|
|
141
198
|
def get(self, key: str, model: type[T]) -> T | None:
|
|
199
|
+
"""Get a value from Redis and validate it against a model.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
key: The key to fetch.
|
|
203
|
+
model: The Pydantic model to validate against.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
T | None: The validated model or None if key does not exist.
|
|
207
|
+
|
|
208
|
+
Raises:
|
|
209
|
+
ValueError: If the key is not a string.
|
|
210
|
+
RedisDeserializationError: If decoding or validation fails.
|
|
211
|
+
RedisConnectionError: If the connection fails.
|
|
212
|
+
RedisOperationError: If the operation fails.
|
|
213
|
+
|
|
214
|
+
"""
|
|
142
215
|
if not isinstance(key, str):
|
|
143
216
|
raise ValueError(f"Key must be a string, got {type(key)}")
|
|
144
217
|
|
|
@@ -175,6 +248,20 @@ class RedisService:
|
|
|
175
248
|
raise RedisOperationError(f"Failed to get key '{key}'") from exc
|
|
176
249
|
|
|
177
250
|
def get_raw(self, key: str) -> Any | None:
|
|
251
|
+
"""Get a raw value from Redis.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
key: The key to fetch.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Any | None: The raw value or None if key does not exist.
|
|
258
|
+
|
|
259
|
+
Raises:
|
|
260
|
+
ValueError: If the key is not a string.
|
|
261
|
+
RedisConnectionError: If the connection fails.
|
|
262
|
+
RedisOperationError: If the operation fails.
|
|
263
|
+
|
|
264
|
+
"""
|
|
178
265
|
if not isinstance(key, str):
|
|
179
266
|
raise ValueError(f"Key must be a string, got {type(key)}")
|
|
180
267
|
|
|
@@ -195,6 +282,20 @@ class RedisService:
|
|
|
195
282
|
raise RedisOperationError(f"Failed to get key '{key}'") from exc
|
|
196
283
|
|
|
197
284
|
def list_keys(self, pattern: str) -> list[str]:
|
|
285
|
+
"""List all keys matching a pattern.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
pattern: The pattern to match.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
list[str]: A list of matching keys.
|
|
292
|
+
|
|
293
|
+
Raises:
|
|
294
|
+
ValueError: If the pattern is not a string.
|
|
295
|
+
RedisConnectionError: If the connection fails.
|
|
296
|
+
RedisOperationError: If the operation fails.
|
|
297
|
+
|
|
298
|
+
"""
|
|
198
299
|
if not isinstance(pattern, str):
|
|
199
300
|
raise ValueError(f"Pattern must be a string, got {type(pattern)}")
|
|
200
301
|
|
|
@@ -215,6 +316,22 @@ class RedisService:
|
|
|
215
316
|
) from exc
|
|
216
317
|
|
|
217
318
|
def mget(self, keys: list[str], model: type[T]) -> list[T | None | FailedValue]:
|
|
319
|
+
"""Get multiple values from Redis and validate them against a model.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
keys: A list of keys to fetch.
|
|
323
|
+
model: The Pydantic model to validate against.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
list[T | None | FailedValue]: A list of validated models, None for
|
|
327
|
+
missing keys, or FailedValue for items that failed validation.
|
|
328
|
+
|
|
329
|
+
Raises:
|
|
330
|
+
ValueError: If the keys are not a list.
|
|
331
|
+
RedisConnectionError: If the connection fails.
|
|
332
|
+
RedisOperationError: If the operation fails.
|
|
333
|
+
|
|
334
|
+
"""
|
|
218
335
|
if not isinstance(keys, list):
|
|
219
336
|
raise ValueError(f"Keys must be a list, got {type(keys)}")
|
|
220
337
|
|
|
@@ -260,6 +377,20 @@ class RedisService:
|
|
|
260
377
|
raise RedisOperationError("Failed to mget keys") from exc
|
|
261
378
|
|
|
262
379
|
def delete(self, key: str) -> bool:
|
|
380
|
+
"""Delete a key from Redis.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
key: The key to delete.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
bool: True if the key was deleted, False otherwise.
|
|
387
|
+
|
|
388
|
+
Raises:
|
|
389
|
+
ValueError: If the key is not a string.
|
|
390
|
+
RedisConnectionError: If the connection fails.
|
|
391
|
+
RedisOperationError: If the operation fails.
|
|
392
|
+
|
|
393
|
+
"""
|
|
263
394
|
if not isinstance(key, str):
|
|
264
395
|
raise ValueError(f"Key must be a string, got {type(key)}")
|
|
265
396
|
|
|
@@ -283,11 +414,22 @@ class AsyncRedisService:
|
|
|
283
414
|
"""Asynchronous Redis service with async/await support."""
|
|
284
415
|
|
|
285
416
|
def __init__(self, config: RedisServiceConfig):
|
|
417
|
+
"""Initialize the asynchronous Redis service.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
config: The service configuration.
|
|
421
|
+
|
|
422
|
+
"""
|
|
286
423
|
self.config = config
|
|
287
424
|
self._client: aioredis.Redis
|
|
288
425
|
|
|
289
426
|
async def connect(self) -> None:
|
|
290
|
-
"""Establish connection to Redis asynchronously.
|
|
427
|
+
"""Establish connection to Redis asynchronously.
|
|
428
|
+
|
|
429
|
+
Raises:
|
|
430
|
+
RedisConnectionError: If the connection fails.
|
|
431
|
+
|
|
432
|
+
"""
|
|
291
433
|
try:
|
|
292
434
|
self._client = aioredis.Redis(
|
|
293
435
|
host=self.config.redis_host,
|
|
@@ -319,7 +461,12 @@ class AsyncRedisService:
|
|
|
319
461
|
logger.info("Async Redis client closed.")
|
|
320
462
|
|
|
321
463
|
async def is_alive(self) -> bool:
|
|
322
|
-
"""Check if Redis connection is alive.
|
|
464
|
+
"""Check if Redis connection is alive.
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
bool: True if the connection is alive, False otherwise.
|
|
468
|
+
|
|
469
|
+
"""
|
|
323
470
|
try:
|
|
324
471
|
return bool(await self._client.ping())
|
|
325
472
|
except aioredis.ConnectionError:
|
|
@@ -330,9 +477,17 @@ class AsyncRedisService:
|
|
|
330
477
|
return False
|
|
331
478
|
|
|
332
479
|
def _prepare_for_serialization(self, value: Any) -> Any:
|
|
333
|
-
"""
|
|
480
|
+
"""Process data structures for serialization.
|
|
481
|
+
|
|
334
482
|
Recursively process data structures, converting BaseModel instances to
|
|
335
483
|
serializable dicts.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
value: The value to process.
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
Any: The processed value.
|
|
490
|
+
|
|
336
491
|
"""
|
|
337
492
|
if isinstance(value, BaseModel):
|
|
338
493
|
return value.model_dump()
|
|
@@ -343,7 +498,22 @@ class AsyncRedisService:
|
|
|
343
498
|
return value
|
|
344
499
|
|
|
345
500
|
async def set(self, key: str, value: Any) -> bool:
|
|
346
|
-
"""Set a key-value pair in Redis.
|
|
501
|
+
"""Set a key-value pair in Redis.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
key: The key to set.
|
|
505
|
+
value: The value to set.
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
bool: True if successful, False otherwise.
|
|
509
|
+
|
|
510
|
+
Raises:
|
|
511
|
+
ValueError: If the key is not a string.
|
|
512
|
+
RedisSerializationError: If serialization fails.
|
|
513
|
+
RedisConnectionError: If the connection fails.
|
|
514
|
+
RedisOperationError: If the operation fails.
|
|
515
|
+
|
|
516
|
+
"""
|
|
347
517
|
if not isinstance(key, str):
|
|
348
518
|
raise ValueError(f"Key must be a string, got {type(key)}")
|
|
349
519
|
if value is None:
|
|
@@ -378,7 +548,22 @@ class AsyncRedisService:
|
|
|
378
548
|
raise RedisOperationError(f"Failed to set key '{key}'") from exc
|
|
379
549
|
|
|
380
550
|
async def get(self, key: str, model: type[T]) -> T | None:
|
|
381
|
-
"""Get a value from Redis and validate it against a Pydantic model.
|
|
551
|
+
"""Get a value from Redis and validate it against a Pydantic model.
|
|
552
|
+
|
|
553
|
+
Args:
|
|
554
|
+
key: The key to fetch.
|
|
555
|
+
model: The Pydantic model to validate against.
|
|
556
|
+
|
|
557
|
+
Returns:
|
|
558
|
+
T | None: The validated model or None if key does not exist.
|
|
559
|
+
|
|
560
|
+
Raises:
|
|
561
|
+
ValueError: If the key is not a string.
|
|
562
|
+
RedisDeserializationError: If decoding or validation fails.
|
|
563
|
+
RedisConnectionError: If the connection fails.
|
|
564
|
+
RedisOperationError: If the operation fails.
|
|
565
|
+
|
|
566
|
+
"""
|
|
382
567
|
if not isinstance(key, str):
|
|
383
568
|
raise ValueError(f"Key must be a string, got {type(key)}")
|
|
384
569
|
|
|
@@ -415,7 +600,20 @@ class AsyncRedisService:
|
|
|
415
600
|
raise RedisOperationError(f"Failed to get key '{key}'") from exc
|
|
416
601
|
|
|
417
602
|
async def get_raw(self, key: str) -> Any | None:
|
|
418
|
-
"""Get a raw value from Redis without deserialization.
|
|
603
|
+
"""Get a raw value from Redis without deserialization.
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
key: The key to fetch.
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
Any | None: The raw value or None if key does not exist.
|
|
610
|
+
|
|
611
|
+
Raises:
|
|
612
|
+
ValueError: If the key is not a string.
|
|
613
|
+
RedisConnectionError: If the connection fails.
|
|
614
|
+
RedisOperationError: If the operation fails.
|
|
615
|
+
|
|
616
|
+
"""
|
|
419
617
|
if not isinstance(key, str):
|
|
420
618
|
raise ValueError(f"Key must be a string, got {type(key)}")
|
|
421
619
|
|
|
@@ -436,7 +634,20 @@ class AsyncRedisService:
|
|
|
436
634
|
raise RedisOperationError(f"Failed to get key '{key}'") from exc
|
|
437
635
|
|
|
438
636
|
async def list_keys(self, pattern: str) -> list[str]:
|
|
439
|
-
"""List keys matching a pattern.
|
|
637
|
+
"""List keys matching a pattern.
|
|
638
|
+
|
|
639
|
+
Args:
|
|
640
|
+
pattern: The pattern to match.
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
list[str]: A list of matching keys.
|
|
644
|
+
|
|
645
|
+
Raises:
|
|
646
|
+
ValueError: If the pattern is not a string.
|
|
647
|
+
RedisConnectionError: If the connection fails.
|
|
648
|
+
RedisOperationError: If the operation fails.
|
|
649
|
+
|
|
650
|
+
"""
|
|
440
651
|
if not isinstance(pattern, str):
|
|
441
652
|
raise ValueError(f"Pattern must be a string, got {type(pattern)}")
|
|
442
653
|
|
|
@@ -459,7 +670,22 @@ class AsyncRedisService:
|
|
|
459
670
|
async def mget(
|
|
460
671
|
self, keys: list[str], model: type[T]
|
|
461
672
|
) -> list[T | None | FailedValue]:
|
|
462
|
-
"""Get multiple values from Redis.
|
|
673
|
+
"""Get multiple values from Redis.
|
|
674
|
+
|
|
675
|
+
Args:
|
|
676
|
+
keys: A list of keys to fetch.
|
|
677
|
+
model: The Pydantic model to validate against.
|
|
678
|
+
|
|
679
|
+
Returns:
|
|
680
|
+
list[T | None | FailedValue]: A list of validated models, None for
|
|
681
|
+
missing keys, or FailedValue for items that failed validation.
|
|
682
|
+
|
|
683
|
+
Raises:
|
|
684
|
+
ValueError: If the keys are not a list.
|
|
685
|
+
RedisConnectionError: If the connection fails.
|
|
686
|
+
RedisOperationError: If the operation fails.
|
|
687
|
+
|
|
688
|
+
"""
|
|
463
689
|
if not isinstance(keys, list):
|
|
464
690
|
raise ValueError(f"Keys must be a list, got {type(keys)}")
|
|
465
691
|
|
|
@@ -505,7 +731,20 @@ class AsyncRedisService:
|
|
|
505
731
|
raise RedisOperationError("Failed to mget keys") from exc
|
|
506
732
|
|
|
507
733
|
async def delete(self, key: str) -> bool:
|
|
508
|
-
"""Delete a key from Redis.
|
|
734
|
+
"""Delete a key from Redis.
|
|
735
|
+
|
|
736
|
+
Args:
|
|
737
|
+
key: The key to delete.
|
|
738
|
+
|
|
739
|
+
Returns:
|
|
740
|
+
bool: True if the key was deleted, False otherwise.
|
|
741
|
+
|
|
742
|
+
Raises:
|
|
743
|
+
ValueError: If the key is not a string.
|
|
744
|
+
RedisConnectionError: If the connection fails.
|
|
745
|
+
RedisOperationError: If the operation fails.
|
|
746
|
+
|
|
747
|
+
"""
|
|
509
748
|
if not isinstance(key, str):
|
|
510
749
|
raise ValueError(f"Key must be a string, got {type(key)}")
|
|
511
750
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Redis-based configuration store with caching and watching capabilities."""
|
|
2
|
+
|
|
1
3
|
import json
|
|
2
4
|
import re
|
|
3
5
|
import time
|
|
@@ -19,6 +21,11 @@ OP_EVENT_FORMAT = "__keyevent@{DB}__:{OPERATION}"
|
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
class RedisConfigStore:
|
|
24
|
+
"""Store for configuration objects in Redis with local caching and pub/sub.
|
|
25
|
+
|
|
26
|
+
Provides updates on configuration changes.
|
|
27
|
+
"""
|
|
28
|
+
|
|
22
29
|
TYPE_NONE = "none"
|
|
23
30
|
TYPE_LIST = "list"
|
|
24
31
|
TYPE_STRING = "string"
|
|
@@ -29,6 +36,13 @@ class RedisConfigStore:
|
|
|
29
36
|
EVENT_LSET = "lset"
|
|
30
37
|
|
|
31
38
|
def __init__(self, redis: Redis, prefix: str | None = None) -> None:
|
|
39
|
+
"""Initialize the RedisConfigStore.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
redis: An initialized Redis client.
|
|
43
|
+
prefix: Optional prefix for keys stored in Redis.
|
|
44
|
+
|
|
45
|
+
"""
|
|
32
46
|
self._redis: Redis = redis
|
|
33
47
|
self._pubsub = redis.pubsub() # type: ignore
|
|
34
48
|
self._db: int = redis.connection_pool.connection_kwargs.get("db")
|
|
@@ -42,18 +56,52 @@ class RedisConfigStore:
|
|
|
42
56
|
self._watcher_thread.start()
|
|
43
57
|
|
|
44
58
|
def is_ready(self) -> bool:
|
|
59
|
+
"""Check if the Redis connection is ready.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
bool: True if Redis responds to ping, False otherwise.
|
|
63
|
+
|
|
64
|
+
"""
|
|
45
65
|
try:
|
|
46
66
|
return self._redis.ping() # type: ignore
|
|
47
67
|
except RedisConnectionError:
|
|
48
68
|
return False
|
|
49
69
|
|
|
50
70
|
def versions(self, key: str) -> int | None:
|
|
71
|
+
"""Get the version (e.g., list length or existence) of a key.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
key: The key to check.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
int | None: The version of the key.
|
|
78
|
+
|
|
79
|
+
"""
|
|
51
80
|
return self._key_versions(key)
|
|
52
81
|
|
|
53
82
|
def cached_version(self, key: str) -> int | None:
|
|
83
|
+
"""Get the version of a key stored in the local cache.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
key: The key to check.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
int | None: The cached version of the key.
|
|
90
|
+
|
|
91
|
+
"""
|
|
54
92
|
return self._cache_verisons.get(key)
|
|
55
93
|
|
|
56
94
|
def fetch(self, cls: type[T], key: str) -> T | None:
|
|
95
|
+
"""Fetch a configuration object from Redis or local cache.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
cls: The class to instantiate with the fetched data.
|
|
99
|
+
key: The key associated with the configuration.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
T | None: The configuration object, or None if not found.
|
|
103
|
+
|
|
104
|
+
"""
|
|
57
105
|
if key not in self._cache:
|
|
58
106
|
new_value = self._key_fetch(key)
|
|
59
107
|
if new_value is None:
|
|
@@ -64,11 +112,24 @@ class RedisConfigStore:
|
|
|
64
112
|
return cls(**json.loads(self._cache[key]))
|
|
65
113
|
|
|
66
114
|
def update(self, key: str, value: T) -> None:
|
|
115
|
+
"""Update a configuration object in Redis and local cache.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
key: The key to update.
|
|
119
|
+
value: The configuration object to store.
|
|
120
|
+
|
|
121
|
+
"""
|
|
67
122
|
self._cache[key] = self._key_update(key, value)
|
|
68
123
|
self._cache_verisons[key] = self._key_versions(key) or -1
|
|
69
124
|
self._key_watch(key)
|
|
70
125
|
|
|
71
126
|
def delete(self, key: str) -> None:
|
|
127
|
+
"""Delete a configuration object from Redis and local cache.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
key: The key to delete.
|
|
131
|
+
|
|
132
|
+
"""
|
|
72
133
|
if key in self._cache:
|
|
73
134
|
del self._cache[key]
|
|
74
135
|
del self._cache_verisons[key]
|
|
@@ -78,12 +139,32 @@ class RedisConfigStore:
|
|
|
78
139
|
def watch(
|
|
79
140
|
self, key: str, callback: Callable[[int, str, str, str], None] | None = None
|
|
80
141
|
) -> None:
|
|
142
|
+
"""Watch a key for changes and execute a callback.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
key: The key to watch.
|
|
146
|
+
callback: The callback function to execute on change.
|
|
147
|
+
|
|
148
|
+
"""
|
|
81
149
|
self._key_watch(key, callback)
|
|
82
150
|
|
|
83
151
|
def __setitem__(self, key: str, value: T) -> None:
|
|
152
|
+
"""Alias for update method.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
key: The key to update.
|
|
156
|
+
value: The value to set.
|
|
157
|
+
|
|
158
|
+
"""
|
|
84
159
|
self.update(key, value)
|
|
85
160
|
|
|
86
161
|
def __delitem__(self, key: str) -> None:
|
|
162
|
+
"""Alias for delete method.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
key: The key to delete.
|
|
166
|
+
|
|
167
|
+
"""
|
|
87
168
|
self.delete(key)
|
|
88
169
|
|
|
89
170
|
def _key_watch(
|
cledar/storage/__init__.py
CHANGED
cledar/storage/constants.py
CHANGED
cledar/storage/exceptions.py
CHANGED
|
@@ -1,50 +1,79 @@
|
|
|
1
|
+
"""Exceptions for object storage operations."""
|
|
2
|
+
|
|
3
|
+
|
|
1
4
|
class ObjectStorageError(Exception):
|
|
5
|
+
"""Base exception for object storage errors."""
|
|
6
|
+
|
|
2
7
|
pass
|
|
3
8
|
|
|
4
9
|
|
|
5
10
|
class RequiredBucketNotFoundError(ObjectStorageError):
|
|
11
|
+
"""Exception raised when a required bucket is not found."""
|
|
12
|
+
|
|
6
13
|
pass
|
|
7
14
|
|
|
8
15
|
|
|
9
16
|
class UploadBufferError(ObjectStorageError):
|
|
17
|
+
"""Exception raised when uploading a buffer fails."""
|
|
18
|
+
|
|
10
19
|
pass
|
|
11
20
|
|
|
12
21
|
|
|
13
22
|
class UploadFileError(ObjectStorageError):
|
|
23
|
+
"""Exception raised when uploading a file fails."""
|
|
24
|
+
|
|
14
25
|
pass
|
|
15
26
|
|
|
16
27
|
|
|
17
28
|
class ReadFileError(ObjectStorageError):
|
|
29
|
+
"""Exception raised when reading a file fails."""
|
|
30
|
+
|
|
18
31
|
pass
|
|
19
32
|
|
|
20
33
|
|
|
21
34
|
class DownloadFileError(ObjectStorageError):
|
|
35
|
+
"""Exception raised when downloading a file fails."""
|
|
36
|
+
|
|
22
37
|
pass
|
|
23
38
|
|
|
24
39
|
|
|
25
40
|
class ListObjectsError(ObjectStorageError):
|
|
41
|
+
"""Exception raised when listing objects fails."""
|
|
42
|
+
|
|
26
43
|
pass
|
|
27
44
|
|
|
28
45
|
|
|
29
46
|
class DeleteFileError(ObjectStorageError):
|
|
47
|
+
"""Exception raised when deleting a file fails."""
|
|
48
|
+
|
|
30
49
|
pass
|
|
31
50
|
|
|
32
51
|
|
|
33
52
|
class GetFileSizeError(ObjectStorageError):
|
|
53
|
+
"""Exception raised when getting file size fails."""
|
|
54
|
+
|
|
34
55
|
pass
|
|
35
56
|
|
|
36
57
|
|
|
37
58
|
class GetFileInfoError(ObjectStorageError):
|
|
59
|
+
"""Exception raised when getting file info fails."""
|
|
60
|
+
|
|
38
61
|
pass
|
|
39
62
|
|
|
40
63
|
|
|
41
64
|
class CopyFileError(ObjectStorageError):
|
|
65
|
+
"""Exception raised when copying a file fails."""
|
|
66
|
+
|
|
42
67
|
pass
|
|
43
68
|
|
|
44
69
|
|
|
45
70
|
class MoveFileError(ObjectStorageError):
|
|
71
|
+
"""Exception raised when moving a file fails."""
|
|
72
|
+
|
|
46
73
|
pass
|
|
47
74
|
|
|
48
75
|
|
|
49
76
|
class CheckFileExistenceError(ObjectStorageError):
|
|
77
|
+
"""Exception raised when checking file existence fails."""
|
|
78
|
+
|
|
50
79
|
pass
|
cledar/storage/models.py
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
+
"""Models for object storage configuration and transfer paths."""
|
|
2
|
+
|
|
1
3
|
from typing import Literal
|
|
2
4
|
|
|
3
5
|
from pydantic import BaseModel
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
class ObjectStorageServiceConfig(BaseModel):
|
|
9
|
+
"""Configuration for object storage service.
|
|
10
|
+
|
|
11
|
+
Attributes:
|
|
12
|
+
s3_endpoint_url: S3 endpoint URL for custom S3-compatible services.
|
|
13
|
+
s3_access_key: S3 access key for authentication.
|
|
14
|
+
s3_secret_key: S3 secret key for authentication.
|
|
15
|
+
s3_max_concurrency: Maximum number of concurrent S3 operations.
|
|
16
|
+
azure_account_name: Azure storage account name.
|
|
17
|
+
azure_account_key: Azure storage account key.
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
|
|
7
21
|
# s3 configuration
|
|
8
22
|
s3_endpoint_url: str | None = None
|
|
9
23
|
s3_access_key: str | None = None
|
|
@@ -15,5 +29,13 @@ class ObjectStorageServiceConfig(BaseModel):
|
|
|
15
29
|
|
|
16
30
|
|
|
17
31
|
class TransferPath(BaseModel):
|
|
32
|
+
"""Transfer path information with backend type.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
backend: Storage backend type (s3, abfs, or local).
|
|
36
|
+
path: Full path to the file or object.
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
|
|
18
40
|
backend: Literal["s3", "abfs", "local"]
|
|
19
41
|
path: str
|