# Django REST base
**Customized features and environment for building a Django REST framework app.**
# Requirements
- Python 3.8
- Django 3.0
- djangorestframework (Django REST framework)
Optional packages required for additional features.
- PyJWT `TokenAuthentication`
- channels `NullURLRouter`, `NullConsumer`
- sentry-sdk `sentry_report` (when [Sentry](https://sentry.io/) reports enabled)
- numpy `rest_base.utils.random`
# Installation
```shell script
pip install django-rest-base
```
#### `settings.py`
```python
INSTALLED_APPS = [
...,
'rest_base',
]
```
# Features
## Error
#### `settings.py`
```python
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'rest_base.errors.exception_handler',
}
```
#### `errors.py`
```python
from rest_base.errors import Error
MyAppError = Error('my_app') # class
MyError = MyAppError('My', 'Error') # class
MyErrorInstance = MyError(detail='caution: this is instance!') # instance (when *args and code not provided)
```
#### `views.py`
```python
from my_app.errors import MyError, MyErrorInstance
def my_view(request):
raise MyError(detail='Something went wrong :(')
def another_view1(request):
raise MyError # raise without parameters is also permitted
def another_view2(request):
raise MyErrorInstance() # __call__ should be used to raise instance
```
`rest_base.errors.exception_handler` is similar with REST framework's default handler, but provides advanced error format.
In Django REST base environment, every exception from views inherits `rest_base.errors.Error`. Even original REST framework's exceptions and unhandled exceptions will be converted to `Error`.
Those `Error`s will generate `Response` which has following error format.
```json
{
"error": {
"code": "My::Error",
"detail": "Something went wrong :(",
"traceback": "(traceback)"
}
}
```
`detail` and `traceback` are optional, and `traceback` will be included automatically when Django's DEBUG mode is enabled.
## View
#### `urls.py`
```python
from django.urls import re_path
from rest_base.urls import method_branch
from . import views
urlpatterns = [
re_path(r'^my_endpoint/?$', method_branch(GET=views.my_view, POST=views.another_view)),
]
```
`rest_base.urls.method_branch` branch the requests by it's method—`GET`, `POST`, `PUT`, `DELETE`.
It also supports `@rest_framework.decorators.authentication_classes` and `@rest_framework.decorators.permission_classes` for each view.
## Model
#### `models.py`
```python
from rest_base.models import BaseModel, BaseUser, BaseToken, semaphore
@semaphore(block=True)
class MyModel(BaseModel):
field = ...
class MyUser(BaseUser):
field = ...
class MyToken(BaseToken):
field = ...
```
By replacing the original Django `models.Model` with `rest_base.models.BaseModel`, several customized features can be used.
- `created`, `last_modified` fields
- `objects.update_or_create` updates instance only if original attributes and `defaults` are not same.
- `bulk_manager`
- Supports deadlock free following methods. (PostgreSQL ONLY)
- `update`, `bulk_update`, `delete`
- `PredefinedDefault` (See [Fields](#fields) for more)
- `related_name` must be provided when field's type is `ForeignKey`, `OneToOneField` or `ManyToManyField`.
You can use customized `BaseUser` and JWT based `BaseToken` by inherit it.
See [Authentication](#authentication) for more information about JWT authentication.
## Fields
#### `models.py`
```python
from django.db import models
from rest_base.fields import UniqueRandomPositiveInt32
from rest_base.models import BaseModel
class MyModel(BaseModel):
unique_field = models.IntegerField(unique=True, default=UniqueRandomPositiveInt32)
```
If your model inherits `rest_base.models.BaseModel`, you can set the default value as
- `UniqueRandomPositiveInt32`
- `UniqueRandomPositiveInt52`
- `UniqueRandomPositiveInt64`
- `UniqueRandomChar` (unique random url-safe characters)
Then default value will be replaced with unique random value in the run-time.
## Admin
#### `admin.py`
```python
from django.contrib import admin
from rest_base.admin import model_admin
from .models import *
admin.site.register(*model_admin(MyModel))
```
You can easily register you models to Django admin page by using `rest_base.admin.model_admin`.
It registers all of the model's fields and supports link to `ForeignKey`, `OneToOneField` and `ManyToManyField` on Django admin.
## Authentication
```shell script
pip install django-rest-base[jwt]
```
#### `models.py`
```python
from rest_base.models import BaseToken
class Token(BaseToken):
pass
```
#### `settings.py`
```python
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_base.authentication.CsrfExemptSessionAuthentication',
'rest_base.authentication.TokenAuthentication',
),
}
REST_BASE = {
'AUTHENTICATION_MODEL': 'my_app.models.Token',
}
```
#### `views.py`
```python
from rest_framework.decorators import permission_classes
from rest_framework.permissions import IsAuthenticated
@permission_classes((IsAuthenticated,))
def my_view(request):
...
```
Each `Token` which inherits `BaseToken` has `public_key` and `secret_key`.
You can make bearer using following format.
```json
// Header
{
"alg": "HS256",
"typ": "JWT",
"public_key": "public_key",
"nonce": 31
}
// Payload
{
"query": {
"key": "value"
}
}
```
Then set the HTTP Authorization header to
```
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsInB1YmxpY19rZXkiOiJwdWJsaWNfa2V5Iiwibm9uY2UiOjMxfQ.eyJxdWVyeSI6eyJrZXkiOiJ2YWx1ZSJ9fQ.q1849tjsSspbtyBYHxsmS98FLUIG2W97aj_gaCxWGlg
```
Remember that nonce must be a positive int32 value which increases for each request.
## Sentry
```shell script
pip install django-rest-base[sentry]
```
#### `settings.py`
```python
REST_BASE = {
'SENTRY_HOST': 'https://<key>@sentry.io/<project>',
'SENTRY_VERBOSE': False, # report handled exceptions, default False
}
```
If `sentry-sdk` installed, `SENTRY_HOST` defined and `rest_base.errors.exception_handler` configured correctly,
every unhandled exception from view will be reported to the Sentry.
Handled exceptions also reported if you set `SENTRY_VERBOSE` to `True`.
## Channels
```shell script
pip install django-rest-base[channels]
```
#### `routing.py`
```python
from django.urls import path
from rest_base.channels.routing import NullURLRouter
websocket_urlpatterns = [
path('app/', NullURLRouter(my_app.routing.websocket_urlpatterns)),
]
```
## Etc
- By default, startapp command will use template in `rest_base/conf/app_template` which contains additional code for Django REST base
- .env can be loaded by
```python
from rest_base.utils import dotenv
dotenv.load('path/to/.env')
```
- You can dump/load predefined model instances by using
```shell script
python manage.py dump my_app.Model
python manage.py load my_app.Model
```
# License
[MIT](./LICENSE)