Apply prettier to the README

This commit is contained in:
Martin Angelov 2022-10-30 11:23:24 +02:00
parent 9c763eec2f
commit 791be866b6

276
README.md
View File

@ -9,7 +9,6 @@
---
**Table of contents:**
<!-- toc -->
@ -20,58 +19,58 @@
- [Why not?](#why-not)
- [Cookie Cutter](#cookie-cutter)
- [Models](#models)
* [Base model](#base-model)
* [Validation - `clean` and `full_clean`](#validation---clean-and-full_clean)
* [Validation - constraints](#validation---constraints)
* [Properties](#properties)
* [Methods](#methods)
* [Testing](#testing)
- [Base model](#base-model)
- [Validation - `clean` and `full_clean`](#validation---clean-and-full_clean)
- [Validation - constraints](#validation---constraints)
- [Properties](#properties)
- [Methods](#methods)
- [Testing](#testing)
- [Services](#services)
* [Example - function-based service](#example---function-based-service)
* [Example - class-based service](#example---class-based-service)
* [Naming convention](#naming-convention)
* [Modules](#modules)
* [Selectors](#selectors)
* [Testing](#testing-1)
- [Example - function-based service](#example---function-based-service)
- [Example - class-based service](#example---class-based-service)
- [Naming convention](#naming-convention)
- [Modules](#modules)
- [Selectors](#selectors)
- [Testing](#testing-1)
- [APIs & Serializers](#apis--serializers)
* [Naming convention](#naming-convention-1)
* [Class-based vs. Function-based](#class-based-vs-function-based)
* [List APIs](#list-apis)
+ [Plain](#plain)
+ [Filters + Pagination](#filters--pagination)
* [Detail API](#detail-api)
* [Create API](#create-api)
* [Update API](#update-api)
* [Fetching objects](#fetching-objects)
* [Nested serializers](#nested-serializers)
* [Advanced serialization](#advanced-serialization)
- [Naming convention](#naming-convention-1)
- [Class-based vs. Function-based](#class-based-vs-function-based)
- [List APIs](#list-apis)
- [Plain](#plain)
- [Filters + Pagination](#filters--pagination)
- [Detail API](#detail-api)
- [Create API](#create-api)
- [Update API](#update-api)
- [Fetching objects](#fetching-objects)
- [Nested serializers](#nested-serializers)
- [Advanced serialization](#advanced-serialization)
- [Urls](#urls)
- [Settings](#settings)
* [Prefixing environment variables with `DJANGO_`](#prefixing-environment-variables-with-django_)
* [Integrations](#integrations)
* [Reading from `.env`](#reading-from-env)
- [Prefixing environment variables with `DJANGO_`](#prefixing-environment-variables-with-django_)
- [Integrations](#integrations)
- [Reading from `.env`](#reading-from-env)
- [Errors & Exception Handling](#errors--exception-handling)
* [How exception handling works (in the context of DRF)](#how-exception-handling-works-in-the-context-of-drf)
+ [DRF's `ValidationError`](#drfs-validationerror)
+ [Django's `ValidationError`](#djangos-validationerror)
* [Describe how your API errors are going to look like.](#describe-how-your-api-errors-are-going-to-look-like)
* [Know how to change the default exception handling behavior.](#know-how-to-change-the-default-exception-handling-behavior)
* [Approach 1 - Use DRF's default exceptions, with very little modifications.](#approach-1---use-drfs-default-exceptions-with-very-little-modifications)
* [Approach 2 - HackSoft's proposed way](#approach-2---hacksofts-proposed-way)
* [More ideas](#more-ideas)
- [How exception handling works (in the context of DRF)](#how-exception-handling-works-in-the-context-of-drf)
- [DRF's `ValidationError`](#drfs-validationerror)
- [Django's `ValidationError`](#djangos-validationerror)
- [Describe how your API errors are going to look like.](#describe-how-your-api-errors-are-going-to-look-like)
- [Know how to change the default exception handling behavior.](#know-how-to-change-the-default-exception-handling-behavior)
- [Approach 1 - Use DRF's default exceptions, with very little modifications.](#approach-1---use-drfs-default-exceptions-with-very-little-modifications)
- [Approach 2 - HackSoft's proposed way](#approach-2---hacksofts-proposed-way)
- [More ideas](#more-ideas)
- [Testing](#testing-2)
* [Naming conventions](#naming-conventions)
- [Naming conventions](#naming-conventions)
- [Celery](#celery)
* [The basics](#the-basics)
* [Error handling](#error-handling)
* [Configuration](#configuration)
* [Structure](#structure)
* [Periodic Tasks](#periodic-tasks)
* [Beyond](#beyond)
- [The basics](#the-basics)
- [Error handling](#error-handling)
- [Configuration](#configuration)
- [Structure](#structure)
- [Periodic Tasks](#periodic-tasks)
- [Beyond](#beyond)
- [Cookbook](#cookbook)
* [Handling updates with a service](#handling-updates-with-a-service)
- [Handling updates with a service](#handling-updates-with-a-service)
- [DX (Developer Experience)](#dx-developer-experience)
* [`mypy` / type annotations](#mypy--type-annotations)
- [`mypy` / type annotations](#mypy--type-annotations)
- [Django Styleguide in the Wild](#django-styleguide-in-the-wild)
- [Additional resources](#additional-resources)
- [Inspiration](#inspiration)
@ -116,24 +115,24 @@ The core of the Django Styleguide can be summarized as follows:
**In Django, business logic should live in:**
* Services - functions, that mostly take care of writing things to the database.
* Selectors - functions, that mostly take care of fetching things from the database.
* Model properties (with some exceptions).
* Model `clean` method for additional validations (with some exceptions).
- Services - functions, that mostly take care of writing things to the database.
- Selectors - functions, that mostly take care of fetching things from the database.
- Model properties (with some exceptions).
- Model `clean` method for additional validations (with some exceptions).
**In Django, business logic should not live in:**
* APIs and Views.
* Serializers and Forms.
* Form tags.
* Model `save` method.
* Custom managers or querysets.
* Signals.
- APIs and Views.
- Serializers and Forms.
- Form tags.
- Model `save` method.
- Custom managers or querysets.
- Signals.
**Model properties vs selectors:**
* If the property spans multiple relations, it should better be a selector.
* If the property is non-trivial & can easily cause `N + 1` queries problem, when serialized, it should better be a selector.
- If the property spans multiple relations, it should better be a selector.
- If the property is non-trivial & can easily cause `N + 1` queries problem, when serialized, it should better be a selector.
The general idea is to "separate concerns" so those concerns can be maintainable / testable.
@ -196,9 +195,9 @@ We recommend starting every new project with some kind of cookiecutter. Having t
Few examples:
* You can use the [`Styleguide-Example`](https://github.com/HackSoftware/Styleguide-Example) project as a starting point.
* You can also use [`cookiecutter-django`](https://github.com/pydanny/cookiecutter-django) since it has a ton of good stuff inside.
* Or you can create something that works for your case & turn it into a [cookiecutter](https://cookiecutter.readthedocs.io/en/latest/) project.
- You can use the [`Styleguide-Example`](https://github.com/HackSoftware/Styleguide-Example) project as a starting point.
- You can also use [`cookiecutter-django`](https://github.com/pydanny/cookiecutter-django) since it has a ton of good stuff inside.
- Or you can create something that works for your case & turn it into a [cookiecutter](https://cookiecutter.readthedocs.io/en/latest/) project.
## Models
@ -715,8 +714,8 @@ If we take the example above, our service is named `user_create`. The pattern is
This is what we prefer in HackSoft's projects. This seems odd at first, but it has few nice features:
* **Namespacing.** It's easy to spot all services starting with `user_` and it's a good idea to put them in a `users.py` module.
* **Greppability.** Or in other words, if you want to see all actions for a specific entity, just grep for `user_`.
- **Namespacing.** It's easy to spot all services starting with `user_` and it's a good idea to put them in a `users.py` module.
- **Greppability.** Or in other words, if you want to see all actions for a specific entity, just grep for `user_`.
### Modules
@ -780,12 +779,12 @@ If you decide to cover the service layer with tests, we have few general rules o
When creating the required state for a given test, one can use a combination of:
* Fakes (We recommend using [`faker`](https://github.com/joke2k/faker))
* Other services, to create the required objects.
* Special test utility & helper methods.
* Factories (We recommend using [`factory_boy`](https://factoryboy.readthedocs.io/en/latest/orms.html))
* Plain `Model.objects.create()` calls, if factories are not yet introduced in the project.
* Usually, whatever suits you better.
- Fakes (We recommend using [`faker`](https://github.com/joke2k/faker))
- Other services, to create the required objects.
- Special test utility & helper methods.
- Factories (We recommend using [`factory_boy`](https://factoryboy.readthedocs.io/en/latest/orms.html))
- Plain `Model.objects.create()` calls, if factories are not yet introduced in the project.
- Usually, whatever suits you better.
**Let's take a look at our service from the example:**
@ -819,9 +818,9 @@ def item_buy(
The service:
* Calls a selector for validation.
* Creates an object.
* Delays a task.
- Calls a selector for validation.
- Creates an object.
- Delays a task.
**Those are our tests:**
@ -879,31 +878,31 @@ When using services & selectors, all of your APIs should look simple & identical
**When we are creating new APIs, we follow those general rules:**
* Have 1 API per operation. This means, for CRUD on a model, having 4 APIs.
* Inherit from the most simple `APIView` or `GenericAPIView`.
* Avoid the more abstract classes, since they tend to manage things via serializers & we want to do that via services & selectors.
* **Don't do business logic in your API.**
* You can do **object fetching / data manipulation in your APIs** (potentially, you can extract that to somewhere else).
* If you are calling `some_service` in your API, you can extract object fetching / data manipulation to `some_service_parse`.
* Basically, keep the APIs are simple as possible. They are an interface towards your core business logic.
- Have 1 API per operation. This means, for CRUD on a model, having 4 APIs.
- Inherit from the most simple `APIView` or `GenericAPIView`.
- Avoid the more abstract classes, since they tend to manage things via serializers & we want to do that via services & selectors.
- **Don't do business logic in your API.**
- You can do **object fetching / data manipulation in your APIs** (potentially, you can extract that to somewhere else).
- If you are calling `some_service` in your API, you can extract object fetching / data manipulation to `some_service_parse`.
- Basically, keep the APIs are simple as possible. They are an interface towards your core business logic.
When we are talking about APIs, we need a way to deal with data serialization - both incoming & outgoing data.
**Here are our rules for API serialization:**
* There should be a dedicated **input serializer** & a dedicated **output serializer**.
* **Input serializer** takes care of the data coming in.
* **Output serializer** takes care of the data coming out.
* In terms of serialization, Use whatever abstraction works for you.
- There should be a dedicated **input serializer** & a dedicated **output serializer**.
- **Input serializer** takes care of the data coming in.
- **Output serializer** takes care of the data coming out.
- In terms of serialization, Use whatever abstraction works for you.
**In case you are using DRF's serializers, here are our rules:**
* Serializer should be **nested in the API** and be named either `InputSerializer` or `OutputSerializer`.
* Our preference is for both serializers to inherit from the simpler `Serializer` and avoid using `ModelSerializer`
* This is a matter of preference and choice. If `ModelSerializer` is working fine for you, use it.
* If you need a nested serializer, use the `inline_serializer` util.
* Reuse serializers as little as possible.
* Reusing serializers may expose you to unexpected behavior, when something changes in the base serializers.
- Serializer should be **nested in the API** and be named either `InputSerializer` or `OutputSerializer`.
- Our preference is for both serializers to inherit from the simpler `Serializer` and avoid using `ModelSerializer`
- This is a matter of preference and choice. If `ModelSerializer` is working fine for you, use it.
- If you need a nested serializer, use the `inline_serializer` util.
- Reuse serializers as little as possible.
- Reusing serializers may expose you to unexpected behavior, when something changes in the base serializers.
### Naming convention
@ -975,7 +974,7 @@ class UserListApi(APIView):
return Response(data)
```
*Keep in mind this API is public by default. Authentication is up to you.*
_Keep in mind this API is public by default. Authentication is up to you._
#### Filters + Pagination
@ -1580,9 +1579,7 @@ def some_service():
The response payload is going to look like this:
```json
[
"Some message"
]
["Some message"]
```
This looks strange, because if we do it like this:
@ -1817,9 +1814,7 @@ Response:
```json
{
"detail": {
"non_field_errors": [
"Some error message"
]
"non_field_errors": ["Some error message"]
}
}
```
@ -1875,9 +1870,7 @@ Response:
```json
{
"detail": [
"Some error message"
]
"detail": ["Some error message"]
}
```
@ -1930,17 +1923,13 @@ Response:
```json
{
"detail": {
"foo": [
"This field is required."
],
"foo": ["This field is required."],
"email": [
"Ensure this field has at least 200 characters.",
"Enter a valid email address."
],
"nested": {
"bar": [
"This field is required."
]
"bar": ["This field is required."]
}
}
}
@ -1981,12 +1970,8 @@ Response:
```json
{
"detail": {
"password": [
"This field cannot be blank."
],
"email": [
"This field cannot be blank."
]
"password": ["This field cannot be blank."],
"email": ["This field cannot be blank."]
}
}
```
@ -2024,14 +2009,10 @@ For example, whenerver we have a `ValidationError` (usually coming from a Serial
"message": "Validation error.",
"extra": {
"fields": {
"password": [
"This field cannot be blank."
],
"email": [
"This field cannot be blank."
]
"password": ["This field cannot be blank."],
"email": ["This field cannot be blank."]
}
}
},
}
```
@ -2123,6 +2104,7 @@ Response:
}
}
```
---
Code:
@ -2139,9 +2121,7 @@ Response:
"message": "Validation error",
"extra": {
"fields": {
"non_field_errors": [
"Some error message"
]
"non_field_errors": ["Some error message"]
}
}
}
@ -2202,9 +2182,7 @@ Response:
{
"message": "Validation error",
"extra": {
"fields": [
"Some error message"
]
"fields": ["Some error message"]
}
}
```
@ -2263,17 +2241,13 @@ Response:
"message": "Validation error",
"extra": {
"fields": {
"foo": [
"This field is required."
],
"foo": ["This field is required."],
"email": [
"Ensure this field has at least 200 characters.",
"Enter a valid email address."
],
"nested": {
"bar": [
"This field is required."
]
"bar": ["This field is required."]
}
}
}
@ -2318,16 +2292,13 @@ Response:
"message": "Validation error",
"extra": {
"fields": {
"password": [
"This field cannot be blank."
],
"email": [
"This field cannot be blank."
]
"password": ["This field cannot be blank."],
"email": ["This field cannot be blank."]
}
}
}
```
---
Now, this can be extended & made to better suit your needs:
@ -2375,8 +2346,8 @@ project_name
We follow 2 general naming conventions:
* The test file names should be `test_the_name_of_the_thing_that_is_tested.py`
* The test case should be `class TheNameOfTheThingThatIsTestedTests(TestCase):`
- The test file names should be `test_the_name_of_the_thing_that_is_tested.py`
- The test case should be `class TheNameOfTheThingThatIsTestedTests(TestCase):`
For example, if we have:
@ -2404,8 +2375,8 @@ For example, if we have `project_name/common/utils.py`, then we are going to hav
If we are to split the `utils.py` module into submodules, the same will happen for the tests:
* `project_name/common/utils/files.py`
* `project_name/common/tests/utils/test_files.py`
- `project_name/common/utils/files.py`
- `project_name/common/tests/utils/test_files.py`
We try to match the structure of our modules with the structure of their respective tests.
@ -2413,9 +2384,9 @@ We try to match the structure of our modules with the structure of their respect
We use [Celery](http://www.celeryproject.org/) for the following general cases:
* Communicating with 3rd party services (sending emails, notifications, etc.)
* Offloading heavier computational tasks outside the HTTP cycle.
* Periodic tasks (using Celery beat)
- Communicating with 3rd party services (sending emails, notifications, etc.)
- Offloading heavier computational tasks outside the HTTP cycle.
- Periodic tasks (using Celery beat)
### The basics
@ -2657,10 +2628,10 @@ class Command(BaseCommand):
Few key things:
* We use this task as part of a deploy procedure.
* We always put a link to [`crontab.guru`](https://crontab.guru) to explain the cron. Otherwise it's unreadable.
* Everything is in one place.
* ⚠️ We use, almost exclusively, a cron schedule. **If you plan on using the other schedule objects, provided by Celery, please read thru their documentation** & the important notes - <https://django-celery-beat.readthedocs.io/en/latest/#example-creating-interval-based-periodic-task> - about pointing to the same schedule object. ⚠️
- We use this task as part of a deploy procedure.
- We always put a link to [`crontab.guru`](https://crontab.guru) to explain the cron. Otherwise it's unreadable.
- Everything is in one place.
- ⚠️ We use, almost exclusively, a cron schedule. **If you plan on using the other schedule objects, provided by Celery, please read thru their documentation** & the important notes - <https://django-celery-beat.readthedocs.io/en/latest/#example-creating-interval-based-periodic-task> - about pointing to the same schedule object. ⚠️
### Beyond
@ -2697,8 +2668,8 @@ def user_update(*, user: User, data) -> User:
return user
```
* We're calling the generic `model_update` service for the fields that have no side-effects related to them (meaning that they're just set to the value that we provide).
* This pattern allows us to extract the repetitive field setting in a generic service and perform only the specific tasks inside of the update service (side-effects).
- We're calling the generic `model_update` service for the fields that have no side-effects related to them (meaning that they're just set to the value that we provide).
- This pattern allows us to extract the repetitive field setting in a generic service and perform only the specific tasks inside of the update service (side-effects).
The generic `model_update` implementation looks like this:
@ -2727,8 +2698,9 @@ def model_update(
```
The full implementations of these services can be found in our example project:
* [`model_update`](https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/common/services.py)
* [`user_update`](https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/users/services.py)
- [`model_update`](https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/common/services.py)
- [`user_update`](https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/users/services.py)
## DX (Developer Experience)
@ -2775,6 +2747,6 @@ Additional resources that we found useful and that can add value to the stylegui
The way we do Django is inspired by the following things:
* The general idea for **separation of concerns**
* [Boundaries by Gary Bernhardt](https://www.youtube.com/watch?v=yTkzNHF6rMs)
* Rails service objects
- The general idea for **separation of concerns**
- [Boundaries by Gary Bernhardt](https://www.youtube.com/watch?v=yTkzNHF6rMs)
- Rails service objects