various-api-tools 0.2.1__tar.gz → 0.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: various-api-tools
3
- Version: 0.2.1
3
+ Version: 0.3.1
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.2.1"
3
+ version = "0.3.1"
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
+ )
@@ -12,7 +12,7 @@ from typing import Final
12
12
  BASE_JSON_ERROR_MESSAGE: Final[str] = "Ошибка конвертации в формате JSON.\n"
13
13
 
14
14
 
15
- class DecodeErrorTranslator:
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
@@ -46,7 +46,7 @@ DEFAULT_LOCATION_PREFIX: Final[str] = "Поле"
46
46
  UNKNOWN_ERROR_TYPE: Final[str] = "Неизвестная ошибка"
47
47
 
48
48
 
49
- class ValidationErrorTranslator:
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 DecodeErrorTranslator
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 = DecodeErrorTranslator.translate(error=exc)
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 = DecodeErrorTranslator.translate(error=exc, line_number=6)
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
- "[1, 2 3]",
215
- None,
216
- "Ошибка конвертации в формате JSON.\n"
217
- "Позиция: 6.\n"
218
- "Описание: ожидается разделитель.",
215
+ "[1, 2 3]",
216
+ None,
217
+ "Ошибка конвертации в формате JSON.\n"
218
+ "Позиция: 6.\n"
219
+ "Описание: ожидается разделитель.",
219
220
  ),
220
221
  (
221
- '{"name": "Alice",}',
222
- None,
223
- "Ошибка конвертации в формате JSON.\n"
224
- "Позиция: 16.\n"
225
- "Описание: неизвестная ошибка.",
222
+ '{"name": "Alice",}',
223
+ None,
224
+ "Ошибка конвертации в формате JSON.\n"
225
+ "Позиция: 16.\n"
226
+ "Описание: неизвестная ошибка.",
226
227
  ),
227
228
  (
228
- '[{"id": 1}',
229
- None,
230
- "Ошибка конвертации в формате JSON.\n"
231
- "Позиция: 10.\n"
232
- "Описание: ожидается разделитель.",
229
+ '[{"id": 1}',
230
+ None,
231
+ "Ошибка конвертации в формате JSON.\n"
232
+ "Позиция: 10.\n"
233
+ "Описание: ожидается разделитель.",
233
234
  ),
234
235
  # Ошибка: не правильно используются двойные кавычки
235
236
  (
236
- '{"name": "Alice"',
237
- None,
238
- "Ошибка конвертации в формате JSON.\n"
239
- "Позиция: 16.\n"
240
- "Описание: ожидается разделитель.",
237
+ '{"name": "Alice"',
238
+ None,
239
+ "Ошибка конвертации в формате JSON.\n"
240
+ "Позиция: 16.\n"
241
+ "Описание: ожидается разделитель.",
241
242
  ),
242
243
  # Ошибка: ожидается значение
243
244
  (
244
- "",
245
- None,
246
- "Ошибка конвертации в формате JSON.\n"
247
- "Позиция: 0.\n"
248
- "Описание: ожидается значение.",
245
+ "",
246
+ None,
247
+ "Ошибка конвертации в формате JSON.\n"
248
+ "Позиция: 0.\n"
249
+ "Описание: ожидается значение.",
249
250
  ),
250
251
  (
251
- '{"id": }',
252
- None,
253
- "Ошибка конвертации в формате JSON.\n"
254
- "Позиция: 7.\n"
255
- "Описание: ожидается значение.",
252
+ '{"id": }',
253
+ None,
254
+ "Ошибка конвертации в формате JSON.\n"
255
+ "Позиция: 7.\n"
256
+ "Описание: ожидается значение.",
256
257
  ),
257
258
  # Ошибка: обнаружены дополнительные данные
258
259
  (
259
- '\n{"name": "Alice"}\n\n{"name": "Bob"}\n',
260
- None,
261
- "Ошибка конвертации в формате JSON.\n"
262
- "Позиция: 20.\n"
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
- '{"text": "Hello\x00World"}',
268
- None,
269
- "Ошибка конвертации в формате JSON.\n"
270
- "Позиция: 15.\n"
271
- "Описание: обнаружен неэкранированный контрольный символ.",
268
+ '{"text": "Hello\x00World"}',
269
+ None,
270
+ "Ошибка конвертации в формате JSON.\n"
271
+ "Позиция: 15.\n"
272
+ "Описание: обнаружен неэкранированный контрольный символ.",
272
273
  ),
273
274
  # Ошибка: незакрытая строка
274
275
  (
275
- '{"name": "Alice"',
276
- None,
277
- "Ошибка конвертации в формате JSON.\n"
278
- "Позиция: 16.\n"
279
- "Описание: ожидается разделитель.",
276
+ '{"name": "Alice"',
277
+ None,
278
+ "Ошибка конвертации в формате JSON.\n"
279
+ "Позиция: 16.\n"
280
+ "Описание: ожидается разделитель.",
280
281
  ),
281
282
  # Корректный JSON
282
283
  (
283
- '[{"name": "Alice"}, {"name": "Bob"}]',
284
- [{"name": "Alice"}, {"name": "Bob"}],
285
- None,
284
+ '[{"name": "Alice"}, {"name": "Bob"}]',
285
+ [{"name": "Alice"}, {"name": "Bob"}],
286
+ None,
286
287
  ),
287
288
  # Корректный JSON
288
289
  (
289
- '{"name": "Alice"}',
290
- {"name": "Alice"},
291
- None,
290
+ '{"name": "Alice"}',
291
+ {"name": "Alice"},
292
+ None,
292
293
  ),
293
294
  ],
294
295
  )
