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.
- cledar/__init__.py +0 -0
- cledar/kafka/README.md +239 -0
- cledar/kafka/__init__.py +40 -0
- cledar/kafka/clients/base.py +98 -0
- cledar/kafka/clients/consumer.py +110 -0
- cledar/kafka/clients/producer.py +80 -0
- cledar/kafka/config/schemas.py +178 -0
- cledar/kafka/exceptions.py +22 -0
- cledar/kafka/handlers/dead_letter.py +82 -0
- cledar/kafka/handlers/parser.py +49 -0
- cledar/kafka/logger.py +3 -0
- cledar/kafka/models/input.py +13 -0
- cledar/kafka/models/message.py +10 -0
- cledar/kafka/models/output.py +8 -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 +19 -0
- cledar/kafka/utils/messages.py +28 -0
- cledar/kafka/utils/topics.py +2 -0
- cledar/kserve/README.md +352 -0
- cledar/kserve/__init__.py +3 -0
- cledar/kserve/tests/__init__.py +0 -0
- cledar/kserve/tests/test_utils.py +64 -0
- cledar/kserve/utils.py +27 -0
- cledar/logging/README.md +53 -0
- cledar/logging/__init__.py +3 -0
- cledar/logging/tests/test_universal_plaintext_formatter.py +249 -0
- cledar/logging/universal_plaintext_formatter.py +94 -0
- cledar/monitoring/README.md +71 -0
- cledar/monitoring/__init__.py +3 -0
- cledar/monitoring/monitoring_server.py +112 -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 +3 -0
- cledar/nonce/nonce_service.py +36 -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 +15 -0
- cledar/redis/async_example.py +111 -0
- cledar/redis/example.py +37 -0
- cledar/redis/exceptions.py +22 -0
- cledar/redis/logger.py +3 -0
- cledar/redis/model.py +10 -0
- cledar/redis/redis.py +525 -0
- cledar/redis/redis_config_store.py +252 -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 +4 -0
- cledar/storage/constants.py +3 -0
- cledar/storage/exceptions.py +50 -0
- cledar/storage/models.py +19 -0
- cledar/storage/object_storage.py +955 -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.1.dist-info → cledar_sdk-2.0.3.dist-info}/METADATA +1 -1
- cledar_sdk-2.0.3.dist-info/RECORD +84 -0
- cledar_sdk-2.0.1.dist-info/RECORD +0 -4
- {cledar_sdk-2.0.1.dist-info → cledar_sdk-2.0.3.dist-info}/WHEEL +0 -0
- {cledar_sdk-2.0.1.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"
|