various-api-tools 0.2.1__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.
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.1
2
+ Name: various-api-tools
3
+ Version: 0.2.1
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
+ Author-Email: dkurchigin <kurchigin.dmitry@yandex.ru>
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python
9
+ Classifier: Typing :: Typed
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Project-URL: Homepage, https://gitverse.ru/dkurchigin/various-api-tools
16
+ Project-URL: Documentation, https://various-api-tools.dkurchigin.ru/
17
+ Project-URL: Source, https://gitverse.ru/dkurchigin/various-api-tools
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: pydantic>=2.11.7
20
+ Description-Content-Type: text/markdown
21
+
22
+ # various_api_tools
23
+
24
+ A lightweight utility package for common API-related tasks in Python, including JSON and Pydantic error translators that provide user-friendly Russian messages.
25
+
26
+ ---
27
+
28
+ ## 📦 Features
29
+
30
+ - ✅ **JSON Error Translator** – Translates `JSONDecodeError` into clear Russian messages.
31
+ - ✅ **Pydantic Validation Error Translator** – Converts Pydantic validation errors into human-readable Russian strings.
32
+ - ✅ **Easy to integrate** – Designed for use in web APIs and data validation pipelines.
33
+
34
+ ---
35
+
36
+ ## 🐍 Installation
37
+
38
+ Install using pip from source or a private repository:
39
+
40
+ ```bash
41
+ pip install various_api_tools
42
+ ```
43
+
44
+ ## 🧪 Basic Usage
45
+
46
+ ### Translate JSON Decode Errors
47
+
48
+ ```python
49
+ import json
50
+ from various_api_tools.translators.json import DecodeErrorTranslator
51
+
52
+ try:
53
+ json.loads('{"name": "Alice",}')
54
+ except json.JSONDecodeError as e:
55
+ print(DecodeErrorTranslator.translate(e))
56
+
57
+ # Output:
58
+ # Ошибка конвертации в формате JSON.
59
+ # Позиция: 16.
60
+ # Описание: не правильно используются двойные кавычки.
61
+ ```
62
+
63
+ ---
64
+
65
+ ### Translate Pydantic Validation Errors
66
+
67
+ ```python
68
+ from pydantic import BaseModel, ValidationError
69
+ from various_api_tools.translators.pydantic import ValidationErrorTranslator
70
+
71
+ class User(BaseModel):
72
+ email: str
73
+
74
+ try:
75
+ User(email=123)
76
+ except ValidationError as e:
77
+ print(ValidationErrorTranslator.translate(e.errors()))
78
+
79
+ # Output:
80
+ # Поле: "email". Ошибка: "Невалидное строковое значение(str)";
81
+ ```
82
+
83
+ ---
84
+
85
+ ## 📄 License
86
+
87
+ MIT License — feel free to use it in any project! 🎉
88
+
89
+ ---
90
+
91
+ ## 🧑‍💻 Made with ❤️ by [@dkurchigin](https://gitverse.ru/dkurchigin)
92
+
93
+ Have questions? Open an issue or contribute to the repo!
94
+
95
+
96
+ ## 🐙 GITVERSE
97
+
98
+
99
+ 🔗 [https://gitverse.ru/dkurchigin/various-api-tools](https://gitverse.ru/dkurchigin/various-api-tools)
@@ -0,0 +1,78 @@
1
+ # various_api_tools
2
+
3
+ A lightweight utility package for common API-related tasks in Python, including JSON and Pydantic error translators that provide user-friendly Russian messages.
4
+
5
+ ---
6
+
7
+ ## 📦 Features
8
+
9
+ - ✅ **JSON Error Translator** – Translates `JSONDecodeError` into clear Russian messages.
10
+ - ✅ **Pydantic Validation Error Translator** – Converts Pydantic validation errors into human-readable Russian strings.
11
+ - ✅ **Easy to integrate** – Designed for use in web APIs and data validation pipelines.
12
+
13
+ ---
14
+
15
+ ## 🐍 Installation
16
+
17
+ Install using pip from source or a private repository:
18
+
19
+ ```bash
20
+ pip install various_api_tools
21
+ ```
22
+
23
+ ## 🧪 Basic Usage
24
+
25
+ ### Translate JSON Decode Errors
26
+
27
+ ```python
28
+ import json
29
+ from various_api_tools.translators.json import DecodeErrorTranslator
30
+
31
+ try:
32
+ json.loads('{"name": "Alice",}')
33
+ except json.JSONDecodeError as e:
34
+ print(DecodeErrorTranslator.translate(e))
35
+
36
+ # Output:
37
+ # Ошибка конвертации в формате JSON.
38
+ # Позиция: 16.
39
+ # Описание: не правильно используются двойные кавычки.
40
+ ```
41
+
42
+ ---
43
+
44
+ ### Translate Pydantic Validation Errors
45
+
46
+ ```python
47
+ from pydantic import BaseModel, ValidationError
48
+ from various_api_tools.translators.pydantic import ValidationErrorTranslator
49
+
50
+ class User(BaseModel):
51
+ email: str
52
+
53
+ try:
54
+ User(email=123)
55
+ except ValidationError as e:
56
+ print(ValidationErrorTranslator.translate(e.errors()))
57
+
58
+ # Output:
59
+ # Поле: "email". Ошибка: "Невалидное строковое значение(str)";
60
+ ```
61
+
62
+ ---
63
+
64
+ ## 📄 License
65
+
66
+ MIT License — feel free to use it in any project! 🎉
67
+
68
+ ---
69
+
70
+ ## 🧑‍💻 Made with ❤️ by [@dkurchigin](https://gitverse.ru/dkurchigin)
71
+
72
+ Have questions? Open an issue or contribute to the repo!
73
+
74
+
75
+ ## 🐙 GITVERSE
76
+
77
+
78
+ 🔗 [https://gitverse.ru/dkurchigin/various-api-tools](https://gitverse.ru/dkurchigin/various-api-tools)
@@ -0,0 +1,172 @@
1
+ [project]
2
+ name = "various-api-tools"
3
+ version = "0.2.1"
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
+ authors = [
6
+ { name = "dkurchigin", email = "kurchigin.dmitry@yandex.ru" },
7
+ ]
8
+ dependencies = [
9
+ "pydantic>=2.11.7",
10
+ ]
11
+ requires-python = ">=3.10"
12
+ readme = "README.md"
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python",
16
+ "Typing :: Typed",
17
+ "Programming Language :: Python :: 3 :: Only",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ ]
23
+
24
+ [project.license]
25
+ text = "MIT"
26
+
27
+ [project.urls]
28
+ Homepage = "https://gitverse.ru/dkurchigin/various-api-tools"
29
+ Documentation = "https://various-api-tools.dkurchigin.ru/"
30
+ Source = "https://gitverse.ru/dkurchigin/various-api-tools"
31
+
32
+ [build-system]
33
+ requires = [
34
+ "pdm-backend",
35
+ ]
36
+ build-backend = "pdm.backend"
37
+
38
+ [tool.pdm]
39
+ distribution = true
40
+
41
+ [tool.pdm.scripts]
42
+ test = "pytest --cov-report term-missing --cov=src tests/"
43
+ lint = "ruff check --fix"
44
+ format = "ruff format"
45
+
46
+ [tool.ruff]
47
+ exclude = [
48
+ ".bzr",
49
+ ".direnv",
50
+ ".eggs",
51
+ ".git",
52
+ ".git-rewrite",
53
+ ".hg",
54
+ ".ipynb_checkpoints",
55
+ ".mypy_cache",
56
+ ".nox",
57
+ ".pants.d",
58
+ ".pyenv",
59
+ ".pytest_cache",
60
+ ".pytype",
61
+ ".ruff_cache",
62
+ ".svn",
63
+ ".tox",
64
+ ".venv",
65
+ ".vscode",
66
+ "__pypackages__",
67
+ "_build",
68
+ "buck-out",
69
+ "build",
70
+ "dist",
71
+ "node_modules",
72
+ "site-packages",
73
+ "venv",
74
+ ]
75
+ line-length = 88
76
+ indent-width = 4
77
+ target-version = "py312"
78
+
79
+ [tool.ruff.lint]
80
+ select = [
81
+ "ANN",
82
+ "ASYNC",
83
+ "BLE",
84
+ "FBT",
85
+ "S",
86
+ "B",
87
+ "A",
88
+ "COM",
89
+ "C4",
90
+ "DTZ",
91
+ "EM",
92
+ "EXE",
93
+ "FIX",
94
+ "FA",
95
+ "INT",
96
+ "ISC",
97
+ "ICN",
98
+ "LOG",
99
+ "G",
100
+ "INP",
101
+ "PIE",
102
+ "T20",
103
+ "PYI",
104
+ "PT",
105
+ "Q",
106
+ "RSE",
107
+ "RET",
108
+ "SLF",
109
+ "SIM",
110
+ "SLOT",
111
+ "TID",
112
+ "TD",
113
+ "TC",
114
+ "ARG",
115
+ "PTH",
116
+ "FLY",
117
+ "I",
118
+ "C90",
119
+ "N",
120
+ "PERF",
121
+ "E",
122
+ "W",
123
+ "DOC",
124
+ "D",
125
+ "F",
126
+ "PL",
127
+ "UP",
128
+ "RUF",
129
+ "TRY",
130
+ ]
131
+ ignore = []
132
+ fixable = [
133
+ "ALL",
134
+ ]
135
+ unfixable = []
136
+ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
137
+
138
+ [tool.ruff.lint.per-file-ignores]
139
+ "tests/*" = [
140
+ "D",
141
+ "RUF",
142
+ "S",
143
+ "E",
144
+ "ANN",
145
+ "EM",
146
+ "TRY",
147
+ "PT",
148
+ ]
149
+ "src/various_api_tools/translators/pydantic.py" = [
150
+ "RUF001",
151
+ ]
152
+
153
+ [tool.ruff.format]
154
+ quote-style = "double"
155
+ indent-style = "space"
156
+ skip-magic-trailing-comma = false
157
+ line-ending = "auto"
158
+ docstring-code-format = false
159
+ docstring-code-line-length = "dynamic"
160
+
161
+ [dependency-groups]
162
+ test = [
163
+ "pytest>=8.4.1",
164
+ "pytest-cov>=6.2.1",
165
+ ]
166
+ lint = [
167
+ "ruff>=0.12.11",
168
+ ]
169
+ doc = [
170
+ "mkdocs>=1.6.1",
171
+ "mkdocstrings-python>=1.18.2",
172
+ ]
@@ -0,0 +1,11 @@
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
+ )
@@ -0,0 +1,5 @@
1
+ """Module for importing and exporting JSON and Pydantic error translators.
2
+
3
+ This module provides access to two utility classes that translate common
4
+ validation and parsing errors into user-friendly Russian messages.
5
+ """
@@ -0,0 +1,80 @@
1
+ """Module for translating JSON decoding errors into user-friendly Russian messages.
2
+
3
+ This module provides a utility class that converts Python's `JSONDecodeError`
4
+ exceptions into more descriptive and readable error messages in Russian.
5
+ It is useful for improving the end-user experience when parsing JSON data.
6
+
7
+ """
8
+
9
+ from json import JSONDecodeError
10
+ from typing import Final
11
+
12
+ BASE_JSON_ERROR_MESSAGE: Final[str] = "Ошибка конвертации в формате JSON.\n"
13
+
14
+
15
+ class DecodeErrorTranslator:
16
+ """Translates JSONDecodeError messages into user-friendly Russian descriptions.
17
+
18
+ This class provides a method to convert Python's JSONDecodeError exceptions
19
+ into more readable error messages for end users.
20
+
21
+ """
22
+
23
+ @classmethod
24
+ def translate(
25
+ cls,
26
+ error: JSONDecodeError,
27
+ *,
28
+ msg: str = BASE_JSON_ERROR_MESSAGE,
29
+ line_number: int | None = None,
30
+ ) -> str:
31
+ """Translate a JSONDecodeError into a human-readable message in Russian.
32
+
33
+ Args:
34
+ error: The JSONDecodeError instance to translate.
35
+ msg: Optional base message to prepend. Defaults to BASE_JSON_ERROR_MESSAGE.
36
+ line_number: Optional line number for better context.
37
+
38
+ Returns:
39
+ A string containing the translated error message(position and description).
40
+
41
+ Raises:
42
+ TypeError: If 'error' is not an instance of JSONDecodeError.
43
+
44
+ Example:
45
+ ```python
46
+ try:
47
+ json.loads('{"name": "Alice",}')
48
+ except JSONDecodeError as e:
49
+ print(DecodeErrorTranslator.translate(e))
50
+
51
+ #> Ошибка конвертации в формате JSON.
52
+ #> Позиция: 16.
53
+ #> Описание: не правильно используются двойные кавычки.
54
+ ```
55
+
56
+ """
57
+ if line_number is not None:
58
+ msg += f"Строка: {line_number}, позиция {error.pos}.\n"
59
+ else:
60
+ msg += f"Позиция: {error.pos}.\n"
61
+ exc_msg = error.msg
62
+
63
+ if "Expecting" in exc_msg and "delimiter" in exc_msg:
64
+ msg += "Описание: ожидается разделитель."
65
+ elif "Expecting property name enclosed in double quotes" in exc_msg:
66
+ msg += "Описание: не правильно используются двойные кавычки."
67
+ elif "Expecting value" in exc_msg:
68
+ msg += "Описание: ожидается значение."
69
+ elif "Extra data" in exc_msg:
70
+ msg += "Описание: обнаружены дополнительные данные."
71
+ elif "Invalid" in exc_msg and "escape" in exc_msg:
72
+ msg += "Описание: обнаружены недопустимые экранирующие символы."
73
+ elif "Invalid control character at" in exc_msg:
74
+ msg += "Описание: обнаружен неэкранированный контрольный символ."
75
+ elif "Unterminated string starting at" in exc_msg:
76
+ msg += "Описание: не правильно заверена закрывающая кавычка."
77
+ else:
78
+ msg += "Описание: неизвестная ошибка."
79
+
80
+ return msg
@@ -0,0 +1,130 @@
1
+ """Module for translating Pydantic validation errors into Russian messages.
2
+
3
+ This module provides a utility class to convert Pydantic error details into
4
+ descriptive error messages in Russian, suitable for end-user feedback.
5
+
6
+ """
7
+
8
+ from collections.abc import Sequence
9
+ from typing import Final
10
+
11
+ from pydantic_core import ErrorDetails
12
+
13
+ pydantic_error_types: dict[str, str] = {
14
+ "missing": "Не заполнено обязательное поле",
15
+ "uuid_parsing": "Невалидное значение для UUID",
16
+ "uuid_type": "Невалидное значение для UUID",
17
+ "uuid_version": "Невалидное значение для UUID",
18
+ "bool_parsing": "Невалидное значение для логического типа(bool)",
19
+ "bool_type": "Невалидное значение для логического типа(bool)",
20
+ "date_type": "Невалидное значение даты(date)",
21
+ "datetime_from_date_parsing": "Невалидное значение даты и времени(datetime)",
22
+ "datetime_type": "Невалидное значение даты и времени(datetime)",
23
+ "dict_type": "Невалидное значение словаря",
24
+ "list_type": "Невалидное значение списка",
25
+ "string_type": "Невалидное строковое значение(str)",
26
+ "enum": "Невалидное значение Enum",
27
+ "float_parsing": "Невалидное значение числа с плавающей точкой(float)",
28
+ "float_type": "Невалидное значение числа с плавающей точкой(float)",
29
+ "int_from_float": "Невалидное значение для целочисленного числа(int)",
30
+ "int_parsing": "Невалидное значение для целочисленного числа(int)",
31
+ "int_parsing_size": "Невалидное значение для целочисленного числа(int)",
32
+ "int_type": "Невалидное значение для целочисленного числа(int)",
33
+ "non_negative_int": "Невалидное значение для целочисленного числа(int), "
34
+ "должно быть больше или равно 0",
35
+ "incorrect_latitude": "Некорректное значение широты, должно быть больше, "
36
+ "чем -90.0 и меньше, чем 90.0",
37
+ "incorrect_longitude": "Некорректное значение долготы, должно быть больше, "
38
+ "чем -180.0 и меньше, чем 180.0",
39
+ "string_too_short": "Cтрока слишком короткая",
40
+ "ip_address": "Невалидное значение адреса IPv4/IPv6",
41
+ "incorrect_email": "Невалидное значение email-адреса",
42
+ "list_expected": "Некорректный тип данных. Ожидается список.",
43
+ }
44
+
45
+ DEFAULT_LOCATION_PREFIX: Final[str] = "Поле"
46
+ UNKNOWN_ERROR_TYPE: Final[str] = "Неизвестная ошибка"
47
+
48
+
49
+ class ValidationErrorTranslator:
50
+ """Translate Pydantic validation errors into human-readable Russian messages.
51
+
52
+ This class provides a method to translate a sequence of Pydantic error details
53
+ into a formatted string with location, type and input information.
54
+
55
+ """
56
+
57
+ @classmethod
58
+ def get_str_pydantic_loc(cls, loc: tuple[str]) -> str:
59
+ """Convert a Pydantic location tuple into a dot-separated string.
60
+
61
+ Args:
62
+ loc: A tuple representing the field path in the model.
63
+
64
+ Returns:
65
+ A string representation of the field path.
66
+
67
+ Example:
68
+ ```python
69
+ print(ValidationErrorTranslator.get_str_pydantic_loc(("user", "name")))
70
+ #> "user.name"
71
+ ```
72
+
73
+ """
74
+ return ".".join([str(loc_value) for loc_value in loc])
75
+
76
+ @classmethod
77
+ def translate(cls, errors: Sequence[ErrorDetails]) -> str:
78
+ """Translate a list of Pydantic error details into a human-readable message.
79
+
80
+ Args:
81
+ errors: A sequence of Pydantic ErrorDetails objects.
82
+
83
+ Returns:
84
+ A formatted string containing all translated errors.
85
+
86
+ Example:
87
+ ```python
88
+ class Model(BaseModel):
89
+ email: str
90
+
91
+ try:
92
+ Model(email=1)
93
+ except ValidationError as exc:
94
+ print(ValidationErrorTranslator.translate(exc.errors())
95
+
96
+ #> Поле: "email". Ошибка: "Невалидное строковое значение(str)";
97
+ ```
98
+
99
+ """
100
+ formatted_errors: list[str] = []
101
+ for error in errors:
102
+ error_str: str = ""
103
+
104
+ location_part: str = ""
105
+ type_part: str = ""
106
+ input_part: str = ""
107
+
108
+ if "loc" in error:
109
+ prefix: str = DEFAULT_LOCATION_PREFIX
110
+ if error["loc"][0] == "query":
111
+ prefix = "Параметр запроса"
112
+ location_part = (
113
+ f'{prefix}: "{cls.get_str_pydantic_loc(loc=error["loc"])}"'
114
+ )
115
+
116
+ if "type" in error:
117
+ msg = pydantic_error_types.get(error["type"], UNKNOWN_ERROR_TYPE)
118
+ type_part = f'Ошибка: "{msg}"'
119
+
120
+ if "input" in error and pydantic_error_types["missing"] not in type_part:
121
+ input_part = f'заполнено неверно: "{error["input"]!r}"'
122
+
123
+ if input_part != "":
124
+ error_str = f"{location_part} {input_part}. {type_part};"
125
+ else:
126
+ error_str = f"{location_part}. {type_part};"
127
+
128
+ formatted_errors.append(error_str)
129
+
130
+ return "\n".join(formatted_errors)
File without changes
File without changes
@@ -0,0 +1,403 @@
1
+ import json
2
+ import sys
3
+
4
+ import pytest
5
+
6
+ from src.various_api_tools.translators.json import DecodeErrorTranslator
7
+
8
+
9
+ class TestDecodeErrorTranslator:
10
+ @pytest.mark.skipif(sys.version_info >= (3, 13), reason="not for Python 3.13")
11
+ @pytest.mark.parametrize(
12
+ "input_data, expected_output, error_msg",
13
+ [
14
+ # Ошибка: ожидается разделитель (Expecting delimiter)
15
+ (
16
+ "[1, 2 3]",
17
+ None,
18
+ "Ошибка конвертации в формате JSON.\n"
19
+ "Позиция: 6.\n"
20
+ "Описание: ожидается разделитель.",
21
+ ),
22
+ (
23
+ '{"name": "Alice",}',
24
+ None,
25
+ "Ошибка конвертации в формате JSON.\n"
26
+ "Позиция: 17.\n"
27
+ "Описание: не правильно используются двойные кавычки.",
28
+ ),
29
+ (
30
+ '[{"id": 1}',
31
+ None,
32
+ "Ошибка конвертации в формате JSON.\n"
33
+ "Позиция: 10.\n"
34
+ "Описание: ожидается разделитель.",
35
+ ),
36
+ # Ошибка: не правильно используются двойные кавычки
37
+ (
38
+ '{"name": "Alice"',
39
+ None,
40
+ "Ошибка конвертации в формате JSON.\n"
41
+ "Позиция: 16.\n"
42
+ "Описание: ожидается разделитель.",
43
+ ),
44
+ # Ошибка: ожидается значение
45
+ (
46
+ "",
47
+ None,
48
+ "Ошибка конвертации в формате JSON.\n"
49
+ "Позиция: 0.\n"
50
+ "Описание: ожидается значение.",
51
+ ),
52
+ (
53
+ '{"id": }',
54
+ None,
55
+ "Ошибка конвертации в формате JSON.\n"
56
+ "Позиция: 7.\n"
57
+ "Описание: ожидается значение.",
58
+ ),
59
+ # Ошибка: обнаружены дополнительные данные
60
+ (
61
+ '\n{"name": "Alice"}\n\n{"name": "Bob"}\n',
62
+ None,
63
+ "Ошибка конвертации в формате JSON.\n"
64
+ "Позиция: 20.\n"
65
+ "Описание: обнаружены дополнительные данные.",
66
+ ),
67
+ # Ошибка: недопустимые экранирующие символы
68
+ (
69
+ '{"text": "Hello\x00World"}',
70
+ None,
71
+ "Ошибка конвертации в формате JSON.\n"
72
+ "Позиция: 15.\n"
73
+ "Описание: обнаружен неэкранированный контрольный символ.",
74
+ ),
75
+ # Ошибка: незакрытая строка
76
+ (
77
+ '{"name": "Alice"',
78
+ None,
79
+ "Ошибка конвертации в формате JSON.\n"
80
+ "Позиция: 16.\n"
81
+ "Описание: ожидается разделитель.",
82
+ ),
83
+ # Корректный JSON
84
+ (
85
+ '[{"name": "Alice"}, {"name": "Bob"}]',
86
+ [{"name": "Alice"}, {"name": "Bob"}],
87
+ None,
88
+ ),
89
+ # Корректный JSON
90
+ (
91
+ '{"name": "Alice"}',
92
+ {"name": "Alice"},
93
+ None,
94
+ ),
95
+ ],
96
+ )
97
+ def test_translate_without_line_number(
98
+ self,
99
+ input_data,
100
+ expected_output,
101
+ error_msg,
102
+ ):
103
+ try:
104
+ res = json.loads(input_data)
105
+ except json.JSONDecodeError as exc:
106
+ msg = DecodeErrorTranslator.translate(error=exc)
107
+ assert msg == error_msg
108
+ else:
109
+ assert res == expected_output
110
+
111
+ @pytest.mark.skipif(sys.version_info >= (3, 13), reason="not for Python 3.13")
112
+ @pytest.mark.parametrize(
113
+ "input_data, expected_output, error_msg",
114
+ [
115
+ # Ошибка: ожидается разделитель (Expecting delimiter)
116
+ (
117
+ "[1, 2 3]",
118
+ None,
119
+ "Ошибка конвертации в формате JSON.\n"
120
+ "Строка: 6, позиция 6.\n"
121
+ "Описание: ожидается разделитель.",
122
+ ),
123
+ (
124
+ '{"name": "Alice",}',
125
+ None,
126
+ "Ошибка конвертации в формате JSON.\n"
127
+ "Строка: 6, позиция 17.\n"
128
+ "Описание: не правильно используются двойные кавычки.",
129
+ ),
130
+ (
131
+ '[{"id": 1}',
132
+ None,
133
+ "Ошибка конвертации в формате JSON.\n"
134
+ "Строка: 6, позиция 10.\n"
135
+ "Описание: ожидается разделитель.",
136
+ ),
137
+ # Ошибка: не правильно используются двойные кавычки
138
+ (
139
+ '{"name": "Alice"',
140
+ None,
141
+ "Ошибка конвертации в формате JSON.\n"
142
+ "Строка: 6, позиция 16.\n"
143
+ "Описание: ожидается разделитель.",
144
+ ),
145
+ # Ошибка: ожидается значение
146
+ (
147
+ "",
148
+ None,
149
+ "Ошибка конвертации в формате JSON.\n"
150
+ "Строка: 6, позиция 0.\n"
151
+ "Описание: ожидается значение.",
152
+ ),
153
+ (
154
+ '{"id": }',
155
+ None,
156
+ "Ошибка конвертации в формате JSON.\n"
157
+ "Строка: 6, позиция 7.\n"
158
+ "Описание: ожидается значение.",
159
+ ),
160
+ # Ошибка: обнаружены дополнительные данные
161
+ (
162
+ '\n{"name": "Alice"}\n\n{"name": "Bob"}\n',
163
+ None,
164
+ "Ошибка конвертации в формате JSON.\n"
165
+ "Строка: 6, позиция 20.\n"
166
+ "Описание: обнаружены дополнительные данные.",
167
+ ),
168
+ # Ошибка: недопустимые экранирующие символы
169
+ (
170
+ '{"text": "Hello\x00World"}',
171
+ None,
172
+ "Ошибка конвертации в формате JSON.\n"
173
+ "Строка: 6, позиция 15.\n"
174
+ "Описание: обнаружен неэкранированный контрольный символ.",
175
+ ),
176
+ # Ошибка: незакрытая строка
177
+ (
178
+ '{"name": "Alice"',
179
+ None,
180
+ "Ошибка конвертации в формате JSON.\n"
181
+ "Строка: 6, позиция 16.\n"
182
+ "Описание: ожидается разделитель.",
183
+ ),
184
+ # Корректный JSON
185
+ (
186
+ '[{"name": "Alice"}, {"name": "Bob"}]',
187
+ [{"name": "Alice"}, {"name": "Bob"}],
188
+ None,
189
+ ),
190
+ # Корректный JSON
191
+ (
192
+ '{"name": "Alice"}',
193
+ {"name": "Alice"},
194
+ None,
195
+ ),
196
+ ],
197
+ )
198
+ def test_translate_with_line_number(self, input_data, expected_output, error_msg):
199
+ try:
200
+ res = json.loads(input_data)
201
+ except json.JSONDecodeError as exc:
202
+ msg = DecodeErrorTranslator.translate(error=exc, line_number=6)
203
+ assert msg == error_msg
204
+ else:
205
+ assert res == expected_output
206
+
207
+ class TestDecodeErrorTranslatorForPython313:
208
+ @pytest.mark.skipif(sys.version_info < (3, 13), reason="for Python 3.13")
209
+ @pytest.mark.parametrize(
210
+ "input_data, expected_output, error_msg",
211
+ [
212
+ # Ошибка: ожидается разделитель (Expecting delimiter)
213
+ (
214
+ "[1, 2 3]",
215
+ None,
216
+ "Ошибка конвертации в формате JSON.\n"
217
+ "Позиция: 6.\n"
218
+ "Описание: ожидается разделитель.",
219
+ ),
220
+ (
221
+ '{"name": "Alice",}',
222
+ None,
223
+ "Ошибка конвертации в формате JSON.\n"
224
+ "Позиция: 16.\n"
225
+ "Описание: неизвестная ошибка.",
226
+ ),
227
+ (
228
+ '[{"id": 1}',
229
+ None,
230
+ "Ошибка конвертации в формате JSON.\n"
231
+ "Позиция: 10.\n"
232
+ "Описание: ожидается разделитель.",
233
+ ),
234
+ # Ошибка: не правильно используются двойные кавычки
235
+ (
236
+ '{"name": "Alice"',
237
+ None,
238
+ "Ошибка конвертации в формате JSON.\n"
239
+ "Позиция: 16.\n"
240
+ "Описание: ожидается разделитель.",
241
+ ),
242
+ # Ошибка: ожидается значение
243
+ (
244
+ "",
245
+ None,
246
+ "Ошибка конвертации в формате JSON.\n"
247
+ "Позиция: 0.\n"
248
+ "Описание: ожидается значение.",
249
+ ),
250
+ (
251
+ '{"id": }',
252
+ None,
253
+ "Ошибка конвертации в формате JSON.\n"
254
+ "Позиция: 7.\n"
255
+ "Описание: ожидается значение.",
256
+ ),
257
+ # Ошибка: обнаружены дополнительные данные
258
+ (
259
+ '\n{"name": "Alice"}\n\n{"name": "Bob"}\n',
260
+ None,
261
+ "Ошибка конвертации в формате JSON.\n"
262
+ "Позиция: 20.\n"
263
+ "Описание: обнаружены дополнительные данные.",
264
+ ),
265
+ # Ошибка: недопустимые экранирующие символы
266
+ (
267
+ '{"text": "Hello\x00World"}',
268
+ None,
269
+ "Ошибка конвертации в формате JSON.\n"
270
+ "Позиция: 15.\n"
271
+ "Описание: обнаружен неэкранированный контрольный символ.",
272
+ ),
273
+ # Ошибка: незакрытая строка
274
+ (
275
+ '{"name": "Alice"',
276
+ None,
277
+ "Ошибка конвертации в формате JSON.\n"
278
+ "Позиция: 16.\n"
279
+ "Описание: ожидается разделитель.",
280
+ ),
281
+ # Корректный JSON
282
+ (
283
+ '[{"name": "Alice"}, {"name": "Bob"}]',
284
+ [{"name": "Alice"}, {"name": "Bob"}],
285
+ None,
286
+ ),
287
+ # Корректный JSON
288
+ (
289
+ '{"name": "Alice"}',
290
+ {"name": "Alice"},
291
+ None,
292
+ ),
293
+ ],
294
+ )
295
+ def test_translate_without_line_number(
296
+ self,
297
+ input_data,
298
+ expected_output,
299
+ error_msg,
300
+ ):
301
+ try:
302
+ res = json.loads(input_data)
303
+ except json.JSONDecodeError as exc:
304
+ msg = DecodeErrorTranslator.translate(error=exc)
305
+ assert msg == error_msg
306
+ else:
307
+ assert res == expected_output
308
+
309
+ @pytest.mark.skipif(sys.version_info < (3, 13), reason="for Python 3.13")
310
+ @pytest.mark.parametrize(
311
+ "input_data, expected_output, error_msg",
312
+ [
313
+ # Ошибка: ожидается разделитель (Expecting delimiter)
314
+ (
315
+ "[1, 2 3]",
316
+ None,
317
+ "Ошибка конвертации в формате JSON.\n"
318
+ "Строка: 6, позиция 6.\n"
319
+ "Описание: ожидается разделитель.",
320
+ ),
321
+ (
322
+ '{"name": "Alice",}',
323
+ None,
324
+ "Ошибка конвертации в формате JSON.\n"
325
+ "Строка: 6, позиция 16.\n"
326
+ "Описание: неизвестная ошибка.",
327
+ ),
328
+ (
329
+ '[{"id": 1}',
330
+ None,
331
+ "Ошибка конвертации в формате JSON.\n"
332
+ "Строка: 6, позиция 10.\n"
333
+ "Описание: ожидается разделитель.",
334
+ ),
335
+ # Ошибка: не правильно используются двойные кавычки
336
+ (
337
+ '{"name": "Alice"',
338
+ None,
339
+ "Ошибка конвертации в формате JSON.\n"
340
+ "Строка: 6, позиция 16.\n"
341
+ "Описание: ожидается разделитель.",
342
+ ),
343
+ # Ошибка: ожидается значение
344
+ (
345
+ "",
346
+ None,
347
+ "Ошибка конвертации в формате JSON.\n"
348
+ "Строка: 6, позиция 0.\n"
349
+ "Описание: ожидается значение.",
350
+ ),
351
+ (
352
+ '{"id": }',
353
+ None,
354
+ "Ошибка конвертации в формате JSON.\n"
355
+ "Строка: 6, позиция 7.\n"
356
+ "Описание: ожидается значение.",
357
+ ),
358
+ # Ошибка: обнаружены дополнительные данные
359
+ (
360
+ '\n{"name": "Alice"}\n\n{"name": "Bob"}\n',
361
+ None,
362
+ "Ошибка конвертации в формате JSON.\n"
363
+ "Строка: 6, позиция 20.\n"
364
+ "Описание: обнаружены дополнительные данные.",
365
+ ),
366
+ # Ошибка: недопустимые экранирующие символы
367
+ (
368
+ '{"text": "Hello\x00World"}',
369
+ None,
370
+ "Ошибка конвертации в формате JSON.\n"
371
+ "Строка: 6, позиция 15.\n"
372
+ "Описание: обнаружен неэкранированный контрольный символ.",
373
+ ),
374
+ # Ошибка: незакрытая строка
375
+ (
376
+ '{"name": "Alice"',
377
+ None,
378
+ "Ошибка конвертации в формате JSON.\n"
379
+ "Строка: 6, позиция 16.\n"
380
+ "Описание: ожидается разделитель.",
381
+ ),
382
+ # Корректный JSON
383
+ (
384
+ '[{"name": "Alice"}, {"name": "Bob"}]',
385
+ [{"name": "Alice"}, {"name": "Bob"}],
386
+ None,
387
+ ),
388
+ # Корректный JSON
389
+ (
390
+ '{"name": "Alice"}',
391
+ {"name": "Alice"},
392
+ None,
393
+ ),
394
+ ],
395
+ )
396
+ def test_translate_with_line_number(self, input_data, expected_output, error_msg):
397
+ try:
398
+ res = json.loads(input_data)
399
+ except json.JSONDecodeError as exc:
400
+ msg = DecodeErrorTranslator.translate(error=exc, line_number=6)
401
+ assert msg == error_msg
402
+ else:
403
+ assert res == expected_output
@@ -0,0 +1,223 @@
1
+ from datetime import date, datetime
2
+ from enum import Enum
3
+ from uuid import UUID
4
+
5
+ from pydantic import UUID5, BaseModel, ValidationError
6
+
7
+ from src.various_api_tools.translators.pydantic import ValidationErrorTranslator
8
+
9
+
10
+ class TestValidationErrorTranslator:
11
+ def test_bool_errors(self):
12
+ class Model(BaseModel):
13
+ a: bool
14
+ b: bool
15
+ c: bool
16
+
17
+ try:
18
+ Model(a="test", b=None)
19
+ except ValidationError as exc:
20
+ message = ValidationErrorTranslator.translate(exc.errors())
21
+ assert isinstance(message, str)
22
+ assert (
23
+ 'Поле: "a" заполнено неверно: "\'test\'". Ошибка: "Невалидное значение для логического типа(bool)";'
24
+ in message
25
+ )
26
+ assert (
27
+ 'Поле: "b" заполнено неверно: "None". Ошибка: "Невалидное значение для логического типа(bool)";'
28
+ in message
29
+ )
30
+ assert 'Поле: "c". Ошибка: "Не заполнено обязательное поле";' in message
31
+ else:
32
+ raise AssertionError("Тест должен был обработать ValidationError")
33
+
34
+ def test_int_errors(self):
35
+ class Model(BaseModel):
36
+ a: int
37
+ b: int
38
+ c: int
39
+ d: int
40
+ e: int
41
+
42
+ try:
43
+ Model(a=0.5, b="test", c="1" * 4_301, d=None)
44
+ except ValidationError as exc:
45
+ message = ValidationErrorTranslator.translate(exc.errors())
46
+ assert isinstance(message, str)
47
+ assert (
48
+ 'Поле: "a" заполнено неверно: "0.5". Ошибка: "Невалидное значение для целочисленного числа(int)";'
49
+ in message
50
+ )
51
+ assert (
52
+ 'Поле: "b" заполнено неверно: "\'test\'". Ошибка: "Невалидное значение для целочисленного числа(int)";'
53
+ in message
54
+ )
55
+ assert (
56
+ f'Поле: "c" заполнено неверно: "\'{"1" * 4_301}\'". Ошибка: "Невалидное значение для целочисленного числа(int)";'
57
+ in message
58
+ )
59
+ assert (
60
+ 'Поле: "d" заполнено неверно: "None". Ошибка: "Невалидное значение для целочисленного числа(int)";'
61
+ in message
62
+ )
63
+ assert 'Поле: "e". Ошибка: "Не заполнено обязательное поле";' in message
64
+ else:
65
+ raise AssertionError("Тест должен был обработать ValidationError")
66
+
67
+ def test_dict_errors(self):
68
+ class Model(BaseModel):
69
+ a: dict
70
+ b: dict
71
+
72
+ try:
73
+ Model(a=["1", "2"])
74
+ except ValidationError as exc:
75
+ message = ValidationErrorTranslator.translate(exc.errors())
76
+ assert isinstance(message, str)
77
+ assert (
78
+ 'Поле: "a" заполнено неверно: "[\'1\', \'2\']". Ошибка: "Невалидное значение словаря";'
79
+ in message
80
+ )
81
+ assert 'Поле: "b". Ошибка: "Не заполнено обязательное поле";' in message
82
+ else:
83
+ raise AssertionError("Тест должен был обработать ValidationError")
84
+
85
+ def test_enum_errors(self):
86
+ class MyEnum(str, Enum):
87
+ option = "option"
88
+
89
+ class Model(BaseModel):
90
+ a: MyEnum
91
+ b: MyEnum
92
+
93
+ try:
94
+ Model(a="other_option")
95
+ except ValidationError as exc:
96
+ message = ValidationErrorTranslator.translate(exc.errors())
97
+ assert isinstance(message, str)
98
+ assert (
99
+ 'Поле: "a" заполнено неверно: "\'other_option\'". Ошибка: "Невалидное значение Enum";'
100
+ in message
101
+ )
102
+ assert 'Поле: "b". Ошибка: "Не заполнено обязательное поле";' in message
103
+
104
+ def test_float_errors(self):
105
+ class Model(BaseModel):
106
+ a: float
107
+ b: float
108
+ c: float
109
+
110
+ try:
111
+ Model(a="test", b=None)
112
+ except ValidationError as exc:
113
+ message = ValidationErrorTranslator.translate(exc.errors())
114
+ assert isinstance(message, str)
115
+ assert (
116
+ 'Поле: "a" заполнено неверно: "\'test\'". Ошибка: "Невалидное значение числа с плавающей точкой(float)";'
117
+ in message
118
+ )
119
+ assert (
120
+ 'Поле: "b" заполнено неверно: "None". Ошибка: "Невалидное значение числа с плавающей точкой(float)";'
121
+ in message
122
+ )
123
+ assert 'Поле: "c". Ошибка: "Не заполнено обязательное поле"' in message
124
+
125
+ def test_uuid_errors(self):
126
+ class Model(BaseModel):
127
+ a: UUID
128
+ b: UUID
129
+ c: UUID5
130
+ d: UUID
131
+
132
+ try:
133
+ Model(
134
+ a="12345678-124-1234-1234-567812345678",
135
+ b=1234567812412341234567812345678,
136
+ c="a6cc5730-2261-11ee-9c43-2eb5a363657c",
137
+ )
138
+ except ValidationError as exc:
139
+ message = ValidationErrorTranslator.translate(exc.errors())
140
+ assert isinstance(message, str)
141
+ assert (
142
+ 'Поле: "a" заполнено неверно: "\'12345678-124-1234-1234-567812345678\'". Ошибка: "Невалидное значение для UUID";'
143
+ in message
144
+ )
145
+ assert (
146
+ 'Поле: "b" заполнено неверно: "1234567812412341234567812345678". Ошибка: "Невалидное значение для UUID";'
147
+ in message
148
+ )
149
+ assert (
150
+ 'Поле: "c" заполнено неверно: "\'a6cc5730-2261-11ee-9c43-2eb5a363657c\'". Ошибка: "Невалидное значение для UUID";'
151
+ in message
152
+ )
153
+ assert 'Поле: "d". Ошибка: "Не заполнено обязательное поле";' in message
154
+
155
+ def test_str_errors(self):
156
+ class Model(BaseModel):
157
+ a: str
158
+ b: str
159
+
160
+ try:
161
+ Model(a=1)
162
+ except ValidationError as exc:
163
+ message = ValidationErrorTranslator.translate(exc.errors())
164
+ assert isinstance(message, str)
165
+ assert (
166
+ 'Поле: "a" заполнено неверно: "1". Ошибка: "Невалидное строковое значение(str)";'
167
+ in message
168
+ )
169
+ assert 'Поле: "b". Ошибка: "Не заполнено обязательное поле";' in message
170
+
171
+ def test_list_errors(self):
172
+ class Model(BaseModel):
173
+ a: list[int]
174
+ b: list[int]
175
+
176
+ try:
177
+ Model(a=1)
178
+ except ValidationError as exc:
179
+ message = ValidationErrorTranslator.translate(exc.errors())
180
+ assert isinstance(message, str)
181
+ assert (
182
+ 'Поле: "a" заполнено неверно: "1". Ошибка: "Невалидное значение списка";'
183
+ in message
184
+ )
185
+ assert 'Поле: "b". Ошибка: "Не заполнено обязательное поле";' in message
186
+
187
+ def test_date_errors(self):
188
+ class Model(BaseModel):
189
+ a: date
190
+ b: date
191
+
192
+ try:
193
+ Model(a=None)
194
+ except ValidationError as exc:
195
+ message = ValidationErrorTranslator.translate(exc.errors())
196
+ assert isinstance(message, str)
197
+ assert (
198
+ 'Поле: "a" заполнено неверно: "None". Ошибка: "Невалидное значение даты(date)";'
199
+ in message
200
+ )
201
+ assert 'Поле: "b". Ошибка: "Не заполнено обязательное поле";' in message
202
+
203
+ def test_datetime_errors(self):
204
+ class Model(BaseModel):
205
+ a: datetime
206
+ b: datetime
207
+ c: datetime
208
+
209
+ try:
210
+ # there is no 13th month
211
+ Model(a="2023-13-01", b=None)
212
+ except ValidationError as exc:
213
+ message = ValidationErrorTranslator.translate(exc.errors())
214
+ assert isinstance(message, str)
215
+ assert (
216
+ 'Поле: "a" заполнено неверно: "\'2023-13-01\'". Ошибка: "Невалидное значение даты и времени(datetime)";'
217
+ in message
218
+ )
219
+ assert (
220
+ 'Поле: "b" заполнено неверно: "None". Ошибка: "Невалидное значение даты и времени(datetime)";'
221
+ in message
222
+ )
223
+ assert 'Поле: "c". Ошибка: "Не заполнено обязательное поле";' in message