django-small-view-set 0.1.3__py3-none-any.whl → 0.2.0__py3-none-any.whl

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.3
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
 
@@ -30,26 +31,41 @@ This guide provides a simple example to get started with the library.
30
31
  Here’s how you can define a basic API endpoint with one collection route and one detail route:
31
32
 
32
33
  ```python
34
+ import asyncio
33
35
  from django.http import JsonResponse
34
36
  from django.urls import path
35
37
  from small_view_set.small_view_set import SmallViewSet
36
- from small_view_set.decorators import default_handle_endpoint_exceptions
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
 
40
43
  def urlpatterns(self):
41
44
  return [
42
45
  path('api/bars/', self.default_router, name='bars_collection'),
46
+ path('api/bars/items/', self.items, name='bars_items'),
43
47
  path('api/bars/<int:pk>/', self.default_router, name='bars_detail'),
44
48
  ]
45
49
 
46
- @default_handle_endpoint_exceptions
47
- def list(self, request, *args, **kwargs):
50
+ @endpoint(allowed_methods=['GET'])
51
+ def list(self, request):
48
52
  self.protect_list(request)
49
53
  return JsonResponse({"message": "Hello, world!"}, status=200)
50
54
 
51
- @default_handle_endpoint_exceptions
52
- def retrieve(self, request, pk, *args, **kwargs):
55
+ @endpoint(allowed_methods=['GET'])
56
+ @endpoint_disabled
57
+ async def items(self, request):
58
+ self.protect_list(request)
59
+ await asyncio.sleep(1)
60
+ return JsonResponse({"message": "List of items"}, status=200)
61
+
62
+ @endpoint(allowed_methods=['PATCH'])
63
+ def patch(self, request, pk):
64
+ self.protect_update(request)
65
+ return JsonResponse({"message": f"Updated {pk}"}, status=200)
66
+
67
+ @endpoint(allowed_methods=['GET'])
68
+ async def retrieve(self, request, pk):
53
69
  self.protect_retrieve(request)
54
70
  return JsonResponse({"message": f"Detail for ID {pk}"}, status=200)
55
71
  ```
