Esta es una propuesta de arquitectura muy SLIM para API REST con Python usando Django.

Personalmente, me gusta el uso de Django REST. Al principio, hacia las cosas muy simples, pero cuando tiene una aplicación grande, esta forma de hacer las cosas simplemente no funcionaría.

Por lo tanto, propongo una forma nueva, escalable y conectable de hacer cosas basadas en la experiencia de 4Geeks en los últimos años usando Django Rest Framework.

Algunas de las decisiones que tomo para hacer esto se basan en los principios SOLID: https://en.wikipedia.org/wiki/SOLID_(object-oriented_design) especialmente separación de responsabilidades.

Propuesta SLIM multicapa

Entonces, mis capas serian estas:

  1. Capa de presentación: se utiliza para la traducción de HTTP a Python y Python a HTTP
  2. Capa de Autenticación y Autorización: para identificar al usuario y su información y permisos
  3. Capa de servicio: para ejecutar la lógica de negocios
  4. Capa de validación: para validar la integridad y la restricción de los datos que se almacenarán
  5. Capa de Datos

Capa de Presentación:

Representada por una URL de la función router y un ViewSet de DRF:ll:

urls.py

router.register(r'bank-account', BankAccountUserView)

views.py

class BankAccountUserView(viewsets.ViewSet):
    """
    Bank Account View for the Users
    """
    permission_classes = [permissions.IsAuthenticated, permissions.IsPilot]
    queryset = BankAccount.objects.all()
    serializer_class = BankAccountUserSerializer

    def create(self, request):
        service = BankAccountService()

        try:
            data = service.create(request.data, request.user)
        except Exception as e:
            return Response(str(e), status=status.HTTP_400_BAD_REQUEST)

        return Response(BankAccountUserSerializer(data, many=False).data, status=status.HTTP_201_CREATED)

    def list(self, request):
        service = BankAccountService()
        try:
            data = service.list(request.user)
        except Exception as e:
            return Response(str(e), status=status.HTTP_400_BAD_REQUEST)

        return Response(BankAccountUserSerializer(data, many=True).data, status=status.HTTP_200_OK)

    def retrieve(self, request, pk=None):
        obj = BankAccount.objects.filter(pk=pk).first()

        if obj is None:
            return Response("Not Found", status=status.HTTP_404_NOT_FOUND)

        serializer = BankAccountUserSerializer(obj)
        data = serializer.data
        return Response(data, status=status.HTTP_200_OK)

    def update(self, request, pk=None):
        service = BankAccountService()
        try:
            data = service.update(pk, request.data, request.user)
        except Exception as e:
            return Response(str(e), status=status.HTTP_400_BAD_REQUEST)

        return Response(BankAccountUserSerializer(data, many=False).data, status=status.HTTP_200_OK)

    def partial_update(self, request, pk=None):
        return self.update(request, pk)

    def destroy(self, request, pk=None):
        service = BankAccountService()
        try:
            service.delete(pk, request.user)
        except BankAccount.DoesNotExist as e:
            return Response(str(e), status=status.HTTP_404_NOT_FOUND)
        except Exception as e:
            return Response(str(e), status=status.HTTP_400_BAD_REQUEST)

        return Response("Deleted", status=status.HTTP_200_OK)

Una de las cosas buenas que me gusta de DRF es que la función del enrutador se combina con el ViewSet para la resolución de URLs y detalles:

GET /bank-account/ maps to list function

GET /bank-account/1/ maps to retrieve function

POST /bank-account/ maps to create function

y asi, mas información en: http://www.django-rest-framework.org/api-guide/viewsets/

(ademas este comportamiento se puede extender usando los decoradores detail_rout y list_route )

Notese que para la Serialización (Python to JSON)  use serpy library, una alternativa mas rapida a los serializadores de DRF

serializers.py

class BankAccountUserSerializer(serpy.Serializer):
    """
    Serializer for the Bank Account Model, for the User purposes
    """
    id = serpy.Field()
    bank_name = serpy.Field()
    number = serpy.Field()
    address = serpy.Field()
    holder_name = serpy.Field()
    swift = serpy.Field()
    aba = serpy.Field()
    phone_number = serpy.Field()
    user_id = serpy.MethodField()
    status = serpy.Field()

    def get_user_id(self, obj):
        if obj.user is not None:
            return obj.user.id

Authentication and Authorization Layer:

Django Rest makes a really good proposal using his permissions classes on a ViewSet level:

permissions.py

class TermsAccepted(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        if request.method in ['POST', 'PUT']:
            return request.user.is_accept_terms()
        return True

    def has_permission(self, request, view):
        if request.method in ['POST', 'PUT']:
            return request.user.is_accept_terms()
        else:
            return True

Es importante notar que, incluso si está implementando permisos de URL / ViewSet, eso no es suficiente y aquí es donde me empiezo a desviarme del DRF.

Si implementa funciones o métodos de Business Logi fuera de los Views, estos están desprotegidos, lo que significa que su aplicación ya no es extensible. Punto de dolor para DRF.

Capa de Servicio:

Para ejecutar Business Logic, elegimos crear services.py separados, donde puede organizar y combinar la lógica empresarial como desee.

Aquí se presenta un ejemplo complejo de transacciones de Base de Datos:

services.py

class BuyFlightServices:
    """
    Services for buying flights
    """

    def buy_as_vip(self, data: dict, user: User) -> PaymentHistory:
        """
        Buy a flight as a VIP Payment Method (only for VIP Users)
        This method fails if the Notification service is not available

        :param data: the flight data
        :param user: The user who performs the action
        :return: the Payment Information
        """
        if user is None or user.is_active is False:
            raise ValueError("A Valid and Active User must be provided")

        if user.is_vip is False:
            raise ValueError("Must be a VIP User to buy a Fligh as VIP")

        flight_id = data.get('flight_id', None)

        if flight_id is None:
            raise ValueError("The field 'flight_id' is required")

        with transaction.atomic():
            flight = Flight.objects.select_for_update().get(id=flight_id)

            if flight.disponibility not in ['di', 'rb']:
                raise ValueError("Flight not available: {}".format(flight.get_disponibility_display()))

            flight.buyer = user

            payment_service = PaymentHistoryServices()
            payment = payment_service.create_vip_payment(float(flight.price), user)

            flight.payment = payment
            flight.disponibility = PAGADO
            flight.save()

            flightPurchase.delay(payment.id)

        return payment

Observe el uso de errores, excepciones, transacciones y validación.

En una revisión posterior, esta validación debe ser del mismo uso en el nivel de ViewSet para mantenerse en el principio DRY.

Capa de Validación:

Para validar la integridad y la restricción de los datos que se almacenarán. Lo intenté antes que los serializadores DRF y Django Forms, ambos demasiado pesados para solo validar, así que probé la biblioteca de Cerberus y hasta ahora todo va bien.

validators.py

class PaymentHistoryVIPValidator:
    """
        Validator Class for VIP Payment
    """

    schema = {
        'buyer_id': {'type': 'integer', 'empty': False, 'nullable': False},
        'status': {'type': 'string', 'allowed': [i[0] for i in STATUS_PAYMENT]},
        'commission_id': {'type': 'integer', 'empty': False, 'nullable': False},
        'payerID': {'type': 'string', 'empty': True, 'nullable': True},
        'paymentId': {'type': 'string', 'empty': True, 'nullable': True},
        'paymentMethod': {'type': 'string', 'allowed': [i[0] for i in PAYMENT_METHOD], 'nullable': False},
        'typeTransaction': {'type': 'string', 'allowed': [i[0] for i in TYPE_TRANSACTION], 'nullable': False},
        'amount': {'type': 'number', 'empty': False, 'nullable': False},
        'liable': {'type': 'string', 'empty': True, 'nullable': True},
        'code': {'type': 'string', 'empty': False, 'nullable': False},
    }

    def __init__(self, data):
        self.validator = Validator()
        self.data = data
        self.schema = self.__class__.schema

    def validate(self):
        return self.validator.validate(self.data, self.schema)

    def errors(self):
        return self.validator.errors

Solo un Wrapper, para ser mejorado en la próxima iteración

Capa de Datos:

Capa de Orm.

Simplemente Django ORM (que me encanta)

return PaymentHistory.objects.create(**data)

Conclusiones:

 

  • Esto es solo un Borrador que estamos probando en la oficina para mejorar la extensión en nuestra base de código
  • Eliminamos casi todas las barreras que hemos encontrado hasta ahora usando DRF, pero estoy seguro de que generaremos algunas nuevas para conquistar.
  • Espero en un par de semanas mejorar todo esto.