various-api-tools 2.0.0__tar.gz → 2.0.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.
Files changed (29) hide show
  1. {various_api_tools-2.0.0 → various_api_tools-2.0.1}/PKG-INFO +1 -1
  2. {various_api_tools-2.0.0 → various_api_tools-2.0.1}/pyproject.toml +1 -1
  3. various_api_tools-2.0.1/src/various_api_tools/translators/psycopg/base.py +279 -0
  4. {various_api_tools-2.0.0 → various_api_tools-2.0.1}/src/various_api_tools/translators/psycopg/constants.py +0 -15
  5. various_api_tools-2.0.1/src/various_api_tools/translators/psycopg/psycopg.py +176 -0
  6. various_api_tools-2.0.1/src/various_api_tools/translators/psycopg/psycopg2.py +178 -0
  7. various_api_tools-2.0.1/tests/translators/test_psycopg.py +153 -0
  8. various_api_tools-2.0.1/tests/translators/test_psycopg2.py +142 -0
  9. various_api_tools-2.0.0/src/various_api_tools/translators/psycopg/base.py +0 -256
  10. various_api_tools-2.0.0/src/various_api_tools/translators/psycopg/psycopg.py +0 -43
  11. various_api_tools-2.0.0/src/various_api_tools/translators/psycopg/psycopg2.py +0 -36
  12. various_api_tools-2.0.0/tests/translators/test_psycopg.py +0 -110
  13. various_api_tools-2.0.0/tests/translators/test_psycopg2.py +0 -104
  14. {various_api_tools-2.0.0 → various_api_tools-2.0.1}/README.md +0 -0
  15. {various_api_tools-2.0.0 → various_api_tools-2.0.1}/src/various_api_tools/__init__.py +0 -0
  16. {various_api_tools-2.0.0 → various_api_tools-2.0.1}/src/various_api_tools/translators/__init__.py +0 -0
  17. {various_api_tools-2.0.0 → various_api_tools-2.0.1}/src/various_api_tools/translators/json.py +0 -0
  18. {various_api_tools-2.0.0 → various_api_tools-2.0.1}/src/various_api_tools/translators/psycopg/__init__.py +0 -0
  19. {various_api_tools-2.0.0 → various_api_tools-2.0.1}/src/various_api_tools/translators/pydantic.py +0 -0
  20. {various_api_tools-2.0.0 → various_api_tools-2.0.1}/src/various_api_tools/validators/__init__.py +0 -0
  21. {various_api_tools-2.0.0 → various_api_tools-2.0.1}/src/various_api_tools/validators/pydantic/__init__.py +0 -0
  22. {various_api_tools-2.0.0 → various_api_tools-2.0.1}/src/various_api_tools/validators/pydantic/constants.py +0 -0
  23. {various_api_tools-2.0.0 → various_api_tools-2.0.1}/src/various_api_tools/validators/pydantic/utils.py +0 -0
  24. {various_api_tools-2.0.0 → various_api_tools-2.0.1}/tests/__init__.py +0 -0
  25. {various_api_tools-2.0.0 → various_api_tools-2.0.1}/tests/translators/__init__.py +0 -0
  26. {various_api_tools-2.0.0 → various_api_tools-2.0.1}/tests/translators/test_json.py +0 -0
  27. {various_api_tools-2.0.0 → various_api_tools-2.0.1}/tests/translators/test_pydantic.py +0 -0
  28. {various_api_tools-2.0.0 → various_api_tools-2.0.1}/tests/validators/__init__.py +0 -0
  29. {various_api_tools-2.0.0 → various_api_tools-2.0.1}/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: 2.0.0
