various-api-tools 1.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.
- {various_api_tools-1.0.0 → various_api_tools-2.0.1}/PKG-INFO +1 -1
- {various_api_tools-1.0.0 → various_api_tools-2.0.1}/pyproject.toml +2 -1
- various_api_tools-2.0.1/src/various_api_tools/translators/psycopg/base.py +279 -0
- {various_api_tools-1.0.0 → various_api_tools-2.0.1}/src/various_api_tools/translators/psycopg/constants.py +2 -0
- various_api_tools-2.0.1/src/various_api_tools/translators/psycopg/psycopg.py +176 -0
- various_api_tools-2.0.1/src/various_api_tools/translators/psycopg/psycopg2.py +178 -0
- various_api_tools-2.0.1/tests/translators/test_psycopg.py +153 -0
- various_api_tools-2.0.1/tests/translators/test_psycopg2.py +142 -0
- various_api_tools-1.0.0/src/various_api_tools/translators/psycopg/base.py +0 -229
- various_api_tools-1.0.0/src/various_api_tools/translators/psycopg/psycopg.py +0 -43
- various_api_tools-1.0.0/src/various_api_tools/translators/psycopg/psycopg2.py +0 -36
- various_api_tools-1.0.0/tests/translators/test_psycopg.py +0 -97
- various_api_tools-1.0.0/tests/translators/test_psycopg2.py +0 -91
- {various_api_tools-1.0.0 → various_api_tools-2.0.1}/README.md +0 -0
- {various_api_tools-1.0.0 → various_api_tools-2.0.1}/src/various_api_tools/__init__.py +0 -0
- {various_api_tools-1.0.0 → various_api_tools-2.0.1}/src/various_api_tools/translators/__init__.py +0 -0
- {various_api_tools-1.0.0 → various_api_tools-2.0.1}/src/various_api_tools/translators/json.py +0 -0
- {various_api_tools-1.0.0 → various_api_tools-2.0.1}/src/various_api_tools/translators/psycopg/__init__.py +0 -0
- {various_api_tools-1.0.0 → various_api_tools-2.0.1}/src/various_api_tools/translators/pydantic.py +0 -0
- {various_api_tools-1.0.0 → various_api_tools-2.0.1}/src/various_api_tools/validators/__init__.py +0 -0
- {various_api_tools-1.0.0 → various_api_tools-2.0.1}/src/various_api_tools/validators/pydantic/__init__.py +0 -0
- {various_api_tools-1.0.0 → various_api_tools-2.0.1}/src/various_api_tools/validators/pydantic/constants.py +0 -0
- {various_api_tools-1.0.0 → various_api_tools-2.0.1}/src/various_api_tools/validators/pydantic/utils.py +0 -0
- {various_api_tools-1.0.0 → various_api_tools-2.0.1}/tests/__init__.py +0 -0
- {various_api_tools-1.0.0 → various_api_tools-2.0.1}/tests/translators/__init__.py +0 -0
- {various_api_tools-1.0.0 → various_api_tools-2.0.1}/tests/translators/test_json.py +0 -0
- {various_api_tools-1.0.0 → various_api_tools-2.0.1}/tests/translators/test_pydantic.py +0 -0
- {various_api_tools-1.0.0 → various_api_tools-2.0.1}/tests/validators/__init__.py +0 -0
- {various_api_tools-1.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:
|
|
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 = "
|
|
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" },
|
|
@@ -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,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
|
|
@@ -49,6 +49,7 @@ 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
51
|
DEFAULT_CODE_MAP: Final[dict[str, str]] = {
|
|
52
|
+
"P0001": "Ошибка БД",
|
|
52
53
|
"23503": "Указан несуществующий идентификатор связанного объекта",
|
|
53
54
|
"23505": "БД уже содержит значение",
|
|
54
55
|
"23514": "Нарушено ограничение данных",
|
|
@@ -56,6 +57,7 @@ DEFAULT_CODE_MAP: Final[dict[str, str]] = {
|
|
|
56
57
|
"""Default mapping of PostgreSQL SQLSTATE codes to user-friendly Russian error messages.
|
|
57
58
|
|
|
58
59
|
Each key is a standardized SQLSTATE code:
|
|
60
|
+
- "P0001" (raise_exception): Custom exception raised in PL/pgSQL
|
|
59
61
|
- "23503" (foreign_key_violation): Referenced object does not exist
|
|
60
62
|
- "23505" (unique_violation): Duplicate key value
|
|
61
63
|
- "23514" (check_violation): Data fails check constraint
|
|
@@ -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()]
|