Djangoのプロジェクト内にFastAPIなどのWebAPIをいかに組み込むか

## 本拙筆を書くまでの経緯

<details><summary>詳細</summary>

　以下Qiitaの続きです。実務未経験から（広義の）エンジニアとして転職して正社員になり１年経ちました。プログラミング学習期間は約２年になりました。今も雑魚なのでPythonとJavascript（＋今後はTypeScript）を中心に、仕事とは別に家のご飯机でゆるゆる学習を続けています。（間違いありましたらガンガン編集リクエストお願いしますm(_ _)m）
 
　なお、学習はWindowsの私用ノートPC、VScodeで基本的にしています。今いじっているDjangoはPoetry配下でver5.1.3のものを使っています。（そろそろ dotfiles 作らないと・・・(-_-;)）

____

①Portfolio移行録（Laravel 9 ⇒Django＆Poetry移行録）①～プロジェクトの準備～
https://qiita.com/thinking-weed/items/79569252644403452d8b

②Django初心者がデフォルトのUserモデル または、CustomUserモデルを設計・作成・使用する際の諸注意
https://qiita.com/thinking-weed/items/321b4f2d8c5be4ef94d6

____

上記のQiitaにあるように、[主な参考資料①](#主な参考資料)をベースに学習を進め、カスタムユーザーモデル、Laravelでいうテストモデル作成Seederを作った後、CRUDをとりあえず作ってみていたのですが、何だか途中で飽きました（´｡｀)。。（中途半端なのもそれはそれでモヤモヤするのでそのうち再開しますが・・）

_____

　そこで所属させてもらっている **無料プログラミングコミ二ティ「Progaku」** でおそらく自分が技術不足で途中で頓挫させてしまったであろうモブプロをリベンジしようと思い、[主な参考資料③](#主な参考資料)を参考に**PythonによるWebAPI（Application Programming Interface）の勉強をし始めました。**
 　[主な参考書籍③](#主な参考資料) は **「fastapi（PythonのAPI作成に特化したフレームワーク）を中心にAPIを学ぼう」** という内容なのですが、プロジェクトを別に作って分けるのもなんか面倒い（´д｀）（モニターとかPCについてる１枚だけだし・・・何より別に環境構築するの面倒いじゃないですか・・）
　というわけで、**「これまで作ってきたDjangoプロジェクトにAPIを組み込めないか」** と考え調べた結果、以下の結果に至ります。

</details>

<h1>本題：DjangoプロジェクトにAPIを組み込む方法は以下のように少なくとも３つあるらしい</h1>

※全然分かっていない部分もあるので、そこについてはなるべく結論のみを書きます。

## プロジェクト構成について
<h3>※Djangoプロジェクトの関係ある部分のフォルダ構成</h3>

<details><summary>中身の通り</summary>

```
Djangoes(プロジェクト格納フォルダ)
   |
   |__django_portfolio（プロジェクト全体）
　　　　　　　|
　　　　　　　|--django_portfolio（プロジェクト作成時にデフォルトでできるフォルダ、プロジェクトと同名）
　　　　　　　|　　　　|--__init__.py（初期化処理を行うスクリプトファイル）
　　　　　　　|　　　　|--asgi.py（ASGIという非同期Webアプリのためのプログラム）  👈（３）
　　　　　　　|　　　　|--settings.py（プロジェクトの設定情報を記述するファイル）  👈（２）・（３）
　　　　　　　|　　　　|--urls.py（プロジェクトで使うURLを管理するファイル）  👈（２）
　　　　　　　|　　　　|--wsgi.py（WSGIというWebアプリケーションのプログラム）
　　　　　　　|
　　　　　　　|--manage.py（このプロジェクトで実行する機能に関するコマンドなどが記述されているデフォルトであるファイル）
　　　　　　　|--APIs（requestsで引っ張ってきたAPIをviewsで動かせるようにしたアプリ）
　　　　　　　|　　　　|--migrations（マイグレーションファイル（.py）がドンドン貯まっていくフォルダ）
　　　　　　　|　　　　|　　　|--__init__.py
　　　　　　　|　　　　|　　　|--・・・
　　　　　　　|　　　　|
　　　　　　　|　　　　|--__init__.py
　　　　　　　|　　　　|--management（フォルダ）
　　　　　　　|　　　　|　　　|--commands
　　　　　　　|　　　　|　　　　　　　|--postcode_search.py　👈（１Ｂ）
　　　　　　　|　　　　|              （主な参考書籍３のrequestsでAPIを利用するコードをいじったもの）
　　　　　　　|　　　　|--urls.py（アプリ毎の最終的な各エンドポイントを設定するファイル、いわゆるRouter）  👈（２）
　　　　　　　|　　　　|--views.py（各エンドポイントにおける処理を記述するファイル、いわゆるController）👈（２）
　　　　　　　|　　　　|--functions.py（views.pyを見やすくする（粒度を細かくするファイル））👈（２）
　　　　　　　|　　　　|
　　　　　　　|　　　・・・・
　　　　　　　|　　　　
　　　　　　　|--scripts
　　　　　　　|　　　　|--postcode_search.py
　　　　　　　|　　　　　（主な参考書籍３のrequestsでAPIを利用するコードをいじったもの）👈（１Ａ）
       　　・・・・
```
</details>

<h3>〇今の pyproject.toml（※）の外部ライブラリーの部分は以下の通り</h3>

<details><summary>pyproject.toml</summary>

```pyproject.toml

--------前略-----------

[tool.poetry.dependencies]
python = "^3.10"
pytest = "^8.3.3"
Django = "^5.1.3"        👈本Qiitaに特に関係あるもの
requests = "^2.32.3"     👈本Qiitaに特に関係あるもの
pyocr = "^0.8.5"
pillow = "^11.0.0"
django-tables2 = "^2.7.0"
django-filter = "^24.3"
SpeechRecognition = "^3.11.0"
django-extensions = "^3.2.3"
python-dotenv = "^1.0.1"
django-environ = "^0.11.2"
mysql-connector-python = "^9.1.0"
img2pdf = "^0.5.1"
factory-boy = "^3.3.1"
Faker = "^33.3.0"
fastapi = "^0.115.6"      👈本Qiitaに特に関係あるもの
uvicorn = "^0.34.0"       👈本Qiitaに特に関係あるもの
httpx = "^0.28.1"
SQLAlchemy = "^2.0.37"
aiosqlite = "^0.20.0"
pydantic = "^2.10.5"
pydantic-core = "^2.27.2"

----------------後略------------------
```

</details>


※poetry配下にプロジェクト（Djangoに限らない）を置いたときにPythonのバージョンとか使用する外部ライブラリーを記述するファイル。venvとrequirements.txtを合体させたみたいなファイル

<h2>（１）requestsで引っ張ってきて、コマンドラインツール（CLI）として組み込む</h2>

<details><summary>詳細</summary>

※上記のフォルダ構成にある👈（１Ａ）、👈（１Ｂ）に相当
※（１Ａ）の方が「プロジェクト全体」の粒度を調整しやすい。 なお、**どこに作るかで実行コマンドも異なります**
※requests ( https://pypi.org/project/requests/ ) は外部ライブラリーなので、

```powershell
poetry add requests
```
とか
```powershell
python -m pip install requests
```
とかでターミナルとかで開発環境に適宜インストールしてください。

<h3>（１Ａ）：（Djangoプロジェクトとは独立した）コマンドラインツール（CLI）</h3>

```scripts/postcode_search.py
import requests
import json
import sys

def postcode_search(zip_code):
    """郵便番号を指定して住所を検索する"""
    
    # 郵便番号APIのURL
    url = "https://zipcloud.ibsnet.co.jp/api/search"

    # APIに送るパラメータ
    params = {"zipcode": zip_code}

    # APIリクエストを送信
    res = requests.get(url, params)

    # レスポンスのデータをJSONに変換
    data = res.json()

    # 住所情報が取得できたかチェック
    if data.get("results"):
        address_info = data["results"][0]
        address = f"{address_info['address1']} {address_info['address2']} {address_info['address3']}"
        print(f"郵便番号: {zip_code}\n住所: {address}")
    else:
        print("住所情報が見つかりませんでした。")

# コマンドライン引数を受け取る
if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("郵便番号を指定してください。例: python scripts/postcode_search.py 1000000")
    else:
        postcode_search(sys.argv[1])
```
<h3>実行コマンド</h3>

```powershell
python3 scripts/postcode_search.py 1000000
#             (スクリプトファイル名)(パラメータ)
#postcodeはここで必要なパラメータ（≒数値）で、使用するAPIによって要否・値は変わります。
#postcode_search.pyはあくまで、scriptsにこの名前のスクリプトを置いたからで適宜変えてください
```

<h3>（１Ｂ）：（Django の管理コマンドとしての）コマンドラインツール（CLI）</h3>

```APIs/management/commands/postcode_search.py

from django.core.management.base import BaseCommand
import requests
import json

class Command(BaseCommand):
    help = "指定した郵便番号の住所を検索"

    def add_arguments(self, parser):
        parser.add_argument("zipcode", type=str, help="検索する郵便番号")

    def handle(self, *args, **options):
        zip_code = options["zipcode"]

        # 郵便番号APIのURL
        url = "https://zipcloud.ibsnet.co.jp/api/search"
        params = {"zipcode": zip_code}

        res = requests.get(url, params)
        data = res.json()

        if data.get("results"):
            address_info = data["results"][0]
            address = f"{address_info['address1']} {address_info['address2']} {address_info['address3']}"
            self.stdout.write(self.style.SUCCESS(f"郵便番号: {zip_code}\n住所: {address}"))
        else:
            self.stdout.write(self.style.ERROR("住所情報が見つかりませんでした。"))
```

<h3>実行コマンド</h3>

```powershell
python3 manage.py postcode_search 1000001
#             (スクリプトファイル名)(パラメータ)
#postcodeはここで必要なパラメータ（≒数値）で、使用するAPIによって要否・値は変わります。
#postcode_search.pyはあくまで、scriptsにこの名前のスクリプトを置いたからで適宜変えてください
```
_____

<h3>動かしてみた様子</h3>

![with_API①.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3584834/327fd5f0-226f-f9df-b95a-a239b7b477b6.png)

</details>

<h2>（２）requestsで引っ張ってきた結果をJSONレスポンスとして返すビュー関数を定義する</h2>

※上記のフォルダ構成にある👈（２）に相当

<details><summary>こんなかんじ</summary>

![with_API②.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3584834/c090896e-a901-4c12-c606-5e9e3ea0de8a.png)

</details>

※views.pyの２番目のimportを見てもらうと分かると思うのですが、functions.py は粒度を細かくする（≒コードを見やすくする＆リファクタリングしやすくするためにコードを細分化する）ためのものです。

<details><summary>より具体的な詳細</summary>

<h3>（i）JSONレスポンスを返す関数処理の部分</h3>

※Flaskはviews.pyにルーティング処理も書くのが一般的だと思うのですが、Djangoは一般的にはLaravel等のように分けるように思います（通例、以下２つのurls.pyフォルダにルーティングを記述）

<h4>JSONレスポンスがデフォルトだとUnicodeに変換されてしまったのが想定外でした・・・</h4>

※Unicode（文字コードの１つ）については、[主な参考資料④](#主な参考資料)を参照

```django_portfolio(プロジェクト全体)/APIs（アプリの１つ）/views.py
from django.http import JsonResponse
# 別ファイルの関数をインポート
from APIs.functions import get_address_from_zipcode

def postcode_search(request):
    """郵便番号を元に住所を検索するAPIビュー"""
    
    zip_code = request.GET.get("zipcode")
    if not zip_code:
        return JsonResponse(
                            {"error": "郵便番号を指定してください"},
                            status=400
                            )

    response_data = get_address_from_zipcode(zip_code)
    return JsonResponse(response_data,
                        json_dumps_params={"ensure_ascii": False}
                        #このjson_dump～は日本語が Unicode に変換されるのを防ぐためのもの
                        )
```

```django_portfolio(プロジェクト全体)/APIs（アプリの１つ）/functions.py
import requests

def get_address_from_zipcode(zip_code):
    """郵便番号を元に住所を取得する関数"""
    url = "https://zipcloud.ibsnet.co.jp/api/search"
    params = {"zipcode": zip_code}
    # サーバー起動時、以下のような感じでつけるクエリパラメータ
    # http://127.0.0.1:8000/apis/postcode_search/?zipcode=5140061
    # 上記の「apis」「postcode_search」は以下の２つのurls.pyを参照

    try:  #Javaとかだとtry-catch、いわゆる例外処理です
        res = requests.get(url, params)
        data = res.json()

        if data.get("results"):
            address_info = data["results"][0]
            return {
                "郵便番号": address_info["zipcode"],
                "住所": f"{address_info['address1']} {address_info['address2']} {address_info['address3']}"
            } #f""は変数を文字列に突っ込むためのPython独特の記法
        else:
            return {"error": "住所情報が見つかりませんでした"}
    except requests.RequestException:
        return {"error": "APIリクエストに失敗しました"}
```

<h3>（ii）ルーティングの部分</h3>
<h4>（Ａ）各アプリのエンドポイント（≒ＵＲＬ）のprefix（最初の部分）を決める部分</h4>

```django_portfolio(プロジェクト全体)/django_portfolio（プロジェクト作成時にデフォルトできるフォルダ）/urls.py

#どのアドレスにアクセスしたら実行するように、このファイルに追記

"""django_portfolio URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/3.2/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path,include
from django.conf import settings
from django.conf.urls.static import static

#pathは、path( アクセスするアドレス, 呼び出す処理 )
urlpatterns = [
    path('admin/', admin.site.urls),
    path('form_components/', include('form_components.urls')),
    path('apis/', include('APIs.urls')), #👈本Qiitaで関係のあるprefix
    #includeという関数は、引数に指定したモジュールを読み込む
    #これで、component内のアドレス割り当ては、すべてcomponentフォルダ内にあるurls.pyに任せることができる
    #/component/というのがprefixのようになっている状態
    #/component/sample1/というエンドポイントを作成するときは、componentフォルダ内にあるurls.pyに 'sample1/'というのを設定する
    path('was_works/', include('was_works.urls')),
    path('acrobat_paro/', include('acrobat_paro.urls')),
    path('accounts/', include('accounts.urls')),
    path('', include('resume.urls'))
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
```

<h4>（Ｂ）各アプリごとに定めた処理のエンドポイントに関するルーティング</h4>

**※注意　ここでのアプリは「各機能」と同義です。**
以下の折りたたみの各フォルダ（APIs,accounts,acrobat_paro,・・・）が各アプリ

<details><summary>VScodeで見るとこんな感じ</summary>


</details>

```django_portfolio(プロジェクト全体)/APIs（アプリの１つ）/urls.py
from django.urls import path
from APIs import views

app_name = 'APIs'  # 名前空間を設定する

urlpatterns = [
    path('postcode_search/', views.postcode_search, name='postcode_search')
]
```

<h3>（iii）ついつい忘れがちな設定</h3>

**※アプリを作ったら必ずINSTALLED_APPSの要素にアプリ名（フォルダ名）を記述。めんどい**

```django_portfolio(プロジェクト全体)/django_portfolio（プロジェクト作成時にデフォルトできるフォルダ）/settings.py
from pathlib import Path

import os
from pathlib import Path
from dotenv import load_dotenv

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

#__file__ は、現在実行中の Python ファイルのファイルパス
#例えば、main.py というスクリプトを /home/user/project/main.py で実行している場合、
# __file__ の値は文字列として /home/user/project/main.py になる
#Path(__file__).resolve() は、__file__ を絶対パスに変換
#総じて「プロジェクトのフォルダ内にあるdb.sqlite3のパス」を表す

# .envファイルを読み込む
load_dotenv(os.path.join(BASE_DIR, '.env'))

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('SECRET_KEY')

# 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

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth', #デフォルトのmodelsを定義
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django_extensions',
    'resume', #resumeアプリケーションの登録
    'form_components', #form_componentsアプリケーションの登録
    'was_works',
    'acrobat_paro',
    'accounts',
    'APIs',  #👈今回追加
    'fastapi' 
]
```
______

<h3>動かしてみた様子</h3>
<h4>①開発用サーバーを以下のコマンドで起動</h4>

```powershell
python3 manage.py runserver
```
以下みたいな感じになる。なお、前者のurls.pyで
```python
path('', include('resume.urls'))
```
みたいな感じで、prefixなし、後者の特定のアプリで指定したurls.pyで
```python
from django.urls import path
from resume import views as resume_views

urlpatterns = [
    path('', resume_views.home, name='home'),
```
としてhome画面を設定すると、たぶん後々開発が楽

____

<h4>②クエリパラメータ等を付け足す</h4>

とりあえず開発用サーバーを動かして、表示されるエンドポイントにとぶと、上記のようにhomeに設定した画面（※以下の画面は冒頭の[本拙筆を書くまでの経緯](#本拙筆を書くまでの経緯)の詳細にあるLaravelから移行途中の転職活動時の生け贄ポートフォリオのhome画面）がブラウザ表示されます。

![with_API③.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3584834/0ab14ce0-6231-70ae-3d40-ec4bb1462fde.png)

とんでみる

![with_API④.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3584834/efb438a4-7784-681e-99c9-98d99654d908.png)

**エンドポイント・クエリパラメータ追加**

![with_API⑤.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3584834/4453a7ea-0684-c2cb-d195-496ec42e0b60.png)

```django_portfolio(プロジェクト全体)/APIs（アプリの１つ）/views.py
def postcode_search(request):
　　　#------前略---------
    return JsonResponse(response_data,
                        #json_dumps_params={"ensure_ascii": False} 👈これがないと下のようになる
                        #このjson_dump～は日本語が Unicode に変換されるのを防ぐためのもの
                        )
```

それにしても皇居ってスゲえな

</details>


<h2>（３）asgi.pyをいじって、DjangoにWebAPIを作るためのPythonフレームワーク FastAPI を同居させる</h2>

<details><summary>詳細</summary>

※上記のasgi.pyの説明にある **「非同期」というのは、Google mapみたいにリロードしなくても、一部分が変わるみたいな処理による挙動**です（ただ、ここは勉強不足で今後要勉強。一般的には、Javascript系（たぶんNode.jsとかガンガン使う）で実装）

結論として、ターミナルでfastapi、uvicorn（ユニコーンと読むらしい。一角獣のユニコーンと同じ綴り。読めねえ（´д｀）[主な参考資料⑦](#主な参考資料)）を入れて(公式 https://www.uvicorn.org/ )

<details><summary>インストールのコマンド達</summary>


```powershell
poetry add fastapi
```
```powershell
poetry add uvicorn
```
または
```powershell
pip install fastapi
```
```powershell
pip install uvicorn
```

</details>

asgi.pyとsettings.pyを以下のようにするといけました。fastapiなどのverは上記の[プロジェクト構成について](#プロジェクト構成について)の pyproject.tomlを参照してください。

```django_portfolio(プロジェクト全体)/django_portfolio（プロジェクト作成時にデフォルトできるフォルダ）/asgi.py
# Django の urls.py では WSGI ベースのアプリケーションを扱うため、
# FastAPI のような ASGI アプリを正しく組み込む必要があるらしい

import os
import django
from django.core.asgi import get_asgi_application
from fastapi import FastAPI
from starlette.middleware.wsgi import WSGIMiddleware
from starlette.routing import Mount
from starlette.applications import Starlette


# Django 環境変数を設定
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_portfolio.settings')

# Django を初期化
django.setup()

# Django ASGI アプリケーションを取得
django_app = get_asgi_application()

# FastAPI アプリを作成
fastapi_app = FastAPI()

@fastapi_app.get("/")
async def get_hello():
    return {"message": "Hello World"}

# FastAPI のエンドポイントを `/fastapi/` にマウント
app = Starlette(routes=[
    Mount("/fastapi", fastapi_app)  # "/fastapi" に FastAPI をマウント
])

# Django と FastAPI を共存させる ASGI アプリ
async def application(scope, receive, send):
    if scope["path"].startswith("/fastapi"):
        await app(scope, receive, send)  # FastAPI にルーティング
    else:
        await django_app(scope, receive, send)  # Django にルーティング
```

```django_portfolio(プロジェクト全体)/django_portfolio（プロジェクト作成時にデフォルトできるフォルダ）/settings.py

-----------------------前略------------------------

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth', #デフォルトのmodelsを定義
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django_extensions',
    'resume', #resumeアプリケーションの登録
    'form_components', #form_componentsアプリケーションの登録
    'was_works',
    'acrobat_paro',
    'accounts',
    'APIs', 
    'fastapi' #👈今回追加
]
```

______

<h3>動かしてみた様子</h3>

**uvicornはASGIに対応したWebサーバーで** Djangoのクセして以下のコマンドで動かします。

```powershell
uvicorn django_portfolio.asgi:application --reload
```
なお、ポート番号を変えればDjangoのサーバーも以下のように同時に動かせます。

```powershell
python3 manage.py runserver 8001
#8001はポート番号
```

![with_API⑥.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3584834/ec63609a-5321-148c-c567-14a77225af61.png)

※注意：とりあえず最初はここに入ると、**「Djangoプロジェクトの」** ホーム画面に入ります。

その上で上記のasgi.pyで設定したエンドポイント（以下の部分）に入ると、FastAPIが動いている（はず・・）
※この認識で合っているのかは今後このfastapi周りを成長させて要検証

```django_portfolio(プロジェクト全体)/django_portfolio（プロジェクト作成時にデフォルトできるフォルダ）/asgi.py
# FastAPI のエンドポイントを `/fastapi/` にマウント
app = Starlette(routes=[
    Mount("/fastapi", fastapi_app)  # "/fastapi" に FastAPI をマウント
])
```

![with_API⑦.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3584834/109c4bb3-0fa2-ea25-283b-cb145fa96d18.png)

※このJSONは上記の以下の部分

```django_portfolio(プロジェクト全体)/django_portfolio（プロジェクト作成時にデフォルトできるフォルダ）/asgi.py
@fastapi_app.get("/")
async def get_hello():
    return {"message": "Hello World"}
```

</details>


_____

## 主な参考資料

①Python Django 4 超入門　掌田津耶乃 (著)　秀和システム
<details><summary>Amazonリンク</summary>

https://www.amazon.co.jp/Python-Django-4-%E8%B6%85%E5%85%A5%E9%96%80-%E6%8E%8C%E7%94%B0%E6%B4%A5%E8%80%B6%E4%B9%83/dp/4798062413/ref=sr_1_2_sspa?crid=MZGZWVELQ6MA&dib=eyJ2IjoiMSJ9.eEG-HVhGiMXwCJ6xOkfrdY5LDwm8oswFB-HEsTcpi7tolQxne-QjUkBh47VWZQ3x4gpZLBuD4C4SqnayOqtCyOBniyYUG3bXWPEWSZmAj1fIy7ZbHDamJ4_lHsR9wXue9mv-YjuM8yXSL0seZrmHPABx8VgWAMcPo71jJeFlQ4DxWIqgTx3W9XULh4FOyhfMkpoRwry1MlAttgFF1WWYY7EjKvJzQn2oP-fSodDEL7TDRVZ0gVNFU2d8wOckHko20_hKYcHBKAlv61MLnPOC6KB9bd6IXk1Zhp75Y2V6lnY.cYHcC9HbRGJ5EP_NWX1lkmumKLak2nM1yfP1DvKPD4A&dib_tag=se&keywords=python+django+4+%E8%B6%85%E5%85%A5%E9%96%80&qid=1735366082&sprefix=Python+Dj%2Caps%2C262&sr=8-2-spons&sp_csd=d2lkZ2V0TmFtZT1zcF9hdGY&psc=1
</details>

②Python FlaskによるWebアプリ開発入門 物体検知アプリ&機械学習APIの作り方 　佐藤 昌基 ・ 平田 哲也 (著), 寺田 学 (監修)　翔泳社

<details><summary>Amazonリンク</summary>

https://www.amazon.co.jp/Python-Flask%E3%81%AB%E3%82%88%E3%82%8BWeb%E3%82%A2%E3%83%97%E3%83%AA%E9%96%8B%E7%99%BA%E5%85%A5%E9%96%80-%E7%89%A9%E4%BD%93%E6%A4%9C%E7%9F%A5%E3%82%A2%E3%83%97%E3%83%AA-%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92API%E3%81%AE%E4%BD%9C%E3%82%8A%E6%96%B9-%E4%BD%90%E8%97%A4/dp/4798166464
</details>

③Python FastAPI本格入門　樹下雅章（著） 技術評論社

<details><summary>Amazonリンク</summary>

https://www.amazon.co.jp/Python-FastAPI%E6%9C%AC%E6%A0%BC%E5%85%A5%E9%96%80-%E6%A8%B9%E4%B8%8B-%E9%9B%85%E7%AB%A0/dp/4297144476/ref=sr_1_2_sspa?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&dib=eyJ2IjoiMSJ9.Q-6iroHbAHX_1hT2vGrkb23niQM05GLqjzWGHrO_iGYRLHOIvdlxAPRkf5lTNGGXD1TXTBoDZXoeODCs48d6n8o1Ug3j0GOsEoqiF4Kc3imSpVdg-voGYMy76oecED6HY4YRjL1S6f4rrIp8GHyEF6yQfZX58yEBRccABJH2IVvptfXgOSkNe7Thf1DRQ5lxT95F0vMp_Qe4VBKptGkO21PXrrqilMJ99uwFlUcVBLZA3tdQS3kJpUdmQq-E54eWIw6OsfnhUnI3NltkwIhWTfrzhMlLpeMqlq24sGPxi6g.a6H51KJsPdLYZyIljq4PI-vqXXnMXgbde2TYifu4Vfs&dib_tag=se&keywords=python+fastapi%E6%9C%AC%E6%A0%BC%E5%85%A5%E9%96%80&qid=1738401931&sr=8-2-spons&sp_csd=d2lkZ2V0TmFtZT1zcF9hdGY&psc=1
</details>

④unicodeとは？文字コードとは？UTF-8とは？　@hiroyuki_mrp さん Qiita
https://qiita.com/hiroyuki_mrp/items/f0b497394f3a5d8a8395

※本Qiitaとは全然関係ないのですが、VScodeはUTF-8で読み込むようにデフォルト設定されていて、業務で一部Shift-JISで書かれたレガシーを扱う際に以下の記事が参考になりました。

https://www.javadrive.jp/vscode/setting/index4.html

⑤[Python] 例外処理について　@yam_dev (masashi yamashita) in 株式会社ダイヤモンドファンタジー さんQiita
https://qiita.com/yam_dev/items/d35a32a350c425f7f369

⑥図解Django超入門ハンズオン  @myasuda220780 in 株式会社スカイウイル さんQiita
https://qiita.com/myasuda220780/items/c0c80742e62939a6eede

⑦FastAPI × Uvicorn：最強のAPI高速化コンビ誕生！　　@Leapcell (leapcell)さんQiita
https://qiita.com/Leapcell/items/0c0bf5e0fe84b3356c82