From f4dbf98c1bdb08985dbb97be9b0517208805dbc7 Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Fri, 13 Aug 2021 17:26:35 +0300 Subject: [PATCH] Remove random python files & update location to `Styleguide-Example` --- README.md | 12 ++- queryset_type.py | 71 ---------------- utils.py | 206 ----------------------------------------------- 3 files changed, 9 insertions(+), 280 deletions(-) delete mode 100644 queryset_type.py delete mode 100644 utils.py diff --git a/README.md b/README.md index 75e17f7..8538e95 100644 --- a/README.md +++ b/README.md @@ -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 @@ -832,7 +832,7 @@ class CourseCreateApi( 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 @@ -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. +**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 In our Django projects, we split our tests depending on the type of code they represent. diff --git a/queryset_type.py b/queryset_type.py deleted file mode 100644 index 89ad656..0000000 --- a/queryset_type.py +++ /dev/null @@ -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]': ... diff --git a/utils.py b/utils.py deleted file mode 100644 index ee84271..0000000 --- a/utils.py +++ /dev/null @@ -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