Remove random python files & update location to Styleguide-Example

This commit is contained in:
Radoslav Georgiev 2021-08-13 17:26:35 +03:00
parent ef2f696e17
commit f4dbf98c1b
No known key found for this signature in database
GPG Key ID: 0B7753A4DFCE646D
3 changed files with 9 additions and 280 deletions

View File

@ -642,7 +642,7 @@ class Serializer(serializers.Serializer):
}) })
``` ```
The implementation of `inline_serializer` can be found in `utils.py` in this repo. The implementation of `inline_serializer` can be found [here](https://github.com/HackSoftware/Styleguide-Example/blob/master/styleguide_example/common/utils.py#L34), in the [Styleguide-Example](https://github.com/HackSoftware/Styleguide-Example) repo.
## Urls ## Urls
@ -832,7 +832,7 @@ class CourseCreateApi(
return Response(status=status.HTTP_201_CREATED) return Response(status=status.HTTP_201_CREATED)
``` ```
All of the code above can be found in `utils.py` in this repository. All of the code above can be found in [here](https://github.com/HackSoftware/Styleguide-Example/blob/master/styleguide_example/api/mixins.py#L70), in the [Styleguide-Example](https://github.com/HackSoftware/Styleguide-Example) repo.
### Error formatting ### Error formatting
@ -918,10 +918,16 @@ REST_FRAMEWORK = {
} }
``` ```
**The magic happens in the `ErrorsFormatter` class.** The implementation of that class can be found in the `utils.py` file, located in that repo. **The magic happens in the `ErrorsFormatter` class.**
The implementation of that class can be found [here](https://github.com/HackSoftware/Styleguide-Example/blob/master/styleguide_example/api/errors.py), in the [Styleguide-Example](https://github.com/HackSoftware/Styleguide-Example) repo.
Combining `ApiErrorsMixin`, the custom exception handler & the errors formatter class, we can have predictable behavior in our APIs, when it comes to errors. Combining `ApiErrorsMixin`, the custom exception handler & the errors formatter class, we can have predictable behavior in our APIs, when it comes to errors.
**A note:**
> We've moved away from this particular way of formatting errors & we'll be updating the styleguide with a more generic approach.
## Testing ## Testing
In our Django projects, we split our tests depending on the type of code they represent. In our Django projects, we split our tests depending on the type of code they represent.

View File

@ -1,71 +0,0 @@
from typing import (
Generic,
Iterator,
Any,
TypeVar,
Optional,
Dict,
Tuple,
Union
)
from collections import Iterable
DjangoModel = TypeVar('DjangoModel')
class QuerySetType(Generic[DjangoModel], extra=Iterable):
"""
This type represents django.db.models.QuerySet interface.
Defined Types:
DjangoModel - model instance
QuerysetType[DjangoModel] - Queryset of DjangoModel instances
Iterator[DjangoModel] - Iterator of DjangoModel instances
"""
def __iter__(self) -> Iterator[DjangoModel]: ...
def all(self) -> 'QuerySetType[DjangoModel]': ...
def order_by(self, *args: Any) -> 'QuerySetType[DjangoModel]': ...
def count(self) -> int: ...
def filter(self, **kwargs: Any) -> 'QuerySetType[DjangoModel]': ...
def exclude(self, **kwargs: Any) -> 'QuerySetType[DjangoModel]': ...
def get(self, **kwargs: Any) -> DjangoModel: ...
def annotate(self, **kwargs: Any) -> 'QuerySetType[DjangoModel]': ...
def first(self) -> Optional[DjangoModel]: ...
def update(self, **kwargs: Any) -> DjangoModel: ...
def delete(self, **kwargs: Any) -> Tuple[int, Dict[str, int]]: ...
def last(self) -> Optional[DjangoModel]: ...
def exists(self) -> bool: ...
def values(self, *args: Any) -> 'QuerySetType[DjangoModel]': ...
def values_list(self, *args: Any) -> 'QuerySetType[DjangoModel]': ...
def __getitem__(
self,
index: int
) -> Union[DjangoModel, "QuerySetType[DjangoModel]"]: ...
def __len__(self) -> int: ...
def __or__(
self,
qs: "QuerySetType[DjangoModel]"
) -> 'QuerySetType[DjangoModel]': ...
def __and__(
self,
qs: "QuerySetType[DjangoModel]"
) -> 'QuerySetType[DjangoModel]': ...

206
utils.py
View File

@ -1,206 +0,0 @@
from rest_framework import serializers
from rest_framework import exceptions as rest_exceptions
from rest_framework.views import exception_handler
from rest_framework.settings import api_settings
from rest_framework import exceptions
from django.core.exceptions import ValidationError
def create_serializer_class(name, fields):
return type(name, (serializers.Serializer, ), fields)
def inline_serializer(*, fields, data=None, **kwargs):
serializer_class = create_serializer_class(name='', fields=fields)
if data is not None:
return serializer_class(data=data, **kwargs)
return serializer_class(**kwargs)
def get_first_matching_attr(obj, *attrs, default=None):
for attr in attrs:
if hasattr(obj, attr):
return getattr(obj, attr)
return default
def get_error_message(exc):
if hasattr(exc, 'message_dict'):
return exc.message_dict
error_msg = get_first_matching_attr(exc, 'message', 'messages')
if isinstance(error_msg, list):
error_msg = ', '.join(error_msg)
if error_msg is None:
error_msg = str(exc)
return error_msg
class ApiErrorsMixin:
"""
Mixin that transforms Django and Python exceptions into rest_framework ones.
without the mixin, they return 500 status code which is not desired.
"""
expected_exceptions = {
ValueError: rest_exceptions.ValidationError,
ValidationError: rest_exceptions.ValidationError,
PermissionError: rest_exceptions.PermissionDenied
}
def handle_exception(self, exc):
if isinstance(exc, tuple(self.expected_exceptions.keys())):
drf_exception_class = self.expected_exceptions[exc.__class__]
drf_exception = drf_exception_class(get_error_message(exc))
return super().handle_exception(drf_exception)
return super().handle_exception(exc)
class ErrorsFormatter:
"""
The current formatter gets invalid serializer errors,
uses DRF standart for code and messaging
and then parses it to the following format:
{
"errors": [
{
"message": "Error message",
"code": "Some code",
"field": "field_name"
},
{
"message": "Error message",
"code": "Some code",
"field": "nested.field_name"
},
...
]
}
"""
FIELD = 'field'
MESSAGE = 'message'
CODE = 'code'
ERRORS = 'errors'
def __init__(self, exception):
self.exception = exception
def __call__(self):
if hasattr(self.exception, 'get_full_details'):
formatted_errors = self._get_response_json_from_drf_errors(
serializer_errors=self.exception.get_full_details()
)
else:
formatted_errors = self._get_response_json_from_error_message(message=str(self.exception))
return formatted_errors
def _get_response_json_from_drf_errors(self, serializer_errors=None):
if serializer_errors is None:
serializer_errors = {}
if type(serializer_errors) is list:
serializer_errors = {
api_settings.NON_FIELD_ERRORS_KEY: serializer_errors
}
list_of_errors = self._get_list_of_errors(errors_dict=serializer_errors)
response_data = {
self.ERRORS: list_of_errors
}
return response_data
def _get_response_json_from_error_message(self, *, message='', field=None, code='error'):
response_data = {
self.ERRORS: [
{
self.MESSAGE: message,
self.CODE: code
}
]
}
if field:
response_data[self.ERRORS][self.FIELD] = field
return response_data
def _unpack(self, obj):
if type(obj) is list and len(obj) == 1:
return obj[0]
return obj
def _get_list_of_errors(self, field_path='', errors_dict=None):
"""
Error_dict is in the following format:
{
'field1': {
'message': 'some message..'
'code' 'some code...'
},
'field2: ...'
}
"""
if errors_dict is None:
return []
message_value = errors_dict.get(self.MESSAGE, None)
# Note: If 'message' is name of a field we don't want to stop the recursion here!
if message_value is not None and\
(type(message_value) in {str, exceptions.ErrorDetail}):
if field_path:
errors_dict[self.FIELD] = field_path
return [errors_dict]
errors_list = []
for key, value in errors_dict.items():
new_field_path = '{0}.{1}'.format(field_path, key) if field_path else key
key_is_non_field_errors = key == api_settings.NON_FIELD_ERRORS_KEY
if type(value) is list:
current_level_error_list = []
new_value = value
for error in new_value:
# if the type of field_error is list we need to unpack it
field_error = self._unpack(error)
if not key_is_non_field_errors:
field_error[self.FIELD] = new_field_path
current_level_error_list.append(field_error)
else:
path = field_path if key_is_non_field_errors else new_field_path
current_level_error_list = self._get_list_of_errors(field_path=path, errors_dict=value)
errors_list += current_level_error_list
return errors_list
def exception_errors_format_handler(exc, context):
response = exception_handler(exc, context)
# If unexpected error occurs (server error, etc.)
if response is None:
return response
formatter = ErrorsFormatter(exc)
response.data = formatter()
return response