Skip to content

Commit 51cd3d3

Browse files
committed
feat: ✨ simplifies the structure, update docs
1 parent 3d65e7d commit 51cd3d3

19 files changed

+567
-172
lines changed

arcstack_api/__init__.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
from .api import arcstack_api
22
from .decorators import api_endpoint
33
from .endpoint import Endpoint
4-
from .errors import APIError
4+
from .errors import APIError, InternalServerError, UnauthorizedError, ValidationError
55

66

77
# isort: off
88

99
__version__ = '0.0.0'
1010

11-
__all__ = ['arcstack_api', 'Endpoint', 'APIError', 'api_endpoint']
11+
__all__ = [
12+
'arcstack_api',
13+
'Endpoint',
14+
'APIError',
15+
'ValidationError',
16+
'UnauthorizedError',
17+
'InternalServerError',
18+
'api_endpoint',
19+
]

arcstack_api/api.py

+25-54
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,21 @@
88
from typing_extensions import Doc
99

1010
from .conf import settings
11-
from .errors import ValidationError
1211
from .logger import logger
12+
from .meta import ArcStackRequestMeta
1313
from .responses import InternalServerErrorResponse
14-
from .serializers import JsonSerializer
15-
from .signature import EndpointSignature
1614

1715

1816
class ArcStackAPI:
19-
_param_middleware: Annotated[
17+
_endpoint_middleware: Annotated[
2018
list[Callable],
2119
Doc(
2220
"""
2321
Middleware that will be called to build the endpoint kwargs.
2422
Should accept the following arguments:
2523
- request: The request object.
26-
- signature: The signature of the endpoint.
27-
- callback_args: The arguments of the callback.
28-
- callback_kwargs: The keyword arguments of the callback.
29-
Should return a tuple of (kwargs, response).
30-
- kwargs: The keyword arguments to pass to the endpoint. Must be a
31-
dictionary. `None` can be returned if there is nothing to add.
32-
- response: The response to return. If the response is not None,
33-
the execution will be terminated because there is probably a
34-
validation error.
24+
- meta: The ArcStackRequestMeta object.
25+
Should either return a new ArcStackRequestMeta object or None.
3526
"""
3627
),
3728
] = []
@@ -62,7 +53,7 @@ def __init__(self):
6253
self.load_middleware()
6354

6455
def load_middleware(self):
65-
self._param_middleware = []
56+
self._endpoint_middleware = []
6657
self._exception_middleware = []
6758
handler = self._get_response
6859
for middleware_path in reversed(settings.API_MIDDLEWARE):
@@ -85,73 +76,53 @@ def load_middleware(self):
8576
f'Middleware factory {middleware_path} returned None.'
8677
)
8778

88-
if hasattr(mw_instance, 'process_params'):
89-
self._param_middleware.insert(0, mw_instance.process_params)
79+
if hasattr(mw_instance, 'process_endpoint'):
80+
self._endpoint_middleware.insert(0, mw_instance.process_endpoint)
9081
if hasattr(mw_instance, 'process_exception'):
9182
self._exception_middleware.append(mw_instance.process_exception)
92-
9383
self._middleware_chain = handler
9484

9585
def __call__(self, endpoint: Callable):
96-
wrapper = self._create_wrapper(endpoint)
97-
return wrapper
86+
return self._create_wrapper(endpoint)
9887

9988
def _create_wrapper(self, endpoint: Callable):
10089
@wraps(endpoint)
10190
def wrapper(request, *args, **kwargs):
102-
request.endpoint = endpoint
103-
request.signature = EndpointSignature(endpoint)
91+
request._arcstack_meta = ArcStackRequestMeta(endpoint, args, kwargs)
10492

10593
try:
106-
response = self._middleware_chain(request, *args, **kwargs)
94+
response = self._middleware_chain(request)
10795
except Exception as e:
10896
response = self._process_exception(e, request)
10997

11098
return response
11199

112100
return wrapper
113101

114-
def _get_response(self, request, *args, **kwargs):
115-
if not hasattr(request, 'endpoint'):
102+
def _get_response(self, request):
103+
if not hasattr(request, '_arcstack_meta'):
116104
raise ImproperlyConfigured(
117-
'The request object must have an `endpoint` attribute. '
118-
'To endpointize a view, use the `api` decorator or '
105+
'The request object must have an `_arcstack_meta` attribute. '
106+
'To endpointify a view, use the `api_endpoint` decorator or '
119107
'subclass `Endpoint`.'
120108
)
121109

122-
response, endpoint_args, endpoint_kwargs = self._process_params(
123-
request, *args, **kwargs
124-
)
125-
126-
if response is None:
127-
response = request.endpoint(request, *endpoint_args, **endpoint_kwargs)
128-
129-
return response
110+
endpoint = request._arcstack_meta.endpoint
111+
args = request._arcstack_meta.args
112+
kwargs = request._arcstack_meta.kwargs
113+
del request._arcstack_meta
130114

