django-small-view-set 0.1.4__tar.gz → 0.2.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.
- {django_small_view_set-0.1.4 → django_small_view_set-0.2.1}/PKG-INFO +20 -10
- {django_small_view_set-0.1.4 → django_small_view_set-0.2.1}/README.md +17 -8
- {django_small_view_set-0.1.4 → django_small_view_set-0.2.1}/pyproject.toml +3 -2
- django_small_view_set-0.2.1/src/small_view_set/__init__.py +32 -0
- django_small_view_set-0.2.1/src/small_view_set/config.py +30 -0
- django_small_view_set-0.2.1/src/small_view_set/decorators.py +87 -0
- django_small_view_set-0.2.1/src/small_view_set/helpers.py +133 -0
- {django_small_view_set-0.1.4 → django_small_view_set-0.2.1}/src/small_view_set/small_view_set.py +46 -25
- django_small_view_set-0.1.4/src/small_view_set/__init__.py +0 -21
- django_small_view_set-0.1.4/src/small_view_set/decorators.py +0 -211
- {django_small_view_set-0.1.4 → django_small_view_set-0.2.1}/LICENSE +0 -0
- {django_small_view_set-0.1.4 → django_small_view_set-0.2.1}/src/small_view_set/README.md +0 -0
- {django_small_view_set-0.1.4 → django_small_view_set-0.2.1}/src/small_view_set/exceptions.py +0 -0
@@ -1,10 +1,10 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: django-small-view-set
|
3
|
-
Version: 0.1
|
3
|
+
Version: 0.2.1
|
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
|
|
@@ -27,41 +28,50 @@ This guide provides a simple example to get started with the library.
|
|
27
28
|
|
28
29
|
### Example Usage
|
29
30
|
|
30
|
-
Here’s how
|
31
|
+
Here’s how to define a basic view set:
|
32
|
+
|
33
|
+
In settings.py
|
34
|
+
```python
|
35
|
+
# Register SmallViewSetConfig in settings
|
36
|
+
from small_view_set SmallViewSetConfig
|
37
|
+
|
38
|
+
SMALL_VIEW_SET_CONFIG = SmallViewSetConfig()
|
39
|
+
```
|
40
|
+
|
31
41
|
|
32
42
|
```python
|
33
43
|
import asyncio
|
34
44
|
from django.http import JsonResponse
|
35
45
|
from django.urls import path
|
36
|
-
from small_view_set
|
46
|
+
from small_view_set import SmallViewSet, endpoint, endpoint_disabled
|
37
47
|
|
38
48
|
class BarViewSet(SmallViewSet):
|
39
49
|
|
40
50
|
def urlpatterns(self):
|
41
51
|
return [
|
42
52
|
path('api/bars/', self.default_router, name='bars_collection'),
|
43
|
-
path('api/bars/items/', self.items,
|
53
|
+
path('api/bars/items/', self.items, name='bars_items'),
|
44
54
|
path('api/bars/<int:pk>/', self.default_router, name='bars_detail'),
|
45
55
|
]
|
46
56
|
|
47
|
-
@
|
57
|
+
@endpoint(allowed_methods=['GET'])
|
48
58
|
def list(self, request):
|
49
59
|
self.protect_list(request)
|
50
60
|
return JsonResponse({"message": "Hello, world!"}, status=200)
|
51
61
|
|
52
|
-
@
|
53
|
-
@
|
62
|
+
@endpoint(allowed_methods=['GET'])
|
63
|
+
@endpoint_disabled
|
54
64
|
async def items(self, request):
|
55
65
|
self.protect_list(request)
|
56
66
|
await asyncio.sleep(1)
|
57
67
|
return JsonResponse({"message": "List of items"}, status=200)
|
58
68
|
|
59
|
-
@
|
69
|
+
@endpoint(allowed_methods=['PATCH'])
|
60
70
|
def patch(self, request, pk):
|
61
71
|
self.protect_update(request)
|
62
72
|
return JsonResponse({"message": f"Updated {pk}"}, status=200)
|
63
73
|
|
64
|
-
@
|
74
|
+
@endpoint(allowed_methods=['GET'])
|
65
75
|
async def retrieve(self, request, pk):
|
66
76
|
self.protect_retrieve(request)
|
67
77
|
return JsonResponse({"message": f"Detail for ID {pk}"}, status=200)
|
@@ -8,41 +8,50 @@ This guide provides a simple example to get started with the library.
|
|
8
8
|
|
9
9
|
### Example Usage
|
10
10
|
|
11
|
-
Here’s how
|
11
|
+
Here’s how to define a basic view set:
|
12
|
+
|
13
|
+
In settings.py
|
14
|
+
```python
|
15
|
+
# Register SmallViewSetConfig in settings
|
16
|
+
from small_view_set SmallViewSetConfig
|
17
|
+
|
18
|
+
SMALL_VIEW_SET_CONFIG = SmallViewSetConfig()
|
19
|
+
```
|
20
|
+
|
12
21
|
|
13
22
|
```python
|
14
23
|
import asyncio
|
15
24
|
from django.http import JsonResponse
|
16
25
|
from django.urls import path
|
17
|
-
from small_view_set
|
26
|
+
from small_view_set import SmallViewSet, endpoint, endpoint_disabled
|
18
27
|
|
19
28
|
class BarViewSet(SmallViewSet):
|
20
29
|
|
21
30
|
def urlpatterns(self):
|
22
31
|
return [
|
23
32
|
path('api/bars/', self.default_router, name='bars_collection'),
|
24
|
-
path('api/bars/items/', self.items,
|
33
|
+
path('api/bars/items/', self.items, name='bars_items'),
|
25
34
|
path('api/bars/<int:pk>/', self.default_router, name='bars_detail'),
|
26
35
|
]
|
27
36
|
|
28
|
-
@
|
37
|
+
@endpoint(allowed_methods=['GET'])
|
29
38
|
def list(self, request):
|
30
39
|
self.protect_list(request)
|
31
40
|
return JsonResponse({"message": "Hello, world!"}, status=200)
|
32
41
|
|
33
|
-
@
|
34
|
-
@
|
42
|
+
@endpoint(allowed_methods=['GET'])
|
43
|
+
@endpoint_disabled
|
35
44
|
async def items(self, request):
|
36
45
|
self.protect_list(request)
|
37
46
|
await asyncio.sleep(1)
|
38
47
|
return JsonResponse({"message": "List of items"}, status=200)
|
39
48
|
|
40
|
-
@
|
49
|
+
@endpoint(allowed_methods=['PATCH'])
|
41
50
|
def patch(self, request, pk):
|
42
51
|
self.protect_update(request)
|
43
52
|
return JsonResponse({"message": f"Updated {pk}"}, status=200)
|
44
53
|
|
45
|
-
@
|
54
|
+
@endpoint(allowed_methods=['GET'])
|
46
55
|
async def retrieve(self, request, pk):
|
47
56
|
self.protect_retrieve(request)
|
48
57
|
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
|
7
|
+
version = "0.2.1"
|
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 .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 .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')
|
{django_small_view_set-0.1.4 → django_small_view_set-0.2.1}/src/small_view_set/small_view_set.py
RENAMED
@@ -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
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
23
|
-
raise MethodNotAllowed('POST')
|
28
|
+
pass
|
24
29
|
|
25
30
|
def protect_list(self, request: Request):
|
26
31
|
"""
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
32
|
-
raise MethodNotAllowed(request.method)
|
41
|
+
pass
|
33
42
|
|
34
43
|
def protect_retrieve(self, request: Request):
|
35
44
|
"""
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
41
|
-
raise MethodNotAllowed(request.method)
|
54
|
+
pass
|
42
55
|
|
43
56
|
def protect_update(self, request: Request):
|
44
57
|
"""
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
50
|
-
raise MethodNotAllowed(request.method)
|
67
|
+
pass
|
51
68
|
|
52
69
|
def protect_delete(self, request: Request):
|
53
70
|
"""
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
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
|
-
|
File without changes
|
File without changes
|
{django_small_view_set-0.1.4 → django_small_view_set-0.2.1}/src/small_view_set/exceptions.py
RENAMED
File without changes
|