Dev Containerを使ってDjango REST Frameworkの環境を構築し、デバッガーを設定した

## 概要
Django REST Framework(以下DRF)環境をVSCodeのDev Containerを使って構築しました。
コードを書く際に、デバッガーを起動させてブレークポイントで処理を止めてステップ実行できたほうが嬉しいのでそのあたりもまとめました。

## 前提
作業環境は以下になります。
- macOS Ventura バージョン13.2.1

VSCodeの拡張機能で以下を使用しています。
- Dev Containers
https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers


## 手順
### Dev ContainerでPythonコンテナを立ち上げる
今回はVSCodeの拡張機能のDev Containerを使ってコンテナを構築しました。
まず作業用のディレクトリをVSCodeで開き、画面左下の緑のボタンをクリックします。
![スクリーンショット 2023-03-04 6.03.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331225/81914953-2905-a35a-1365-7baea74474ff.png)

エディタ上部に表示されるメニューからCreate Dev Containerをクリック
![スクリーンショット 2023-03-04 6.05.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331225/f03dcb6b-6fe6-1064-0770-5dba64c96f02.png)

Python3をクリック
![スクリーンショット 2023-03-04 6.07.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331225/d4bf1b2b-43f7-2907-e5eb-7fdd839a0c9e.png)


Create Dev Containerをクリック
![スクリーンショット 2023-03-04 6.08.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331225/5b41f14d-3039-0a38-5dbd-5ddeca22e9dd.png)

コンテナが作成されます。ターミナルからログも確認できます。
![スクリーンショット 2023-03-04 6.09.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331225/606944d8-d599-7bb1-4bc3-378c6bdccf93.png)

しばらく待つとコンテナが立ち上がります。
![スクリーンショット 2023-03-04 6.11.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331225/e2cec1b9-bb1f-413e-3c34-9946f97f22d2.png)

Pythonが動作することを確認するため、`demo.py`ファイルを作成し適当なコードを書いてみます。
```bash
touch demo.py
```

```python
example = 1

print(example)
```
![スクリーンショット 2023-03-04 6.15.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331225/ec6d254d-452d-e5ec-902d-f12afb7c492b.png)

ブレークポイントを設定してF5ボタンを押下し、デバッガーが起動することを確認します。
![スクリーンショット 2023-03-04 6.16.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331225/6feb2104-63dc-dfd2-b582-e76518bf1a36.png)

Pythonが動くことは確認できたので不要なファイルを削除します。
```bash
rm demo.py
```

### コンテナにDRFをインストールする
PythonコンテナにDjangoとDRFをインストールします。
```bash
pip install django
pip install djangorestframework
```

もしくは以下のようなrequirements.txtを書いて、`pip install -r requirements.txt`でも可能です。

```
Django~=3.1.7
djangorestframework~=3.11.1
```

### ローカルサーバーを起動する
以下の手順は公式ドキュメントの「Tutorial 1: Serialization」内容を参考に行います。
https://www.django-rest-framework.org/tutorial/1-serialization/


django-adminコマンドで新規プロジェクトを作成します。
```bash
django-admin startproject tutorial
```

treeコマンドで以下のようなディレクトリとファイルが作成されていることが確認できます。
```bash
vscode ➜ /workspaces/python $ tree
.
└── tutorial
    ├── manage.py
    └── tutorial
        ├── __init__.py
        ├── asgi.py
        ├── settings.py
        ├── urls.py
        └── wsgi.py

2 directories, 6 files
```
![スクリーンショット 2023-03-04 6.31.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331225/3bdeff79-c1d9-e771-6aa0-2dc4029d5292.png)

### ローカルサーバーを起動する
以下のコマンドを実行します。
```
cd tutorial
python manage.py migrate
python manage.py runserver
```
![スクリーンショット 2023-03-04 6.36.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331225/c16789c2-b69d-a2c4-85b8-ef4deb49d114.png)

デフォルトの8000ポートでサーバーが立ち上がります。
![スクリーンショット 2023-03-04 6.37.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331225/ae7bf481-4a0f-3e27-339e-224292c74488.png)

以下、公式ドキュメントの内容に沿って新規appを作成しmodel、serializer、viewを作成します。


snippetsという名前で新規appを作成します。
```
python manage.py startapp snippets
```

`tutorial/settings.py`の`INSTALLED_APPS`に以下を追加。
```
INSTALLED_APPS = [
    ...
    'rest_framework',
    'snippets',
]
```

modelの定義で使うpygmentsをインストールしておきます。
```
pip install pygments
```

`snippets/models.py`を以下の内容に編集します。

