PythonでRESTAPIを構築する #導入

## 今回やること
前にJWT認証の記事を上げたのですがバックエンドの説明が皆無だったのでこれから複数回に分けてあげていきます（単に暇なだけというのもある）。使う言語はGoとPythonで迷ったのですが、Gin（Goの言語のAPIフレームワーク）は日本語の文献がほぼ皆無なので需要がありそうなPythonのDjangoを使います。今回からはサンプルとして簡単な記事を投稿できるシステムを作ります。

## APIの設計
[API](https://www.otsuka-shokai.co.jp/words/api.html)とはアプリケーションプログラミングインターフェースのことで、特にこの記事ではクライアント（Webブラウザなど）からの[HTTP](https://e-words.jp/w/HTTP.html)リクエストに応じてデータベースなどとやり取りして特定のデータフォーマット([JSON](https://e-words.jp/w/JSON.html)など)のレスポンスを返すシステムを指すと思ってください。
今回の[APIエンドポイント](https://e-words.jp/w/%E3%82%A8%E3%83%B3%E3%83%89%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88.html)の設計はこんな感じ。

|path|method|params|detail|
|----|----|----|----|
|/api/users|GET, POST|null|false|
|/api/user/:id|GET, PATCH, PUT, DELETE|id: int|true|
|/api/posts|GET, POST|null|false|
|/api/posts/:slug|GET, PATCH, PUT, DELETE|slug: string|true|
|/api/token|POST|null|true|
|/api/token/refresh|POST|null|true|
|/api/token/logout|POST|null|true|

slugとはurlによくくっついてるやつです(hello-worldみたいな)。5つ目からはJWTにかかわる話なのでここでは割愛します。

## 今回必要なもの

まず今回必須になるものから説明します。
* Python 3
* Django
* Django rest framework
* pip
* virtualenv
virtualenvはpythonの[仮想環境](https://e-words.jp/w/%E4%BB%AE%E6%83%B3%E5%8C%96%E7%92%B0%E5%A2%83.html)を作成＋管理するためのパッケージです。このおかげでバージョンの管理などで悩まなくて済みます。作業をする時は必ず仮想環境に入るようにしてください。
pipはpythonのパッケージ管理ソフトウェアです。[Anaconda](https://www.anaconda.com/)をインストールされている方はすでに入っていると思います。

必須でないものまでまとめるとこんな感じ。

```pip:requirements.txt
appdirs==1.4.4
asgiref==3.2.10
black==21.6b0
certifi==2020.12.5
cffi==1.14.5
chardet==4.0.0
click==8.0.1
colorama==0.4.4
coreapi==2.3.3
coreschema==0.0.4
cryptography==3.4.7
defusedxml==0.7.1
Django==3.1
django-cors-headers==3.7.0
django-filter==2.4.0
django-templated-mail==1.1.1
django-versatileimagefield==2.0
djangorestframework==3.12.4
djangorestframework-jwt==1.11.0
djangorestframework-simplejwt==4.6.0
idna==2.10
itypes==1.2.0
Jinja2==3.0.0
jwt==1.2.0
MarkupSafe==2.0.0
mypy-extensions==0.4.3
oauthlib==3.1.0
pathspec==0.8.1
Pillow==8.2.0
pycparser==2.20
PyJWT==2.1.0
python-magic-bin==0.4.14
python-slugify==5.0.2
python3-openid==3.2.0
pytz==2021.1
requests==2.25.1
requests-oauthlib==1.3.0
six==1.16.0
social-auth-app-django==4.0.0
social-auth-core==4.1.0
sqlparse==0.4.1
text-unidecode==1.3
toml==0.10.2
uritemplate==3.0.1
urllib3==1.26.4
```

インストールするときはまとめて一つのファイルに記しておくと同時にインストールできます。コマンドは後程。

## インストールと環境構築

まず仮想環境を作成します。virtualenvは以下のコマンドでインストールできます。

```bash
pip install virtualenv
// pathが通っていない方はこちら
python -m pip install virtualenv
```
次に仮想環境を作成します。

```bash
virtualenv myenv
```

これでmyenvという仮想環境ができました（pythonのバージョンも指定できます)。
仮想環境を起動するには、myenvディレクトリに入って次のコマンドを打ってください。

```shell
D:/myenv> ../Scripts/activate.bat

(myenv) D:/myenv>
```
こんな感じで(name)みたいなのが出てくるのでこれが印です。出るときは同じ場所でdeactivateと打ってください。
ではここから先ほど紹介したものをインストールします。同ディレクトリに先ほどのrequirements.txtファイルを作成しインストールします。

```bash
(myenv) D:/myenv> pip install -r requirements.txt
```
たまに信じられないほどインストールに時間がかかりますが仏の心で待ちましょう。

これですべてインストールできたので、いよいよプロジェクトを起動します。

```bash
(myenv) D:/myenv> django-admin startproject backend
// backendのところはプロジェクト名
```

フォルダ移動。そしてアプリを登録。

```bash
(myenv) D:/myenv> cd backend
(myenv) D:/myenv/backend> python manage.py startapp accounts
(myenv) D:/myenv/backend> python manage.py startapp posts
```
これでpostsとaccountsというフォルダがbackendディレクトリに作成されます。構成はこんな感じ。

```
backend
├─.pytest_cache
│  └─v
│      └─cache
├─.vscode
├─accounts
│  ├─migrations
│  │  └─__pycache__
│  └─__pycache__
├─backend
│  └─__pycache__
├─posts
│  ├─migrations
│  │  └─__pycache__
│  └─__pycache__
└─utils
    └─__pycache__
```
これから話すのはVScodeを使っている人向けです。.vscodeフォルダ(上に書いてあるやつ)にlaunch.jsonというファイルを作成し以下を入力すると便利です。

```json:launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "run server",
      "type": "python",
      "request": "launch",
      "program": "${workspaceFolder}\\manage.py",
      "args": ["runserver"],
      "console": "externalTerminal",
      "stopOnEntry": false,
    },
    {
      "name": "make migration files",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "type": "python",
      "program": "${workspaceFolder}\\manage.py",
      "args": ["makemigrations"],
      "console": "externalTerminal"
    },
    {
      "name": "migrate",
      "type": "python",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "program": "${workspaceFolder}\\manage.py",
      "args": ["migrate"],
      "console": "externalTerminal"
    }
  ]
}
```
これで毎回コマンド入力しなくてもF5で実行できます。

## プロジェクトの設定

backendフォルダ内にsettings.pyというファイルがあるのでそこに先ほど追加したアプリを登録します。

```python:settings.py
# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework', # 追加
    'corsheaders', #追加
    'posts', # 追加
    'accounts', #追加
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
```
>これから新しいアプリを登録するときには必ず同じ操作をしてください

言語の設定もしておきます

```python:settings.py
# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/

LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'

USE_I18N = True

USE_L10N = True

USE_TZ = True
```

初回からいきなりハードですが、カスタムユーザーモデルを作ります。後で変更しようとして泥沼にはまったことがあるので先に作っておきます。デフォルトから大きく変更する必要はありません。ただ作っておいたほうがかなり便利です。認証にはemailとpasswordを使います。
accountsフォルダのmodels.pyファイルに以下を入力します。

```python:accounts/models.py
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager

class MyAccountManager(BaseUserManager):
    def create_user(self, email, username, password=None, **extra_kwargs):
        if not email:
            raise ValueError('Users must have an email address')
        if not username:
            raise ValueError('Users must have an username')

        user = self.model(
            email = self.normalize_email(email),
            username = username,
            **extra_kwargs
        )
        
        user.set_password(password)
        user.save(using = self._db)
        return user
    
    def create_superuser(self, email, username, password):
        user = self.create_user(
            email = self.normalize_email(email),
            username = username,
            password = password
        )

        user.is_admin = True
        user.is_staff = True
        user.is_superuser = True
        user.save(using=self._db)
        return user

class User(AbstractBaseUser):
    email = models.EmailField(verbose_name="email", max_length=60, unique=True)
    username = models.CharField(max_length=30, unique=True)
    date_joined = models.DateTimeField(
        verbose_name='date joined', auto_now_add=True)
    last_login = models.DateTimeField(verbose_name='last login', auto_now=True)
    is_admin = models.BooleanField(default=False)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    is_superuser = models.BooleanField(default=False)

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['username']

    objects = MyAccountManager()

    def __str__(self):
        return self.username
    
    def get_absolute_url(self):
        return '/users/{}'.format(self.id)
    
        # 絶対書くこと(必須)
    def has_perm(self, perm, obj=None):
        return self.is_admin

        # これも絶対書くこと(必須)
    def has_module_perms(self, app_label):
        return True
    
    class Meta:
        ordering = ['-date_joined']

```
何が起こっているのか知りたくなる気持ちはわかりますがこれは水面下でかなり複雑なことが起こっているのでできればコピペしてください。
さらにこれよりもカスタマイズする方法は次回やります。
このモデルをプロジェクト設定に登録します。

```python:settings.py
from pathlib import Path
import os
from corsheaders.defaults import default_headers

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/


# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []

CORS_ALLOWED_ORIGINS = [
#frontend側の設定
]

CORS_ALLOW_HEADERS = default_headers + ('contenttype', 'Authorization')

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'corsheaders',
    'posts',
    'accounts',
    'rest_framework_simplejwt.token_blacklist', #JWT認証を使わないなら要らない
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'backend.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'backend.wsgi.application'

# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME':
        'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME':
        'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME':
        'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME':
        'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]

# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/

LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'

USE_I18N = True

USE_L10N = True

USE_TZ = True

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/

STATIC_URL = '/static/'

# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL = 'accounts.User' #この部分を新たに追加

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.AllowAny', ),
    'DEFAULT_AUTHENTICATION_CLASSES':
    ('rest_framework_simplejwt.authentication.JWTAuthentication', ),
}
```

次に管理サイトにこのモデルを追加します。同ディレクトリにあるadmin.pyファイルに移動し以下を入力します。

```python:accounts/admin.py

from django.contrib import admin
from .models import User

admin.site.register(User)
```
最後にマイグレーションしてサーバーを起動してみてください。

```bash
python manage.py makemigrations
python manage.py migrate
python manage.py runserver
```
管理サイトが開くはずです。

## おまけ
utilsなんてフォルダないぞ？と思ったあなた、よく見てますね。これは僕が勝手に作ったもので、ユーザーの権限とかの制限を管理するスクリプトを入れてます。あると便利なのでよかったらどうぞ。

```python:utils/permissions.py
from rest_framework import permissions

class IsAdminOrReadOnly(permissions.BasePermission):

    def has_permission(self, request, view):

        if request.method in permissions.SAFE_METHODS:
            return True
        
        return request.user.is_staff

class IsOwnerOrReadOnly(permissions.BasePermission):

    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:
            return True
        
        return obj.posts.author == request.user


class IsSelfOrReadOnly(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):

        if request.method in permissions.SAFE_METHODS:
            return True
        
        return obj == request.user
```

```python:utils/authentications.py
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework_simplejwt.exceptions import InvalidToken

class JWTAuthenticationSafe(JWTAuthentication):
    def authenticate(self, request):
        try:
            return super().authenticate(request=request)
        except InvalidToken:
            return None
```

## 最後に
次回はいろいろやります。
お疲れさまでした。