3
+ Version: 2.0.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "various-api-tools"
3
- version = "2.0.0"
3
+ version = "2.0.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" },
@@ -0,0 +1,279 @@
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
+ UNIQUE_VIOLATION_PATTERN,
16
+ UNKNOWN_CHECK_DESCRIPTION,
17
+ UNKNOWN_CODE,
18
+ )
19
+
20
+
21
+ class BaseErrorHandler:
22
+ """Base class for PostgreSQL error handlers.
23
+
24
+ Each handler is responsible for detecting and translating specific
25
+ PostgreSQL error codes (e.g., ``23505`` for unique violations).
26
+ Subclasses must implement `_get_pgcode`, `_get_pgerror`, and `handle`.
27
+ """
28
+
29
+ codes: list[str]
30
+ """PostgreSQL error codes (SQLSTATE) this handler supports."""
31
+
32
+ message_map: dict[str, str]
33
+ """Optional mapping to customize error messages per field/constraint name."""
34
+
35
+ def __init__(self, codes: list[str], message_map: dict[str, str]) -> None:
36
+ """Initialize the error handler with codes and optional message map.
37
+
38
+ Args:
39
+ codes: List of PostgreSQL SQLSTATE error codes this handler handles.
40
+ message_map: Optional mapping of constraint/field names to descriptions.
41
+
42
+ """
43
+ self.codes = codes
44
+ self.message_map = message_map
45
+
46
+ def can_handle(self, error: Any) -> bool:
47
+ """Check if this handler can process the given error.
48
+
49
+ Args:
50
+ error: A PostgreSQL exception instance (e.g., `psycopg.Error`).
51
+
52
+ Returns:
53
+ ``True`` if the error code matches one of the supported codes,
54
+ ``False`` otherwise.
55
+
56
+ """
57
+ return self._get_pgcode(error=error) in self.codes
58
+
59
+ def handle(self, error: Any) -> str:
60
+ """Process the error and return a translated message.
61
+
62
+ Args:
63
+ error: A PostgreSQL exception instance.
64
+
65
+ Returns:
66
+ A human-readable error message.
67
+
68
+ Raises:
69
+ NotImplementedError: Must be implemented by subclasses.
70
+
71
+ """
72
+ raise NotImplementedError
73
+
74
+ def _get_pgcode(self, error: Any) -> str | None:
75
+ """Extract the PostgreSQL SQLSTATE error code from the error object.
76
+
77
+ Args:
78
+ error: A PostgreSQL exception instance.
79
+
80
+ Returns:
81
+ The error code (e.g., ``'23505'``), or ``None`` if not available.
82
+
83
+ Raises:
84
+ NotImplementedError: Must be implemented by subclasses (e.g., in mixin).
85
+
86
+ """
87
+ raise NotImplementedError
88
+
89
+ def _get_pgerror(self, error: Any) -> str | None:
90
+ """Extract the human-readable PostgreSQL error message.
91
+
92
+ Args:
93
+ error: A PostgreSQL exception instance.
94
+
95
+ Returns:
96
+ The full error message string, or ``None`` if not available.
97
+
98
+ Raises:
99
+ NotImplementedError: Must be implemented by subclasses (e.g., in mixin).
100
+
101
+ """
102
+ raise NotImplementedError
103
+
104
+
105
+ class BaseCheckErrorHandler(BaseErrorHandler):
106
+ """Handler for PostgreSQL check constraint violations (SQLSTATE ``23514``).
107
+
108
+ Extracts the check constraint field name from the error message
109
+ and returns a human-readable description (custom or default).
110
+ """
111
+
112
+ def __init__(
113
+ self,
114
+ codes: list[str] | None = None,
115
+ message_map: dict[str, str] | None = None,
116
+ ) -> None:
117
+ """Initialize the check constraint error handler.
118
+
119
+ Args:
120
+ codes: PostgreSQL error codes. Defaults to ``["23514"]``.
121
+ message_map: Optional mapping of field/constraint names to custom messages.
122
+ If a field name isn't in the map, uses `UNKNOWN_CHECK_DESCRIPTION`.
123
+
124
+ """
125
+ super().__init__(
126
+ codes=["23514"] if codes is None else codes,
127
+ message_map={} if message_map is None else message_map,
128
+ )
129
+
130
+ def handle(self, error: Any) -> str:
131
+ """Handle a PostgreSQL check constraint violation.
132
+
133
+ Args:
134
+ error: A PostgreSQL exception instance (must contain ``pgerror``).
135
+
136
+ Returns:
137
+ A human-readable error message for the check constraint violation.
138
+
139
+ Returns:
140
+ The translated string with an appropriate explanation
141
+
142
+ """
143
+ pg_error = self._get_pgerror(error=error)
144
+
145
+ pattern = re.compile(CHECK_VIOLATION_PATTERN)
146
+ matched = pattern.search(pg_error)
147
+
148
+ if matched is not None:
149
+ value: str = matched.group(1)
150
+ return self.message_map.get(
151
+ value,
152
+ UNKNOWN_CHECK_DESCRIPTION,
153
+ )
154
+
155
+ return UNKNOWN_CODE
156
+
157
+
158
+ class BaseUniqueErrorHandler(BaseErrorHandler):
159
+ """Handler for PostgreSQL unique constraint violations (SQLSTATE 23505, 23503).
160
+
161
+ Extracts the field/constraint name from the error message
162
+ and returns a human-readable description (custom or default).
163
+ """
164
+
165
+ def __init__(
166
+ self,
167
+ codes: list[str] | None = None,
168
+ message_map: dict[str, str] | None = None,
169
+ ) -> None:
170
+ """Initialize the unique constraint error handler.
171
+
172
+ Args:
173
+ codes: PostgreSQL error codes. Defaults to ``["23505", "23503"]``.
174
+ message_map: Optional mapping of field/constraint names to custom messages.
175
+ If a field name isn't in the map, uses `UNKNOWN_CHECK_DESCRIPTION`.
176
+
177
+ """
178
+ super().__init__(
179
+ codes=["23505", "23503"] if codes is None else codes,
180
+ message_map={} if message_map is None else message_map,
181
+ )
182
+
183
+ def handle(self, error: Any) -> str:
184
+ """Handle a PostgreSQL unique constraint violation.
185
+
186
+ Args:
187
+ error: A PostgreSQL exception instance (must contain ``pgerror``).
188
+
189
+ Returns:
190
+ A human-readable error message for the unique constraint violation.
191
+
192
+ Example:
193
+ Assuming error message:
194
+ ```
195
+ duplicate key value violates unique constraint "users_email_key"
196
+ DETAIL: Key (email)=(test@example.com) already exists.
197
+ ```
198
+
199
+ And ``message_map = {"users_email_key": "This email already exists."}``
200
+
201
+ Returns:
202
+ ```python
203
+ "This email already exists."
204
+ ```
205
+
206
+ """
207
+ pg_error = self._get_pgerror(error=error)
208
+
209
+ pattern = re.compile(UNIQUE_VIOLATION_PATTERN)
210
+ matched = pattern.search(pg_error)
211
+
212
+ if matched is not None:
213
+ key = matched.group(2)
214
+ # value = matched.group(3)
215
+
216
+ return self.message_map.get(
217
+ key,
218
+ UNKNOWN_CHECK_DESCRIPTION,
219
+ )
220
+
221
+ return UNKNOWN_CODE
222
+
223
+
224
+ class BasePsycopgTranslator:
225
+ """Base class for translating PostgreSQL errors using error handlers.
226
+
227
+ Implements the Chain of Responsibility pattern over error handlers.
228
+ Each handler decides if it can process the error; the first matching one does.
229
+ Concrete subclasses should provide implementation
230
+ of ``_get_pgcode``/``_get_pgerror``
231
+ (typically via mixin like `Psycopg3HandlerMixin`).
232
+ """
233
+
234
+ handers: list[BaseErrorHandler]
235
+
236
+ def __init__(self) -> None:
237
+ """Initialize the translator with default handlers."""
238
+ self.handlers = self._default_handlers()
239
+
240
+ @classmethod
241
+ def _default_handlers(cls) -> list[BaseErrorHandler]:
242
+ """Return the list of default error handlers.
243
+
244
+ Returns:
245
+ A list containing:
246
+ - `BaseUniqueErrorHandler`
247
+ - `BaseCheckErrorHandler`
248
+
249
+ """
250
+ return [BaseUniqueErrorHandler(), BaseCheckErrorHandler()]
251
+
252
+ def translate(
253
+ self,
254
+ error: Any,
255
+ ) -> str:
256
+ """Translate a PostgreSQL error into a human-readable message.
257
+
258
+ Iterates through registered handlers and returns the first match.
259
+
260
+ Args:
261
+ error: A PostgreSQL exception instance (e.g., `psycopg.Error`).
262
+
263
+ Returns:
264
+ A translated error message string, or `UNKNOWN_CODE` if no handler matches.
265
+
266
+ Example:
267
+ ```python
268
+ translator = SomeTranslator()
269
+ error = psycopg2.IntegrityError(...)
270
+ print(translator.translate(error))
271
+ #> "This email already exists."
272
+ ```
273
+
274
+ """
275
+ for handler in self.handlers:
276
+ if handler.can_handle(error):
277
+ return handler.handle(error)
278
+
279
+ return UNKNOWN_CODE
@@ -48,21 +48,6 @@ 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
-
66
51
  DEFAULT_CODE_MAP: Final[dict[str, str]] = {
67
52
  "P0001": "Ошибка БД",
68
53
  "23503": "Указан несуществующий идентификатор связанного объекта",
@@ -0,0 +1,176 @@
1
+ """Translator implementation for psycopg3 (psycopg) database errors.
2
+
3
+ This module provides a concrete implementation of `BasePsycopgTranslator` tailored
4
+ for `psycopg` (aka psycopg3) exceptions.
5
+ It extracts error details using psycopg3's exception interface:
6
+ - `sqlstate`: the SQLSTATE code (e.g. '23505' for unique violation)
7
+ - `str(error)`: the full error message, as psycopg3 does not expose `pgerror` directly
8
+ """
9
+
10
+ from typing import Any
11
+
12
+ from .base import (
13
+ BaseCheckErrorHandler,
14
+ BaseErrorHandler,
15
+ BasePsycopgTranslator,
16
+ BaseUniqueErrorHandler,
17
+ )
18
+
19
+
20
+ class PsycopgHandlerMixin:
21
+ """Mixin providing psycopg3-specific error extraction logic.
22
+
23
+ Implements :meth:`BaseErrorHandler._get_pgcode` and
24
+ :meth:`BaseErrorHandler._get_pgerror` using psycopg3's native interface.
25
+
26
+ Example:
27
+ ```python
28
+ import psycopg
29
+
30
+ class MyTranslator(PsycopgHandlerMixin, BasePsycopgTranslator): ...
31
+
32
+ try:
33
+ ...
34
+ except psycopg.Error as e:
35
+ translator = MyTranslator()
36
+ message = translator.translate(e)
37
+ print(message) # Translated, human-readable
38
+
39
+ """
40
+
41
+ def _get_pgcode(self, error: Any) -> str | None:
42
+ """Extract the PostgreSQL SQLSTATE code from a psycopg3 error.
43
+
44
+ Args:
45
+ error: A psycopg3 exception instance (e.g., `IntegrityError`).
46
+
47
+ Returns:
48
+ The SQLSTATE code as a 5-character string (e.g., ``'23505'``),
49
+ or ``None`` if the attribute is missing or not set.
50
+
51
+ Note:
52
+ psycopg3 uses ``error.sqlstate`` instead of the deprecated ``error.pgcode``.
53
+ The value is a string (not int) — e.g., ``"23505"``.
54
+
55
+ """
56
+ return getattr(error, "sqlstate", None)
57
+
58
+ def _get_pgerror(self, error: Any) -> str | None:
59
+ """Extract the full human-readable error message from a psycopg3 error.
60
+
61
+ Args:
62
+ error: A psycopg3 exception instance.
63
+
64
+ Returns:
65
+ The full error message as a string, or ``None`` if not available.
66
+
67
+ Note:
68
+ psycopg3 does *not* expose a dedicated ``pgerror`` attribute.
69
+ The full message is obtained via ``str(error)``, which includes
70
+ both the main error text and the ``DETAIL:`` portion.
71
+
72
+ Example:
73
+ For an error with message:
74
+
75
+ ```
76
+ duplicate key value violates unique constraint "users_email_key"
77
+ DETAIL: Key (email)=(test@example.com) already exists.
78
+ ```
79
+
80
+ This method returns the full string above.
81
+
82
+ """
83
+ return str(error)
84
+
85
+
86
+ class CheckErrorHandler(PsycopgHandlerMixin, BaseCheckErrorHandler):
87
+ """Handler for PostgreSQL check constraint violations(sqlstate='23514') in psycopg3.
88
+
89
+ Extracts the constraint name from the error message using the pattern:
90
+
91
+ ``Key (value)=(...) violates check constraint "(<name>)".``
92
+
93
+ and returns a custom description from :attr:`BaseCheckErrorHandler.message_map`
94
+ if available; otherwise falls back to ``UNKNOWN_CHECK_DESCRIPTION``.
95
+
96
+ Args:
97
+ codes: List of SQLSTATE codes. Defaults to ``["23514"]``.
98
+ message_map: Optional mapping from constraint name → custom message.
99
+
100
+ Example:
101
+ ```python
102
+ handler = CheckErrorHandler(
103
+ message_map={"age_must_be_positive": "Age must be greater than zero."}
104
+ )
105
+ # Error: "Key (age)=(0) violates check constraint "age_must_be_positive"."
106
+ # → "Age must be greater than zero."
107
+ ```
108
+
109
+ """
110
+
111
+
112
+ class UniqueErrorHandler(PsycopgHandlerMixin, BaseUniqueErrorHandler):
113
+ r"""Handler for PostgreSQL unique/concurrent constraint violations in psycopg3.
114
+
115
+ Handles SQLSTATE codes:
116
+ - ``'23505'``: unique violation
117
+ - ``'23503'``: foreign key violation
118
+
119
+ Extracts the constraint name and value from:
120
+
121
+ ``Key (<field>)=(<value>) already exists.``
122
+
123
+ Uses :attr:`BaseUniqueErrorHandler.message_map` for custom messages.
124
+
125
+ Args:
126
+ codes: List of SQLSTATE codes. Defaults to ``["23505", "23503"]``.
127
+ message_map: Optional mapping from constraint name → custom message.
128
+
129
+ Example:
130
+ ```python
131
+ handler = UniqueErrorHandler(
132
+ message_map={"users_email_key": "A user with this email already exists."}
133
+ )
134
+ # Error: "duplicate key value violates unique constraint \"users_email_key\""
135
+ # "DETAIL: Key (email)=(test@example.com) already exists."
136
+ # → "A user with this email already exists."
137
+ ```
138
+
139
+ """
140
+
141
+
142
+ class PsycopgErrorTranslator(BasePsycopgTranslator):
143
+ """Translator for PostgreSQL errors in psycopg3.
144
+
145
+ Implements :meth:`BasePsycopgTranslator._default_handlers` to provide
146
+ a chain of psycopg3-specific handlers:
147
+
148
+ 1. :class:`UniqueErrorHandler` (for ``23505``, ``23503``)
149
+ 2. :class:`CheckErrorHandler` (for ``23514``)
150
+
151
+ Use this class directly for psycopg3, or subclass to override defaults.
152
+
153
+ Example:
154
+ ```python
155
+ translator = PsycopgErrorTranslator()
156
+ error = psycopg.Error(...)
157
+ print(translator.translate(error))
158
+ #> "A user with this email already exists."
159
+ ```
160
+
161
+ See Also:
162
+ :class:`Psycopg2ErrorTranslator` — for ``psycopg2`` compatibility.
163
+
164
+ """
165
+
166
+ @classmethod
167
+ def _default_handlers(cls) -> list[BaseErrorHandler]:
168
+ """Return a list of default handlers for psycopg3 errors.
169
+
170
+ Returns:
171
+ A list containing:
172
+ - :class:`UniqueErrorHandler`
173
+ - :class:`CheckErrorHandler`
174
+
175
+ """
176
+ return [UniqueErrorHandler(), CheckErrorHandler()]
@@ -0,0 +1,178 @@
1
+ """Translator implementation for psycopg2-specific database errors.
2
+
3
+ This module provides a concrete implementation of `BasePsycopgTranslator`
4
+ tailored for `psycopg2`exceptions.
5
+ It extracts error details using `psycopg2`'s attribute-based interface:
6
+ - `pgcode`: the SQLSTATE code (e.g. '23505' for unique violation)
7
+ - `pgerror`: the full error message from PostgreSQL
8
+ """
9
+
10
+ from typing import Any
11
+
12
+ from .base import (
13
+ BaseCheckErrorHandler,
14
+ BaseErrorHandler,
15
+ BasePsycopgTranslator,
16
+ BaseUniqueErrorHandler,
17
+ )
18
+
19
+
20
+ class Psycopg2HandlerMixin:
21
+ """Mixin providing psycopg2-specific error extraction logic.
22
+
23
+ Implements :meth:`BaseErrorHandler._get_pgcode` and
24
+ :meth:`BaseErrorHandler._get_pgerror` using psycopg2's native interface.
25
+
26
+ Example:
27
+ ```python
28
+ import psycopg2
29
+
30
+ class MyTranslator(Psycopg2HandlerMixin, BasePsycopgTranslator): ...
31
+
32
+ try:
33
+ ...
34
+ except psycopg2.Error as e:
35
+ translator = MyTranslator()
36
+ message = translator.translate(e)
37
+ print(message) # Translated, human-readable
38
+
39
+ """
40
+
41
+ def _get_pgcode(self, error: Any) -> str | None:
42
+ """Extract the PostgreSQL SQLSTATE code from a psycopg2 error.
43
+
44
+ Args:
45
+ error: A psycopg2 exception instance (e.g., `IntegrityError`).
46
+
47
+ Returns:
48
+ The SQLSTATE code as a string (e.g., ``'23505'``),
49
+ or ``None`` if the attribute is missing or not set.
50
+
51
+ Note:
52
+ psycopg2 uses ``error.pgcode`` — a string (not integer), as required
53
+ by PostgreSQL's SQLSTATE standard.
54
+
55
+ See Also:
56
+ :meth:`PsycopgHandlerMixin._get_pgcode` — for psycopg3 equivalent.
57
+
58
+ """
59
+ return getattr(error, "pgcode", None)
60
+
61
+ def _get_pgerror(self, error: Any) -> str | None:
62
+ """Extract the full human-readable error message from a psycopg2 error.
63
+
64
+ Args:
65
+ error: A psycopg2 exception instance.
66
+
67
+ Returns:
68
+ The full error message as a string, or ``None`` if not available.
69
+
70
+ Note:
71
+ psycopg2 stores the complete PostgreSQL error message in ``error.pgerror``,
72
+ which includes both the main error text and the ``DETAIL:`` portion.
73
+
74
+ Example:
75
+ For an error with message:
76
+
77
+ ```
78
+ duplicate key value violates unique constraint "users_email_key"
79
+ DETAIL: Key (email)=(test@example.com) already exists.
80
+ ```
81
+
82
+ This method returns the full string above.
83
+
84
+ """
85
+ return getattr(error, "pgerror", None)
86
+
87
+
88
+ class CheckErrorHandler(Psycopg2HandlerMixin, BaseCheckErrorHandler):
89
+ """Handler for PostgreSQL check constraint violations (pgcode='23514') in psycopg2.
90
+
91
+ Extracts the constraint name from the error message using the pattern:
92
+
93
+ ``Key (value)=(...) violates check constraint "(<name>)".``
94
+
95
+ and returns a custom description from :attr:`BaseCheckErrorHandler.message_map`
96
+ if available; otherwise falls back to ``UNKNOWN_CHECK_DESCRIPTION``.
97
+
98
+ Args:
99
+ codes: List of SQLSTATE codes. Defaults to ``["23514"]``.
100
+ message_map: Optional mapping from constraint name → custom message.
101
+
102
+ Example:
103
+ ```python
104
+ handler = CheckErrorHandler(
105
+ message_map={"age_must_be_positive": "Age must be greater than zero."}
106
+ )
107
+ # Error: "Key (age)=(0) violates check constraint "age_must_be_positive"."
108
+ # → "Age must be greater than zero."
109
+ ```
110
+
111
+ """
112
+
113
+
114
+ class UniqueErrorHandler(Psycopg2HandlerMixin, BaseUniqueErrorHandler):
115
+ r"""Handler for PostgreSQL unique/concurrent constraint violations in psycopg2.
116
+
117
+ Handles SQLSTATE codes:
118
+ - ``'23505'``: unique violation
119
+ - ``'23503'``: foreign key violation
120
+
121
+ Extracts the constraint name and value from:
122
+
123
+ ``Key (<field>)=(<value>) already exists.``
124
+
125
+ Uses :attr:`BaseUniqueErrorHandler.message_map` for custom messages.
126
+
127
+ Args:
128
+ codes: List of SQLSTATE codes. Defaults to ``["23505", "23503"]``.
129
+ message_map: Optional mapping from constraint name → custom message.
130
+
131
+ Example:
132
+ ```python
133
+ handler = UniqueErrorHandler(
134
+ message_map={"users_email_key": "A user with this email already exists."}
135
+ )
136
+ # Error: "duplicate key value violates unique constraint \"users_email_key\""
137
+ # "DETAIL: Key (email)=(test@example.com) already exists."
138
+ # → "A user with this email already exists."
139
+ ```
140
+
141
+ """
142
+
143
+
144
+ class Psycopg2ErrorTranslator(BasePsycopgTranslator):
145
+ """Translator for PostgreSQL errors in psycopg2.
146
+
147
+ Implements :meth:`BasePsycopgTranslator._default_handlers` to provide
148
+ a chain of psycopg2-specific handlers:
149
+
150
+ 1. :class:`UniqueErrorHandler` (for ``23505``, ``23503``)
151
+ 2. :class:`CheckErrorHandler` (for ``23514``)
152
+
153
+ Use this class directly for psycopg2, or subclass to override defaults.
154
+
155
+ Example:
156
+ ```python
157
+ translator = Psycopg2ErrorTranslator()
158
+ error = psycopg2.IntegrityError(...)
159
+ print(translator.translate(error))
160
+ #> "A user with this email already exists."
161
+ ```
162
+
163
+ See Also:
164
+ :class:`PsycopgErrorTranslator` — for psycopg3 compatibility.
165
+
166
+ """
167
+
168
+ @classmethod
169
+ def _default_handlers(cls) -> list[BaseErrorHandler]:
170
+ """Return a list of default handlers for psycopg2 errors.
171
+
172
+ Returns:
173
+ A list containing:
174
+ - :class:`UniqueErrorHandler`
175
+ - :class:`CheckErrorHandler`
176
+
177
+ """
178
+ return [UniqueErrorHandler(), CheckErrorHandler()]