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:** **Table of contents:**
<!-- toc --> <!-- toc -->
@ -20,58 +19,58 @@
- [Why not?](#why-not) - [Why not?](#why-not)
- [Cookie Cutter](#cookie-cutter) - [Cookie Cutter](#cookie-cutter)
- [Models](#models) - [Models](#models)
* [Base model](#base-model) - [Base model](#base-model)
* [Validation - `clean` and `full_clean`](#validation---clean-and-full_clean) - [Validation - `clean` and `full_clean`](#validation---clean-and-full_clean)
* [Validation - constraints](#validation---constraints) - [Validation - constraints](#validation---constraints)
* [Properties](#properties) - [Properties](#properties)
* [Methods](#methods) - [Methods](#methods)
* [Testing](#testing) - [Testing](#testing)
- [Services](#services) - [Services](#services)
* [Example - function-based service](#example---function-based-service) - [Example - function-based service](#example---function-based-service)
* [Example - class-based service](#example---class-based-service) - [Example - class-based service](#example---class-based-service)
* [Naming convention](#naming-convention) - [Naming convention](#naming-convention)
* [Modules](#modules) - [Modules](#modules)
* [Selectors](#selectors) - [Selectors](#selectors)
* [Testing](#testing-1) - [Testing](#testing-1)
- [APIs & Serializers](#apis--serializers) - [APIs & Serializers](#apis--serializers)
* [Naming convention](#naming-convention-1) - [Naming convention](#naming-convention-1)
* [Class-based vs. Function-based](#class-based-vs-function-based) - [Class-based vs. Function-based](#class-based-vs-function-based)
* [List APIs](#list-apis) - [List APIs](#list-apis)
+ [Plain](#plain) - [Plain](#plain)
+ [Filters + Pagination](#filters--pagination) - [Filters + Pagination](#filters--pagination)
* [Detail API](#detail-api) - [Detail API](#detail-api)
* [Create API](#create-api) - [Create API](#create-api)
* [Update API](#update-api) - [Update API](#update-api)
* [Fetching objects](#fetching-objects) - [Fetching objects](#fetching-objects)
* [Nested serializers](#nested-serializers) - [Nested serializers](#nested-serializers)
* [Advanced serialization](#advanced-serialization) - [Advanced serialization](#advanced-serialization)
- [Urls](#urls) - [Urls](#urls)
- [Settings](#settings) - [Settings](#settings)
* [Prefixing environment variables with `DJANGO_`](#prefixing-environment-variables-with-django_) - [Prefixing environment variables with `DJANGO_`](#prefixing-environment-variables-with-django_)
* [Integrations](#integrations) - [Integrations](#integrations)
* [Reading from `.env`](#reading-from-env) - [Reading from `.env`](#reading-from-env)
- [Errors & Exception Handling](#errors--exception-handling) - [Errors & Exception Handling](#errors--exception-handling)
* [How exception handling works (in the context of DRF)](#how-exception-handling-works-in-the-context-of-drf) - [How exception handling works (in the context of DRF)](#how-exception-handling-works-in-the-context-of-drf)
+ [DRF's `ValidationError`](#drfs-validationerror) - [DRF's `ValidationError`](#drfs-validationerror)
+ [Django's `ValidationError`](#djangos-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) - [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) - [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 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) - [Approach 2 - HackSoft's proposed way](#approach-2---hacksofts-proposed-way)
* [More ideas](#more-ideas) - [More ideas](#more-ideas)
- [Testing](#testing-2) - [Testing](#testing-2)
* [Naming conventions](#naming-conventions) - [Naming conventions](#naming-conventions)
- [Celery](#celery) - [Celery](#celery)
* [The basics](#the-basics) - [The basics](#the-basics)
* [Error handling](#error-handling) - [Error handling](#error-handling)
* [Configuration](#configuration) - [Configuration](#configuration)
* [Structure](#structure) - [Structure](#structure)
* [Periodic Tasks](#periodic-tasks) - [Periodic Tasks](#periodic-tasks)
* [Beyond](#beyond) - [Beyond](#beyond)
- [Cookbook](#cookbook) - [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) - [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) - [Django Styleguide in the Wild](#django-styleguide-in-the-wild)
- [Additional resources](#additional-resources) - [Additional resources](#additional-resources)
- [Inspiration](#inspiration) - [Inspiration](#inspiration)
@ -116,24 +115,24 @@ The core of the Django Styleguide can be summarized as follows:
**In Django, business logic should live in:** **In Django, business logic should live in:**
* Services - functions, that mostly take care of writing things to the database. - Services - functions, that mostly take care of writing things to the database.
* Selectors - functions, that mostly take care of fetching things from the database. - Selectors - functions, that mostly take care of fetching things from the database.
* Model properties (with some exceptions). - Model properties (with some exceptions).
* Model `clean` method for additional validations (with some exceptions). - Model `clean` method for additional validations (with some exceptions).
**In Django, business logic should not live in:** **In Django, business logic should not live in:**
* APIs and Views. - APIs and Views.
* Serializers and Forms. - Serializers and Forms.
* Form tags. - Form tags.
* Model `save` method. - Model `save` method.
* Custom managers or querysets. - Custom managers or querysets.
* Signals. - Signals.
**Model properties vs selectors:** **Model properties vs selectors:**
* If the property spans multiple relations, 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. - 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. 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: Few examples:
* You can use the [`Styleguide-Example`](https://github.com/HackSoftware/Styleguide-Example) project as a starting point. - 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. - 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. - Or you can create something that works for your case & turn it into a [cookiecutter](https://cookiecutter.readthedocs.io/en/latest/) project.
## Models ## 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: 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. - **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_`. - **Greppability.** Or in other words, if you want to see all actions for a specific entity, just grep for `user_`.
### Modules ### 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: 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)) - Fakes (We recommend using [`faker`](https://github.com/joke2k/faker))
* Other services, to create the required objects. - Other services, to create the required objects.
* Special test utility & helper methods. - Special test utility & helper methods.
* Factories (We recommend using [`factory_boy`](https://factoryboy.readthedocs.io/en/latest/orms.html)) - 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. - Plain `Model.objects.create()` calls, if factories are not yet introduced in the project.
* Usually, whatever suits you better. - Usually, whatever suits you better.
**Let's take a look at our service from the example:** **Let's take a look at our service from the example:**
@ -819,9 +818,9 @@ def item_buy(
The service: The service:
* Calls a selector for validation. - Calls a selector for validation.
* Creates an object. - Creates an object.
* Delays a task. - Delays a task.
**Those are our tests:** **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:** **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. - Have 1 API per operation. This means, for CRUD on a model, having 4 APIs.
* Inherit from the most simple `APIView` or `GenericAPIView`. - 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. - 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.** - **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). - 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`. - 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. - 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. 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:** **Here are our rules for API serialization:**
* There should be a dedicated **input serializer** & a dedicated **output serializer**. - There should be a dedicated **input serializer** & a dedicated **output serializer**.
* **Input serializer** takes care of the data coming in. - **Input serializer** takes care of the data coming in.
* **Output serializer** takes care of the data coming out. - **Output serializer** takes care of the data coming out.
* In terms of serialization, Use whatever abstraction works for you. - In terms of serialization, Use whatever abstraction works for you.
**In case you are using DRF's serializers, here are our rules:** **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`. - 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` - 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. - 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. - If you need a nested serializer, use the `inline_serializer` util.
* Reuse serializers as little as possible. - Reuse serializers as little as possible.
* Reusing serializers may expose you to unexpected behavior, when something changes in the base serializers. - Reusing serializers may expose you to unexpected behavior, when something changes in the base serializers.
### Naming convention ### Naming convention
@ -975,7 +974,7 @@ class UserListApi(APIView):
return Response(data) 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 #### Filters + Pagination
@ -1580,9 +1579,7 @@ def some_service():
The response payload is going to look like this: The response payload is going to look like this:
```json ```json
[ ["Some message"]
"Some message"
]
``` ```
This looks strange, because if we do it like this: This looks strange, because if we do it like this:
@ -1817,9 +1814,7 @@ Response:
```json ```json
{ {
"detail": { "detail": {
"non_field_errors": [ "non_field_errors": ["Some error message"]
"Some error message"
]
} }
} }
``` ```
@ -1875,9 +1870,7 @@ Response:
```json ```json
{ {
"detail": [ "detail": ["Some error message"]
"Some error message"
]
} }
``` ```
@ -1930,17 +1923,13 @@ Response:
```json ```json
{ {
"detail": { "detail": {
"foo": [ "foo": ["This field is required."],
"This field is required."
],
"email": [ "email": [
"Ensure this field has at least 200 characters.", "Ensure this field has at least 200 characters.",
"Enter a valid email address." "Enter a valid email address."
], ],
"nested": { "nested": {
"bar": [ "bar": ["This field is required."]
"This field is required."
]
} }
} }
} }
@ -1981,12 +1970,8 @@ Response:
```json ```json
{ {
"detail": { "detail": {
"password": [ "password": ["This field cannot be blank."],
"This field cannot be blank." "email": ["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.", "message": "Validation error.",
"extra": { "extra": {
"fields": { "fields": {
"password": [ "password": ["This field cannot be blank."],
"This field cannot be blank." "email": ["This field cannot be blank."]
], }
"email": [
"This field cannot be blank."
]
} }
},
} }
``` ```
@ -2123,6 +2104,7 @@ Response:
} }
} }
``` ```
--- ---
Code: Code:
@ -2139,9 +2121,7 @@ Response:
"message": "Validation error", "message": "Validation error",
"extra": { "extra": {
"fields": { "fields": {
"non_field_errors": [ "non_field_errors": ["Some error message"]
"Some error message"
]
} }
} }
} }
@ -2202,9 +2182,7 @@ Response:
{ {
"message": "Validation error", "message": "Validation error",
"extra": { "extra": {
"fields": [ "fields": ["Some error message"]
"Some error message"
]
} }
} }
``` ```
@ -2263,17 +2241,13 @@ Response:
"message": "Validation error", "message": "Validation error",
"extra": { "extra": {
"fields": { "fields": {
"foo": [ "foo": ["This field is required."],
"This field is required."
],
"email": [ "email": [
"Ensure this field has at least 200 characters.", "Ensure this field has at least 200 characters.",
"Enter a valid email address." "Enter a valid email address."
], ],
"nested": { "nested": {
"bar": [ "bar": ["This field is required."]
"This field is required."
]
} }
} }
} }
@ -2318,16 +2292,13 @@ Response:
"message": "Validation error", "message": "Validation error",
"extra": { "extra": {
"fields": { "fields": {
"password": [ "password": ["This field cannot be blank."],
"This field cannot be blank." "email": ["This field cannot be blank."]
],
"email": [
"This field cannot be blank."
]
} }
} }
} }
``` ```
--- ---
Now, this can be extended & made to better suit your needs: Now, this can be extended & made to better suit your needs:
@ -2375,8 +2346,8 @@ project_name
We follow 2 general naming conventions: We follow 2 general naming conventions:
* The test file names should be `test_the_name_of_the_thing_that_is_tested.py` - 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 case should be `class TheNameOfTheThingThatIsTestedTests(TestCase):`
For example, if we have: 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: 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/utils/files.py`
* `project_name/common/tests/utils/test_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. 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: We use [Celery](http://www.celeryproject.org/) for the following general cases:
* Communicating with 3rd party services (sending emails, notifications, etc.) - Communicating with 3rd party services (sending emails, notifications, etc.)
* Offloading heavier computational tasks outside the HTTP cycle. - Offloading heavier computational tasks outside the HTTP cycle.
* Periodic tasks (using Celery beat) - Periodic tasks (using Celery beat)
### The basics ### The basics
@ -2657,10 +2628,10 @@ class Command(BaseCommand):
Few key things: Few key things:
* We use this task as part of a deploy procedure. - 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. - We always put a link to [`crontab.guru`](https://crontab.guru) to explain the cron. Otherwise it's unreadable.
* Everything is in one place. - 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, 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 ### Beyond
@ -2697,8 +2668,8 @@ def user_update(*, user: User, data) -> User:
return 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). - 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). - 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: 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: 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) ## 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 way we do Django is inspired by the following things:
* The general idea for **separation of concerns** - The general idea for **separation of concerns**
* [Boundaries by Gary Bernhardt](https://www.youtube.com/watch?v=yTkzNHF6rMs) - [Boundaries by Gary Bernhardt](https://www.youtube.com/watch?v=yTkzNHF6rMs)
* Rails service objects - Rails service objects