Porto is a modern software architectural pattern, consisting of guidelines, principles and patterns to help developers organize their code in a highly maintainable and reusable way.
See more about Porto
PyPorto is an implementing Porto architectural pattern for DRF
Why do we need to use PyPorto?
Usually in Django or DRF we have business logic in next files
views/my_view.py
serializers/some_serializer.py
models/my_model.py
services/my_business_logic_n1.py
It is usefull for Minimum Viable Product (MVP) projects, but if you are going to split your project into a microservice architecture (in the future), you will have to deal with wild connection in your business logic, also you can't extract the business logic layer from the framework layer.
Сomes to the help Porto and PyPorto
See flake8 plugin flake8-pyporto
dj/
- holds all configurations for your Django project.
core/
- holds the infrastructure code (your shared code between all Containers).
containers/
- holds all your application and business logic code.
manage.py
- command-line utility for django.
These layers core
and containers
can be created anywhere inside Django framework.
Every Container consists of a number of Components, in Porto one Component can be built from these types of components:
Main Components
and Framework Components
.
actions/
- holds all actions.
subactions/
- holds all subactions.
tasks/
handlers/
- holds all handlers. Why handlers is better than tasks?
repositories/
- holds all repositories.
dto/
- holds all Data Transfer Objects (it does not include in Porto, it is new for PyPorto).
types/
- holds all types.
values/
- holds all values.
entities/
- holds all entities.
constants/
- holds all constants.
According to Porto every small part of the logic should name a task
, but usually Django project has Celery Task Queue
, see more about: Celery, so we need to avoid naming conflicts.
api (urls, views)/
- holds all urls and views.
migrations/
- holds all migrations.
models/
- holds all models.
paginations/
- holds all paginations.
serializers/
- holds all serializers.
tests/
- holds all tests.
You can make new components, if you need it.
Click on the arrows below to read about each component.
Data Transfer Object (DTO)
Data Transfer Object (DTO)
DTO is used for data transport between layers. DTO is an immutable object, it does not have ID's, it does not have any logic.
Controller/View -> DTO -> Action
Usually we have something that in DRF view:
from mainapp.services.my_serivice import get_all_products
def get(self, request, *args, **kwargs):
product_list = get_all_products(
user_id=request.user.id,
filter_by={'price__gte': 20},
sort_by='id',
... # More other args.
)
serializer = ProductListSerializer(product_list, many=True)
return Response(serializer.data)
You need to collect all arguments with DTO
and send them as one argument to the business logic:
from dataclasses import dataclass
from core.parents.dto.dto import DTO
@dataclass(init=True, eq=True, frozen=True)
class GetProductListDTO(DTO):
user_id: int
filter_by: dict
sort_by: str
... # More other fields.
params = GetProductListDTO(
user_id=request.user.id,
filter_by={'price__gte': 20},
sort_by='id',
...
)
product_list = get_all_products(params)
serializer = ProductListSerializer(product_list, many=True)
return Response(serializer.data)
Bad idea to pass all request
to DTO
instead of to pass request.user.id
or request.user.is_authenticated
, why? because if you want to debug that part of business logic in console (for example: django shell), you have to build all request:
Bad idea:
def get(self, request, *args, **kwargs):
# WRONG! You pass all HttpRequest objects, lika that [method, content_type, GET, POST, ...].
# and you will have to build it during debugging or testing it.
params = GetProductListDTO(
request=request,
)
...
Good idea:
def get(self, request, *args, **kwargs):
# It's even easy to test and debug outside of the framework environment, because you have only int and bool types.
params = GetProductListDTO(
user_id=request.user.id, # You pass only int.
user_is_authenticated=request.user.is_authenticated # You pass only bool.
...
)
...
app
├── dj
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ ├── asgi.py
│ └── wsgi.py
├── core
│ ├── __init__.py
│ ├── configs
│ │ ├── __init__.py
│ │ └── ...
│ ├── helpers
│ │ ├── __init__.py
│ │ └── ...
│ └── parents
│ │ ├── __init__.py
│ │ ├── actions
│ │ │ ├── __init__.py
│ │ │ ├── subaction.py
│ │ │ └── action.py
│ │ ├── entities
│ │ │ ├── __init__.py
│ │ │ └── entity.py
│ │ ├── dto
│ │ │ ├── __init__.py
│ │ │ └── dto.py
│ │ ├── repositories
│ │ │ ├── __init__.py
│ │ │ └── repository.py
│ │ ├── handlers
│ │ │ ├── __init__.py
│ │ │ └── handler.py
│ │ ├── values
│ │ │ ├── __init__.py
│ │ │ └── value.py
├── containers
│ ├── __init__.py
│ ├── container_1
│ │ ├── actions
│ │ │ ├── __init__.py
│ │ │ ├── get_product_list_action.py
│ │ │ └── retrieve_product_action.py
│ │ ├── api
│ │ │ ├── __init__.py
│ │ │ ├── urls
│ │ │ │ ├── __init__.py
│ │ │ │ ├── product_list_v1.py
│ │ │ │ └── product_retrieve_v1.py
│ │ │ ├── views
│ │ │ │ ├── __init__.py
│ │ │ │ ├── product_list_view.py
│ │ │ │ └── product_retrieve_view.py
│ │ ├── dto
│ │ │ ├── __init__.py
│ │ │ ├── product_list_dto.py
│ │ │ └── product_retrieve_dto.py
│ │ ├── entities
│ │ │ ├── __init__.py
│ │ │ └── product_entity.py
│ │ ├── migrations
│ │ │ ├── __init__.py
│ │ │ └── ...
│ │ ├── models
│ │ │ ├── __init__.py
│ │ │ └── product.py
│ │ ├── paginations
│ │ │ ├── __init__.py
│ │ │ └── product_list_pagination.py
│ │ ├── repositories
│ │ │ ├── __init__.py
│ │ │ └── product_repository.py
│ │ ├── serializers
│ │ │ ├── __init__.py
│ │ │ └── product_list_serializer.py
│ │ ├── handlers
│ │ │ ├── __init__.py
│ │ │ ├── get_product_list_handler.py
│ │ │ └── retrieve_product_handler.py
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ └── test_get_product_list.py
│ │ ├── types
│ │ │ ├── __init__.py
│ │ │ └── product_id_type.py
│ │ ├── values
│ │ │ ├── __init__.py
│ │ │ └── product_list_row.py
│ │ ├── constants
│ │ │ ├── __init__.py
│ │ │ │ const_x.py
│ │ │ └── const_y.py
│ │ ├── __init__.py
│ │ └── apps.py
│ ├── container_2
│ │ ├── actions
│ │ │ ├── __init__.py
│ │ │ └── ...
│ │ ├── api
│ │ │ ├── __init__.py
│ │ │ ├── urls
│ │ │ │ ├── __init__.py
│ │ │ │ └── ...
│ │ │ ├── views
│ │ │ │ ├── __init__.py
│ │ │ │ └── ...
│ │ ├── dto
│ │ │ ├── __init__.py
│ │ │ └── ...
│ │ ├── entities
│ │ │ └── ...
│ │ ├── handlers
│ │ │ ├── __init__.py
│ │ │ └── ...
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ └── ...
│ │ └── __init__.py
│ ├── foo_section
│ │ ├── foo
│ │ │ ├── api
│ │ │ │ ├── __init__.py
│ │ │ │ ├── urls
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── ...
│ │ │ │ ├── views
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── ...
│ │ │ ├── actions
│ │ │ │ ├── __init__.py
│ │ │ │ └── ...
│ │ │ ├── handlers
│ │ │ │ ├── __init__.py
│ │ │ │ └── ...
│ │ │ ├── tests
│ │ │ │ ├── __init__.py
│ │ │ │ └── ...
│ │ │ └── __init__.py
│ │ ├── bar
│ │ │ ├── api
│ │ │ │ ├── __init__.py
│ │ │ │ ├── urls
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── ...
│ │ │ │ ├── views
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── ...
│ │ │ ├── actions
│ │ │ │ ├── __init__.py
│ │ │ │ └── ...
│ │ │ ├── handlers
│ │ │ │ ├── __init__.py
│ │ │ │ └── ...
│ │ │ ├── tests
│ │ │ │ ├── __init__.py
│ │ │ │ └── ...
│ │ │ └── __init__.py
│ │ └── apps.py
├── static
│ └── ...
├── media
│ └── ...
└──manage.py
Your feedback is important.
For feedbacks, questions, or suggestions? We are on telegram.