Skip to content

Commit 43a91de

Browse files
committed
Update drf_simple_api_errors modules
1 parent 8488a63 commit 43a91de

File tree

7 files changed

+156
-75
lines changed

7 files changed

+156
-75
lines changed
Lines changed: 5 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,11 @@
11
import logging
2-
from typing import Dict, Union
32

4-
from django.core.exceptions import (
5-
PermissionDenied,
6-
ValidationError as DjangoValidationError,
7-
)
8-
from django.http import Http404
93
from rest_framework import exceptions
104
from rest_framework.response import Response
11-
from rest_framework.serializers import as_serializer_error
125
from rest_framework.views import set_rollback
136

14-
from drf_simple_api_errors import formatter
7+
from drf_simple_api_errors import formatter, handlers
158
from drf_simple_api_errors.exceptions import ServerError
16-
from drf_simple_api_errors.settings import api_settings
179
from drf_simple_api_errors.types import ExceptionHandlerContext
1810

1911
logger = logging.getLogger(__name__)
@@ -41,22 +33,22 @@ def exception_handler(exc: Exception, context: ExceptionHandlerContext) -> Respo
4133
# This allows for custom exception handling logic.
4234
# If other kinds of exceptions are raised and should be handled,
4335
# they can be added to the EXTRA_HANDLERS setting.
44-
_apply_extra_handlers(exc)
36+
handlers.apply_extra_handlers(exc)
4537

4638
# If the exception is not an instance of APIException, we can try to convert it
4739
# to DRF APIException if it's a Django exception.
48-
exc = _convert_django_exc_to_drf_api_exc(exc)
40+
exc = handlers.convert_django_exc_to_drf_api_exc(exc)
4941
# If the exception is still not an instance of APIException, thus could be
5042
# converted to one, we cannot handle it.
5143
# This will result in a 500 error response without any detail.
5244
# This is because it's not good practice to expose the details of
5345
# unhandled exceptions to the client.
5446
if not isinstance(exc, exceptions.APIException):
55-
logger.debug("Server error", exc_info=True)
47+
logger.info("Server error (500) from unexpected exception.", exc_info=True)
5648
return ServerError
5749

