various-api-tools 1.0.0__tar.gz → 2.0.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.
Files changed (25) hide show
  1. {various_api_tools-1.0.0 → various_api_tools-2.0.0}/PKG-INFO +1 -1
  2. {various_api_tools-1.0.0 → various_api_tools-2.0.0}/pyproject.toml +2 -1
  3. various_api_tools-2.0.0/src/various_api_tools/translators/psycopg/base.py +256 -0
  4. {various_api_tools-1.0.0 → various_api_tools-2.0.0}/src/various_api_tools/translators/psycopg/constants.py +17 -0
  5. {various_api_tools-1.0.0 → various_api_tools-2.0.0}/tests/translators/test_psycopg.py +32 -19
  6. {various_api_tools-1.0.0 → various_api_tools-2.0.0}/tests/translators/test_psycopg2.py +34 -21
  7. various_api_tools-1.0.0/src/various_api_tools/translators/psycopg/base.py +0 -229
  8. {various_api_tools-1.0.0 → various_api_tools-2.0.0}/README.md +0 -0
  9. {various_api_tools-1.0.0 → various_api_tools-2.0.0}/src/various_api_tools/__init__.py +0 -0
  10. {various_api_tools-1.0.0 → various_api_tools-2.0.0}/src/various_api_tools/translators/__init__.py +0 -0
  11. {various_api_tools-1.0.0 → various_api_tools-2.0.0}/src/various_api_tools/translators/json.py +0 -0
  12. {various_api_tools-1.0.0 → various_api_tools-2.0.0}/src/various_api_tools/translators/psycopg/__init__.py +0 -0
  13. {various_api_tools-1.0.0 → various_api_tools-2.0.0}/src/various_api_tools/translators/psycopg/psycopg.py +0 -0
  14. {various_api_tools-1.0.0 → various_api_tools-2.0.0}/src/various_api_tools/translators/psycopg/psycopg2.py +0 -0
  15. {various_api_tools-1.0.0 → various_api_tools-2.0.0}/src/various_api_tools/translators/pydantic.py +0 -0
  16. {various_api_tools-1.0.0 → various_api_tools-2.0.0}/src/various_api_tools/validators/__init__.py +0 -0
  17. {various_api_tools-1.0.0 → various_api_tools-2.0.0}/src/various_api_tools/validators/pydantic/__init__.py +0 -0
  18. {various_api_tools-1.0.0 → various_api_tools-2.0.0}/src/various_api_tools/validators/pydantic/constants.py +0 -0
  19. {various_api_tools-1.0.0 → various_api_tools-2.0.0}/src/various_api_tools/validators/pydantic/utils.py +0 -0
  20. {various_api_tools-1.0.0 → various_api_tools-2.0.0}/tests/__init__.py +0 -0
  21. {various_api_tools-1.0.0 → various_api_tools-2.0.0}/tests/translators/__init__.py +0 -0
  22. {various_api_tools-1.0.0 → various_api_tools-2.0.0}/tests/translators/test_json.py +0 -0
  23. {various_api_tools-1.0.0 → various_api_tools-2.0.0}/tests/translators/test_pydantic.py +0 -0
  24. {various_api_tools-1.0.0 → various_api_tools-2.0.0}/tests/validators/__init__.py +0 -0
  25. {various_api_tools-1.0.0 → various_api_tools-2.0.0}/tests/validators/test_pydantic.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: various-api-tools
3
- Version: 1.0.0
3
+ Version: 2.0.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "various-api-tools"
3
- version = "1.0.0"
3
+ version = "2.0.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" },
@@ -160,6 +160,7 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
160
160
  ]
161
161
  "src/various_api_tools/translators/psycopg/base.py" = [
162
162
  "ANN401",
163
+ "RUF012",
163
164
  ]