@@ -76,5 +92,6 @@ urlpatterns = [
76
92
  - [Handling Endpoint Exceptions](./README_HANDLE_ENDPOINT_EXCEPTIONS.md): Understand how to write your own decorators for exception handling.
77
93
  - [Custom Protections](./README_CUSTOM_PROTECTIONS.md): Learn how to subclass `SmallViewSet` to add custom protections like logged-in checks.
78
94
  - [DRF Compatibility](./README_DRF_COMPATIBILITY.md): Learn how to use some of Django Rest Framework's tools, like Serializers.
95
+ - [Disabling an endpoint](./README_DISABLE_ENDPOINT.md): Learn how to disable an endpoint without needing to delete it or comment it out.
79
96
  - [Reason](./README_REASON.md): Reasoning behind this package.
80
97
 
@@ -0,0 +1,11 @@
1
+ small_view_set/README.md,sha256=oovLoOhBsg5wK8AwGe9iyuWXStHUo9x4fsrlb-oxHt4,1061
2
+ small_view_set/__init__.py,sha256=lJ09qERCFrf4WAY6mRFyFsEG1R3ErpWqblIKi4TVggI,625
3
+ small_view_set/config.py,sha256=GyF69JONC9t0xt1Xhcc4iVEJXx_i_22_2ufxLvNmubs,1460
4
+ small_view_set/decorators.py,sha256=sWnv0629TA1IVWD-5VRDBlpCIc8J5gBK_tA0aQm6BAc,3214
5
+ small_view_set/exceptions.py,sha256=5VaPS9m9syhag7p-gveG7Dep4DV3YQ7WjI1Sv5C_1N0,966
6
+ small_view_set/helpers.py,sha256=EKx71YLS-5cN-JrMJXYR39O15qf4O6TDiaQsal7BVwY,5117
7
+ small_view_set/small_view_set.py,sha256=wvNY1s3DRe2pLLLxz_91MCruEDl3iScOtDa8Y8L4siQ,5566
8
+ django_small_view_set-0.2.0.dist-info/LICENSE,sha256=M4ZuHeiGHHuewaZyqz7tol4E6E2GMz7fF1ywNoXD1tA,1069
9
+ django_small_view_set-0.2.0.dist-info/METADATA,sha256=W2Vr6uGFpO3_6eZXarG1EIMjLJLvOj0OVQJhYeUwNm0,3601
10
+ django_small_view_set-0.2.0.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
11
+ django_small_view_set-0.2.0.dist-info/RECORD,,
small_view_set/README.md CHANGED
@@ -12,25 +12,25 @@
12
12
  3. **Install Dependencies**:
13
13
  - Install dependencies inside the container after mounting the directory:
14
14
  ```bash
15
- docker compose run django-small-view-set-builder poetry install
15
+ docker compose run --remove-orphans django-small-view-set-builder poetry install
16
16
  ```
17
17
 
18
18
  4. **Run Tests**:
19
19
  - Run tests before releasing:
20
20
  ```bash
21
- docker compose run django-small-view-set-builder python /app/tests/manage.py test
21
+ docker compose run --remove-orphans django-small-view-set-builder python /app/tests/manage.py test
22
22
  ```
23
23
 
24
24
  5. **Build the Package**:
25
25
  Inside the Docker container, build the package:
26
26
  ```bash
27
- docker compose run django-small-view-set-builder poetry build
27
+ docker compose run --remove-orphans django-small-view-set-builder poetry build
28
28
  ```
29
29
 
30
30
  6. **Publish to PyPI**:
31
31
  Upload the package to PyPI:
32
32
  ```bash
33
- docker compose run django-small-view-set-builder poetry publish
33
+ docker compose run --remove-orphans django-small-view-set-builder poetry publish
34
34
  ```
35
35
 
36
36
  7. **Verify the Release**:
@@ -1,7 +1,12 @@
1
1
  from .small_view_set import SmallViewSet
2
+ from .config import SmallViewSetConfig
2
3
  from .decorators import (
3
- default_handle_endpoint_exceptions,
4
- disable_endpoint,
4
+ endpoint,
5
+ endpoint_disabled,
6
+ )
7
+ from.helpers import (
8
+ default_exception_handler,
9
+ default_options_and_head_handler,
5
10
  )
6
11
  from .exceptions import (
7
12
  BadRequest,
@@ -12,8 +17,14 @@ from .exceptions import (
12
17
 
13
18
  __all__ = [
14
19
  "SmallViewSet",
15
- "default_handle_endpoint_exceptions",
16
- "disable_endpoint",
20
+ "SmallViewSetConfig",
21
+
22
+ "endpoint",
23
+ "endpoint_disabled",
24
+
25
+ "default_exception_handler",
26
+ "default_options_and_head_handler",
27
+
17
28
  "BadRequest",
18
29
  "EndpointDisabledException",
19
30
  "MethodNotAllowed",
@@ -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
@@ -1,160 +1,52 @@
1
- import logging
1
+ import inspect
2
2
  from django.conf import settings
3
- from functools import wraps
4
3
 
5
- import json
6
- import functools
7
- from django.conf import settings
8
- from django.core.exceptions import ObjectDoesNotExist, SuspiciousOperation, PermissionDenied
9
- from django.http import (
10
- Http404,
11
- JsonResponse,
12
- )
13
-
14
- from .exceptions import EndpointDisabledException, MethodNotAllowed, Unauthorized
15
-
16
-
17
- def _get_logger(name):
18
- """
19
- Retrieves a logger by name. If no logger is configured in settings.LOGGING,
20
- it provides a fallback logger with a StreamHandler.
21
-
22
- To control this logger in `settings.py`, add a logger configuration
23
- under the `LOGGING` dictionary. For example:
24
-
25
- LOGGING = {
26
- 'version': 1,
27
- 'disable_existing_loggers': False,
28
- 'handlers': {
29
- 'console': {
30
- 'class': 'logging.StreamHandler',
31
- },
32
- },
33
- 'loggers': {
34
- 'django-small-view-set.default_handle_endpoint_exceptions': {
35
- 'handlers': ['console'],
36
- 'level': 'INFO',
37
- 'propagate': True,
38
- },
39
- },
40
- }
41
-
42
- This configuration ensures that the logger named
43
- 'django-small-view-set.default_handle_endpoint_exceptions' uses the specified
44
- handlers and logging level.
45
- """
46
- logger = logging.getLogger(name)
47
-
48
- # Check if Django's logging is configured
49
- if not logger.hasHandlers():
50
- # Fallback logger setup
51
- handler = logging.StreamHandler()
52
- formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
53
- handler.setFormatter(formatter)
54
- logger.addHandler(handler)
55
- logger.setLevel(logging.INFO)
56
-
57
- return logger
58
-
59
-
60
- def default_handle_endpoint_exceptions(func):
61
- @functools.wraps(func)
62
- def wrapper(*args, **kwargs):
63
- logger = _get_logger('django-small-view-set.default_handle_endpoint_exceptions')
64
-
65
- try:
66
- return func(*args, **kwargs)
67
-
68
- except json.JSONDecodeError:
69
- return JsonResponse(data={"errors": "Invalid JSON"}, status=400)
70
-
71
- except (TypeError, ValueError) as e:
72
- if hasattr(e, 'detail'):
73
- return JsonResponse(data={'errors': e.detail}, status=400)
74
- if hasattr(e, 'message'):
75
- return JsonResponse(data={'errors': e.message}, status=400)
76
- return JsonResponse(data=None, safe=False, status=400)
77
-
78
- except Unauthorized:
79
- return JsonResponse(data=None, safe=False, status=401)
80
-
81
- except (PermissionDenied, SuspiciousOperation):
82
- return JsonResponse(data=None, safe=False, status=403)
83
-
84
- except (EndpointDisabledException, Http404, ObjectDoesNotExist):
85
- return JsonResponse(data=None, safe=False, status=404)
86
-
87
- except MethodNotAllowed as e:
88
- return JsonResponse(
89
- data={'errors': f"Method {e.method} is not allowed"},
90
- status=405)
91
-
92
- except Exception as e:
93
- # Catch-all exception handler for API endpoints.
94
- #
95
- # - Always defaults to HTTP 500 with "Internal server error" unless the exception provides a more specific status code and error details.
96
- # - Duck types to extract error information from `detail` or `message` attributes, if available.
97
- # - Never exposes internal exception contents to end users for 5xx server errors unless settings.DEBUG is True.
98
- # - Allows structured error payloads (string, list, or dict) without assumptions about the error format.
99
- # - Logs exceptions fully for server-side diagnostics, distinguishing handled vs unhandled cases.
100
- #
101
- # This design prioritizes API security, developer debugging, and future portability across projects.
102
-
103
- status_code = getattr(e, 'status_code', 500)
104
- error_contents = None
105
-
106
- if hasattr(e, 'detail'):
107
- error_contents = e.detail
108
- elif hasattr(e, 'message') and isinstance(e.message, str):
109
- error_contents = e.message
110
-
111
- if 400 <= status_code <= 499:
112
- if status_code == 400:
113
- message = 'Bad request'
114
- elif status_code == 401:
115
- message = 'Unauthorized'
116
- elif status_code == 403:
117
- message = 'Forbidden'
118
- elif status_code == 404:
119
- message = 'Not found'
120
- elif status_code == 405:
121
- message = 'Method not allowed'
122
- elif status_code == 429:
123
- message = 'Too many requests'
124
- elif error_contents:
125
- message = error_contents
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)
126
21
  else:
127
- message = 'An error occurred'
128
-
129
- if settings.DEBUG and error_contents:
130
- message = error_contents
131
- else:
132
- status_code = 500
133
- message = 'Internal server error'
134
- if settings.DEBUG:
135
- message = error_contents if error_contents else str(e)
136
-
137
- func_name = func.__name__
138
- e_name = type(e).__name__
139
- if error_contents:
140
- msg = f"Handled API exception in {func_name}: {e_name}: {error_contents}"
141
- logger.error(msg)
142
-
143
- else:
144
- msg = f"Unhandled exception in {func_name}: {e_name}: {e}"
145
- logger.error(msg)
146
-
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)
147
40
 
148
- return JsonResponse(
149
- data={'errors': message},
150
- safe=False,
151
- status=status_code,
152
- content_type='application/json')
41
+ if inspect.iscoroutinefunction(func):
42
+ return async_wrapper
43
+ else:
44
+ return sync_wrapper
153
45
 
154
- return wrapper
46
+ return decorator
155
47
 
156
48
 
157
- def disable_endpoint(view_func):
49
+ def endpoint_disabled(func):
158
50
  """
159
51
  Temporarily disables an API endpoint based on the SMALL_VIEWSET_RESPECT_DISABLED_ENDPOINTS setting.
160
52
 
@@ -169,17 +61,27 @@ def disable_endpoint(view_func):
169
61
  ```python
170
62
  class ExampleViewSet(SmallViewSet):
171
63
 
172
- @disable_endpoint
173
64
  @default_handle_endpoint_exceptions
65
+ @endpoint_disabled
174
66
  def retrieve(self, request: Request) -> JsonResponse:
175
67
  self.protect_retrieve(request)
176
68
  . . .
177
69
  ```
178
70
  """
179
- @wraps(view_func)
180
- def wrapper(*args, **kwargs):
181
- if settings.SMALL_VIEWSET_RESPECT_DISABLED_ENDPOINTS:
71
+ config: SmallViewSetConfig = getattr(settings, 'SMALL_VIEW_SET_CONFIG', SmallViewSetConfig())
72
+ def sync_wrapper(*args, **kwargs):
73
+ if config.respect_disabled_endpoints:
182
74
  raise EndpointDisabledException()
183
- return view_func(*args, **kwargs)
184
- return wrapper
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
185
87
 
@@ -1,7 +1,7 @@
1
1
  class EndpointDisabledException(Exception):
2
- status_code = 404
3
- message = "Not Found"
4
- error_code = "not_found"
2
+ status_code = 405
3
+ message = "Method Not Allowed"
4
+ error_code = "method_not_allowed"
5
5
 
6
6
  class Unauthorized(Exception):
7
7
  status_code = 401
@@ -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,3 +1,4 @@
1
+ import inspect
1
2
  import json
2
3
  import logging
3
4
  from django.http import JsonResponse
@@ -8,24 +9,6 @@ from .exceptions import BadRequest, MethodNotAllowed
8
9
  logger = logging.getLogger('app')
9
10
 
10
11
  class SmallViewSet:
11
- def create(self, request: Request, *args, **kwargs):
12
- raise MethodNotAllowed('POST')
13
-
14
- def list(self, request: Request, *args, **kwarg):
15
- raise MethodNotAllowed('GET')
16
-
17
- def retrieve(self, request: Request, pk, *args, **kwargs):
18
- raise MethodNotAllowed('GET')
19
-
20
- def put(self, request: Request, pk, *args, **kwargs):
21
- raise MethodNotAllowed('PUT')
22
-
23
- def patch(self, request: Request, pk, *args, **kwargs):
24
- raise MethodNotAllowed('PATCH')
25
-
26
- def delete(self, request: Request, pk, *args, **kwargs):
27
- raise MethodNotAllowed('DELETE')
28
-
29
12
  def parse_json_body(self, request: Request):
30
13
  if request.content_type != 'application/json':
31
14
  raise BadRequest('Invalid content type')
@@ -33,50 +16,70 @@ class SmallViewSet:
33
16
 
34
17
  def protect_create(self, request: Request):
35
18
  """
36
- Ensures that the request method is POST.
37
- Raises:
38
- 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.
39
27
  """
40
- if request.method != 'POST':
41
- raise MethodNotAllowed('POST')
28
+ pass
42
29
 
43
30
  def protect_list(self, request: Request):
44
31
  """
45
- Ensures that the request method is GET.
46
- Raises:
47
- 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.
48
40
  """
49
- if request.method != 'GET':
50
- raise MethodNotAllowed(request.method)
41
+ pass
51
42
 
52
43
  def protect_retrieve(self, request: Request):
53
44
  """
54
- Ensures that the request method is GET.
55
- Raises:
56
- 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.
57
53
  """
58
- if request.method != 'GET':
59
- raise MethodNotAllowed(request.method)
54
+ pass
60
55
 
61
56
  def protect_update(self, request: Request):
62
57
  """
63
- Ensures that the request method is PUT or PATCH.
64
- Raises:
65
- 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.
66
66
  """
67
- if request.method not in ['PUT', 'PATCH']:
68
- raise MethodNotAllowed(request.method)
67
+ pass
69
68
 
70
69
  def protect_delete(self, request: Request):
71
70
  """
72
- Ensures that the request method is DELETE.
73
- Raises:
74
- 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.
75
79
  """
76
- if request.method != 'DELETE':
77
- raise MethodNotAllowed(request.method)
80
+ pass
78
81
 
79
- def default_router(self, request: Request, pk=None, *args, **kwargs):
82
+ async def default_router(self, request: Request, pk=None, *args, **kwargs):
80
83
  """
81
84
  This method routes requests to the appropriate method based on the HTTP method and presence of a primary key (pk).
82
85
 
@@ -116,23 +119,41 @@ class SmallViewSet:
116
119
  def some_disabled_endpoint(self, request: Request):
117
120
  self.protect_retrieve(request)
118
121
  . . .
119
-
120
122
  ```
121
123
  """
124
+ func = None
122
125
  if pk is None:
123
126
  if request.method == 'GET':
124
- return self.list(request, *args, **kwargs)
127
+ if hasattr(self, 'list'):
128
+ func = self.list
129
+
125
130
  elif request.method == 'POST':
126
- return self.create(request, *args, **kwargs)
131
+ if hasattr(self, 'create'):
132
+ func = self.create
127
133
  else:
128
134
  if request.method == 'GET':
129
- return self.retrieve(request, pk, *args, **kwargs)
135
+ if hasattr(self, 'retrieve'):
136
+ func = self.retrieve
137
+
130
138
  elif request.method == 'PUT':
131
- return self.put(request, pk, *args, **kwargs)
139
+ if hasattr(self, 'put'):
140
+ func = self.put
141
+
132
142
  elif request.method == 'PATCH':
133
- return self.patch(request, pk, *args, **kwargs)
143
+ if hasattr(self, 'patch'):
144
+ func = self.patch
145
+
134
146
  elif request.method == 'DELETE':
135
- return self.delete(request, pk, *args, **kwargs)
136
- endpoint_type = "detail" if pk else "collection"
137
- logger.error(f'Got a none response from request_router for {endpoint_type} method {request.method}')
138
- return JsonResponse(data=None, safe=False, status=500)
147
+ if hasattr(self, 'delete'):
148
+ func = self.delete
149
+
150
+ if func is None:
151
+ raise MethodNotAllowed(request.method)
152
+
153
+ if pk is not None:
154
+ kwargs['pk'] = pk
155
+
156
+ if inspect.iscoroutinefunction(func):
157
+ return await func(request, *args, **kwargs)
158
+ else:
159
+ return func(request, *args, **kwargs)
@@ -1,9 +0,0 @@
1
- small_view_set/README.md,sha256=AVHm5FrLxNzFKt9-PGvx0vkAcfMEO9ZtFoCG__lQp-s,993
2
- small_view_set/__init__.py,sha256=FoYcTj03W41-w0jC1EOv1_VD4iBxO_03fIr_plSKnp8,441
3
- small_view_set/decorators.py,sha256=OSwGz5SfTw7hUjOcPNmCmUl8mLqaHOMXaLcqg9JBHXo,6750
4
- small_view_set/exceptions.py,sha256=8CAnzvNZFlqjez8z66jLo-4P3Tlj5Y5MioDPmrb9wQg,948
5
- small_view_set/small_view_set.py,sha256=THDeJp1PmodaWzFUUnowpDmhH8w1El4p97jvQnjyU4k,5192
6
- django_small_view_set-0.1.3.dist-info/LICENSE,sha256=M4ZuHeiGHHuewaZyqz7tol4E6E2GMz7fF1ywNoXD1tA,1069
7
- django_small_view_set-0.1.3.dist-info/METADATA,sha256=CfEGIqD7i2PPsYsDf-k8ksloNXlOGCutgaaqhH_BQd8,2881
8
- django_small_view_set-0.1.3.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
9
- django_small_view_set-0.1.3.dist-info/RECORD,,