5850
# Get the API response headers from the exception.
59-
headers = _get_response_headers(exc)
51+
headers = handlers.get_response_headers(exc)
6052
# Get the API response data from the exception.
6153
# If the exception is an instance of APIException, we can handle it and
6254
# will format it to a structured API response data.
@@ -65,61 +57,3 @@ def exception_handler(exc: Exception, context: ExceptionHandlerContext) -> Respo
6557
set_rollback()
6658
# Finally, return the API response \(◕ ◡ ◕\)
6759
return Response(data, status=exc.status_code, headers=headers)
68-
69-
70-
def _apply_extra_handlers(exc: Exception):
71-
"""
72-
Apply any extra exception handlers defined in the settings.
73-
74-
Args:
75-
exc (Exception): The exception to handle.
76-
"""
77-
extra_handlers = api_settings.EXTRA_HANDLERS
78-
if extra_handlers:
79-
for handler in extra_handlers:
80-
handler(exc)
81-
82-
83-
def _convert_django_exc_to_drf_api_exc(
84-
exc: Exception,
85-
) -> Union[exceptions.APIException, Exception]:
86-
"""
87-
Convert Django exceptions to DRF APIException, if possible.
88-
89-
Args:
90-
exc (Exception): The exception to convert.
91-
92-
Returns:
93-
exceptions.APIException | Exception: The converted exception or the original.
94-
"""
95-
if isinstance(exc, DjangoValidationError):
96-
return exceptions.ValidationError(as_serializer_error(exc))
97-
98-
if isinstance(exc, Http404):
99-
return exceptions.NotFound()
100-
101-
if isinstance(exc, PermissionDenied):
102-
return exceptions.PermissionDenied()
103-
104-
return exc
105-
106-
107-
def _get_response_headers(exc: exceptions.APIException) -> Dict:
108-
"""
109-
Get the response headers for the given exception.
110-
111-
Args:
112-
exc (exceptions.APIException): The exception to get headers for.
113-
114-
Returns:
115-
dict: A dictionary containing the response headers.
116-
"""
117-
# This is from DRF's default exception handler.
118-
# https://github.com/encode/django-rest-framework/blob/48a21aa0eb3a95d32456c2a927eff9552a04231e/rest_framework/views.py#L87-L91
119-
headers = {}
120-
if getattr(exc, "auth_header", None):
121-
headers["WWW-Authenticate"] = exc.auth_header
122-
if getattr(exc, "wait", None):
123-
headers["Retry-After"] = "%d" % exc.wait
124-
125-
return headers
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""
2+
This module contains custom error handlers for DRF exceptions.
3+
It allows you to define additional handlers for specific exceptions
4+
or to modify the behavior of existing exceptions.
5+
6+
Functions:
7+
- `set_default_detail_to_formatted_exc_default_code`:
8+
Formats the `default_detail` for specific DRF exceptions
9+
by setting it to a human-readable string based on the exception `default_code`.
10+
"""
11+
12+
from django.utils.translation import gettext_lazy as _
13+
from rest_framework import exceptions
14+
15+
16+
def set_default_detail_to_formatted_exc_default_code(
17+
exc: exceptions.APIException,
18+
) -> exceptions.APIException:
19+
"""
20+
Formats the `default_detail` for specific DRF exceptions
21+
by setting it to a human-readable string based on the exception `default_code`.
22+
23+
This ensures that the `default_detail` is consistent and descriptive, such as
24+
'Method not allowed.' instead of using a template string, leaving the exception
25+
`detail` to be more informative.
26+
"""
27+
exceptions_to_format = (
28+
exceptions.MethodNotAllowed,
29+
exceptions.UnsupportedMediaType,
30+
)
31+
32+
if not isinstance(exc, exceptions_to_format):
33+
return exc
34+
35+
# Compose the title based on the exception code.
36+
title = exc.default_code.replace("_", " ").capitalize() + "."
37+
exc.default_detail = _(title)
38+
return exc

drf_simple_api_errors/formatter.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""
2-
This module provides functionality to format API exceptions into a structured error response.
2+
This module provides functionality to format API exceptions into a structured
3+
error response.
34
45
It defines the `APIErrorResponse` class, which represents
56
the structure of the error response, and the `format_exc` function, which
@@ -22,7 +23,6 @@
2223
from rest_framework.settings import api_settings as drf_api_settings
2324

2425
from drf_simple_api_errors import utils
25-
from drf_simple_api_errors.settings import api_settings
2626
from drf_simple_api_errors.types import APIErrorResponseDict
2727

2828
logger = logging.getLogger(__name__)
@@ -52,7 +52,7 @@ class APIErrorResponse:
5252
- invalid_params: A list of invalid parameters, if any
5353
"""
5454

55-
title: str = dataclass_field(init=False)
55+
title: str = dataclass_field(default="")
5656
detail: Optional[List[str]] = dataclass_field(default=None)
5757
invalid_params: Optional[List[InvalidParam]] = dataclass_field(default=None)
5858