295
296
  def test_translate_without_line_number(
296
- self,
297
- input_data,
298
- expected_output,
299
- error_msg,
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 = DecodeErrorTranslator.translate(error=exc)
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
- "[1, 2 3]",
316
- None,
317
- "Ошибка конвертации в формате JSON.\n"
318
- "Строка: 6, позиция 6.\n"
319
- "Описание: ожидается разделитель.",
316
+ "[1, 2 3]",
317
+ None,
318
+ "Ошибка конвертации в формате JSON.\n"
319
+ "Строка: 6, позиция 6.\n"
320
+ "Описание: ожидается разделитель.",
320
321
  ),
321
322
  (
322
- '{"name": "Alice",}',
323
- None,
324
- "Ошибка конвертации в формате JSON.\n"
325
- "Строка: 6, позиция 16.\n"
326
- "Описание: неизвестная ошибка.",
323
+ '{"name": "Alice",}',
324
+ None,
325
+ "Ошибка конвертации в формате JSON.\n"
326
+ "Строка: 6, позиция 16.\n"
327
+ "Описание: неизвестная ошибка.",
327
328
  ),
328
329
  (
329
- '[{"id": 1}',
330
- None,
331
- "Ошибка конвертации в формате JSON.\n"
332
- "Строка: 6, позиция 10.\n"
333
- "Описание: ожидается разделитель.",
330
+ '[{"id": 1}',
331
+ None,
332
+ "Ошибка конвертации в формате JSON.\n"
333
+ "Строка: 6, позиция 10.\n"
334
+ "Описание: ожидается разделитель.",
334
335
  ),
335
336
  # Ошибка: не правильно используются двойные кавычки
336
337
  (
337
- '{"name": "Alice"',
338
- None,
339
- "Ошибка конвертации в формате JSON.\n"
340
- "Строка: 6, позиция 16.\n"
341
- "Описание: ожидается разделитель.",
338
+ '{"name": "Alice"',
339
+ None,
340
+ "Ошибка конвертации в формате JSON.\n"
341
+ "Строка: 6, позиция 16.\n"
342
+ "Описание: ожидается разделитель.",
342
343
  ),
343
344
  # Ошибка: ожидается значение
344
345
  (
345
- "",
346
- None,
347
- "Ошибка конвертации в формате JSON.\n"
348
- "Строка: 6, позиция 0.\n"
349
- "Описание: ожидается значение.",
346
+ "",
347
+ None,
348
+ "Ошибка конвертации в формате JSON.\n"
349
+ "Строка: 6, позиция 0.\n"
350
+ "Описание: ожидается значение.",
350
351
  ),
351
352
  (
352
- '{"id": }',
353
- None,
354
- "Ошибка конвертации в формате JSON.\n"
355
- "Строка: 6, позиция 7.\n"
356
- "Описание: ожидается значение.",
353
+ '{"id": }',
354
+ None,
355
+ "Ошибка конвертации в формате JSON.\n"
356
+ "Строка: 6, позиция 7.\n"
357
+ "Описание: ожидается значение.",
357
358
  ),
358
359
  # Ошибка: обнаружены дополнительные данные
359
360
  (
360
- '\n{"name": "Alice"}\n\n{"name": "Bob"}\n',
361
- None,
362
- "Ошибка конвертации в формате JSON.\n"
363
- "Строка: 6, позиция 20.\n"
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
- '{"text": "Hello\x00World"}',
369
- None,
370
- "Ошибка конвертации в формате JSON.\n"
371
- "Строка: 6, позиция 15.\n"
372
- "Описание: обнаружен неэкранированный контрольный символ.",
369
+ '{"text": "Hello\x00World"}',
370
+ None,
371
+ "Ошибка конвертации в формате JSON.\n"
372
+ "Строка: 6, позиция 15.\n"
373
+ "Описание: обнаружен неэкранированный контрольный символ.",
373
374
  ),
374
375
  # Ошибка: незакрытая строка
375
376
  (
376
- '{"name": "Alice"',
377
- None,
378
- "Ошибка конвертации в формате JSON.\n"
379
- "Строка: 6, позиция 16.\n"
380
- "Описание: ожидается разделитель.",
377
+ '{"name": "Alice"',
378
+ None,
379
+ "Ошибка конвертации в формате JSON.\n"
380
+ "Строка: 6, позиция 16.\n"
381
+ "Описание: ожидается разделитель.",
381
382
  ),
382
383
  # Корректный JSON
383
384
  (
384
- '[{"name": "Alice"}, {"name": "Bob"}]',
385
- [{"name": "Alice"}, {"name": "Bob"}],
386
- None,
385
+ '[{"name": "Alice"}, {"name": "Bob"}]',
386
+ [{"name": "Alice"}, {"name": "Bob"}],
387
+ None,
387
388
  ),
388
389
  # Корректный JSON
389
390
  (
390
- '{"name": "Alice"}',
391
- {"name": "Alice"},
392
- None,
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 = DecodeErrorTranslator.translate(error=exc, line_number=6)
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 ValidationErrorTranslator
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 = ValidationErrorTranslator.translate(exc.errors())
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 = ValidationErrorTranslator.translate(exc.errors())
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 = ValidationErrorTranslator.translate(exc.errors())
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 = ValidationErrorTranslator.translate(exc.errors())
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 = ValidationErrorTranslator.translate(exc.errors())
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 = ValidationErrorTranslator.translate(exc.errors())
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 = ValidationErrorTranslator.translate(exc.errors())
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 = ValidationErrorTranslator.translate(exc.errors())
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 = ValidationErrorTranslator.translate(exc.errors())
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 = ValidationErrorTranslator.translate(exc.errors())
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
- )