131-
def _process_params(self, request, *args, **kwargs):
132-
"""Process the parameters of the request through the middleware."""
133115
response = None
134116

135-
validation_errors = []
136-
for middleware in self._param_middleware:
137-
try:
138-
args, kwargs = middleware(args, kwargs)
139-
except ValidationError as e:
140-
validation_errors.append(e)
141-
except Exception as e:
142-
response = self._process_exception(e, request)
117+
for middleware in self._endpoint_middleware:
118+
response = middleware(request, endpoint, *args, **kwargs)
119+
if response is not None:
120+
break
143121

144-
if validation_errors:
145-
response = HttpResponse(
146-
content=JsonSerializer.serialize(
147-
{
148-
'errors': [str(e) for e in validation_errors],
149-
}
150-
),
151-
status=400,
152-
)
122+
if response is None:
123+
response = endpoint(request, *args, **kwargs)
153124

154-
return response, args, kwargs
125+
return response
155126

156127
def _process_exception(
157128
self, exception: Exception, request: HttpRequest

arcstack_api/conf.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55

66
class ArcStackAPIConf(AppConf):
7-
MIDDLEWARE = ['arcstack_api.middleware.common.CommonMiddleware']
7+
MIDDLEWARE = ['arcstack_api.middleware.CommonMiddleware']
88

99
JSON_ENCODER = 'django.core.serializers.json.DjangoJSONEncoder'
1010

arcstack_api/constants.py

-10
This file was deleted.

arcstack_api/endpoint.py

+1-71
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,10 @@
1-
from asgiref.sync import markcoroutinefunction
21
from django.utils.decorators import classonlymethod
32
from django.views import View
43

54
from .api import arcstack_api
6-
from .conf import settings
7-
from .responses import MethodNotAllowedResponse, NotFoundResponse
85

96

107
class Endpoint(View):
118
@classonlymethod
129
def as_endpoint(cls, **initkwargs):
13-
"""Serve the view as an endpoint.
14-
15-
The `View` class is a base class for all Django class-based views.
16-
"""
17-
for key in initkwargs:
18-
if key in cls.http_method_names:
19-
raise TypeError(
20-
f'The method name {key} is not accepted as a keyword argument '
21-
f'to {cls.__name__}().'
22-
)
23-
if not hasattr(cls, key):
24-
raise TypeError(
25-
f'{cls.__name__}() received an invalid keyword {key}. '
26-
'as_endpoint only accepts arguments that are already '
27-
'attributes of the class.'
28-
)
29-
30-
def endpoint(request, *args, **kwargs):
31-
self = cls(**initkwargs)
32-
self.http_method = request.method.lower()
33-
34-
if 'setup_kwargs' in kwargs:
35-
self.setup(request, **kwargs['setup_kwargs'])
36-
del kwargs['setup_kwargs']
37-
else:
38-
self.setup(request, *args, **kwargs)
39-
40-
if self.http_method not in self.http_method_names:
41-
return MethodNotAllowedResponse()
42-
43-
return self.dispatch(request, *args, **kwargs)
44-
45-
endpoint.view_class = cls
46-
endpoint.view_initkwargs = initkwargs
47-
48-
# __name__ and __qualname__ are intentionally left unchanged as
49-
# view_class should be used to robustly determine the name of the view
50-
# instead.
51-
endpoint.__doc__ = cls.__doc__
52-
endpoint.__module__ = cls.__module__
53-
endpoint.__annotations__ = cls.dispatch.__annotations__
54-
# Copy possible attributes set by decorators, e.g. @csrf_exempt, from
55-
# the dispatch method.
56-
endpoint.__dict__.update(cls.dispatch.__dict__)
57-
58-
# Mark the callback if the view class is async.
59-
if cls.view_is_async:
60-
markcoroutinefunction(endpoint)
61-
62-
endpoint.LOGIN_REQUIRED = getattr(
63-
cls, 'LOGIN_REQUIRED', settings.API_DEFAULT_LOGIN_REQUIRED
64-
)
65-
66-
return arcstack_api(endpoint)
67-
68-
def setup(self, request, *args, **kwargs):
69-
pass
70-
71-
def dispatch(self, request, *args, **kwargs):
72-
handler = getattr(self, self.http_method, None)
73-
74-
if handler is None:
75-
return NotFoundResponse()
76-
77-
if 'method_args' in kwargs:
78-
return handler(**kwargs['method_args'])
79-
else:
80-
return handler(request, *args, **kwargs)
10+
return arcstack_api(cls.as_view(**initkwargs))

arcstack_api/errors.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
# pragma: exclude file
2+
3+
from typing import Any
4+
5+
16
class APIError(Exception):
27
"""Represents errors that should be returned as a response.
38
49
Classes that inherit from this class should modify the `status_code`
510
and `message` attributes.
611
"""
712

8-
def __init__(self, message: str, status_code: int = 400):
13+
def __init__(self, message: Any, status_code: int = 400):
914
self.message = message
1015
self.status_code = status_code
1116

arcstack_api/meta.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from collections.abc import Callable
2+
3+
from .signature import EndpointSignature
4+
5+
6+
class ArcStackRequestMeta:
7+
endpoint: Callable
8+
args: tuple
9+
kwargs: dict
10+
_signature: EndpointSignature = None
11+
12+
def __init__(self, endpoint: Callable, args: tuple, kwargs: dict):
13+
self.endpoint = endpoint
14+
self.args = args
15+
self.kwargs = kwargs
16+
17+
@property
18+
def signature(self) -> EndpointSignature:
19+
"""Lazy signature creation."""
20+
if self._signature is None:
21+
self._signature = EndpointSignature(self.endpoint)
22+
return self._signature

arcstack_api/middleware/common.py renamed to arcstack_api/middleware.py

+22-19
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
from django.http import HttpRequest, HttpResponse
22

3-
from ..conf import settings
4-
from ..errors import APIError, UnauthorizedError
5-
from ..responses import JsonResponse, UnauthorizedResponse
6-
from ..serializers import JsonSerializer
7-
from .mixin import MiddlewareMixin
3+
from .conf import settings
4+
from .errors import APIError, InternalServerError, UnauthorizedError
5+
from .mixins import MiddlewareMixin
6+
from .responses import InternalServerErrorResponse, UnauthorizedResponse
7+
from .serializers import JsonSerializer
88

99

1010
class CommonMiddleware(MiddlewareMixin):
11-
def process_request(self, request, *args, **kwargs):
12-
endpoint = getattr(request, 'endpoint', None)
11+
def process_request(self, request):
12+
meta = getattr(request, '_arcstack_meta', None)
1313

14-
if endpoint is None:
14+
if meta is None:
1515
# Not a valid request as an API endpoint
1616
return
1717

18-
self._check_login_required(request, endpoint)
18+
self._check_login_required(request, meta.endpoint)
1919

2020
return None
2121

22-
def process_response(self, request, response, *args, **kwargs):
22+
def process_response(self, request, response):
2323
if isinstance(response, HttpResponse):
2424
# noop: The response is already an HttpResponse
2525
pass
@@ -30,10 +30,16 @@ def process_response(self, request, response, *args, **kwargs):
3030
or isinstance(response, bool)
3131
):
3232
# Convert the response to a string and return it as an HttpResponse
33-
response = HttpResponse(content=f'{response}')
33+
response = HttpResponse(
34+
content=f'{response}',
35+
content_type='text/plain',
36+
)
3437
elif JsonSerializer.is_json_serializable(response):
3538
data = JsonSerializer.serialize(response)
36-
response = HttpResponse(content=data, content_type='application/json')
39+
response = HttpResponse(
40+
content=data,
41+
content_type='application/json',
42+
)
3743
else:
3844
raise ValueError(f'Unsupported response type: {type(response)}')
3945

