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,489 @@
1
+ """
2
+ Comprehensive tests for InputParser covering JSON parsing, message validation,
3
+ error handling, and edge cases.
4
+ """
5
+
6
+ import json
7
+ from typing import Any
8
+
9
+ import pytest
10
+ from pydantic import BaseModel, ValidationError
11
+
12
+ from cledar.kafka.handlers.parser import IncorrectMessageValueError, InputParser
13
+ from cledar.kafka.models.message import KafkaMessage
14
+
15
+
16
+ class SimpleModel(BaseModel):
17
+ """Simple test model."""
18
+
19
+ id: str
20
+ name: str
21
+ value: int
22
+
23
+
24
+ class OptionalModel(BaseModel):
25
+ """Model with optional fields."""
26
+
27
+ id: str
28
+ name: str | None = None
29
+ value: int | None = None
30
+
31
+
32
+ class NestedModel(BaseModel):
33
+ """Model with nested structure."""
34
+
35
+ id: str
36
+ data: dict[str, Any]
37
+ metadata: dict[str, str] | None = None
38
+
39
+
40
+ class ComplexModel(BaseModel):
41
+ """Complex model with various field types."""
42
+
43
+ id: str
44
+ count: int
45
+ active: bool
46
+ tags: list[str]
47
+ config: dict[str, Any]
48
+ nested: NestedModel | None = None
49
+
50
+
51
+ @pytest.fixture
52
+ def simple_parser() -> InputParser[SimpleModel]:
53
+ """Create a parser for SimpleModel."""
54
+ return InputParser(SimpleModel)
55
+
56
+
57
+ @pytest.fixture
58
+ def optional_parser() -> InputParser[OptionalModel]:
59
+ """Create a parser for OptionalModel."""
60
+ return InputParser(OptionalModel)
61
+
62
+
63
+ @pytest.fixture
64
+ def nested_parser() -> InputParser[NestedModel]:
65
+ """Create a parser for NestedModel."""
66
+ return InputParser(NestedModel)
67
+
68
+
69
+ @pytest.fixture
70
+ def complex_parser() -> InputParser[ComplexModel]:
71
+ """Create a parser for ComplexModel."""
72
+ return InputParser(ComplexModel)
73
+
74
+
75
+ @pytest.fixture
76
+ def valid_simple_json() -> str:
77
+ """Valid JSON for SimpleModel."""
78
+ return '{"id": "123", "name": "test", "value": 42}'
79
+
80
+
81
+ @pytest.fixture
82
+ def valid_nested_json() -> str:
83
+ """Valid JSON for NestedModel."""
84
+ return (
85
+ '{"id": "456", "data": {"key": "value", "number": 123}, '
86
+ '"metadata": {"type": "test"}}'
87
+ )
88
+
89
+
90
+ @pytest.fixture
91
+ def valid_complex_json() -> str:
92
+ """Valid JSON for ComplexModel."""
93
+ return json.dumps(
94
+ {
95
+ "id": "789",
96
+ "count": 10,
97
+ "active": True,
98
+ "tags": ["tag1", "tag2"],
99
+ "config": {"setting": "value", "enabled": True},
100
+ "nested": {
101
+ "id": "nested-123",
102
+ "data": {"inner": "data"},
103
+ "metadata": {"type": "nested"},
104
+ },
105
+ }
106
+ )
107
+
108
+
109
+ @pytest.fixture
110
+ def sample_message() -> KafkaMessage:
111
+ """Create a sample KafkaMessage."""
112
+ return KafkaMessage(
113
+ topic="test-topic",
114
+ value='{"id": "123", "name": "test", "value": 42}',
115
+ key="test-key",
116
+ offset=100,
117
+ partition=0,
118
+ )
119
+
120
+
121
+ def test_init() -> None:
122
+ """Test InputParser initialization."""
123
+ parser = InputParser(SimpleModel)
124
+ assert parser.model == SimpleModel
125
+
126
+
127
+ def test_parse_json_valid(
128
+ simple_parser: InputParser[SimpleModel], valid_simple_json: str
129
+ ) -> None:
130
+ """Test parsing valid JSON."""
131
+ result = simple_parser.parse_json(valid_simple_json)
132
+
133
+ assert isinstance(result, SimpleModel)
134
+ assert result.id == "123"
135
+ assert result.name == "test"
136
+ assert result.value == 42
137
+
138
+
139
+ def test_parse_json_invalid_json(simple_parser: InputParser[SimpleModel]) -> None:
140
+ """Test parsing invalid JSON."""
141
+ invalid_json = '{"id": "123", "name": "test", "value": 42' # Missing closing brace
142
+
143
+ with pytest.raises(IncorrectMessageValueError):
144
+ simple_parser.parse_json(invalid_json)
145
+
146
+
147
+ def test_parse_json_missing_required_field(
148
+ simple_parser: InputParser[SimpleModel],
149
+ ) -> None:
150
+ """Test parsing JSON with missing required field."""
151
+ incomplete_json = '{"id": "123", "name": "test"}' # Missing 'value' field
152
+
153
+ with pytest.raises(ValidationError):
154
+ simple_parser.parse_json(incomplete_json)
155
+
156
+
157
+ def test_parse_json_wrong_type(simple_parser: InputParser[SimpleModel]) -> None:
158
+ """Test parsing JSON with wrong field type."""
159
+ wrong_type_json = '{"id": "123", "name": "test", "value": "not-a-number"}'
160
+
161
+ with pytest.raises(ValidationError):
162
+ simple_parser.parse_json(wrong_type_json)
163
+
164
+
165
+ def test_parse_json_extra_fields(simple_parser: InputParser[SimpleModel]) -> None:
166
+ """Test parsing JSON with extra fields (should be ignored by default)."""
167
+ extra_fields_json = '{"id": "123", "name": "test", "value": 42, "extra": "ignored"}'
168
+
169
+ result = simple_parser.parse_json(extra_fields_json)
170
+
171
+ assert isinstance(result, SimpleModel)
172
+ assert result.id == "123"
173
+ assert result.name == "test"
174
+ assert result.value == 42
175
+
176
+
177
+ def test_parse_json_empty_string(simple_parser: InputParser[SimpleModel]) -> None:
178
+ """Test parsing empty string."""
179
+ with pytest.raises(IncorrectMessageValueError):
180
+ simple_parser.parse_json("")
181
+
182
+
183
+ def test_parse_json_null_values(optional_parser: InputParser[OptionalModel]) -> None:
184
+ """Test parsing JSON with null values for optional fields."""
185
+ null_json = '{"id": "123", "name": null, "value": null}'
186
+
187
+ result = optional_parser.parse_json(null_json)
188
+
189
+ assert isinstance(result, OptionalModel)
190
+ assert result.id == "123"
191
+ assert result.name is None
192
+ assert result.value is None
193
+
194
+
195
+ def test_parse_json_nested_structure(
196
+ nested_parser: InputParser[NestedModel], valid_nested_json: str
197
+ ) -> None:
198
+ """Test parsing JSON with nested structure."""
199
+ result = nested_parser.parse_json(valid_nested_json)
200
+
201
+ assert isinstance(result, NestedModel)
202
+ assert result.id == "456"
203
+ assert result.data == {"key": "value", "number": 123}
204
+ assert result.metadata == {"type": "test"}
205
+
206
+
207
+ def test_parse_json_complex_structure(
208
+ complex_parser: InputParser[ComplexModel], valid_complex_json: str
209
+ ) -> None:
210
+ """Test parsing JSON with complex structure."""
211
+ result = complex_parser.parse_json(valid_complex_json)
212
+
213
+ assert isinstance(result, ComplexModel)
214
+ assert result.id == "789"
215
+ assert result.count == 10
216
+ assert result.active is True
217
+ assert result.tags == ["tag1", "tag2"]
218
+ assert result.config == {"setting": "value", "enabled": True}
219
+ assert result.nested is not None
220
+ assert result.nested.id == "nested-123"
221
+
222
+
223
+ def test_parse_message_valid(
224
+ simple_parser: InputParser[SimpleModel], sample_message: KafkaMessage
225
+ ) -> None:
226
+ """Test parsing a valid KafkaMessage."""
227
+ result = simple_parser.parse_message(sample_message)
228
+
229
+ assert result.key == sample_message.key
230
+ assert result.value == sample_message.value
231
+ assert result.topic == sample_message.topic
232
+ assert result.offset == sample_message.offset
233
+ assert result.partition == sample_message.partition
234
+
235
+ assert isinstance(result.payload, SimpleModel)
236
+ assert result.payload.id == "123"
237
+ assert result.payload.name == "test"
238
+ assert result.payload.value == 42
239
+
240
+
241
+ def test_parse_message_none_value(simple_parser: InputParser[SimpleModel]) -> None:
242
+ """Test parsing a message with None value."""
243
+ message = KafkaMessage(
244
+ topic="test-topic",
245
+ value=None,
246
+ key="test-key",
247
+ offset=100,
248
+ partition=0,
249
+ )
250
+
251
+ with pytest.raises(IncorrectMessageValueError):
252
+ simple_parser.parse_message(message)
253
+
254
+
255
+ def test_parse_message_empty_value(simple_parser: InputParser[SimpleModel]) -> None:
256
+ """Test parsing a message with empty string value."""
257
+ message = KafkaMessage(
258
+ topic="test-topic",
259
+ value="",
260
+ key="test-key",
261
+ offset=100,
262
+ partition=0,
263
+ )
264
+
265
+ with pytest.raises(IncorrectMessageValueError):
266
+ simple_parser.parse_message(message)
267
+
268
+
269
+ def test_parse_message_invalid_json_value(
270
+ simple_parser: InputParser[SimpleModel],
271
+ ) -> None:
272
+ """Test parsing a message with invalid JSON value."""
273
+ message = KafkaMessage(
274
+ topic="test-topic",
275
+ value='{"id": "123", "name": "test", "value": 42', # Invalid JSON
276
+ key="test-key",
277
+ offset=100,
278
+ partition=0,
279
+ )
280
+
281
+ with pytest.raises(IncorrectMessageValueError):
282
+ simple_parser.parse_message(message)
283
+
284
+
285
+ def test_parse_message_missing_required_field(
286
+ simple_parser: InputParser[SimpleModel],
287
+ ) -> None:
288
+ """Test parsing a message with missing required field."""
289
+ message = KafkaMessage(
290
+ topic="test-topic",
291
+ value='{"id": "123", "name": "test"}', # Missing 'value' field
292
+ key="test-key",
293
+ offset=100,
294
+ partition=0,
295
+ )
296
+
297
+ with pytest.raises(ValidationError):
298
+ simple_parser.parse_message(message)
299
+
300
+
301
+ def test_parse_message_with_special_characters(
302
+ simple_parser: InputParser[SimpleModel],
303
+ ) -> None:
304
+ """Test parsing a message with special characters."""
305
+ special_json = (
306
+ '{"id": "123", "name": "test with special chars: \\n\\t\\"\'", "value": 42}'
307
+ )
308
+ message = KafkaMessage(
309
+ topic="test-topic",
310
+ value=special_json,
311
+ key="test-key",
312
+ offset=100,
313
+ partition=0,
314
+ )
315
+
316
+ result = simple_parser.parse_message(message)
317
+
318
+ assert result.payload.name == "test with special chars: \n\t\"'"
319
+
320
+
321
+ def test_parse_message_with_unicode(simple_parser: InputParser[SimpleModel]) -> None:
322
+ """Test parsing a message with unicode characters."""
323
+ unicode_json = '{"id": "123", "name": "测试名称", "value": 42}'
324
+ message = KafkaMessage(
325
+ topic="test-topic",
326
+ value=unicode_json,
327
+ key="test-key",
328
+ offset=100,
329
+ partition=0,
330
+ )
331
+
332
+ result = simple_parser.parse_message(message)
333
+
334
+ assert result.payload.name == "测试名称"
335
+
336
+
337
+ def test_parse_message_with_large_data(nested_parser: InputParser[NestedModel]) -> None:
338
+ """Test parsing a message with large data."""
339
+ large_data = {"key" + str(i): "value" + str(i) for i in range(1000)}
340
+ large_json = json.dumps(
341
+ {"id": "123", "data": large_data, "metadata": {"type": "large"}}
342
+ )
343
+
344
+ message = KafkaMessage(
345
+ topic="test-topic",
346
+ value=large_json,
347
+ key="test-key",
348
+ offset=100,
349
+ partition=0,
350
+ )
351
+
352
+ result = nested_parser.parse_message(message)
353
+
354
+ assert len(result.payload.data) == 1000
355
+ assert result.payload.data["key0"] == "value0"
356
+ assert result.payload.data["key999"] == "value999"
357
+
358
+
359
+ def test_parse_message_with_none_key(simple_parser: InputParser[SimpleModel]) -> None:
360
+ """Test parsing a message with None key."""
361
+ message = KafkaMessage(
362
+ topic="test-topic",
363
+ value='{"id": "123", "name": "test", "value": 42}',
364
+ key=None,
365
+ offset=100,
366
+ partition=0,
367
+ )
368
+
369
+ result = simple_parser.parse_message(message)
370
+
371
+ assert result.key is None
372
+ assert result.payload.id == "123"
373
+
374
+
375
+ def test_parse_message_with_none_offset_partition(
376
+ simple_parser: InputParser[SimpleModel],
377
+ ) -> None:
378
+ """Test parsing a message with None offset and partition."""
379
+ message = KafkaMessage(
380
+ topic="test-topic",
381
+ value='{"id": "123", "name": "test", "value": 42}',
382
+ key="test-key",
383
+ offset=None,
384
+ partition=None,
385
+ )
386
+
387
+ result = simple_parser.parse_message(message)
388
+
389
+ assert result.offset is None
390
+ assert result.partition is None
391
+ assert result.payload.id == "123"
392
+
393
+
394
+ def test_incorrect_message_value_error_message(
395
+ simple_parser: InputParser[SimpleModel],
396
+ ) -> None:
397
+ """Test that IncorrectMessageValueError is raised for None value."""
398
+ message = KafkaMessage(
399
+ topic="test-topic",
400
+ value=None,
401
+ key="test-key",
402
+ offset=100,
403
+ partition=0,
404
+ )
405
+
406
+ with pytest.raises(IncorrectMessageValueError):
407
+ simple_parser.parse_message(message)
408
+
409
+
410
+ def test_parser_with_different_models() -> None:
411
+ """Test that parsers work with different model types."""
412
+ simple_parser = InputParser(SimpleModel)
413
+ optional_parser = InputParser(OptionalModel)
414
+
415
+ simple_json = '{"id": "123", "name": "test", "value": 42}'
416
+ optional_json = '{"id": "456"}'
417
+
418
+ simple_result = simple_parser.parse_json(simple_json)
419
+ optional_result = optional_parser.parse_json(optional_json)
420
+
421
+ assert isinstance(simple_result, SimpleModel)
422
+ assert isinstance(optional_result, OptionalModel)
423
+ assert simple_result.id == "123"
424
+ assert optional_result.id == "456"
425
+
426
+
427
+ def test_parse_json_with_boolean_values(
428
+ complex_parser: InputParser[ComplexModel],
429
+ ) -> None:
430
+ """Test parsing JSON with boolean values."""
431
+ boolean_json = (
432
+ '{"id": "123", "count": 5, "active": true, "tags": ["tag1"], '
433
+ '"config": {"enabled": false}}'
434
+ )
435
+
436
+ result = complex_parser.parse_json(boolean_json)
437
+
438
+ assert result.active is True
439
+ assert result.config["enabled"] is False
440
+
441
+
442
+ def test_parse_json_with_numeric_types(
443
+ complex_parser: InputParser[ComplexModel],
444
+ ) -> None:
445
+ """Test parsing JSON with various numeric types."""
446
+ numeric_json = (
447
+ '{"id": "123", "count": 42, "active": true, "tags": ["tag1"], '
448
+ '"config": {"float": 3.14, "negative": -10}}'
449
+ )
450
+
451
+ result = complex_parser.parse_json(numeric_json)
452
+
453
+ assert result.count == 42
454
+ assert result.config["float"] == 3.14
455
+ assert result.config["negative"] == -10
456
+
457
+
458
+ def test_parse_json_with_array_types(complex_parser: InputParser[ComplexModel]) -> None:
459
+ """Test parsing JSON with array types."""
460
+ array_json = (
461
+ '{"id": "123", "count": 3, "active": true, "tags": ["tag1", "tag2", "tag3"], '
462
+ '"config": {"empty_array": []}}'
463
+ )
464
+
465
+ result = complex_parser.parse_json(array_json)
466
+
467
+ assert result.tags == ["tag1", "tag2", "tag3"]
468
+ assert result.config["empty_array"] == []
469
+
470
+
471
+ def test_parse_message_preserves_all_fields(
472
+ simple_parser: InputParser[SimpleModel],
473
+ ) -> None:
474
+ """Test that parse_message preserves all KafkaMessage fields."""
475
+ message = KafkaMessage(
476
+ topic="custom-topic",
477
+ value='{"id": "123", "name": "test", "value": 42}',
478
+ key="custom-key",
479
+ offset=999,
480
+ partition=5,
481
+ )
482
+
483
+ result = simple_parser.parse_message(message)
484
+
485
+ assert result.topic == "custom-topic"
486
+ assert result.key == "custom-key"
487
+ assert result.offset == 999
488
+ assert result.partition == 5
489
+ assert result.value == message.value
@@ -0,0 +1,25 @@
1
+ from cledar.kafka.utils.messages import extract_id_from_value
2
+
3
+
4
+ def test_extract_id_from_value_correct_value() -> None:
5
+ input_value = '{"id": "3"}'
6
+ expected_id = "3"
7
+ extracted_id = extract_id_from_value(input_value)
8
+
9
+ assert expected_id == extracted_id, "Extracted and expected id doesn't match"
10
+
11
+
12
+ def test_extract_id_from_value_incorrect_value() -> None:
13
+ input_value = "s"
14
+ expected_id = "<unknown_id>"
15
+ extracted_id = extract_id_from_value(input_value)
16
+
17
+ assert expected_id == extracted_id, "Extracted and expected id doesn't match"
18
+
19
+
20
+ def test_extract_id_from_value_none_value() -> None:
21
+ input_value = None
22
+ expected_id = "<unknown_id>"
23
+ extracted_id = extract_id_from_value(input_value)
24
+
25
+ assert expected_id == extracted_id, "Extracted and expected id doesn't match"