django-small-view-set 0.1.4__tar.gz → 0.2.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.
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-small-view-set
3
- Version: 0.1.4
3
+ Version: 0.2.0
4
4
  Summary: A lightweight Django ViewSet alternative with minimal abstraction.
5
5
  Home-page: https://github.com/nateonguitar/django-small-view-set
6
6
  License: MIT
7
- Keywords: django,viewset
7
+ Keywords: django,small,viewset,view set
8
8
  Author: Nate Brooks
9
9
  Requires-Python: >=3.8
10
10
  Classifier: License :: OSI Approved :: MIT License
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.9
14
14
  Classifier: Programming Language :: Python :: 3.10
15
15
  Classifier: Programming Language :: Python :: 3.11
16
16
  Requires-Dist: django (>=3.2)
17
+ Requires-Dist: pytest-django (>=4.11.1,<5.0.0)
17
18
  Project-URL: Repository, https://github.com/nateonguitar/django-small-view-set
18
19
  Description-Content-Type: text/markdown
19
20
 
@@ -33,7 +34,9 @@ Here’s how you can define a basic API endpoint with one collection route and o
33
34
  import asyncio
34
35
  from django.http import JsonResponse
35
36
  from django.urls import path
36
- from small_view_set.small_view_set import SmallViewSet, default_handle_endpoint_exceptions, disable_endpoint
37
+ from small_view_set.small_view_set import SmallViewSet
38
+ from small_view_set.decorators import endpoint, endpoint_disabled
39
+ from small_view_set.config import SmallViewSetConfig
37
40
 
38
41
  class BarViewSet(SmallViewSet):
39
42
 
@@ -44,24 +47,24 @@ class BarViewSet(SmallViewSet):
44
47
  path('api/bars/<int:pk>/', self.default_router, name='bars_detail'),
45
48
  ]
46
49
 
47
- @default_handle_endpoint_exceptions
50
+ @endpoint(allowed_methods=['GET'])
48
51
  def list(self, request):
49
52
  self.protect_list(request)
50
53
  return JsonResponse({"message": "Hello, world!"}, status=200)
51
54
 
52
- @default_handle_endpoint_exceptions
53
- @disable_endpoint
55
+ @endpoint(allowed_methods=['GET'])
56
+ @endpoint_disabled
54
57
  async def items(self, request):
55
58
  self.protect_list(request)
56
59
  await asyncio.sleep(1)
57
60
  return JsonResponse({"message": "List of items"}, status=200)
58
61
 
59
- @default_handle_endpoint_exceptions
62
+ @endpoint(allowed_methods=['PATCH'])
60
63
  def patch(self, request, pk):
61
64
  self.protect_update(request)
62
65
  return JsonResponse({"message": f"Updated {pk}"}, status=200)
63
66
 
64
- @default_handle_endpoint_exceptions
67
+ @endpoint(allowed_methods=['GET'])
65
68
  async def retrieve(self, request, pk):
66
69
  self.protect_retrieve(request)
67
70
  return JsonResponse({"message": f"Detail for ID {pk}"}, status=200)
@@ -14,7 +14,9 @@ Here’s how you can define a basic API endpoint with one collection route and o
14
14
  import asyncio
15
15
  from django.http import JsonResponse
16
16
  from django.urls import path
17
- from small_view_set.small_view_set import SmallViewSet, default_handle_endpoint_exceptions, disable_endpoint
17
+ from small_view_set.small_view_set import SmallViewSet
18
+ from small_view_set.decorators import endpoint, endpoint_disabled
19
+ from small_view_set.config import SmallViewSetConfig
18
20
 
19
21
  class BarViewSet(SmallViewSet):
20
22
 
@@ -25,24 +27,24 @@ class BarViewSet(SmallViewSet):
25
27
  path('api/bars/<int:pk>/', self.default_router, name='bars_detail'),
26
28
  ]
27
29
 
28
- @default_handle_endpoint_exceptions
30
+ @endpoint(allowed_methods=['GET'])
29
31
  def list(self, request):
30
32
  self.protect_list(request)
31
33
  return JsonResponse({"message": "Hello, world!"}, status=200)
32
34
 
