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