164
165
  "src/various_api_tools/translators/psycopg/psycopg.py" = [
165
166
  "ANN401",
@@ -0,0 +1,256 @@
1
+ """Abstract base class for translating psycopg/psycopg2 database errors.
2
+
3
+ This module provides a foundation for parsing PostgreSQL error
4
+ details (such as `pgcode` and `pgerror`) from psycopg exceptions and converting
5
+ them into structured, human-readable error descriptions.
6
+ It supports both `psycopg2` and `psycopg3` by abstracting access to error attributes,
7
+ allowing concrete implementations to handle version-specific details.
8
+ """
9
+
10
+ import re
11
+ from typing import Any
12
+
13
+ from .constants import (
14
+ CHECK_VIOLATION_PATTERN,
15
+ DEFAULT_CODE_MAP,
16
+ UNIQUE_VIOLATION_PATTERN,
17
+ UNKNOWN_CHECK_DESCRIPTION,
18
+ UNKNOWN_CODE,
19
+ UNKNOWN_USER_RAISE_DESCRIPTION,
20
+ )
21
+
22
+
23
+ class BasePsycopgTranslator:
24
+ """Base translator for turning psycopg errors into user-friendly messages.
25
+
26
+ Supports customization via:
27
+ - `code_map`: maps SQLSTATE codes to human-readable messages
28
+ - `check_map`: maps check constraint names to meaningful descriptions
29
+ - `user_map`: maps database column names to user-friendly field names
30
+
31
+ Subclasses must implement `_get_pgcode()` and `_get_pgerror()` to support specific
32
+ psycopg versions (e.g., psycopg2 vs. psycopg3).
33
+
34
+ Attributes:
35
+ code_map (dict): Mapping from SQLSTATE codes (str) to error messages (str).
36
+ check_map (dict): Mapping from check constraint names (str)
37
+ to descriptions (str).
38
+ user_map (dict): Mapping from DB column/field names (str)
39
+ to user-friendly names (str).
40
+
41
+ """
42
+
43
+ code_map: dict
44
+ check_map: dict
45
+ user_map: dict
46
+
47
+ check_codes: list[str] = ["23514"]
48
+ unique_codes: list[str] = ["23505", "23503"]
49
+ user_codes: list[str] = ["P0001"]
50
+
51
+ def __init__(
52
+ self,
53
+ *,
54
+ code_map: dict | None = None,
55
+ check_map: dict | None = None,
56
+ user_map: dict | None = None,
57
+ ) -> None:
58
+ """Initialize the translator with optional custom message mappings.
59
+
60
+ Args:
61
+ code_map: Optional mapping from SQLSTATE codes (e.g. '23505') to
62
+ error messages. If None, defaults to `DEFAULT_CODE_MAP`.
63
+ check_map: Optional mapping from check constraint names to
64
+ descriptive messages. If None, defaults to an empty dict.
65
+ user_map: Optional mapping from database column names to
66
+ user-friendly field names.
67
+ Used to improve readability in error messages.
68
+ Example: {"email": "Email пользователя", "phone": "Номер телефона"}
69
+ If None, defaults to an empty dict.
70
+
71
+ Example:
72
+ ```python
73
+ custom_codes = {"23505": "Значение уже существует"}
74
+ custom_checks = {"age_check": "Возраст должен быть от 18 до 120"}
75
+ t = BasePsycopgTranslator(code_map=custom_codes, check_map=custom_checks)
76
+ ```
77
+
78
+ """
79
+ self.code_map = code_map if code_map is not None else DEFAULT_CODE_MAP
80
+ self.check_map = check_map if check_map is not None else {}
81
+ self.user_map = user_map if user_map is not None else {}
82
+
83
+ def translate(
84
+ self,
85
+ error: Any,
86
+ ) -> str:
87
+ """Translate a psycopg database error into a user-friendly message.
88
+
89
+ Extracts `pgcode` and `pgerror` from the error and formats a meaningful response
90
+ based on the type of constraint violation (unique, check, foreign key).
91
+
92
+ Args:
93
+ error: A psycopg exception (e.g. IntegrityError, DatabaseError).
94
+
95
+ Returns:
96
+ A formatted error message, or a fallback if no match is found.
97
+
98
+ Example:
99
+ ```python
100
+ # Given error: unique_violation on email = 'test@example.com'
101
+ print(translator.translate(error))
102
+ #> "БД уже содержит значение: ключ email, значение test@example.com"
103
+ ```
104
+
105
+ """
106
+ msg: str = UNKNOWN_CODE
107
+
108
+ pgcode = self._get_pgcode(error)
109
+ pgerror = self._get_pgerror(error)
110
+
111
+ if pgcode is not None:
112
+ code_msg: str | None = self.code_map.get(pgcode, UNKNOWN_CODE)
113
+
114
+ if pgcode in self.check_codes:
115
+ msg = self._translate_check_violation(
116
+ code_msg=code_msg,
117
+ error_msg=pgerror,
118
+ )
119
+ elif pgcode in self.unique_codes:
120
+ msg = self._translate_unique_violation(
121
+ code_msg=code_msg,
122
+ error_msg=pgerror,
123
+ )
124
+ elif pgcode in self.user_codes:
125
+ msg = self._translate_user_raise(code_msg=code_msg, error_msg=pgerror)
126
+
127
+ return msg
128
+
129
+ def _get_pgcode(self, error: Any) -> str | None:
130
+ """Extract pgcode code from the error.
131
+
132
+ Must be implemented by subclasses to support specific psycopg versions.
133
+
134
+ Args:
135
+ error: The raw database exception.
136
+
137
+ Returns:
138
+ The SQLSTATE code as a string (e.g. '23505'), or None if not available.
139
+
140
+ """
141
+ raise NotImplementedError
142
+
143
+ def _get_pgerror(self, error: Any) -> str | None:
144
+ """Extract the full error message from the exception.
145
+
146
+ Must be implemented by subclasses to support specific psycopg versions.
147
+
148
+ Args:
149
+ error: The raw database exception.
150
+
151
+ Returns:
152
+ Full error message string, or None if not available.
153
+
154
+ """
155
+ raise NotImplementedError
156
+
157
+ @classmethod
158
+ def _translate_unique_violation(cls, code_msg: str, error_msg: str) -> str:
159
+ """Extract details from a unique constraint violation and format a message.
160
+
161
+ Parses the PostgreSQL error message to extract the violated field and
162
+ its conflicting value using a predefined regex pattern.
163
+
164
+ Args:
165
+ code_msg: Base message from code_map for this error type.
166
+ error_msg: Full PostgreSQL error text.
167
+
168
+ Returns:
169
+ Formatted message with field name and value if found;
170
+ otherwise returns the base code_msg.
171
+
172
+ Example:
173
+ For error_msg = "DETAIL: Key (email)=(user@example.com) already exists",
174
+ returns: "БД уже содержит значение: ключ email, значение user@example.com"
175
+
176
+ """
177
+ msg = code_msg
178
+
179
+ pattern = re.compile(UNIQUE_VIOLATION_PATTERN)
180
+ matched = pattern.search(error_msg)
181
+
182
+ if matched is not None:
183
+ constraint = matched.group(2)
184
+ description = matched.group(3)
185
+ if constraint and description:
186
+ msg = f"{code_msg}: ключ {constraint}, значение {description}"
187
+
188
+ return msg
189
+
190
+ def _translate_check_violation(self, code_msg: str, error_msg: str) -> str:
191
+ """Extract details from a check constraint violation and return a message.
192
+
193
+ Looks up the violated constraint in `check_map` for a custom description.
194
+ Falls back to `UNKNOWN_CHECK_DESCRIPTION` if not found.
195
+
196
+ Args:
197
+ code_msg: Base message from code_map for this error type.
198
+ error_msg: Full PostgreSQL error text.
199
+
200
+ Returns:
201
+ Formatted message with constraint name and its description if available;
202
+ otherwise returns the base code_msg.
203
+
204
+ Example:
205
+ If constraint 'age_check' is violated and check_map provides a description,
206
+ returns: "Нарушено ограничение данных: ключ age_check.
207
+ Описание: Возраст должен быть от 18 до 120"
208
+
209
+ """
210
+ msg = code_msg
211
+
212
+ pattern = re.compile(CHECK_VIOLATION_PATTERN)
213
+ matched = pattern.search(error_msg)
214
+ if matched is not None:
215
+ constraint = matched.group(1)
216
+ description = self.check_map.get(
217
+ constraint,
218
+ UNKNOWN_CHECK_DESCRIPTION,
219
+ )
220
+
221
+ if constraint and description:
222
+ msg = f"{code_msg}: ключ {constraint}. Описание: {description}"
223
+
224
+ return msg
225
+
226
+ def _translate_user_raise(self, code_msg: str, error_msg: str) -> str:
227
+ """Extract details from a user-raised exception and return a message.
228
+
229
+ Searches the error message for known field keys defined in `user_map`.
230
+ Returns a user-friendly description if a match is found; otherwise falls back
231
+ to `UNKNOWN_USER_RAISE_DESCRIPTION`.
232
+
233
+ Args:
234
+ code_msg: Base message from code_map for this error type.
235
+ error_msg: Full PostgreSQL error text from RAISE EXCEPTION.
236
+
237
+ Returns:
238
+ Formatted message with a friendly field description if found;
239
+ otherwise returns the base code_msg with a generic description.
240
+
241
+ Example:
242
+ If error_msg contains 'status' and user_map={"status": "Статус"},
243
+ returns: "Ошибка БД: Статус"
244
+
245
+ If no match is found:
246
+ returns: "Ошибка БД: Невозможно выполнить операцию"
247
+
248
+ """
249
+ description = UNKNOWN_USER_RAISE_DESCRIPTION
250
+
251
+ for key in self.user_map:
252
+ if key in error_msg:
253
+ description = self.user_map.get(key)
254
+ break
255
+
256
+ return f"{code_msg}: {description}"
@@ -48,7 +48,23 @@ is configured.
48
48
  Used as the default value when a constraint name is not found in `check_map`.
49
49
  Displayed to users when the system cannot provide more specific validation feedback."""
50
50
 
51
+ UNKNOWN_USER_RAISE_DESCRIPTION: Final[str] = "Невозможно выполнить операцию"
52
+ """Default message shown when a custom database RAISE EXCEPTION is caught,
53
+ but no matching field or rule is found in `user_map`.
54
+
55
+ Used as a fallback in `BasePsycopgTranslator._translate_user_raise`
56
+ to avoid exposing raw internal messages to end users."""
57
+
58
+ USER_RAISE_KEY: Final[str] = "Ошибка БД"
59
+ """Default display key used in error messages generated from RAISE EXCEPTION.
60
+
61
+ Used as a generic identifier when formatting user-facing errors from custom
62
+ database raises, especially when no specific field mapping is available.
63
+
64
+ Example: '{Ошибка БД}: {описание}'"""
65
+
51
66
  DEFAULT_CODE_MAP: Final[dict[str, str]] = {
67
+ "P0001": "Ошибка БД",
52
68
  "23503": "Указан несуществующий идентификатор связанного объекта",
53
69
  "23505": "БД уже содержит значение",
54
70
  "23514": "Нарушено ограничение данных",
@@ -56,6 +72,7 @@ DEFAULT_CODE_MAP: Final[dict[str, str]] = {
56
72
  """Default mapping of PostgreSQL SQLSTATE codes to user-friendly Russian error messages.
57
73
 
58
74
  Each key is a standardized SQLSTATE code:
75
+ - "P0001" (raise_exception): Custom exception raised in PL/pgSQL
59
76
  - "23503" (foreign_key_violation): Referenced object does not exist
60
77
  - "23505" (unique_violation): Duplicate key value
61
78
  - "23514" (check_violation): Data fails check constraint
@@ -1,4 +1,3 @@
1
- from various_api_tools.translators.psycopg.base import ErrorData
2
1
  from various_api_tools.translators.psycopg.psycopg import PsycopgErrorTranslator
3
2
 
4
3
 
@@ -7,14 +6,21 @@ class TestPsycopgErrorTranslator:
7
6
  error_msg = (
8
7
  "DETAIL: Key (uuid)=(a6cc5730-2261-11ee-9c43-2eb5a363657c) already exists."
9
8
  )
10
- result = PsycopgErrorTranslator._translate_unique_violation(error_msg)
11
- assert isinstance(result, ErrorData)
12
- assert result.key == "uuid"
13
- assert result.value == "a6cc5730-2261-11ee-9c43-2eb5a363657c"
9
+ result = PsycopgErrorTranslator._translate_unique_violation(
10
+ code_msg="Ошибка БД",
11
+ error_msg=error_msg,
12
+ )
13
+ assert (
14
+ result
15
+ == "Ошибка БД: ключ uuid, значение a6cc5730-2261-11ee-9c43-2eb5a363657c"
16
+ )
14
17
 
15
18
  error_msg = "No violation here."
16
- result = PsycopgErrorTranslator._translate_unique_violation(error_msg)
17
- assert result is None
19
+ result = PsycopgErrorTranslator._translate_unique_violation(
20
+ code_msg="Ошибка БД",
21
+ error_msg=error_msg,
22
+ )
23
+ assert result == "Ошибка БД"
18
24
 
19
25
  def test_translate_check_violation(self):
20
26
  translator = PsycopgErrorTranslator(
@@ -22,20 +28,27 @@ class TestPsycopgErrorTranslator:
22
28
  )
23
29
 
24
30
  error_msg = 'violates check constraint "valid_email"'
25
- result = translator._translate_check_violation(error_msg)
26
- assert isinstance(result, ErrorData)
27
- assert result.key == "valid_email"
28
- assert result.value == "Невалидный email"
31
+ result = translator._translate_check_violation(
32
+ code_msg="Ошибка БД",
33
+ error_msg=error_msg,
34
+ )
35
+ assert result == "Ошибка БД: ключ valid_email. Описание: Невалидный email"
29
36
 
30
37
  error_msg = 'violates check constraint "unknown_check"'
31
- result = translator._translate_check_violation(error_msg)
32
- assert isinstance(result, ErrorData)
33
- assert result.key == "unknown_check"
34
- assert result.value == "Невалидная запись в БД"
38
+ result = translator._translate_check_violation(
39
+ code_msg="Ошибка БД",
40
+ error_msg=error_msg,
41
+ )
42
+ assert (
43
+ result == "Ошибка БД: ключ unknown_check. Описание: Невалидная запись в БД"
44
+ )
35
45
 
36
46
  error_msg = "No violation here."
37
- result = translator._translate_check_violation(error_msg)
38
- assert result is None
47
+ result = translator._translate_check_violation(
48
+ code_msg="Ошибка БД",
49
+ error_msg=error_msg,
50
+ )
51
+ assert result == "Ошибка БД"
39
52
 
40
53
  def test_translate_with_unique_violation(self):
41
54
  class MockError(Exception):
@@ -48,7 +61,7 @@ class TestPsycopgErrorTranslator:
48
61
  translator = PsycopgErrorTranslator()
49
62
  result = translator.translate(MockError())
50
63
  assert (
51
- "БД уже содержит значение: ключ email, значение invalid@example.com."
64
+ "БД уже содержит значение: ключ email, значение invalid@example.com"
52
65
  in result
53
66
  )
54
67
 
@@ -65,7 +78,7 @@ class TestPsycopgErrorTranslator:
65
78
  )