33
- @default_handle_endpoint_exceptions
34
- @disable_endpoint
35
+ @endpoint(allowed_methods=['GET'])
36
+ @endpoint_disabled
35
37
  async def items(self, request):
36
38
  self.protect_list(request)
37
39
  await asyncio.sleep(1)
38
40
  return JsonResponse({"message": "List of items"}, status=200)
39
41
 
40
- @default_handle_endpoint_exceptions
42
+ @endpoint(allowed_methods=['PATCH'])
41
43
  def patch(self, request, pk):
42
44
  self.protect_update(request)
43
45
  return JsonResponse({"message": f"Updated {pk}"}, status=200)
44
46
 
45
- @default_handle_endpoint_exceptions
47
+ @endpoint(allowed_methods=['GET'])
46
48
  async def retrieve(self, request, pk):
47
49
  self.protect_retrieve(request)
48
50
  return JsonResponse({"message": f"Detail for ID {pk}"}, status=200)
@@ -4,13 +4,13 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "django-small-view-set"
7
- version = "0.1.4"
7
+ version = "0.2.0"
8
8
  description = "A lightweight Django ViewSet alternative with minimal abstraction."
9
9
  readme = "README.md"
10
10
  authors = ["Nate Brooks"]
11
11
  license = "MIT"
12
12
  repository = "https://github.com/nateonguitar/django-small-view-set"
13
- keywords = ["django", "viewset"]
13
+ keywords = ["django", "small", "viewset", "view set"]
14
14
  packages = [
15
15
  { include = "small_view_set", from = "src" }
16
16
  ]