@@ -45,15 +51,12 @@ def process_exception(
4551
response = None
4652

4753
if isinstance(exception, APIError):
48-
response = JsonResponse(
49-
{
50-
'error': exception.message,
51-
'status': exception.status_code,
52-
},
53-
status=exception.status_code,
54-
)
54+
response = self.process_response(request, exception.message)
55+
response.status_code = exception.status_code
5556
elif isinstance(exception, UnauthorizedError):
5657
response = UnauthorizedResponse()
58+
elif isinstance(exception, InternalServerError):
59+
response = InternalServerErrorResponse()
5760

5861
# Not an exception defined in the ArcStack API.
5962
# Let the other middleware handle it.

arcstack_api/middleware/__init__.py

Whitespace-only changes.

arcstack_api/middleware/mixin.py renamed to arcstack_api/mixins.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@ def __repr__(self):
1313
)
1414
return f'<{qualname} get_response={get_response_qualname}>'
1515

16-
def __call__(self, request, *args, **kwargs):
16+
def __call__(self, request):
1717
response = None
1818

1919
if hasattr(self, 'process_request'):
20-
response = self.process_request(request, *args, **kwargs)
20+
response = self.process_request(request)
2121

22-
response = response or self.get_response(request, *args, **kwargs)
22+
response = response or self.get_response(request)
2323

2424
if hasattr(self, 'process_response'):
25-
response = self.process_response(request, response, *args, **kwargs)
25+
response = self.process_response(request, response)
2626

2727
return response

0 commit comments

Comments
 (0)