66
79
  result = translator.translate(MockError())
67
80
  assert (
68
- "Нарушено ограничение данных: ключ valid_email, значение Невалидный email."
81
+ "Нарушено ограничение данных: ключ valid_email. Описание: Невалидный email"
69
82
  in result
70
83
  )
71
84
 
@@ -1,4 +1,3 @@
1
- from various_api_tools.translators.psycopg.base import ErrorData
2
1
  from various_api_tools.translators.psycopg.psycopg2 import Psycopg2ErrorTranslator
3
2
 
4
3
 
@@ -7,14 +6,21 @@ class TestPsycopg2ErrorTranslator:
7
6
  error_msg = (
8
7
  "DETAIL: Key (uuid)=(a6cc5730-2261-11ee-9c43-2eb5a363657c) already exists."
9
8
  )
10
- result = Psycopg2ErrorTranslator._translate_unique_violation(error_msg)
11
- assert isinstance(result, ErrorData)
12
- assert result.key == "uuid"
13
- assert result.value == "a6cc5730-2261-11ee-9c43-2eb5a363657c"
9
+ result = Psycopg2ErrorTranslator._translate_unique_violation(
10
+ code_msg="Ошибка БД",
11
+ error_msg=error_msg,
12
+ )
13
+ assert (
14
+ result
15
+ == "Ошибка БД: ключ uuid, значение a6cc5730-2261-11ee-9c43-2eb5a363657c"
16
+ )
14
17
 