@@ -18,6 +18,7 @@ packages = [
18
18
  [tool.poetry.dependencies]
19
19
  python = ">=3.8"
20
20
  django = ">=3.2"
21
+ pytest-django = "^4.11.1"
21
22
 
22
23
  [tool.poetry.dev-dependencies]
23
24
  pytest = "*"
@@ -0,0 +1,32 @@
1
+ from .small_view_set import SmallViewSet
2
+ from .config import SmallViewSetConfig
3
+ from .decorators import (
4
+ endpoint,
5
+ endpoint_disabled,
6
+ )
7
+ from.helpers import (
8
+ default_exception_handler,
9
+ default_options_and_head_handler,
10
+ )
11
+ from .exceptions import (
12
+ BadRequest,
13
+ EndpointDisabledException,
14
+ MethodNotAllowed,
15
+ Unauthorized,
16
+ )
17
+
18
+ __all__ = [
19
+ "SmallViewSet",
20
+ "SmallViewSetConfig",
21
+
22
+ "endpoint",
23
+ "endpoint_disabled",
24
+
25
+ "default_exception_handler",
26
+ "default_options_and_head_handler",
27
+
28
+ "BadRequest",
29
+ "EndpointDisabledException",
30
+ "MethodNotAllowed",
31
+ "Unauthorized",
32
+ ]
@@ -0,0 +1,30 @@
1
+ from typing import Callable
2
+ from urllib.request import Request
3
+ from small_view_set.helpers import default_exception_handler, default_options_and_head_handler
4
+
5
+
6
+ class SmallViewSetConfig:
7
+ """
8
+ Configuration class for SmallViewSet.
9
+
10
+ This class allows customization of exception handling and handling of
11
+ OPTIONS and HEAD requests for endpoints.
12
+
13
+ Args:
14
+ exception_handler (Callable[[str, Exception], None]): A callback function
15
+ for handling exceptions. The function takes two parameters:
16
+ 1. The name of the endpoint function (e.g., 'list', 'retrieve', or a custom endpoint name).
17
+ 2. The exception that was thrown.
18
+ options_and_head_handler (Callable[[Request, list[str]], None]): A callback function
19
+ for handling OPTIONS and HEAD requests. The function takes two parameters:
20
+ 1. The Django Request object.
21
+ 2. A list of allowed HTTP methods for the endpoint (e.g., ['PUT', 'PATCH']).
22
+ """
23
+ def __init__(
24
+ self,
25
+ exception_handler: Callable[[str, Exception], None] = default_exception_handler,
26
+ options_and_head_handler: Callable[[Request, list[str]], None] = default_options_and_head_handler,
27
+ respect_disabled_endpoints=True):
28
+ self.exception_handler = exception_handler
29
+ self.options_and_head_handler = options_and_head_handler
30
+ self.respect_disabled_endpoints = respect_disabled_endpoints
@@ -0,0 +1,87 @@
1
+ import inspect
2
+ from django.conf import settings
3
+
4
+ from small_view_set.config import SmallViewSetConfig
5
+ from .exceptions import EndpointDisabledException
6
+
7
+ def endpoint(
8
+ allowed_methods: list[str]):
9
+ def decorator(func):
10
+ func_name = func.__name__
11
+ def sync_wrapper(viewset, *args, **kwargs):
12
+ request = args[0]
13
+ try:
14
+ config: SmallViewSetConfig = getattr(settings, 'SMALL_VIEW_SET_CONFIG', SmallViewSetConfig())
15
+ pre_response = config.options_and_head_handler(request, allowed_methods)
16
+ if pre_response:
17
+ return pre_response
18
+ pk = kwargs.pop('pk', None)
19
+ if pk is None:
20
+ return func(viewset, request=request)
21
+ else:
22
+ return func(viewset, request=request, pk=pk)
23
+ except Exception as e:
24
+ return config.exception_handler(request, func_name, e)
25
+
26
+ async def async_wrapper(viewset, *args, **kwargs):
27
+ request = args[0]
28
+ try:
29
+ config: SmallViewSetConfig = getattr(settings, 'SMALL_VIEW_SET_CONFIG', SmallViewSetConfig())
30
+ pre_response = config.options_and_head_handler(request, allowed_methods)
31
+ if pre_response:
32
+ return pre_response
33
+ pk = kwargs.pop('pk', None)
34
+ if pk is None:
35
+ return await func(viewset, request=request)
36
+ else:
37
+ return await func(viewset, request=request, pk=pk)
38
+ except Exception as e:
39
+ return config.exception_handler(request, func_name, e)
40
+
41
+ if inspect.iscoroutinefunction(func):
42
+ return async_wrapper
43
+ else:
44
+ return sync_wrapper
45
+
46
+ return decorator
47
+
48
+
49
+ def endpoint_disabled(func):
50
+ """
51
+ Temporarily disables an API endpoint based on the SMALL_VIEWSET_RESPECT_DISABLED_ENDPOINTS setting.
52
+
53
+ When `SMALL_VIEWSET_RESPECT_DISABLED_ENDPOINTS` in Django settings is set to `True`, this decorator
54
+ will raise an `EndpointDisabledException`, resulting in a 404 response. When set to `False`,
55
+ the endpoint will remain active, which is useful for testing environments.
56
+
57
+ Usage:
58
+ - Apply this decorator directly to a view method or action.
59
+ - Example:
60
+
61
+ ```python
62
+ class ExampleViewSet(SmallViewSet):
63
+
64
+ @default_handle_endpoint_exceptions
65
+ @endpoint_disabled
66
+ def retrieve(self, request: Request) -> JsonResponse:
67
+ self.protect_retrieve(request)
68
+ . . .
69
+ ```
70
+ """
71
+ config: SmallViewSetConfig = getattr(settings, 'SMALL_VIEW_SET_CONFIG', SmallViewSetConfig())
72
+ def sync_wrapper(*args, **kwargs):
73
+ if config.respect_disabled_endpoints:
74
+ raise EndpointDisabledException()
75
+ return func(*args, **kwargs)
76
+
77
+ async def async_wrapper(*args, **kwargs):
78
+ if config.respect_disabled_endpoints:
79
+ raise EndpointDisabledException()
80
+ return await func(*args, **kwargs)
81
+
82
+
83
+ if inspect.iscoroutinefunction(func):
84
+ return async_wrapper
85
+ else:
86
+ return sync_wrapper
87
+
@@ -0,0 +1,133 @@
1
+ import json
2
+ import logging
3
+
4
+ from django.conf import settings
5
+ from django.core.exceptions import ObjectDoesNotExist, SuspiciousOperation, PermissionDenied
6
+ from django.http import Http404, JsonResponse
7
+ from urllib.request import Request
8
+
9
+ from .exceptions import EndpointDisabledException, MethodNotAllowed, Unauthorized
10
+
11
+
12
+ _logger = logging.getLogger('django-small-view-set.default_handle_endpoint_exceptions')
13
+ if not _logger.hasHandlers():
14
+ handler = logging.StreamHandler()
15
+ formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
16
+ handler.setFormatter(formatter)
17
+ _logger.addHandler(handler)
18
+ _logger.setLevel(logging.INFO)
19
+
20
+
21
+ def default_options_and_head_handler(request, allowed_methods: list[str]):
22
+ if request.method == 'OPTIONS':
23
+ response = JsonResponse(
24
+ data=None,
25
+ safe=False,
26
+ status=200,
27
+ content_type='application/json')
28
+ response['Allow'] = ', '.join(allowed_methods)
29
+ return response
30
+
31
+ if request.method == 'HEAD':
32
+ response = JsonResponse(
33
+ data=None,
34
+ safe=False,
35
+ status=200,
36
+ content_type='application/json')
37
+ response['Allow'] = ', '.join(allowed_methods)
38
+ return response
39
+
40
+ if request.method not in allowed_methods:
41
+ raise MethodNotAllowed(method=request.method)
42
+
43
+
44
+ def default_exception_handler(request: Request, endpoint_name: str, exception):
45
+ try:
46
+ raise exception
47
+
48
+ except json.JSONDecodeError:
49
+ return JsonResponse(data={"errors": "Invalid JSON"}, status=400)
50
+
51
+ except (TypeError, ValueError) as exception:
52
+ if hasattr(exception, 'detail'):
53
+ return JsonResponse(data={'errors': exception.detail}, status=400)
54
+ if hasattr(exception, 'message'):
55
+ return JsonResponse(data={'errors': exception.message}, status=400)
56
+ return JsonResponse(data=None, safe=False, status=400)
57
+
58
+ except Unauthorized:
59
+ return JsonResponse(data=None, safe=False, status=401)
60
+
61
+ except (PermissionDenied, SuspiciousOperation):
62
+ return JsonResponse(data=None, safe=False, status=403)
63
+
64
+ except (Http404, ObjectDoesNotExist):
65
+ return JsonResponse(data=None, safe=False, status=404)
66
+
67
+ except EndpointDisabledException:
68
+ return JsonResponse(data=None, safe=False, status=405)
69
+
70
+ except MethodNotAllowed as exception:
71
+ return JsonResponse(
72
+ data={'errors': f"Method {exception.method} is not allowed"},
73
+ status=405)
74
+
75
+ except Exception as exception:
76
+ # Catch-all exception handler for API endpoints.
77
+ #
78
+ # - Always defaults to HTTP 500 with "Internal server error" unless the exception provides a more specific status code and error details.
79
+ # - Duck types to extract error information from `detail` or `message` attributes, if available.
80
+ # - Never exposes internal exception contents to end users for 5xx server errors unless settings.DEBUG is True.
81
+ # - Allows structured error payloads (string, list, or dict) without assumptions about the error format.
82
+ # - Logs exceptions fully for server-side diagnostics, distinguishing handled vs unhandled cases.
83
+ #
84
+ # This design prioritizes API security, developer debugging, and future portability across projects.
85
+
86
+ status_code = getattr(exception, 'status_code', 500)
87
+ error_contents = None
88
+
89
+ if hasattr(exception, 'detail'):
90
+ error_contents = exception.detail
91
+ elif hasattr(exception, 'message') and isinstance(exception.message, str):
92
+ error_contents = exception.message
93
+
94
+ if 400 <= status_code <= 499:
95
+ if status_code == 400:
96
+ message = 'Bad request'
97
+ elif status_code == 401:
98
+ message = 'Unauthorized'
99
+ elif status_code == 403:
100
+ message = 'Forbidden'
101
+ elif status_code == 404:
102
+ message = 'Not found'
103
+ elif status_code == 405:
104
+ message = 'Method not allowed'
105
+ elif status_code == 429:
106
+ message = 'Too many requests'
107
+ elif error_contents:
108
+ message = error_contents
109
+ else:
110
+ message = 'An error occurred'
111
+
112
+ if settings.DEBUG and error_contents:
113
+ message = error_contents
114
+ else:
115
+ status_code = 500
116
+ message = 'Internal server error'
117
+ if settings.DEBUG:
118
+ message = error_contents if error_contents else str(exception)
119
+
120
+ e_name = type(exception).__name__
121
+ if error_contents:
122
+ msg = f"Handled API exception in {endpoint_name}: {e_name}: {error_contents}"
123
+ _logger.error(msg)
124
+
125
+ else:
126
+ msg = f"Unhandled exception in {endpoint_name}: {e_name}: {exception}"
127
+ _logger.error(msg)
128
+
129
+ return JsonResponse(
130
+ data={'errors': message},
131
+ safe=False,
132
+ status=status_code,
133
+ content_type='application/json')
@@ -1,6 +1,7 @@
1
1
  import inspect
2
2
  import json
3
3
  import logging
4
+ from django.http import JsonResponse
4
5
  from urllib.request import Request
5
6
 
6
7
  from .exceptions import BadRequest, MethodNotAllowed
@@ -15,48 +16,68 @@ class SmallViewSet:
15
16
 
16
17
  def protect_create(self, request: Request):
17
18
  """
18
- Ensures that the request method is POST.
19
- Raises:
20
- MethodNotAllowed: If the request method is not GET.
19
+ Stub for adding any custom business logic to protect the create method.
20
+ For example:
21
+ - Check if the user is authenticated
22
+ - Check if the user has validated their email
23
+ - Throttle requests
24
+
25
+ Recommended to call super().protect_create(request) in the subclass in case
26
+ this library adds logic in the future.
21
27
  """
22
- if request.method != 'POST':
23
- raise MethodNotAllowed('POST')
28
+ pass
24
29
 
25
30
  def protect_list(self, request: Request):
26
31
  """
27
- Ensures that the request method is GET.
28
- Raises:
29
- MethodNotAllowed: If the request method is not GET.
32
+ Stub for adding any custom business logic to protect the list method.
33
+ For example:
34
+ - Check if the user is authenticated
35
+ - Check if the user has validated their email
36
+ - Throttle requests
37
+
38
+ Recommended to call super().protect_create(request) in the subclass in case
39
+ this library adds logic in the future.
30
40
  """
31
- if request.method != 'GET':
32
- raise MethodNotAllowed(request.method)
41
+ pass
33
42
 
34
43
  def protect_retrieve(self, request: Request):
35
44
  """
36
- Ensures that the request method is GET.
37
- Raises:
38
- MethodNotAllowed: If the request method is not GET.
45
+ Stub for adding any custom business logic to protect the retrieve method.
46
+ For example:
47
+ - Check if the user is authenticated
48
+ - Check if the user has validated their email
49
+ - Throttle requests
50
+
51
+ Recommended to call super().protect_create(request) in the subclass in case
52
+ this library adds logic in the future.
39
53
  """
40
- if request.method != 'GET':
41
- raise MethodNotAllowed(request.method)
54
+ pass
42
55
 
43
56
  def protect_update(self, request: Request):
44
57
  """
45
- Ensures that the request method is PUT or PATCH.
46
- Raises:
47
- MethodNotAllowed: If the request method is not PUT or PATCH.
58
+ Stub for adding any custom business logic to protect the update method.
59
+ For example:
60
+ - Check if the user is authenticated
61
+ - Check if the user has validated their email
62
+ - Throttle requests
63
+
64
+ Recommended to call super().protect_create(request) in the subclass in case
65
+ this library adds logic in the future.
48
66
  """
49
- if request.method not in ['PUT', 'PATCH']:
50
- raise MethodNotAllowed(request.method)
67
+ pass
51
68
 
52
69
  def protect_delete(self, request: Request):
53
70
  """
54
- Ensures that the request method is DELETE.
55
- Raises:
56
- MethodNotAllowed: If the request method is not DELETE.
71
+ Stub for adding any custom business logic to protect the delete method.
72
+ For example:
73
+ - Check if the user is authenticated
74
+ - Check if the user has validated their email
75
+ - Throttle requests
76
+
77
+ Recommended to call super().protect_create(request) in the subclass in case
78
+ this library adds logic in the future.
57
79
  """
58
- if request.method != 'DELETE':
59
- raise MethodNotAllowed(request.method)
80
+ pass
60
81
 
61
82
  async def default_router(self, request: Request, pk=None, *args, **kwargs):
62
83
  """
@@ -1,21 +0,0 @@
1
- from .small_view_set import SmallViewSet
2
- from .decorators import (
3
- default_handle_endpoint_exceptions,
4
- disable_endpoint,
5
- )
6
- from .exceptions import (
7
- BadRequest,
8
- EndpointDisabledException,
9
- MethodNotAllowed,
10
- Unauthorized,
11
- )
12
-
13
- __all__ = [
14
- "SmallViewSet",
15
- "default_handle_endpoint_exceptions",
16
- "disable_endpoint",
17
- "BadRequest",
18
- "EndpointDisabledException",
19
- "MethodNotAllowed",
20
- "Unauthorized",
21
- ]
@@ -1,211 +0,0 @@
1
- import inspect
2
- import logging
3
- from django.conf import settings
4
-
5
- import json
6
- from django.conf import settings
7
- from django.core.exceptions import ObjectDoesNotExist, SuspiciousOperation, PermissionDenied
8
- from django.http import (
9
- Http404,
10
- JsonResponse,
11
- )
12
-
13
- from .exceptions import EndpointDisabledException, MethodNotAllowed, Unauthorized
14
-
15
-
16
- def _get_logger(name):
17
- """
18
- Retrieves a logger by name. If no logger is configured in settings.LOGGING,
19
- it provides a fallback logger with a StreamHandler.
20
-
21
- To control this logger in `settings.py`, add a logger configuration
22
- under the `LOGGING` dictionary. For example:
23
-
24
- LOGGING = {
25
- 'version': 1,
26
- 'disable_existing_loggers': False,
27
- 'handlers': {
28
- 'console': {
29
- 'class': 'logging.StreamHandler',
30
- },
31
- },
32
- 'loggers': {
33
- 'django-small-view-set.default_handle_endpoint_exceptions': {
34
- 'handlers': ['console'],
35
- 'level': 'INFO',
36
- 'propagate': True,
37
- },
38
- },
39
- }
40
-
41
- This configuration ensures that the logger named
42
- 'django-small-view-set.default_handle_endpoint_exceptions' uses the specified
43
- handlers and logging level.
44
- """
45
- logger = logging.getLogger(name)
46
-
47
- # Check if Django's logging is configured
48
- if not logger.hasHandlers():
49
- # Fallback logger setup
50
- handler = logging.StreamHandler()
51
- formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
52
- handler.setFormatter(formatter)
53
- logger.addHandler(handler)
54
- logger.setLevel(logging.INFO)
55
-
56
- return logger
57
-
58
-
59
- def default_handle_endpoint_exceptions(func):
60
- logger = _get_logger('django-small-view-set.default_handle_endpoint_exceptions')
61
-
62
- def _exception_handler(e):
63
- try:
64
- raise e
65
-
66
- except json.JSONDecodeError:
67
- return JsonResponse(data={"errors": "Invalid JSON"}, status=400)
68
-
69
- except (TypeError, ValueError) as e:
70
- if hasattr(e, 'detail'):
71
- return JsonResponse(data={'errors': e.detail}, status=400)
72
- if hasattr(e, 'message'):
73
- return JsonResponse(data={'errors': e.message}, status=400)
74
- return JsonResponse(data=None, safe=False, status=400)
75
-
76
- except Unauthorized:
77
- return JsonResponse(data=None, safe=False, status=401)
78
-
79
- except (PermissionDenied, SuspiciousOperation):
80
- return JsonResponse(data=None, safe=False, status=403)
81
-
82
- except (Http404, ObjectDoesNotExist):
83
- return JsonResponse(data=None, safe=False, status=404)
84
-
85
- except EndpointDisabledException:
86
- return JsonResponse(data=None, safe=False, status=405)
87
-
88
- except MethodNotAllowed as e:
89
- return JsonResponse(
90
- data={'errors': f"Method {e.method} is not allowed"},
91
- status=405)
92
-
93
- except Exception as e:
94
- # Catch-all exception handler for API endpoints.
95
- #
96
- # - Always defaults to HTTP 500 with "Internal server error" unless the exception provides a more specific status code and error details.
97
- # - Duck types to extract error information from `detail` or `message` attributes, if available.
98
- # - Never exposes internal exception contents to end users for 5xx server errors unless settings.DEBUG is True.
99
- # - Allows structured error payloads (string, list, or dict) without assumptions about the error format.
100
- # - Logs exceptions fully for server-side diagnostics, distinguishing handled vs unhandled cases.
101
- #
102
- # This design prioritizes API security, developer debugging, and future portability across projects.
103
-
104
- status_code = getattr(e, 'status_code', 500)
105
- error_contents = None
106
-
107
- if hasattr(e, 'detail'):
108
- error_contents = e.detail
109
- elif hasattr(e, 'message') and isinstance(e.message, str):
110
- error_contents = e.message
111
-
112
- if 400 <= status_code <= 499:
113
- if status_code == 400:
114
- message = 'Bad request'
115
- elif status_code == 401:
116
- message = 'Unauthorized'
117
- elif status_code == 403:
118
- message = 'Forbidden'
119
- elif status_code == 404:
120
- message = 'Not found'
121
- elif status_code == 405:
122
- message = 'Method not allowed'
123
- elif status_code == 429:
124
- message = 'Too many requests'
125
- elif error_contents:
126
- message = error_contents
127
- else:
128
- message = 'An error occurred'
129
-
130
- if settings.DEBUG and error_contents:
131
- message = error_contents
132
- else:
133
- status_code = 500
134
- message = 'Internal server error'
135
- if settings.DEBUG:
136
- message = error_contents if error_contents else str(e)
137
-
138
- func_name = func.__name__
139
- e_name = type(e).__name__
140
- if error_contents:
141
- msg = f"Handled API exception in {func_name}: {e_name}: {error_contents}"
142
- logger.error(msg)
143
-
144
- else:
145
- msg = f"Unhandled exception in {func_name}: {e_name}: {e}"
146
- logger.error(msg)
147
-
148
-
149
- return JsonResponse(
150
- data={'errors': message},
151
- safe=False,
152
- status=status_code,
153
- content_type='application/json')
154
-
155
- def sync_wrapper(*args, **kwargs):
156
- try:
157
- return func(*args, **kwargs)
158
- except Exception as e:
159
- return _exception_handler(e)
160
-
161
- async def async_wrapper(*args, **kwargs):
162
- try:
163
- return await func(*args, **kwargs)
164
- except Exception as e:
165
- return _exception_handler(e)
166
-
167
- if inspect.iscoroutinefunction(func):
168
- return async_wrapper
169
- else:
170
- return sync_wrapper
171
-
172
-
173
- def disable_endpoint(func):
174
- """
175
- Temporarily disables an API endpoint based on the SMALL_VIEWSET_RESPECT_DISABLED_ENDPOINTS setting.
176
-
177
- When `SMALL_VIEWSET_RESPECT_DISABLED_ENDPOINTS` in Django settings is set to `True`, this decorator
178
- will raise an `EndpointDisabledException`, resulting in a 404 response. When set to `False`,
179
- the endpoint will remain active, which is useful for testing environments.
180
-
181
- Usage:
182
- - Apply this decorator directly to a view method or action.
183
- - Example:
184
-
185
- ```python
186
- class ExampleViewSet(SmallViewSet):
187
-
188
- @default_handle_endpoint_exceptions
189
- @disable_endpoint
190
- def retrieve(self, request: Request) -> JsonResponse:
191
- self.protect_retrieve(request)
192
- . . .
193
- ```
194
- """
195
- respect_disabled_endpoints = getattr(settings, 'SMALL_VIEWSET_RESPECT_DISABLED_ENDPOINTS', True)
196
- def sync_wrapper(*args, **kwargs):
197
- if respect_disabled_endpoints:
198
- raise EndpointDisabledException()
199
- return func(*args, **kwargs)
200
-
201
- async def async_wrapper(*args, **kwargs):
202
- if respect_disabled_endpoints:
203
- raise EndpointDisabledException()
204
- return await func(*args, **kwargs)
205
-
206
-
207
- if inspect.iscoroutinefunction(func):
208
- return async_wrapper
209
- else:
210
- return sync_wrapper
211
-