cledar-sdk 1.2.0__py3-none-any.whl → 1.2.1__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_sdk-1.2.0.dist-info → cledar_sdk-1.2.1.dist-info}/METADATA +1 -1
- {cledar_sdk-1.2.0.dist-info → cledar_sdk-1.2.1.dist-info}/RECORD +40 -4
- common_logging/README.md +53 -0
- common_logging/__init__.py +0 -0
- common_logging/tests/test_universal_plaintext_formatter.py +249 -0
- common_logging/universal_plaintext_formatter.py +94 -0
- kserve_service/README.md +352 -0
- kserve_service/__init__.py +3 -0
- kserve_service/tests/__init__.py +0 -0
- kserve_service/tests/test_utils.py +64 -0
- kserve_service/utils.py +27 -0
- nonce_service/README.md +99 -0
- nonce_service/__init__.py +3 -0
- nonce_service/nonce_service.py +36 -0
- nonce_service/tests/__init__.py +0 -0
- nonce_service/tests/test_nonce_service.py +136 -0
- redis_service/README.md +396 -0
- redis_service/__init__.py +0 -0
- redis_service/example.py +37 -0
- redis_service/exceptions.py +22 -0
- redis_service/logger.py +3 -0
- redis_service/model.py +10 -0
- redis_service/redis.py +278 -0
- redis_service/redis_config_store.py +252 -0
- redis_service/tests/test_integration_redis.py +119 -0
- redis_service/tests/test_redis_service.py +319 -0
- storage_service/README.md +529 -0
- storage_service/__init__.py +0 -0
- storage_service/constants.py +3 -0
- storage_service/exceptions.py +50 -0
- storage_service/models.py +19 -0
- storage_service/object_storage.py +955 -0
- storage_service/tests/conftest.py +18 -0
- storage_service/tests/test_abfs.py +165 -0
- storage_service/tests/test_integration_filesystem.py +359 -0
- storage_service/tests/test_integration_s3.py +453 -0
- storage_service/tests/test_local.py +384 -0
- storage_service/tests/test_s3.py +521 -0
- {cledar_sdk-1.2.0.dist-info → cledar_sdk-1.2.1.dist-info}/WHEEL +0 -0
- {cledar_sdk-1.2.0.dist-info → cledar_sdk-1.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
common_logging/README.md,sha256=DIGwhCPugbeQ_tggXIVGi4NYkPZFtJcrO1RQksXxdPk,1798
|
|
2
|
+
common_logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
common_logging/universal_plaintext_formatter.py,sha256=ExuXwfZmc_4tp3RIsoe_ByYpnTN0iQBOi101FB8hksc,3309
|
|
4
|
+
common_logging/tests/test_universal_plaintext_formatter.py,sha256=bY6vvpra81_YXN2MLVjNuA8Iu7kOaZH0lmQMsohjRPI,8560
|
|
1
5
|
kafka_service/README.md,sha256=3lKulsfEUs6qaKExlpPkxT_X1NYBAHraiqQRdvkQ48I,7021
|
|
2
6
|
kafka_service/__init__.py,sha256=yHPDvgkwcM4vtJ2nvSsw9Sc0XqEoWUwhewRXH7x1V3A,1012
|
|
3
7
|
kafka_service/exceptions.py,sha256=LJ-mUYCwoQsDM-bL4ms9ZCiWIOw5kt1KStbN04Mavl0,501
|
|
@@ -33,12 +37,44 @@ kafka_service/tests/unit/test_utils_comprehensive.py,sha256=VjyHgZDhilYtLEBFNedc
|
|
|
33
37
|
kafka_service/utils/callbacks.py,sha256=mqs94wLhnVQTYrN-taD57FRU4K3jXHnNaZLIbBbgFGc,588
|
|
34
38
|
kafka_service/utils/messages.py,sha256=j3Lr1_Rwr2FqepfhM-Jw4kaU3PkvqgugDeYzQsH7L60,738
|
|
35
39
|
kafka_service/utils/topics.py,sha256=ekh7nJwUOojgfil7Ntef_FbebA1ExUcgp1QWUTj0Ywc,118
|
|
40
|
+
kserve_service/README.md,sha256=b15WY_y96UiLAlbbtioua2esswtp3m7pLrhoOCy9q_o,8722
|
|
41
|
+
kserve_service/__init__.py,sha256=XROt6VPOY8_vzt6vkiNAB4ffbgOAHOBlNrWrqmJOmgQ,66
|
|
42
|
+
kserve_service/utils.py,sha256=Q6V-sws5dkIGZKW7XCKIPQ1Qs6itu2eLZ2mfW0HM20w,897
|
|
43
|
+
kserve_service/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
44
|
+
kserve_service/tests/test_utils.py,sha256=5XoSkr6tbDCJ7mV-jqaT7AlrKFAyk3hnRQh-u67KJd0,1678
|
|
36
45
|
monitoring_service/README.md,sha256=UdcfEXK3IhCeBhYdrrV45DXh7FTAK72bXBmUDnqZY8E,2148
|
|
37
46
|
monitoring_service/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
47
|
monitoring_service/monitoring_server.py,sha256=lBnImACViG6QE70RFTziDg8zh0FJsiza6knPI-rvsE8,3488
|
|
39
48
|
monitoring_service/tests/test_monitoring_server.py,sha256=urL2seruM_peNIr7c4hblGfEDjqQUzhPCE2gXIJ_Vf8,1657
|
|
40
49
|
monitoring_service/tests/integration/test_monitoring_server_int.py,sha256=5PP-nm-gZPMyShccYUU17TfRprWq2Ij6Y1YA6JnN9jE,5359
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
nonce_service/README.md,sha256=sPxpA9UJfrqCWidNmzgdcXGT_-6SxPvysnhsSvC5FEo,3533
|
|
51
|
+
nonce_service/__init__.py,sha256=Rh6JWML_ncfb_t_mVl7PIKOXpELRdfunMsFWAtbt5EE,68
|
|
52
|
+
nonce_service/nonce_service.py,sha256=6RhA2eEztH_pNdjkBI5eqmRh14I96NjgVYy2v_z7vdM,1100
|
|
53
|
+
nonce_service/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
54
|
+
nonce_service/tests/test_nonce_service.py,sha256=sWzCBXghZNDOnSo8NQB6tyJJ95g2tZGRPlThFqD2P7g,4047
|
|
55
|
+
redis_service/README.md,sha256=GFzNRpCvPRl-QoOFiDTmj1ncgUoU_HaJV74zVtdwG9M,10176
|
|
56
|
+
redis_service/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
57
|
+
redis_service/example.py,sha256=OApl0JU59viBUZORLxlYBhVMdCc5QvOBdOjscxbm_aQ,1091
|
|
58
|
+
redis_service/exceptions.py,sha256=vvD7SO0xHutSLvUf0ttMo7C6OeaVi8f3bxMservsSVI,737
|
|
59
|
+
redis_service/logger.py,sha256=OBOTx6zk_6wkpB2N_FRV7gXR3xy4dpy4iX2B0oFfZ90,60
|
|
60
|
+
redis_service/model.py,sha256=ykW_KHygNHhfHPvP2RyJj0g_WENXxJEP2HvGy7Yb4uo,174
|
|
61
|
+
redis_service/redis.py,sha256=d8hqkVokVs6eMsteJEsEYZ56nb7WhTQ8Gc_sT4zkSRY,9974
|
|
62
|
+
redis_service/redis_config_store.py,sha256=EFhyvg_Eklrh2tc5dtFpe6nnjMDkTqYT_egclhf0KaI,8919
|
|
63
|
+
redis_service/tests/test_integration_redis.py,sha256=O3z5EgkeB6Sd9C1S3rRMW916bybDCAHb-DgTFZGLTqI,3345
|
|
64
|
+
redis_service/tests/test_redis_service.py,sha256=n3FIYBk9SGzpDkRWnPj-Mi3NKYtDEpbwgjpjb_JwjF4,10252
|
|
65
|
+
storage_service/README.md,sha256=Rkew1joVijHVEfyXS9C7dDw55rQ4HYRbHkRYlZiREak,15655
|
|
66
|
+
storage_service/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
67
|
+
storage_service/constants.py,sha256=O6qHRQafs35_3TUShxtE2kq1lI4HQ-NRs2oWQFMt7Ko,85
|
|
68
|
+
storage_service/exceptions.py,sha256=ItDsFU0Jzdg94yfXkLoC-Wp0Pz-XryNAypZDxpDNRnk,715
|
|
69
|
+
storage_service/models.py,sha256=m1h1xx0YkdY6TIwrcBKKtZrzF1CltKzL-UQZgmP0MOQ,484
|
|
70
|
+
storage_service/object_storage.py,sha256=wpZyEZvidLWgZli5BH_zCFWL42AwK9oN2pXBPVTmsHY,35830
|
|
71
|
+
storage_service/tests/conftest.py,sha256=UfEjadRrCQuLbc9kNU8Y_8NWTyEp1DXmJ4nmb8hWU6k,467
|
|
72
|
+
storage_service/tests/test_abfs.py,sha256=i7-yAlIjIDB8fkdOVz994bU62SMDK4VcRR9uu1OB7ic,5869
|
|
73
|
+
storage_service/tests/test_integration_filesystem.py,sha256=-H3Skc_geYIjXW1si-8uHVxVImmPzr8h6aGYMolzORk,11174
|
|
74
|
+
storage_service/tests/test_integration_s3.py,sha256=Ivg_52LXibqVGMS-53z4zda_Yh4u6FO8WplUUu5WWBc,13614
|
|
75
|
+
storage_service/tests/test_local.py,sha256=3CgtxQ_lBBaPR4t9Ip0i7T98scrTOCdkmHYMVansNCc,11256
|
|
76
|
+
storage_service/tests/test_s3.py,sha256=zAppsvVCeLx_NN1tQfqHo57mONEjeFDmiwyecrS3ZgQ,16355
|
|
77
|
+
cledar_sdk-1.2.1.dist-info/METADATA,sha256=9087LMoW_Hd1TC5jwPFbg_w8a43r1ug9ssJJ_02gEzA,6752
|
|
78
|
+
cledar_sdk-1.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
79
|
+
cledar_sdk-1.2.1.dist-info/licenses/LICENSE,sha256=Pz2eACSxkhsGfW9_iN60pgy-enjnbGTj8df8O3ebnQQ,16726
|
|
80
|
+
cledar_sdk-1.2.1.dist-info/RECORD,,
|
common_logging/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Universal Formatter
|
|
2
|
+
|
|
3
|
+
The `UniversalPlaintextFormatter` is a custom logging formatter that extends the standard `logging.Formatter` class. It adds the ability to include extra attributes from log records while excluding standard attributes and configurable keys.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
To use the `UniversalPlaintextFormatter` in your logging configuration, add the following to your `logging.conf` file:
|
|
8
|
+
|
|
9
|
+
```ini
|
|
10
|
+
[formatter_plaintextFormatter]
|
|
11
|
+
class=questions_generator.common_services.logging.universal_formatter.UniversalPlaintextFormatter
|
|
12
|
+
format=%(asctime)s %(name)s [%(levelname)s]: %(message)s
|
|
13
|
+
datefmt=%Y-%m-%d %H:%M:%S
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- Extends the standard logging.Formatter
|
|
19
|
+
- Automatically includes extra attributes from log records
|
|
20
|
+
- Excludes standard LogRecord attributes to keep logs clean
|
|
21
|
+
- Configurable exclusion of additional keys
|
|
22
|
+
|
|
23
|
+
## Configuration Options
|
|
24
|
+
|
|
25
|
+
In addition to the standard formatter options, you can configure which keys to exclude from the log output:
|
|
26
|
+
|
|
27
|
+
```ini
|
|
28
|
+
[formatter_plaintextFormatter]
|
|
29
|
+
class=questions_generator.common_services.logging.universal_formatter.UniversalPlaintextFormatter
|
|
30
|
+
format=%(asctime)s %(name)s [%(levelname)s]: %(message)s
|
|
31
|
+
datefmt=%Y-%m-%d %H:%M:%S
|
|
32
|
+
exclude_keys=key1,key2,key3
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The `exclude_keys` option allows you to specify a comma-separated list of keys that should be excluded from the log output, in addition to the standard LogRecord attributes.
|
|
36
|
+
|
|
37
|
+
## Example
|
|
38
|
+
|
|
39
|
+
When using this formatter, any extra attributes added to the log record will be automatically included in the log output:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import logging
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger(__name__)
|
|
45
|
+
logger.info("User logged in", extra={"user_id": 123, "ip_address": "192.168.1.1"})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Output:
|
|
49
|
+
```
|
|
50
|
+
2023-08-04 12:34:56 my_module [INFO]: User logged in
|
|
51
|
+
user_id: 123
|
|
52
|
+
ip_address: 192.168.1.1
|
|
53
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# pylint: disable=unused-argument, protected-access
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from common_logging.universal_plaintext_formatter import UniversalPlaintextFormatter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture(name="formatter")
|
|
13
|
+
def fixture_formatter() -> UniversalPlaintextFormatter:
|
|
14
|
+
"""Create a basic formatter instance for testing."""
|
|
15
|
+
return UniversalPlaintextFormatter(
|
|
16
|
+
fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture(name="log_record")
|
|
21
|
+
def fixture_log_record() -> logging.LogRecord:
|
|
22
|
+
"""Create a basic log record for testing."""
|
|
23
|
+
return logging.LogRecord(
|
|
24
|
+
name="test_logger",
|
|
25
|
+
level=logging.INFO,
|
|
26
|
+
pathname="/path/to/file.py",
|
|
27
|
+
lineno=42,
|
|
28
|
+
msg="Test message",
|
|
29
|
+
args=(),
|
|
30
|
+
exc_info=None,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_basic_formatting_without_extras(
|
|
35
|
+
formatter: UniversalPlaintextFormatter, log_record: logging.LogRecord
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Test that basic formatting works without extra attributes."""
|
|
38
|
+
formatted = formatter.format(log_record)
|
|
39
|
+
assert "Test message" in formatted
|
|
40
|
+
assert "test_logger" in formatted
|
|
41
|
+
assert "INFO" in formatted
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_standard_attributes_excluded(
|
|
45
|
+
formatter: UniversalPlaintextFormatter, log_record: logging.LogRecord
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Test that standard LogRecord attributes are excluded from extras."""
|
|
48
|
+
formatted = formatter.format(log_record)
|
|
49
|
+
# Standard attributes should not appear as extras
|
|
50
|
+
assert "pathname:" not in formatted
|
|
51
|
+
assert "lineno:" not in formatted
|
|
52
|
+
assert "levelname:" not in formatted
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_extra_attributes_included(
|
|
56
|
+
formatter: UniversalPlaintextFormatter, log_record: logging.LogRecord
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Test that extra attributes are included in the formatted output."""
|
|
59
|
+
log_record.user_id = "12345"
|
|
60
|
+
log_record.request_id = "abc-def-ghi"
|
|
61
|
+
|
|
62
|
+
formatted = formatter.format(log_record)
|
|
63
|
+
|
|
64
|
+
assert "user_id: 12345" in formatted
|
|
65
|
+
assert "request_id: abc-def-ghi" in formatted
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_default_exclude_keys(
|
|
69
|
+
formatter: UniversalPlaintextFormatter, log_record: logging.LogRecord
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Test that DEFAULT_EXCLUDE_KEYS (message, asctime) are excluded."""
|
|
72
|
+
# Add 'message' and 'asctime' as extra attributes (shouldn't appear in extras)
|
|
73
|
+
log_record.message = "This should be excluded"
|
|
74
|
+
log_record.asctime = "2025-01-01 12:00:00"
|
|
75
|
+
|
|
76
|
+
formatted = formatter.format(log_record)
|
|
77
|
+
|
|
78
|
+
# These should not appear as extras
|
|
79
|
+
lines = formatted.split("\n")
|
|
80
|
+
extra_lines = [
|
|
81
|
+
line for line in lines if line.strip().startswith(("message:", "asctime:"))
|
|
82
|
+
]
|
|
83
|
+
assert len(extra_lines) == 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_multiple_extras_formatting(
|
|
87
|
+
formatter: UniversalPlaintextFormatter, log_record: logging.LogRecord
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Test formatting with multiple extra attributes."""
|
|
90
|
+
log_record.user_id = "12345"
|
|
91
|
+
log_record.session_id = "session-xyz"
|
|
92
|
+
log_record.ip_address = "192.168.1.1"
|
|
93
|
+
|
|
94
|
+
formatted = formatter.format(log_record)
|
|
95
|
+
|
|
96
|
+
assert "user_id: 12345" in formatted
|
|
97
|
+
assert "session_id: session-xyz" in formatted
|
|
98
|
+
assert "ip_address: 192.168.1.1" in formatted
|
|
99
|
+
|
|
100
|
+
# Check that extras are indented
|
|
101
|
+
lines = formatted.split("\n")
|
|
102
|
+
extra_lines = [
|
|
103
|
+
line
|
|
104
|
+
for line in lines
|
|
105
|
+
if any(key in line for key in ("user_id:", "session_id:", "ip_address:"))
|
|
106
|
+
]
|
|
107
|
+
for line in extra_lines:
|
|
108
|
+
assert line.startswith(" ")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_config_exclude_keys_from_file(log_record: logging.LogRecord) -> None:
|
|
112
|
+
"""Test that exclude_keys from configuration file are properly excluded."""
|
|
113
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
114
|
+
config_path = Path(tmpdir) / "logging.conf"
|
|
115
|
+
config_content = """[formatter_plaintextFormatter]
|
|
116
|
+
exclude_keys = custom_field, another_field
|
|
117
|
+
"""
|
|
118
|
+
config_path.write_text(config_content)
|
|
119
|
+
|
|
120
|
+
# Change to temp directory to read config
|
|
121
|
+
original_dir = os.getcwd()
|
|
122
|
+
try:
|
|
123
|
+
os.chdir(tmpdir)
|
|
124
|
+
formatter = UniversalPlaintextFormatter(fmt="%(message)s")
|
|
125
|
+
|
|
126
|
+
# Add attributes that should be excluded
|
|
127
|
+
log_record.custom_field = "should be excluded"
|
|
128
|
+
log_record.another_field = "also excluded"
|
|
129
|
+
log_record.included_field = "should be included"
|
|
130
|
+
|
|
131
|
+
formatted = formatter.format(log_record)
|
|
132
|
+
|
|
133
|
+
assert "custom_field:" not in formatted
|
|
134
|
+
assert "another_field:" not in formatted
|
|
135
|
+
assert "included_field: should be included" in formatted
|
|
136
|
+
finally:
|
|
137
|
+
os.chdir(original_dir)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_config_exclude_keys_with_whitespace(
|
|
141
|
+
log_record: logging.LogRecord,
|
|
142
|
+
) -> None:
|
|
143
|
+
"""Test that whitespace in exclude_keys configuration is handled correctly."""
|
|
144
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
145
|
+
config_path = Path(tmpdir) / "logging.conf"
|
|
146
|
+
config_content = """[formatter_plaintextFormatter]
|
|
147
|
+
exclude_keys = field1 , field2 , field3
|
|
148
|
+
"""
|
|
149
|
+
config_path.write_text(config_content)
|
|
150
|
+
|
|
151
|
+
original_dir = os.getcwd()
|
|
152
|
+
try:
|
|
153
|
+
os.chdir(tmpdir)
|
|
154
|
+
formatter = UniversalPlaintextFormatter(fmt="%(message)s")
|
|
155
|
+
|
|
156
|
+
log_record.field1 = "excluded"
|
|
157
|
+
log_record.field2 = "excluded"
|
|
158
|
+
log_record.field3 = "excluded"
|
|
159
|
+
|
|
160
|
+
formatted = formatter.format(log_record)
|
|
161
|
+
|
|
162
|
+
assert "field1:" not in formatted
|
|
163
|
+
assert "field2:" not in formatted
|
|
164
|
+
assert "field3:" not in formatted
|
|
165
|
+
finally:
|
|
166
|
+
os.chdir(original_dir)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_no_config_file(
|
|
170
|
+
formatter: UniversalPlaintextFormatter, log_record: logging.LogRecord
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Test that formatter works correctly when config file doesn't exist."""
|
|
173
|
+
log_record.some_extra = "value"
|
|
174
|
+
formatted = formatter.format(log_record)
|
|
175
|
+
|
|
176
|
+
# Should still format correctly
|
|
177
|
+
assert "Test message" in formatted
|
|
178
|
+
assert "some_extra: value" in formatted
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_empty_extras(
|
|
182
|
+
formatter: UniversalPlaintextFormatter, log_record: logging.LogRecord
|
|
183
|
+
) -> None:
|
|
184
|
+
"""Test formatting when there are no extra attributes."""
|
|
185
|
+
formatted = formatter.format(log_record)
|
|
186
|
+
|
|
187
|
+
# Should only contain the base formatted message without extra newlines
|
|
188
|
+
lines = formatted.split("\n")
|
|
189
|
+
assert len([line for line in lines if line.strip()]) == 1
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_standard_attrs_caching(
|
|
193
|
+
formatter: UniversalPlaintextFormatter,
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Test that standard attributes are cached after first call."""
|
|
196
|
+
assert formatter._standard_attrs is None
|
|
197
|
+
|
|
198
|
+
# First call should set the cache
|
|
199
|
+
standard_attrs = formatter._get_standard_attrs()
|
|
200
|
+
assert formatter._standard_attrs is not None
|
|
201
|
+
assert formatter._standard_attrs == standard_attrs
|
|
202
|
+
|
|
203
|
+
# Second call should return cached value
|
|
204
|
+
standard_attrs_2 = formatter._get_standard_attrs()
|
|
205
|
+
assert standard_attrs_2 is standard_attrs # Same object
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_formatter_with_custom_format_string(
|
|
209
|
+
log_record: logging.LogRecord,
|
|
210
|
+
) -> None:
|
|
211
|
+
"""Test formatter with a custom format string."""
|
|
212
|
+
formatter = UniversalPlaintextFormatter(fmt="[%(levelname)s] %(message)s")
|
|
213
|
+
log_record.extra_data = "test"
|
|
214
|
+
|
|
215
|
+
formatted = formatter.format(log_record)
|
|
216
|
+
|
|
217
|
+
assert "[INFO] Test message" in formatted
|
|
218
|
+
assert "extra_data: test" in formatted
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_exclude_keys_combination(log_record: logging.LogRecord) -> None:
|
|
222
|
+
"""Test that all exclusion sources are combined correctly."""
|
|
223
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
224
|
+
config_path = Path(tmpdir) / "logging.conf"
|
|
225
|
+
config_content = """[formatter_plaintextFormatter]
|
|
226
|
+
exclude_keys = config_excluded
|
|
227
|
+
"""
|
|
228
|
+
config_path.write_text(config_content)
|
|
229
|
+
|
|
230
|
+
original_dir = os.getcwd()
|
|
231
|
+
try:
|
|
232
|
+
os.chdir(tmpdir)
|
|
233
|
+
formatter = UniversalPlaintextFormatter(fmt="%(message)s")
|
|
234
|
+
|
|
235
|
+
# Add various attributes
|
|
236
|
+
log_record.pathname = "standard_attr" # Standard LogRecord attribute
|
|
237
|
+
log_record.message = "default_excluded" # DEFAULT_EXCLUDE_KEYS
|
|
238
|
+
log_record.config_excluded = "from_config" # From config file
|
|
239
|
+
log_record.should_appear = "yes" # Should appear
|
|
240
|
+
|
|
241
|
+
formatted = formatter.format(log_record)
|
|
242
|
+
|
|
243
|
+
# Only should_appear should be in extras
|
|
244
|
+
assert "pathname:" not in formatted # Standard attribute
|
|
245
|
+
assert "message:" not in formatted # DEFAULT_EXCLUDE_KEYS
|
|
246
|
+
assert "config_excluded:" not in formatted # Config exclude
|
|
247
|
+
assert "should_appear: yes" in formatted # Should be included
|
|
248
|
+
finally:
|
|
249
|
+
os.chdir(original_dir)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import configparser
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class UniversalPlaintextFormatter(logging.Formatter):
|
|
7
|
+
"""
|
|
8
|
+
A custom formatter for logging that extends the standard logging.Formatter.
|
|
9
|
+
|
|
10
|
+
This formatter adds the ability to include extra attributes from log records while
|
|
11
|
+
excluding standard attributes and configurable keys.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# Predefined exclusions - keys that should always be excluded
|
|
15
|
+
DEFAULT_EXCLUDE_KEYS = {"message", "asctime"}
|
|
16
|
+
|
|
17
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Initialize the formatter with standard formatter parameters.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
*args: Variable length argument list for the parent class.
|
|
23
|
+
**kwargs: Arbitrary keyword arguments for the parent class.
|
|
24
|
+
"""
|
|
25
|
+
super().__init__(*args, **kwargs)
|
|
26
|
+
self._standard_attrs: set[str] | None = None
|
|
27
|
+
self._config_exclude_keys = self._load_exclude_keys_from_config()
|
|
28
|
+
|
|
29
|
+
def _load_exclude_keys_from_config(self) -> set[str]:
|
|
30
|
+
"""
|
|
31
|
+
Load additional keys to exclude from the configuration file.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
set: A set of keys to exclude from log records.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
config = configparser.ConfigParser()
|
|
38
|
+
config.read("logging.conf")
|
|
39
|
+
if config.has_option("formatter_plaintextFormatter", "exclude_keys"):
|
|
40
|
+
exclude_str = config.get("formatter_plaintextFormatter", "exclude_keys")
|
|
41
|
+
return set(key.strip() for key in exclude_str.split(",") if key.strip())
|
|
42
|
+
except (configparser.Error, FileNotFoundError, PermissionError, ValueError):
|
|
43
|
+
pass
|
|
44
|
+
return set()
|
|
45
|
+
|
|
46
|
+
def _get_standard_attrs(self) -> set[str]:
|
|
47
|
+
"""
|
|
48
|
+
Get the set of standard attributes to exclude from log records.
|
|
49
|
+
|
|
50
|
+
This includes standard LogRecord attributes, predefined exclusions,
|
|
51
|
+
and exclusions from configuration.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
set: A set of attribute names to exclude.
|
|
55
|
+
"""
|
|
56
|
+
if self._standard_attrs is None:
|
|
57
|
+
dummy_record = logging.LogRecord(
|
|
58
|
+
name="dummy",
|
|
59
|
+
level=logging.INFO,
|
|
60
|
+
pathname="",
|
|
61
|
+
lineno=0,
|
|
62
|
+
msg="",
|
|
63
|
+
args=(),
|
|
64
|
+
exc_info=None,
|
|
65
|
+
)
|
|
66
|
+
# Combine standard attributes + predefined + from configuration
|
|
67
|
+
all_excludes = (
|
|
68
|
+
set(dummy_record.__dict__.keys())
|
|
69
|
+
| self.DEFAULT_EXCLUDE_KEYS
|
|
70
|
+
| self._config_exclude_keys
|
|
71
|
+
)
|
|
72
|
+
self._standard_attrs = all_excludes
|
|
73
|
+
return self._standard_attrs
|
|
74
|
+
|
|
75
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
76
|
+
"""
|
|
77
|
+
Format the log record, adding any extra attributes not in the standard set.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
record: The log record to format.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
str: The formatted log message with extra attributes appended.
|
|
84
|
+
"""
|
|
85
|
+
base = super().format(record)
|
|
86
|
+
extras = {
|
|
87
|
+
k: v
|
|
88
|
+
for k, v in record.__dict__.items()
|
|
89
|
+
if k not in self._get_standard_attrs()
|
|
90
|
+
}
|
|
91
|
+
if extras:
|
|
92
|
+
extras_str = "\n".join(f" {k}: {v}" for k, v in extras.items())
|
|
93
|
+
return f"{base}\n{extras_str}"
|
|
94
|
+
return base
|