various-api-tools 0.2.1__tar.gz → 0.3.0__tar.gz
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.
- {various_api_tools-0.2.1 → various_api_tools-0.3.0}/PKG-INFO +2 -1
- {various_api_tools-0.2.1 → various_api_tools-0.3.0}/pyproject.toml +2 -1
- various_api_tools-0.3.0/src/various_api_tools/__init__.py +14 -0
- {various_api_tools-0.2.1 → various_api_tools-0.3.0}/src/various_api_tools/translators/json.py +1 -1
- various_api_tools-0.3.0/src/various_api_tools/translators/psycopg2.py +167 -0
- {various_api_tools-0.2.1 → various_api_tools-0.3.0}/src/various_api_tools/translators/pydantic.py +1 -1
- {various_api_tools-0.2.1 → various_api_tools-0.3.0}/tests/translators/test_json.py +112 -111
- various_api_tools-0.3.0/tests/translators/test_psycopg2.py +97 -0
- {various_api_tools-0.2.1 → various_api_tools-0.3.0}/tests/translators/test_pydantic.py +11 -11
- various_api_tools-0.2.1/src/various_api_tools/__init__.py +0 -11
- {various_api_tools-0.2.1 → various_api_tools-0.3.0}/README.md +0 -0
- {various_api_tools-0.2.1 → various_api_tools-0.3.0}/src/various_api_tools/translators/__init__.py +0 -0
- {various_api_tools-0.2.1 → various_api_tools-0.3.0}/tests/__init__.py +0 -0
- {various_api_tools-0.2.1 → various_api_tools-0.3.0}/tests/translators/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: various-api-tools
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A lightweight utility package for common API-related tasks in Python, including JSON and Pydantic error translators that provide user-friendly Russian messages.
|
|
5
5
|
Author-Email: dkurchigin <kurchigin.dmitry@yandex.ru>
|
|
6
6
|
License: MIT
|
|
@@ -17,6 +17,7 @@ Project-URL: Documentation, https://various-api-tools.dkurchigin.ru/
|
|
|
17
17
|
Project-URL: Source, https://gitverse.ru/dkurchigin/various-api-tools
|
|
18
18
|
Requires-Python: >=3.10
|
|
19
19
|
Requires-Dist: pydantic>=2.11.7
|
|
20
|
+
Requires-Dist: psycopg2-binary>=2.9.10
|
|
20
21
|
Description-Content-Type: text/markdown
|
|
21
22
|
|
|
22
23
|
# various_api_tools
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "various-api-tools"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "A lightweight utility package for common API-related tasks in Python, including JSON and Pydantic error translators that provide user-friendly Russian messages."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "dkurchigin", email = "kurchigin.dmitry@yandex.ru" },
|
|
7
7
|
]
|
|
8
8
|
dependencies = [
|
|
9
9
|
"pydantic>=2.11.7",
|
|
10
|
+
"psycopg2-binary>=2.9.10",
|
|
10
11
|
]
|
|
11
12
|
requires-python = ">=3.10"
|
|
12
13
|
readme = "README.md"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""A package for various API utility tools.
|
|
2
|
+
|
|
3
|
+
Including JSON and Pydantic error translators.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .translators.json import JSONDecodeErrorTranslator
|
|
7
|
+
from .translators.psycopg2 import Psycopg2ErrorTranslator
|
|
8
|
+
from .translators.pydantic import PydanticValidationErrorTranslator
|
|
9
|
+
|
|
10
|
+
__all__ = (
|
|
11
|
+
"JSONDecodeErrorTranslator",
|
|
12
|
+
"Psycopg2ErrorTranslator",
|
|
13
|
+
"PydanticValidationErrorTranslator",
|
|
14
|
+
)
|
{various_api_tools-0.2.1 → various_api_tools-0.3.0}/src/various_api_tools/translators/json.py
RENAMED
|
@@ -12,7 +12,7 @@ from typing import Final
|
|
|
12
12
|
BASE_JSON_ERROR_MESSAGE: Final[str] = "Ошибка конвертации в формате JSON.\n"
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
class
|
|
15
|
+
class JSONDecodeErrorTranslator:
|
|
16
16
|
"""Translates JSONDecodeError messages into user-friendly Russian descriptions.
|
|
17
17
|
|
|
18
18
|
This class provides a method to convert Python's JSONDecodeError exceptions
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Module for translating PostgreSQL (psycopg2) database errors into messages.
|
|
2
|
+
|
|
3
|
+
This module provides a utility class to parse and translate psycopg2 error messages
|
|
4
|
+
into readable Russian strings suitable for end-user feedback.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any, Final
|
|
10
|
+
|
|
11
|
+
from psycopg2._psycopg import Error
|
|
12
|
+
|
|
13
|
+
BASE_PSYCOPG2_ERROR_MESSAGE: Final[str] = (
|
|
14
|
+
"Ошибка во время сохранения/изменения в базе данных. Проверьте исходные данные."
|
|
15
|
+
)
|
|
16
|
+
UNIQUE_VIOLATION_PATTERN: Final[str] = r"DETAIL:.*(Key|Ключ).*\((.*?)\)=\((.*?)\)"
|
|
17
|
+
CHECK_VIOLATION_PATTERN: Final[str] = r'violates check constraint.*?"(.*?)"'
|
|
18
|
+
|
|
19
|
+
UNKNOWN_CHECK_DESCRIPTION: Final[str] = "Невалидная запись в БД"
|
|
20
|
+
DEFAULT_CODE_DESCRIPTION_MAP: Final[dict[int, str]] = {
|
|
21
|
+
23503: "Указан несуществующий идентификатор связанного объекта",
|
|
22
|
+
23505: "БД уже содержит значение",
|
|
23
|
+
23514: "Нарушено ограничение данных",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ErrorData:
|
|
29
|
+
"""Holds extracted key-value pairs from database error messages."""
|
|
30
|
+
|
|
31
|
+
key: str
|
|
32
|
+
value: Any
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Psycopg2ErrorTranslator:
|
|
36
|
+
"""Translates psycopg2 database errors into human-readable Russian messages.
|
|
37
|
+
|
|
38
|
+
This class extracts error details from PostgreSQL exceptions and converts them
|
|
39
|
+
into clear, user-friendly messages based on error codes and patterns.
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
code_map: dict[int, str]
|
|
44
|
+
constraint_map: dict[str, str]
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
*,
|
|
49
|
+
code_map: dict[int, str] | None = None,
|
|
50
|
+
constraint_map: dict[str, str] | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Initialize the translator with optional custom error message mappings.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
code_map: Optional mapping of PostgreSQL error codes to messages.
|
|
56
|
+
constraint_map: Optional mapping of check constraints to descriptions.
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
if constraint_map is not None:
|
|
60
|
+
self.constraint_map = constraint_map
|
|
61
|
+
else:
|
|
62
|
+
self.constraint_map = {}
|
|
63
|
+
|
|
64
|
+
if code_map is not None:
|
|
65
|
+
self.code_map = code_map
|
|
66
|
+
else:
|
|
67
|
+
self.code_map = DEFAULT_CODE_DESCRIPTION_MAP
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def get_unique_violation_error(cls, error_msg: str) -> ErrorData:
|
|
71
|
+
"""Extract key and value from a unique constraint violation message.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
error_msg: Raw error message string.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
An `ErrorData` object with extracted key and value, or None if no match.
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
```python
|
|
81
|
+
error_msg = "DETAIL: Key (value)=(a6cc5730) already exists."
|
|
82
|
+
result = Psycopg2ErrorTranslator.get_unique_violation_error(error_msg)
|
|
83
|
+
#> ErrorData(key='uuid', value='a6cc5730-2261-11ee-9c43-2eb5a363657c')
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
"""
|
|
87
|
+
data: ErrorData | None = None
|
|
88
|
+
|
|
89
|
+
pattern = re.compile(UNIQUE_VIOLATION_PATTERN)
|
|
90
|
+
matched = pattern.search(error_msg)
|
|
91
|
+
|
|
92
|
+
if matched is not None:
|
|
93
|
+
data = ErrorData(key=matched.group(2), value=matched.group(3))
|
|
94
|
+
|
|
95
|
+
return data
|
|
96
|
+
|
|
97
|
+
def get_check_violation_error(self, error_msg: str) -> ErrorData | None:
|
|
98
|
+
"""Extract constraint name and description from a check constraint violation.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
error_msg: Raw error message string.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
An `ErrorData` object with constraint name and description, or None.
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
```python
|
|
108
|
+
error_msg = 'violates check constraint "valid_email"'
|
|
109
|
+
result = translator.get_check_violation_error(error_msg)
|
|
110
|
+
#> ErrorData(key='valid_email', value='Невалидный email')
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
"""
|
|
114
|
+
data: ErrorData | None = None
|
|
115
|
+
|
|
116
|
+
pattern = re.compile(CHECK_VIOLATION_PATTERN)
|
|
117
|
+
matched = pattern.search(error_msg)
|
|
118
|
+
if matched is not None:
|
|
119
|
+
check_constraint = matched.group(1)
|
|
120
|
+
constraint_description = self.constraint_map.get(
|
|
121
|
+
check_constraint,
|
|
122
|
+
UNKNOWN_CHECK_DESCRIPTION,
|
|
123
|
+
)
|
|
124
|
+
data = ErrorData(key=check_constraint, value=constraint_description)
|
|
125
|
+
|
|
126
|
+
return data
|
|
127
|
+
|
|
128
|
+
def translate(
|
|
129
|
+
self,
|
|
130
|
+
error: Error,
|
|
131
|
+
*,
|
|
132
|
+
msg: str = BASE_PSYCOPG2_ERROR_MESSAGE,
|
|
133
|
+
) -> str:
|
|
134
|
+
"""Translate a psycopg2 Error into a user-friendly Russian message.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
error: A psycopg2 Error instance.
|
|
138
|
+
msg: Optional base message to use if no specific translation is found.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
A string containing the translated error message.
|
|
142
|
+
|
|
143
|
+
Example:
|
|
144
|
+
```python
|
|
145
|
+
translator = Psycopg2ErrorTranslator()
|
|
146
|
+
result = translator.translate(exc)
|
|
147
|
+
#> "БД уже содержит значение: ключ email, значение invalid@example.com."
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
"""
|
|
151
|
+
pgcode = int(value) if (value := error.pgcode) is not None else None
|
|
152
|
+
if pgcode is not None:
|
|
153
|
+
msg_prefix: str | None = self.code_map.get(pgcode)
|
|
154
|
+
data: ErrorData | None = None
|
|
155
|
+
|
|
156
|
+
if pgcode == 23514: # noqa: PLR2004
|
|
157
|
+
data = self.get_check_violation_error(error_msg=error.pgerror)
|
|
158
|
+
elif pgcode in {23505, 23503}:
|
|
159
|
+
data = self.get_unique_violation_error(error_msg=error.pgerror)
|
|
160
|
+
|
|
161
|
+
if data:
|
|
162
|
+
if msg_prefix:
|
|
163
|
+
msg = f"{msg_prefix}: ключ {data.key}, значение {data.value}."
|
|
164
|
+
elif msg_prefix:
|
|
165
|
+
msg = f"{msg_prefix}."
|
|
166
|
+
|
|
167
|
+
return msg
|
{various_api_tools-0.2.1 → various_api_tools-0.3.0}/src/various_api_tools/translators/pydantic.py
RENAMED
|
@@ -46,7 +46,7 @@ DEFAULT_LOCATION_PREFIX: Final[str] = "Поле"
|
|
|
46
46
|
UNKNOWN_ERROR_TYPE: Final[str] = "Неизвестная ошибка"
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
class
|
|
49
|
+
class PydanticValidationErrorTranslator:
|
|
50
50
|
"""Translate Pydantic validation errors into human-readable Russian messages.
|
|
51
51
|
|
|
52
52
|
This class provides a method to translate a sequence of Pydantic error details
|
|
@@ -3,7 +3,7 @@ import sys
|
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
5
|
|
|
6
|
-
from src.various_api_tools.translators.json import
|
|
6
|
+
from src.various_api_tools.translators.json import JSONDecodeErrorTranslator
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class TestDecodeErrorTranslator:
|
|
@@ -103,7 +103,7 @@ class TestDecodeErrorTranslator:
|
|
|
103
103
|
try:
|
|
104
104
|
res = json.loads(input_data)
|
|
105
105
|
except json.JSONDecodeError as exc:
|
|
106
|
-
msg =
|
|
106
|
+
msg = JSONDecodeErrorTranslator.translate(error=exc)
|
|
107
107
|
assert msg == error_msg
|
|
108
108
|
else:
|
|
109
109
|
assert res == expected_output
|
|
@@ -199,11 +199,12 @@ class TestDecodeErrorTranslator:
|
|
|
199
199
|
try:
|
|
200
200
|
res = json.loads(input_data)
|
|
201
201
|
except json.JSONDecodeError as exc:
|
|
202
|
-
msg =
|
|
202
|
+
msg = JSONDecodeErrorTranslator.translate(error=exc, line_number=6)
|
|
203
203
|
assert msg == error_msg
|
|
204
204
|
else:
|
|
205
205
|
assert res == expected_output
|
|
206
206
|
|
|
207
|
+
|
|
207
208
|
class TestDecodeErrorTranslatorForPython313:
|
|
208
209
|
@pytest.mark.skipif(sys.version_info < (3, 13), reason="for Python 3.13")
|
|
209
210
|
@pytest.mark.parametrize(
|
|
@@ -211,97 +212,97 @@ class TestDecodeErrorTranslatorForPython313:
|
|
|
211
212
|
[
|
|
212
213
|
# Ошибка: ожидается разделитель (Expecting delimiter)
|
|
213
214
|
(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
215
|
+
"[1, 2 3]",
|
|
216
|
+
None,
|
|
217
|
+
"Ошибка конвертации в формате JSON.\n"
|
|
218
|
+
"Позиция: 6.\n"
|
|
219
|
+
"Описание: ожидается разделитель.",
|
|
219
220
|
),
|
|
220
221
|
(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
222
|
+
'{"name": "Alice",}',
|
|
223
|
+
None,
|
|
224
|
+
"Ошибка конвертации в формате JSON.\n"
|
|
225
|
+
"Позиция: 16.\n"
|
|
226
|
+
"Описание: неизвестная ошибка.",
|
|
226
227
|
),
|
|
227
228
|
(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
229
|
+
'[{"id": 1}',
|
|
230
|
+
None,
|
|
231
|
+
"Ошибка конвертации в формате JSON.\n"
|
|
232
|
+
"Позиция: 10.\n"
|
|
233
|
+
"Описание: ожидается разделитель.",
|
|
233
234
|
),
|
|
234
235
|
# Ошибка: не правильно используются двойные кавычки
|
|
235
236
|
(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
237
|
+
'{"name": "Alice"',
|
|
238
|
+
None,
|
|
239
|
+
"Ошибка конвертации в формате JSON.\n"
|
|
240
|
+
"Позиция: 16.\n"
|
|
241
|
+
"Описание: ожидается разделитель.",
|
|
241
242
|
),
|
|
242
243
|
# Ошибка: ожидается значение
|
|
243
244
|
(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
245
|
+
"",
|
|
246
|
+
None,
|
|
247
|
+
"Ошибка конвертации в формате JSON.\n"
|
|
248
|
+
"Позиция: 0.\n"
|
|
249
|
+
"Описание: ожидается значение.",
|
|
249
250
|
),
|
|
250
251
|
(
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
252
|
+
'{"id": }',
|
|
253
|
+
None,
|
|
254
|
+
"Ошибка конвертации в формате JSON.\n"
|
|
255
|
+
"Позиция: 7.\n"
|
|
256
|
+
"Описание: ожидается значение.",
|
|
256
257
|
),
|
|
257
258
|
# Ошибка: обнаружены дополнительные данные
|
|
258
259
|
(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
260
|
+
'\n{"name": "Alice"}\n\n{"name": "Bob"}\n',
|
|
261
|
+
None,
|
|
262
|
+
"Ошибка конвертации в формате JSON.\n"
|
|
263
|
+
"Позиция: 20.\n"
|
|
264
|
+
"Описание: обнаружены дополнительные данные.",
|
|
264
265
|
),
|
|
265
266
|
# Ошибка: недопустимые экранирующие символы
|
|
266
267
|
(
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
268
|
+
'{"text": "Hello\x00World"}',
|
|
269
|
+
None,
|
|
270
|
+
"Ошибка конвертации в формате JSON.\n"
|
|
271
|
+
"Позиция: 15.\n"
|
|
272
|
+
"Описание: обнаружен неэкранированный контрольный символ.",
|
|
272
273
|
),
|
|
273
274
|
# Ошибка: незакрытая строка
|
|
274
275
|
(
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
276
|
+
'{"name": "Alice"',
|
|
277
|
+
None,
|
|
278
|
+
"Ошибка конвертации в формате JSON.\n"
|
|
279
|
+
"Позиция: 16.\n"
|
|
280
|
+
"Описание: ожидается разделитель.",
|
|
280
281
|
),
|
|
281
282
|
# Корректный JSON
|
|
282
283
|
(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
284
|
+
'[{"name": "Alice"}, {"name": "Bob"}]',
|
|
285
|
+
[{"name": "Alice"}, {"name": "Bob"}],
|
|
286
|
+
None,
|
|
286
287
|
),
|
|
287
288
|
# Корректный JSON
|
|
288
289
|
(
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
290
|
+
'{"name": "Alice"}',
|
|
291
|
+
{"name": "Alice"},
|
|
292
|
+
None,
|
|
292
293
|
),
|
|
293
294
|
],
|
|
294
295
|
)
|
|
295
296
|
def test_translate_without_line_number(
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
297
|
+
self,
|
|
298
|
+
input_data,
|
|
299
|
+
expected_output,
|
|
300
|
+
error_msg,
|
|
300
301
|
):
|
|
301
302
|
try:
|
|
302
303
|
res = json.loads(input_data)
|
|
303
304
|
except json.JSONDecodeError as exc:
|
|
304
|
-
msg =
|
|
305
|
+
msg = JSONDecodeErrorTranslator.translate(error=exc)
|
|
305
306
|
assert msg == error_msg
|
|
306
307
|
else:
|
|
307
308
|
assert res == expected_output
|
|
@@ -312,84 +313,84 @@ class TestDecodeErrorTranslatorForPython313:
|
|
|
312
313
|
[
|
|
313
314
|
# Ошибка: ожидается разделитель (Expecting delimiter)
|
|
314
315
|
(
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
316
|
+
"[1, 2 3]",
|
|
317
|
+
None,
|
|
318
|
+
"Ошибка конвертации в формате JSON.\n"
|
|
319
|
+
"Строка: 6, позиция 6.\n"
|
|
320
|
+
"Описание: ожидается разделитель.",
|
|
320
321
|
),
|
|
321
322
|
(
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
323
|
+
'{"name": "Alice",}',
|
|
324
|
+
None,
|
|
325
|
+
"Ошибка конвертации в формате JSON.\n"
|
|
326
|
+
"Строка: 6, позиция 16.\n"
|
|
327
|
+
"Описание: неизвестная ошибка.",
|
|
327
328
|
),
|
|
328
329
|
(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
330
|
+
'[{"id": 1}',
|
|
331
|
+
None,
|
|
332
|
+
"Ошибка конвертации в формате JSON.\n"
|
|
333
|
+
"Строка: 6, позиция 10.\n"
|
|
334
|
+
"Описание: ожидается разделитель.",
|
|
334
335
|
),
|
|
335
336
|
# Ошибка: не правильно используются двойные кавычки
|
|
336
337
|
(
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
338
|
+
'{"name": "Alice"',
|
|
339
|
+
None,
|
|
340
|
+
"Ошибка конвертации в формате JSON.\n"
|
|
341
|
+
"Строка: 6, позиция 16.\n"
|
|
342
|
+
"Описание: ожидается разделитель.",
|
|
342
343
|
),
|
|
343
344
|
# Ошибка: ожидается значение
|
|
344
345
|
(
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
346
|
+
"",
|
|
347
|
+
None,
|
|
348
|
+
"Ошибка конвертации в формате JSON.\n"
|
|
349
|
+
"Строка: 6, позиция 0.\n"
|
|
350
|
+
"Описание: ожидается значение.",
|
|
350
351
|
),
|
|
351
352
|
(
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
353
|
+
'{"id": }',
|
|
354
|
+
None,
|
|
355
|
+
"Ошибка конвертации в формате JSON.\n"
|
|
356
|
+
"Строка: 6, позиция 7.\n"
|
|
357
|
+
"Описание: ожидается значение.",
|
|
357
358
|
),
|
|
358
359
|
# Ошибка: обнаружены дополнительные данные
|
|
359
360
|
(
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
361
|
+
'\n{"name": "Alice"}\n\n{"name": "Bob"}\n',
|
|
362
|
+
None,
|
|
363
|
+
"Ошибка конвертации в формате JSON.\n"
|
|
364
|
+
"Строка: 6, позиция 20.\n"
|
|
365
|
+
"Описание: обнаружены дополнительные данные.",
|
|
365
366
|
),
|
|
366
367
|
# Ошибка: недопустимые экранирующие символы
|
|
367
368
|
(
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
369
|
+
'{"text": "Hello\x00World"}',
|
|
370
|
+
None,
|
|
371
|
+
"Ошибка конвертации в формате JSON.\n"
|
|
372
|
+
"Строка: 6, позиция 15.\n"
|
|
373
|
+
"Описание: обнаружен неэкранированный контрольный символ.",
|
|
373
374
|
),
|
|
374
375
|
# Ошибка: незакрытая строка
|
|
375
376
|
(
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
377
|
+
'{"name": "Alice"',
|
|
378
|
+
None,
|
|
379
|
+
"Ошибка конвертации в формате JSON.\n"
|
|
380
|
+
"Строка: 6, позиция 16.\n"
|
|
381
|
+
"Описание: ожидается разделитель.",
|
|
381
382
|
),
|
|
382
383
|
# Корректный JSON
|
|
383
384
|
(
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
385
|
+
'[{"name": "Alice"}, {"name": "Bob"}]',
|
|
386
|
+
[{"name": "Alice"}, {"name": "Bob"}],
|
|
387
|
+
None,
|
|
387
388
|
),
|
|
388
389
|
# Корректный JSON
|
|
389
390
|
(
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
391
|
+
'{"name": "Alice"}',
|
|
392
|
+
{"name": "Alice"},
|
|
393
|
+
None,
|
|
393
394
|
),
|
|
394
395
|
],
|
|
395
396
|
)
|
|
@@ -397,7 +398,7 @@ class TestDecodeErrorTranslatorForPython313:
|
|
|
397
398
|
try:
|
|
398
399
|
res = json.loads(input_data)
|
|
399
400
|
except json.JSONDecodeError as exc:
|
|
400
|
-
msg =
|
|
401
|
+
msg = JSONDecodeErrorTranslator.translate(error=exc, line_number=6)
|
|
401
402
|
assert msg == error_msg
|
|
402
403
|
else:
|
|
403
404
|
assert res == expected_output
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from src.various_api_tools.translators.psycopg2 import (
|
|
2
|
+
BASE_PSYCOPG2_ERROR_MESSAGE,
|
|
3
|
+
ErrorData,
|
|
4
|
+
Psycopg2ErrorTranslator,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestPsycopg2ErrorTranslator:
|
|
9
|
+
def test_get_unique_violation_error(self):
|
|
10
|
+
error_msg = (
|
|
11
|
+
"DETAIL: Key (uuid)=(a6cc5730-2261-11ee-9c43-2eb5a363657c) already exists."
|
|
12
|
+
)
|
|
13
|
+
result = Psycopg2ErrorTranslator.get_unique_violation_error(error_msg)
|
|
14
|
+
assert isinstance(result, ErrorData)
|
|
15
|
+
assert result.key == "uuid"
|
|
16
|
+
assert result.value == "a6cc5730-2261-11ee-9c43-2eb5a363657c"
|
|
17
|
+
|
|
18
|
+
# No match case
|
|
19
|
+
error_msg = "No violation here."
|
|
20
|
+
result = Psycopg2ErrorTranslator.get_unique_violation_error(error_msg)
|
|
21
|
+
assert result is None
|
|
22
|
+
|
|
23
|
+
def test_get_check_violation_error(self):
|
|
24
|
+
translator = Psycopg2ErrorTranslator(
|
|
25
|
+
constraint_map={"valid_email": "Невалидный email"},
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
error_msg = 'violates check constraint "valid_email"'
|
|
29
|
+
result = translator.get_check_violation_error(error_msg)
|
|
30
|
+
assert isinstance(result, ErrorData)
|
|
31
|
+
assert result.key == "valid_email"
|
|
32
|
+
assert result.value == "Невалидный email"
|
|
33
|
+
|
|
34
|
+
# Unknown constraint
|
|
35
|
+
error_msg = 'violates check constraint "unknown_check"'
|
|
36
|
+
result = translator.get_check_violation_error(error_msg)
|
|
37
|
+
assert isinstance(result, ErrorData)
|
|
38
|
+
assert result.key == "unknown_check"
|
|
39
|
+
assert result.value == "Невалидная запись в БД"
|
|
40
|
+
|
|
41
|
+
# No match case
|
|
42
|
+
error_msg = "No violation here."
|
|
43
|
+
result = translator.get_check_violation_error(error_msg)
|
|
44
|
+
assert result is None
|
|
45
|
+
|
|
46
|
+
def test_translate_with_unique_violation(self):
|
|
47
|
+
class MockError(Exception):
|
|
48
|
+
def __init__(self):
|
|
49
|
+
self.pgcode = "23505"
|
|
50
|
+
self.pgerror = (
|
|
51
|
+
"DETAIL: Key (email)=(invalid@example.com) already exists."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
translator = Psycopg2ErrorTranslator()
|
|
55
|
+
result = translator.translate(MockError())
|
|
56
|
+
assert (
|
|
57
|
+
"БД уже содержит значение: ключ email, значение invalid@example.com."
|
|
58
|
+
in result
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def test_translate_with_check_violation(self):
|
|
62
|
+
class MockError(Exception):
|
|
63
|
+
def __init__(self):
|
|
64
|
+
self.pgcode = "23514"
|
|
65
|
+
self.pgerror = 'violates check constraint "valid_email"'
|
|
66
|
+
|
|
67
|
+
translator = Psycopg2ErrorTranslator(
|
|
68
|
+
constraint_map={"valid_email": "Невалидный email"},
|
|
69
|
+
)
|
|
70
|
+
result = translator.translate(MockError())
|
|
71
|
+
assert (
|
|
72
|
+
"Нарушено ограничение данных: ключ valid_email, значение Невалидный email."
|
|
73
|
+
in result
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def test_translate_with_unknown_code(self):
|
|
77
|
+
class MockError(Exception):
|
|
78
|
+
def __init__(self):
|
|
79
|
+
self.pgcode = "00000"
|
|
80
|
+
self.pgerror = ""
|
|
81
|
+
|
|
82
|
+
translator = Psycopg2ErrorTranslator()
|
|
83
|
+
result = translator.translate(MockError())
|
|
84
|
+
assert result == BASE_PSYCOPG2_ERROR_MESSAGE
|
|
85
|
+
|
|
86
|
+
def test_translate_with_custom_messages(self):
|
|
87
|
+
class MockError(Exception):
|
|
88
|
+
def __init__(self):
|
|
89
|
+
self.pgcode = "23503"
|
|
90
|
+
self.pgerror = ""
|
|
91
|
+
|
|
92
|
+
custom_code_map = {
|
|
93
|
+
23503: "Указан несуществующий внешний идентификатор",
|
|
94
|
+
}
|
|
95
|
+
translator = Psycopg2ErrorTranslator(code_map=custom_code_map)
|
|
96
|
+
result = translator.translate(MockError())
|
|
97
|
+
assert result == "Указан несуществующий внешний идентификатор."
|
|
@@ -4,7 +4,7 @@ from uuid import UUID
|
|
|
4
4
|
|
|
5
5
|
from pydantic import UUID5, BaseModel, ValidationError
|
|
6
6
|
|
|
7
|
-
from src.various_api_tools.translators.pydantic import
|
|
7
|
+
from src.various_api_tools.translators.pydantic import PydanticValidationErrorTranslator
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class TestValidationErrorTranslator:
|
|
@@ -17,7 +17,7 @@ class TestValidationErrorTranslator:
|
|
|
17
17
|
try:
|
|
18
18
|
Model(a="test", b=None)
|
|
19
19
|
except ValidationError as exc:
|
|
20
|
-
message =
|
|
20
|
+
message = PydanticValidationErrorTranslator.translate(exc.errors())
|
|
21
21
|
assert isinstance(message, str)
|
|
22
22
|
assert (
|
|
23
23
|
'Поле: "a" заполнено неверно: "\'test\'". Ошибка: "Невалидное значение для логического типа(bool)";'
|
|
@@ -42,7 +42,7 @@ class TestValidationErrorTranslator:
|
|
|
42
42
|
try:
|
|
43
43
|
Model(a=0.5, b="test", c="1" * 4_301, d=None)
|
|
44
44
|
except ValidationError as exc:
|
|
45
|
-
message =
|
|
45
|
+
message = PydanticValidationErrorTranslator.translate(exc.errors())
|
|
46
46
|
assert isinstance(message, str)
|
|
47
47
|
assert (
|
|
48
48
|
'Поле: "a" заполнено неверно: "0.5". Ошибка: "Невалидное значение для целочисленного числа(int)";'
|
|
@@ -72,7 +72,7 @@ class TestValidationErrorTranslator:
|
|
|
72
72
|
try:
|
|
73
73
|
Model(a=["1", "2"])
|
|
74
74
|
except ValidationError as exc:
|
|
75
|
-
message =
|
|
75
|
+
message = PydanticValidationErrorTranslator.translate(exc.errors())
|
|
76
76
|
assert isinstance(message, str)
|
|
77
77
|
assert (
|
|
78
78
|
'Поле: "a" заполнено неверно: "[\'1\', \'2\']". Ошибка: "Невалидное значение словаря";'
|
|
@@ -93,7 +93,7 @@ class TestValidationErrorTranslator:
|
|
|
93
93
|
try:
|
|
94
94
|
Model(a="other_option")
|
|
95
95
|
except ValidationError as exc:
|
|
96
|
-
message =
|
|
96
|
+
message = PydanticValidationErrorTranslator.translate(exc.errors())
|
|
97
97
|
assert isinstance(message, str)
|
|
98
98
|
assert (
|
|
99
99
|
'Поле: "a" заполнено неверно: "\'other_option\'". Ошибка: "Невалидное значение Enum";'
|
|
@@ -110,7 +110,7 @@ class TestValidationErrorTranslator:
|
|
|
110
110
|
try:
|
|
111
111
|
Model(a="test", b=None)
|
|
112
112
|
except ValidationError as exc:
|
|
113
|
-
message =
|
|
113
|
+
message = PydanticValidationErrorTranslator.translate(exc.errors())
|
|
114
114
|
assert isinstance(message, str)
|
|
115
115
|
assert (
|
|
116
116
|
'Поле: "a" заполнено неверно: "\'test\'". Ошибка: "Невалидное значение числа с плавающей точкой(float)";'
|
|
@@ -136,7 +136,7 @@ class TestValidationErrorTranslator:
|
|
|
136
136
|
c="a6cc5730-2261-11ee-9c43-2eb5a363657c",
|
|
137
137
|
)
|
|
138
138
|
except ValidationError as exc:
|
|
139
|
-
message =
|
|
139
|
+
message = PydanticValidationErrorTranslator.translate(exc.errors())
|
|
140
140
|
assert isinstance(message, str)
|
|
141
141
|
assert (
|
|
142
142
|
'Поле: "a" заполнено неверно: "\'12345678-124-1234-1234-567812345678\'". Ошибка: "Невалидное значение для UUID";'
|
|
@@ -160,7 +160,7 @@ class TestValidationErrorTranslator:
|
|
|
160
160
|
try:
|
|
161
161
|
Model(a=1)
|
|
162
162
|
except ValidationError as exc:
|
|
163
|
-
message =
|
|
163
|
+
message = PydanticValidationErrorTranslator.translate(exc.errors())
|
|
164
164
|
assert isinstance(message, str)
|
|
165
165
|
assert (
|
|
166
166
|
'Поле: "a" заполнено неверно: "1". Ошибка: "Невалидное строковое значение(str)";'
|
|
@@ -176,7 +176,7 @@ class TestValidationErrorTranslator:
|
|
|
176
176
|
try:
|
|
177
177
|
Model(a=1)
|
|
178
178
|
except ValidationError as exc:
|
|
179
|
-
message =
|
|
179
|
+
message = PydanticValidationErrorTranslator.translate(exc.errors())
|
|
180
180
|
assert isinstance(message, str)
|
|
181
181
|
assert (
|
|
182
182
|
'Поле: "a" заполнено неверно: "1". Ошибка: "Невалидное значение списка";'
|
|
@@ -192,7 +192,7 @@ class TestValidationErrorTranslator:
|
|
|
192
192
|
try:
|
|
193
193
|
Model(a=None)
|
|
194
194
|
except ValidationError as exc:
|
|
195
|
-
message =
|
|
195
|
+
message = PydanticValidationErrorTranslator.translate(exc.errors())
|
|
196
196
|
assert isinstance(message, str)
|
|
197
197
|
assert (
|
|
198
198
|
'Поле: "a" заполнено неверно: "None". Ошибка: "Невалидное значение даты(date)";'
|
|
@@ -210,7 +210,7 @@ class TestValidationErrorTranslator:
|
|
|
210
210
|
# there is no 13th month
|
|
211
211
|
Model(a="2023-13-01", b=None)
|
|
212
212
|
except ValidationError as exc:
|
|
213
|
-
message =
|
|
213
|
+
message = PydanticValidationErrorTranslator.translate(exc.errors())
|
|
214
214
|
assert isinstance(message, str)
|
|
215
215
|
assert (
|
|
216
216
|
'Поле: "a" заполнено неверно: "\'2023-13-01\'". Ошибка: "Невалидное значение даты и времени(datetime)";'
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
"""A package for various API utility tools.
|
|
2
|
-
|
|
3
|
-
Including JSON and Pydantic error translators.
|
|
4
|
-
"""
|
|
5
|
-
from .translators.json import DecodeErrorTranslator
|
|
6
|
-
from .translators.pydantic import ValidationErrorTranslator
|
|
7
|
-
|
|
8
|
-
__all__ = (
|
|
9
|
-
"DecodeErrorTranslator",
|
|
10
|
-
"ValidationErrorTranslator",
|
|
11
|
-
)
|
|
File without changes
|
{various_api_tools-0.2.1 → various_api_tools-0.3.0}/src/various_api_tools/translators/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|