15
18
  error_msg = "No violation here."
16
- result = Psycopg2ErrorTranslator._translate_unique_violation(error_msg)
17
- assert result is None
19
+ result = Psycopg2ErrorTranslator._translate_unique_violation(
20
+ code_msg="Ошибка БД",
21
+ error_msg=error_msg,
22
+ )
23
+ assert result == "Ошибка БД"
18
24
 
19
25
  def test_translate_check_violation(self):
20
26
  translator = Psycopg2ErrorTranslator(
@@ -22,20 +28,27 @@ class TestPsycopg2ErrorTranslator:
22
28
  )
23
29
 
24
30
  error_msg = 'violates check constraint "valid_email"'
25
- result = translator._translate_check_violation(error_msg)
26
- assert isinstance(result, ErrorData)
27
- assert result.key == "valid_email"
28
- assert result.value == "Невалидный email"
31
+ result = translator._translate_check_violation(
32
+ code_msg="Ошибка БД",
33
+ error_msg=error_msg,
34
+ )
35
+ assert result == "Ошибка БД: ключ valid_email. Описание: Невалидный email"
29
36
 
30
37
  error_msg = 'violates check constraint "unknown_check"'
31
- result = translator._translate_check_violation(error_msg)
32
- assert isinstance(result, ErrorData)
33
- assert result.key == "unknown_check"
34
- assert result.value == "Невалидная запись в БД"
38
+ result = translator._translate_check_violation(
39
+ code_msg="Ошибка БД",
40
+ error_msg=error_msg,
41
+ )
42
+ assert (
43
+ result == "Ошибка БД: ключ unknown_check. Описание: Невалидная запись в БД"
44
+ )
35
45
 
