2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

PythonでRESTAPIを構築する #導入

Last updated at Posted at 2021-06-21

今回やること

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

APIの設計

APIとはアプリケーションプログラミングインターフェースのことで、特にこの記事ではクライアント(Webブラウザなど)からのHTTPリクエストに応じてデータベースなどとやり取りして特定のデータフォーマット(JSONなど)のレスポンスを返すシステムを指すと思ってください。
今回のAPIエンドポイントの設計はこんな感じ。

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の仮想環境を作成+管理するためのパッケージです。このおかげでバージョンの管理などで悩まなくて済みます。作業をする時は必ず仮想環境に入るようにしてください。
    pipはpythonのパッケージ管理ソフトウェアです。Anacondaをインストールされている方はすでに入っていると思います。

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

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は以下のコマンドでインストールできます。

pip install virtualenv
// pathが通っていない方はこちら
python -m pip install virtualenv

次に仮想環境を作成します。

virtualenv myenv

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

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

(myenv) D:/myenv>

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

(myenv) D:/myenv> pip install -r requirements.txt

たまに信じられないほどインストールに時間がかかりますが仏の心で待ちましょう。

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

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

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

(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というファイルを作成し以下を入力すると便利です。

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というファイルがあるのでそこに先ほど追加したアプリを登録します。

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',
]

これから新しいアプリを登録するときには必ず同じ操作をしてください

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

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ファイルに以下を入力します。

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']

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

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ファイルに移動し以下を入力します。

accounts/admin.py

from django.contrib import admin
from .models import User

admin.site.register(User)

最後にマイグレーションしてサーバーを起動してみてください。

python manage.py makemigrations
python manage.py migrate
python manage.py runserver

管理サイトが開くはずです。

おまけ

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

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
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

最後に

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

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?