drf_simple_api_errors/handlers.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""
2+
Handlers to deal with different types of exceptions and format them into
3+
API error responses, and other utility functions.
4+
5+
Functions:
6+
- `apply_extra_handlers`:
7+
Applies any extra exception handlers defined in the settings.
8+
- `convert_django_exc_to_drf_api_exc`:
9+
Converts Django exceptions to DRF APIException, if possible.
10+
- `get_response_headers`: Gets the response headers for the given exception.
11+
"""
12+
13+
from typing import Dict, Union
14+
15+
from django.core.exceptions import (
16+
PermissionDenied,
17+
ValidationError as DjangoValidationError,
18+
)
19+
from django.http import Http404
20+
from rest_framework import exceptions
21+
from rest_framework.serializers import as_serializer_error
22+
23+
from drf_simple_api_errors import extra_handlers
24+
from drf_simple_api_errors.settings import api_settings
25+
26+
27+
def apply_extra_handlers(exc: Exception):
28+
"""
29+
Apply any extra exception handlers defined in the settings.
30+
31+
Args:
32+
exc (Exception): The exception to handle.
33+
"""
34+
# Get the default extra handlers and the ones defined in the settings.
35+
# The default handlers are always applied to ensure that exceptions
36+
# are formatted correctly.
37+
default_extra_handlers = [
38+
extra_handlers.set_default_detail_to_formatted_exc_default_code
39+
]
40+
settings_extra_handlers = api_settings.EXTRA_HANDLERS
41+
42+
extra_handlers_to_apply = default_extra_handlers + settings_extra_handlers
43+
if extra_handlers_to_apply:
44+
for handler in extra_handlers_to_apply:
45+
handler(exc)
46+
47+
48+
def convert_django_exc_to_drf_api_exc(
49+
exc: Exception,
50+
) -> Union[exceptions.APIException, Exception]:
51+
"""
52+
Convert Django exceptions to DRF APIException, if possible.
53+
54+
Args:
55+
exc (Exception): The exception to convert.
56+
57+
Returns:
58+
exceptions.APIException | Exception: The converted exception or the original.
59+
"""
60+
if isinstance(exc, DjangoValidationError):
61+
return exceptions.ValidationError(as_serializer_error(exc))
62+
63+
if isinstance(exc, Http404):
64+
return exceptions.NotFound()
65+
66+
if isinstance(exc, PermissionDenied):
67+
return exceptions.PermissionDenied()
68+
69+
return exc
70+
71+
72+
def get_response_headers(exc: exceptions.APIException) -> Dict:
73+
"""
74+
Get the response headers for the given exception.
75+
76+
Args:
77+
exc (exceptions.APIException): The exception to get headers for.
78+
79+
Returns:
80+
dict: A dictionary containing the response headers.
81+
"""
82+
# This is from DRF's default exception handler.
83+
# https://github.com/encode/django-rest-framework/blob/48a21aa0eb3a95d32456c2a927eff9552a04231e/rest_framework/views.py#L87-L91
84+
headers = {}
85+
if getattr(exc, "auth_header", None):
86+
headers["WWW-Authenticate"] = exc.auth_header
87+
if getattr(exc, "wait", None):
88+
headers["Retry-After"] = "%d" % exc.wait
89+
90+
return headers

drf_simple_api_errors/settings.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
"""
2+
Settings for the DRF Simple API Errors package.
3+
This module defines the default settings and user settings for the package.
4+
"""
5+
16
from django.conf import settings
27
from rest_framework.settings import APISettings
38

drf_simple_api_errors/types.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
"""
2+
Types for exception handler and its modules.
3+
"""
4+
15
from typing import Dict, List, Optional, Tuple, TypedDict
26

37
from rest_framework.request import Request

drf_simple_api_errors/utils.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
"""
2+
Utility functions for handling API errors and formatting responses.
3+
4+
Functions:
5+
- `camelize`:
6+
Converts a snake_case string to camelCase according to the CAMELIZE setting.
7+
- `flatten_dict`: Flattens a nested dictionary into a single-level dictionary
8+
according to the specified FIELDS_SEPARATOR setting.
9+
"""
10+
111
import re
212

313
from drf_simple_api_errors.settings import api_settings
@@ -11,7 +21,7 @@ def camelize(s: str) -> str:
1121
Args:
1222
s (str): The string to convert.
1323
Returns:
14-
str: The camelCase version of the string, or the original if CAMELIZE is `False`.
24+
str: The camelCase version of a string, or the original if CAMELIZE is `False`.
1525
"""
1626

1727
def underscore_to_camel(match: re.Match) -> str:

0 commit comments

Comments
 (0)