36
46
  error_msg = "No violation here."
37
- result = translator._translate_check_violation(error_msg)
38
- assert result is None
47
+ result = translator._translate_check_violation(
48
+ code_msg="Ошибка БД",
49
+ error_msg=error_msg,
50
+ )
51
+ assert result == "Ошибка БД"
39
52
 
40
53
  def test_translate_with_unique_violation(self):
41
54
  class MockError(Exception):
@@ -48,8 +61,8 @@ class TestPsycopg2ErrorTranslator:
48
61
  translator = Psycopg2ErrorTranslator()
49
62
  result = translator.translate(MockError())
50
63
  assert (
51
- "БД уже содержит значение: ключ email, значение invalid@example.com."
52
- in result
64
+ result
65
+ == "БД уже содержит значение: ключ email, значение invalid@example.com"
53
66
  )
54
67
 
55
68
  def test_translate_with_check_violation(self):
@@ -63,8 +76,8 @@ class TestPsycopg2ErrorTranslator:
63
76
  )
64
77
  result = translator.translate(MockError())
65
78
  assert (
66
- "Нарушено ограничение данных: ключ valid_email, значение Невалидный email."
67
- in result
79
+ result
80
+ == "Нарушено ограничение данных: ключ valid_email. Описание: Невалидный email"
68
81
  )