```python
from django.db import models
from pygments.lexers import get_all_lexers
from pygments.styles import get_all_styles

LEXERS = [item for item in get_all_lexers() if item[1]]
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
STYLE_CHOICES = sorted([(item, item) for item in get_all_styles()])


class Snippet(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    title = models.CharField(max_length=100, blank=True, default='')
    code = models.TextField()
    linenos = models.BooleanField(default=False)
    language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
    style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)

    class Meta:
        ordering = ['created']
```

migrateを行います。

```bash
python manage.py makemigrations snippets
python manage.py migrate snippets
```


`snippets/serializers.py`を作成し、以下の内容にします。
```python
from rest_framework import serializers
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES


class SnippetSerializer(serializers.ModelSerializer):
    class Meta:
        model = Snippet
        fields = ['id', 'title', 'code', 'linenos', 'language', 'style']
```

`snippets/views.py`を以下の内容にします。
```python
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.parsers import JSONParser
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer

@csrf_exempt
def snippet_list(request):
    """
    List all code snippets, or create a new snippet.
    """
    if request.method == 'GET':
        snippets = Snippet.objects.all()
        serializer = SnippetSerializer(snippets, many=True)
        return JsonResponse(serializer.data, safe=False)

    elif request.method == 'POST':
        data = JSONParser().parse(request)
        serializer = SnippetSerializer(data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data, status=201)
        return JsonResponse(serializer.errors, status=400)

@csrf_exempt
def snippet_detail(request, pk):
    """
    Retrieve, update or delete a code snippet.
    """
    try:
        snippet = Snippet.objects.get(pk=pk)
    except Snippet.DoesNotExist:
        return HttpResponse(status=404)

    if request.method == 'GET':
        serializer = SnippetSerializer(snippet)
        return JsonResponse(serializer.data)

    elif request.method == 'PUT':
        data = JSONParser().parse(request)
        serializer = SnippetSerializer(snippet, data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data)
        return JsonResponse(serializer.errors, status=400)

    elif request.method == 'DELETE':
        snippet.delete()
        return HttpResponse(status=204)
```

`snippets/urls.py`を作成し、以下の内容にします。

```python
from django.urls import path
from snippets import views

urlpatterns = [
    path('snippets/', views.snippet_list),
    path('snippets/<int:pk>/', views.snippet_detail),
]
```

`tutorial/urls.py`を以下の内容に編集します。
```python
from django.urls import path, include

urlpatterns = [
    path('', include('snippets.urls')),
]
```

Pythonのshellからデータを保存します。

```
python manage.py shell
```

```bash
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from rest_framework.renderers import JSONRenderer
from rest_framework.parsers import JSONParser

snippet = Snippet(code='foo = "bar"\n')
snippet.save()
```

ローカルサーバーが起動している状態でcurlコマンドでhttpリクエストを送信して、レスポンスが返ってくることを確認します。

```bash
curl localhost:8000/snippets/
[{"id": 1, "title": "", "code": "foo = \"bar\"\n", "linenos": false, "language": "python", "style": "friendly"}]
```

ここまでで基本的な処理が動作することが確認できました。

### デバッガーが起動するようにlaunch.jsonを作成

デバッガーメニューからcreate a launch.json fileをクリックします。
![スクリーンショット 2023-03-04 7.01.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331225/7986ea1b-60dd-052a-7584-3fedb0d3ac50.png)

Djangoをクリック
![スクリーンショット 2023-03-08 6.06.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331225/634e2f0d-0904-899c-1062-3099b0108e22.png)

manage.pyへのパスを入力します。(今回で言うと${workspaceFolder}の次にturorialディレクトリがある)
![スクリーンショット 2023-03-04 7.10.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331225/b92c67b6-5260-a33e-1150-ab4a122d5d23.png)

Enterを押すと以下のlaunch.jsonが作成されます。
argsプロパティでDRFサーバーを起動するコマンドのrunserverが設定されています。
```json
{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Django",
            "type": "python",
            "request": "launch",
            "program": "${workspaceFolder}/tutorial/manage.py",
            "args": [
                "runserver"
            ],
            "django": true,
            "justMyCode": true
        }
    ]
}
```

試しにブレークポイントを貼ってみます。
![スクリーンショット 2023-03-04 7.13.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331225/799c7265-5162-666e-4d10-41d81cadb0d1.png)

F5ボタンでデバッガーを起動します。
![スクリーンショット 2023-03-04 7.14.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331225/edc1d6dc-f845-f7ca-d4e7-adf09e21821d.png)

curlコマンドでリクエストを投げます。
```bash
curl localhost:8000/snippets/
```

ブレークポイントで処理が止まることが確認できました。
![スクリーンショット 2023-03-04 7.15.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331225/1c4ab4fb-3574-e13f-11a0-bf5eb62fb362.png)

以上です。