本拙筆を書くまでの経緯
詳細
以下Qiitaの続きです。実務未経験から(広義の)エンジニアとして転職して正社員になり1年経ちました。プログラミング学習期間は約2年になりました。今も雑魚なので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についてる1枚だけだし・・・何より別に環境構築するの面倒いじゃないですか・・)
というわけで、「これまで作ってきたDjangoプロジェクトにAPIを組み込めないか」 と考え調べた結果、以下の結果に至ります。
本題:DjangoプロジェクトにAPIを組み込む方法は以下のように少なくとも3つあるらしい
※全然分かっていない部分もあるので、そこについてはなるべく結論のみを書きます。
プロジェクト構成について
※Djangoプロジェクトの関係ある部分のフォルダ構成
中身の通り
Djangoes(プロジェクト格納フォルダ)
|
|__django_portfolio(プロジェクト全体)
|
|--django_portfolio(プロジェクト作成時にデフォルトでできるフォルダ、プロジェクトと同名)
| |--__init__.py(初期化処理を行うスクリプトファイル)
| |--asgi.py(ASGIという非同期Webアプリのためのプログラム) 👈(3)
| |--settings.py(プロジェクトの設定情報を記述するファイル) 👈(2)・(3)
| |--urls.py(プロジェクトで使うURLを管理するファイル) 👈(2)
| |--wsgi.py(WSGIというWebアプリケーションのプログラム)
|
|--manage.py(このプロジェクトで実行する機能に関するコマンドなどが記述されているデフォルトであるファイル)
|--APIs(requestsで引っ張ってきたAPIをviewsで動かせるようにしたアプリ)
| |--migrations(マイグレーションファイル(.py)がドンドン貯まっていくフォルダ)
| | |--__init__.py
| | |--・・・
| |
| |--__init__.py
| |--management(フォルダ)
| | |--commands
| | |--postcode_search.py 👈(1B)
| | (主な参考書籍3のrequestsでAPIを利用するコードをいじったもの)
| |--urls.py(アプリ毎の最終的な各エンドポイントを設定するファイル、いわゆるRouter) 👈(2)
| |--views.py(各エンドポイントにおける処理を記述するファイル、いわゆるController)👈(2)
| |--functions.py(views.pyを見やすくする(粒度を細かくするファイル))👈(2)
| |
| ・・・・
|
|--scripts
| |--postcode_search.py
| (主な参考書籍3のrequestsでAPIを利用するコードをいじったもの)👈(1A)
・・・・
〇今の pyproject.toml(※)の外部ライブラリーの部分は以下の通り
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"
----------------後略------------------
※poetry配下にプロジェクト(Djangoに限らない)を置いたときにPythonのバージョンとか使用する外部ライブラリーを記述するファイル。venvとrequirements.txtを合体させたみたいなファイル
(1)requestsで引っ張ってきて、コマンドラインツール(CLI)として組み込む
詳細
※上記のフォルダ構成にある👈(1A)、👈(1B)に相当
※(1A)の方が「プロジェクト全体」の粒度を調整しやすい。 なお、どこに作るかで実行コマンドも異なります
※requests ( https://pypi.org/project/requests/ ) は外部ライブラリーなので、
poetry add requests
とか
python -m pip install requests
とかでターミナルとかで開発環境に適宜インストールしてください。
(1A):(Djangoプロジェクトとは独立した)コマンドラインツール(CLI)
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])
実行コマンド
python3 scripts/postcode_search.py 1000000
# (スクリプトファイル名)(パラメータ)
#postcodeはここで必要なパラメータ(≒数値)で、使用するAPIによって要否・値は変わります。
#postcode_search.pyはあくまで、scriptsにこの名前のスクリプトを置いたからで適宜変えてください
(1B):(Django の管理コマンドとしての)コマンドラインツール(CLI)
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("住所情報が見つかりませんでした。"))
実行コマンド
python3 manage.py postcode_search 1000001
# (スクリプトファイル名)(パラメータ)
#postcodeはここで必要なパラメータ(≒数値)で、使用するAPIによって要否・値は変わります。
#postcode_search.pyはあくまで、scriptsにこの名前のスクリプトを置いたからで適宜変えてください
動かしてみた様子
(2)requestsで引っ張ってきた結果をJSONレスポンスとして返すビュー関数を定義する
※上記のフォルダ構成にある👈(2)に相当
※views.pyの2番目のimportを見てもらうと分かると思うのですが、functions.py は粒度を細かくする(≒コードを見やすくする&リファクタリングしやすくするためにコードを細分化する)ためのものです。
より具体的な詳細
(i)JSONレスポンスを返す関数処理の部分
※Flaskはviews.pyにルーティング処理も書くのが一般的だと思うのですが、Djangoは一般的にはLaravel等のように分けるように思います(通例、以下2つのurls.pyフォルダにルーティングを記述)
JSONレスポンスがデフォルトだとUnicodeに変換されてしまったのが想定外でした・・・
※Unicode(文字コードの1つ)については、主な参考資料④を参照
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 に変換されるのを防ぐためのもの
)
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」は以下の2つの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リクエストに失敗しました"}
(ii)ルーティングの部分
(A)各アプリのエンドポイント(≒URL)のprefix(最初の部分)を決める部分
#どのアドレスにアクセスしたら実行するように、このファイルに追記
"""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)
(B)各アプリごとに定めた処理のエンドポイントに関するルーティング
※注意 ここでのアプリは「各機能」と同義です。
以下の折りたたみの各フォルダ(APIs,accounts,acrobat_paro,・・・)が各アプリ
VScodeで見るとこんな感じ
from django.urls import path
from APIs import views
app_name = 'APIs' # 名前空間を設定する
urlpatterns = [
path('postcode_search/', views.postcode_search, name='postcode_search')
]
(iii)ついつい忘れがちな設定
※アプリを作ったら必ずINSTALLED_APPSの要素にアプリ名(フォルダ名)を記述。めんどい
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'
]
動かしてみた様子
①開発用サーバーを以下のコマンドで起動
python3 manage.py runserver
以下みたいな感じになる。なお、前者のurls.pyで
path('', include('resume.urls'))
みたいな感じで、prefixなし、後者の特定のアプリで指定したurls.pyで
from django.urls import path
from resume import views as resume_views
urlpatterns = [
path('', resume_views.home, name='home'),
としてhome画面を設定すると、たぶん後々開発が楽
②クエリパラメータ等を付け足す
とりあえず開発用サーバーを動かして、表示されるエンドポイントにとぶと、上記のようにhomeに設定した画面(※以下の画面は冒頭の本拙筆を書くまでの経緯の詳細にあるLaravelから移行途中の転職活動時の生け贄ポートフォリオのhome画面)がブラウザ表示されます。
とんでみる
エンドポイント・クエリパラメータ追加
def postcode_search(request):
#------前略---------
return JsonResponse(response_data,
#json_dumps_params={"ensure_ascii": False} 👈これがないと下のようになる
#このjson_dump~は日本語が Unicode に変換されるのを防ぐためのもの
)
それにしても皇居ってスゲえな
(3)asgi.pyをいじって、DjangoにWebAPIを作るためのPythonフレームワーク FastAPI を同居させる
詳細
※上記のasgi.pyの説明にある 「非同期」というのは、Google mapみたいにリロードしなくても、一部分が変わるみたいな処理による挙動です(ただ、ここは勉強不足で今後要勉強。一般的には、Javascript系(たぶんNode.jsとかガンガン使う)で実装)
結論として、ターミナルでfastapi、uvicorn(ユニコーンと読むらしい。一角獣のユニコーンと同じ綴り。読めねえ(´д`)主な参考資料⑦)を入れて(公式 https://www.uvicorn.org/ )
インストールのコマンド達
poetry add fastapi
poetry add uvicorn
または
pip install fastapi
pip install uvicorn
asgi.pyとsettings.pyを以下のようにするといけました。fastapiなどのverは上記のプロジェクト構成についての pyproject.tomlを参照してください。
# 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 にルーティング
-----------------------前略------------------------
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' #👈今回追加
]
動かしてみた様子
uvicornはASGIに対応したWebサーバーで Djangoのクセして以下のコマンドで動かします。
uvicorn django_portfolio.asgi:application --reload
なお、ポート番号を変えればDjangoのサーバーも以下のように同時に動かせます。
python3 manage.py runserver 8001
#8001はポート番号
※注意:とりあえず最初はここに入ると、「Djangoプロジェクトの」 ホーム画面に入ります。
その上で上記のasgi.pyで設定したエンドポイント(以下の部分)に入ると、FastAPIが動いている(はず・・)
※この認識で合っているのかは今後このfastapi周りを成長させて要検証
# FastAPI のエンドポイントを `/fastapi/` にマウント
app = Starlette(routes=[
Mount("/fastapi", fastapi_app) # "/fastapi" に FastAPI をマウント
])
※このJSONは上記の以下の部分
@fastapi_app.get("/")
async def get_hello():
return {"message": "Hello World"}
主な参考資料
①Python Django 4 超入門 掌田津耶乃 (著) 秀和システム
②Python FlaskによるWebアプリ開発入門 物体検知アプリ&機械学習APIの作り方 佐藤 昌基 ・ 平田 哲也 (著), 寺田 学 (監修) 翔泳社
③Python FastAPI本格入門 樹下雅章(著) 技術評論社
④unicodeとは?文字コードとは?UTF-8とは? @hiroyuki_mrp さん Qiita
https://qiita.com/hiroyuki_mrp/items/f0b497394f3a5d8a8395
※本Qiitaとは全然関係ないのですが、VScodeはUTF-8で読み込むようにデフォルト設定されていて、業務で一部Shift-JISで書かれたレガシーを扱う際に以下の記事が参考になりました。
⑤[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