69
82
 
70
83
  def test_translate_with_unknown_code(self):
@@ -1,229 +0,0 @@
1
- """Abstract base class for translating psycopg/psycopg2 database errors.
2
-
3
- This module provides a foundation for parsing PostgreSQL error
4
- details (such as `pgcode` and `pgerror`) from psycopg exceptions and converting
5
- them into structured, human-readable error descriptions.
6
- It supports both `psycopg2` and `psycopg3` by abstracting access to error attributes,
7
- allowing concrete implementations to handle version-specific details.
8
- """
9
-
10
- import re
11
- from dataclasses import dataclass
12
- from typing import Any
13
-
14
- from .constants import (
15
- CHECK_VIOLATION_PATTERN,
16
- DEFAULT_CODE_MAP,
17
- UNIQUE_VIOLATION_PATTERN,
18
- UNKNOWN_CHECK_DESCRIPTION,
19
- UNKNOWN_CODE,
20
- )
21
-
22
-
23
- @dataclass
24
- class ErrorData:
25
- """Represents structured data extracted from a database error.
26
-
27
- Used to capture constraint details such as the violated field and
28
- its conflicting value.
29
-
30
- Attributes:
31
- key: The name of the field or constraint involved in the error.
32
- value: The value that caused the constraint violation.
33
-
34
- """
35
-
36
- key: str
37
- value: Any
38
-
39
-
40
- class BasePsycopgTranslator:
41
- """Base translator for turning psycopg errors into user-friendly messages.
42
-
43
- Supports customizing error messages via:
44
- - `code_map`: maps SQLSTATE codes to human-readable messages
45
- - `check_map`: maps check constraint names to meaningful descriptions
46
-
47
- Subclasses must implement `_get_pgcode()` and `_get_pgerror()` to support specific
48
- psycopg versions (e.g., psycopg2 vs. psycopg3).
49
-
50
- Attributes:
51
- code_map (dict): Mapping from SQLSTATE codes (str) to error messages (str).
52
- check_map (dict): Mapping from check constraint names (str)
53
- to descriptions (str).
54
-
55
- """
56
-
57
- code_map: dict
58
- check_map: dict
59
-
60
- def __init__(
61
- self,
62
- *,
63
- code_map: dict | None = None,
64
- check_map: dict | None = None,
65
- ) -> None:
66
- """Initialize the translator with optional custom message mappings.
67
-
68
- Args:
69
- code_map: Optional mapping from SQLSTATE codes (e.g. '23505')
70
- to error messages. If None, defaults to `DEFAULT_CODE_MAP`.
71
- check_map: Optional mapping from check constraint names
72
- to descriptive messages. If None, defaults to an empty dict.
73
-
74
- Example:
75
- ```python
76
- custom_codes = {"23505": "Значение уже существует"}
77
- custom_checks = {"age_check": "Возраст должен быть от 18 до 120"}
78
- t = BasePsycopgTranslator(code_map=custom_codes, check_map=custom_checks)
79
- ```
80
-
81
- """
82
- if code_map is not None:
83
- self.code_map = code_map
84
- else:
85
- self.code_map = DEFAULT_CODE_MAP
86
-
87
- if check_map is not None:
88
- self.check_map = check_map
89
- else:
90
- self.check_map = {}
91
-
92
- def translate(
93
- self,
94
- error: Any,
95
- ) -> str:
96
- """Translate a psycopg database error into a user-friendly message.
97
-
98
- Extracts `pgcode` and `pgerror` from the error and formats a meaningful response
99
- based on the type of constraint violation (unique, check, foreign key).
100
-
101
- Args:
102
- error: A psycopg exception (e.g. IntegrityError, DatabaseError).
103
-
104
- Returns:
105
- A formatted error message, or a fallback if no match is found.
106
-
107
- Example:
108
- ```python
109
- # Given error: unique_violation on email = 'test@example.com'
110
- print(translator.translate(error))
111
- #> "БД уже содержит значение: ключ email, значение test@example.com"
112
- ```
113
-
114
- """
115
- msg: str = UNKNOWN_CODE
116
-
117
- pgcode = self._get_pgcode(error)
118
- pgerror = self._get_pgerror(error)
119
-
120
- if pgcode is not None:
121
- code_msg: str | None = self.code_map.get(pgcode, UNKNOWN_CODE)
122
- data: ErrorData | None = None
123
-
124
- if pgcode == "23514":
125
- data = self._translate_check_violation(error_msg=pgerror)
126
- elif pgcode in ["23505", "23503"]:
127
- data = self._translate_unique_violation(error_msg=pgerror)
128
-
129
- if data and code_msg:
130
- msg = f"{code_msg}: ключ {data.key}, значение {data.value}."
131
- else:
132
- msg = code_msg
133
-
134
- return msg
135
-
136
- def _get_pgcode(self, error: Any) -> str | None:
137
- """Extract pgcode code from the error.
138
-
139
- Must be implemented by subclasses to support specific psycopg versions.
140
-
141
- Args:
142
- error: The raw database exception.
143
-
144
- Returns:
145
- The SQLSTATE code as a string (e.g. '23505'), or None if not available.
146
-
147
- """
148
- raise NotImplementedError
149
-
150
- def _get_pgerror(self, error: Any) -> str | None:
151
- """Extract the full error message from the exception.
152
-
153
- Must be implemented by subclasses to support specific psycopg versions.
154
-
155
- Args:
156
- error: The raw database exception.
157
-
158
- Returns:
159
- Full error message string, or None if not available.
160
-
161
- """
162
- raise NotImplementedError
163
-
164
- @classmethod
165
- def _translate_unique_violation(cls, error_msg: str) -> ErrorData:
166
- """Extract field and value from a unique constraint violation error.
167
-
168
- Uses a regex to parse the PostgreSQL
169
- "DETAIL: Key (field)=(value) already exists" message.
170
-
171
- Args:
172
- error_msg: The full error text from the database.
173
-
174
- Returns:
175
- ErrorData with extracted field name and value, or None if not matched.
176
-
177
- Example:
178
- ```python
179
- error_msg = "DETAIL: Key (email)=(user@example.com) already exists."
180
- print(BasePsycopgTranslator._translate_unique_violation(error_msg))
181
- #> ErrorData(key='email', value='user@example.com')
182
- ```
183
-
184
- """
185
- data: ErrorData | None = None
186
-
187
- pattern = re.compile(UNIQUE_VIOLATION_PATTERN)
188
- matched = pattern.search(error_msg)
189
-
190
- if matched is not None:
191
- data = ErrorData(key=matched.group(2), value=matched.group(3))
192
-
193
- return data
194
-
195
- def _translate_check_violation(self, error_msg: str) -> ErrorData | None:
196
- """Extract constraint name and description from a check constraint violation.
197
-
198
- Looks up a human-readable description in `self.check_map`. If not found,
199
- returns a default message.
200
-
201
- Args:
202
- error_msg: The full error text from the database.
203
-
204
- Returns:
205
- ErrorData with constraint name and description, or None if not matched.
206
-
207
- Example:
208
- ```python
209
- error_msg = "violates check constraint 'age_check'"
210
- check_map = {"age_check": "Возраст должен быть от 18 до 120"}
211
- translator = BasePsycopgTranslator(check_map=check_map)
212
- print(translator._translate_check_violation(error_msg))
213
- #> ErrorData(key='age_check', value='Возраст должен быть от 18 до 120')
214
- ```
215
-
216
- """
217
- data: ErrorData | None = None
218
-
219
- pattern = re.compile(CHECK_VIOLATION_PATTERN)
220
- matched = pattern.search(error_msg)
221
- if matched is not None:
222
- check_constraint = matched.group(1)
223
- constraint_description = self.check_map.get(
224
- check_constraint,
225
- UNKNOWN_CHECK_DESCRIPTION,
226
- )
227
- data = ErrorData(key=check_constraint, value=constraint_description)
228
-
229
- return data