- 公式ドキュメント: Django Ninja
- FastAPIに強く影響を受けた、DjangoのREST API作成用フレームワーク
- Python の型ヒントを利用して Web API のスキーマを定義でき、バリデーションも実行できる
- https://django-ninja.dev/tutorial/ を一通り眺めると基本要素をカバーできる
- URLの登録処理
- HTTP method (GET, POST, PUT, etc)の登録方法
- Requestのパース処理
- Responseの扱い
- https://django-ninja.dev/tutorial/other/crud/ をもとにCRUDを実装したイメージができる。
- 公式ドキュメント: Django Ninja Extra
- クラスベースの実装をしたい場合はDjango Ninjaはサポートしていないため、Django Ninja Extraの導入を検討する。
- 既存の処理がクラスベースで実装しているので、この実装を活かす場合はこちらの選択になる。
以下の機能はデフォルトでサポートしている。
Pydantic v2 サポート
- 今回は入出力の指定にSchemaを定義するときにPydanticが使われる。
OpenAPIドキュメント: API Docs
- RequestとResponseのSchemaを書いて指定すればRedoc形式でいい感じに表示できる。
- Responseを他の仕組みで(例: bpmappers)処理している場合、二度手間に感じるかもしれない。
OAuth2やAPI Keyでの認証対応: Authentication
- Django セッション認証やBasic認証、APIキー(クエリー文字列、Cookie、リクエストヘッダー)などサポートしている。
- Django セッション認証はデフォルトでCookieを用いている。
- APIキーの認証(authenticate)の実装自体は自分で行う必要がある。
- OAuth2はHttpBearerを拡張してdjango-oauth-toolkitを組み合わせて実装する手法が紹介されている。DRFでも同様のパッケージが紹介されているのでここは差はなさそう。
- extra x jwtの場合 django-ninja-jwt · PyPI を使う
Throttling: v1.2.0でサポート
- extraを使うとバーストモードと持続モードで分けて記述したり、認証済みユーザーとそうでないユーザーでしきい値を変更など柔軟に対応可能。
- https://eadwincode.github.io/django-ninja-extra/tutorial/throttling/
# main.py
import time
import requests
# NOTE: nicknameは適時書き換え
url = "https://example.com/api/v1/user/?nickname=kashew_local"
requests_per_second = 100
interval = 1 / requests_per_second
def send_requests(url):
while True:
response = requests.get(url)
print(f"Status Code: {response.status_code}, Response: {response.text}")
time.sleep(interval)
if __name__ == "__main__":
send_requests(url)
python3 main.py
Status Code: 200, Response: {"results_start": 1, "results_returned": 1, "results_available": 1, "users": [{"user_id": 578232, "nickname": "kashew_local", "display_name": "kashewkashewkashewkashewkashew", "description": "\u4eba\u751f\u3068\u3044\u3046\u58c1\u3092\u767b\u308bSoftwareDeveloper. \u4ed5\u4e8b\u3067\u306fPython\u3068\u304bDjango\u3068\u304b. \u6687\u3055\u3048\u3042\u308c\u3070Vim\u3063\u305f\u308a\u3001\u30dc\u30eb\u30c0\u30ea\u30f3\u30b0\u3057\u305f\u308a\u3059\u308b. Wish List: https://t.co/tA2Psk0Ot3", "user_url": "https://example.com/user/kashew_local/", "user_image_url": "https://media.dev-connpass.owo3.net/thumbs/4c/2f/4c2fa504a87486db29c73b5ab397d2c3.png", "created_at": "2023-10-13T10:50:41+09:00", "attended_event_count": 6, "organize_event_count": 27, "presenter_event_count": 1, "bookmark_event_count": 1}]}
Status Code: 200, Response: {"results_start": 1, "results_returned": 1, "results_available": 1, "users": [{"user_id": 578232, "nickname": "kashew_local", "display_name": "kashewkashewkashewkashewkashew", "description": "\u4eba\u751f\u3068\u3044\u3046\u58c1\u3092\u767b\u308bSoftwareDeveloper. \u4ed5\u4e8b\u3067\u306fPython\u3068\u304bDjango\u3068\u304b. \u6687\u3055\u3048\u3042\u308c\u3070Vim\u3063\u305f\u308a\u3001\u30dc\u30eb\u30c0\u30ea\u30f3\u30b0\u3057\u305f\u308a\u3059\u308b. Wish List: https://t.co/tA2Psk0Ot3", "user_url": "https://example.com/user/kashew_local/", "user_image_url": "https://media.dev-connpass.owo3.net/thumbs/4c/2f/4c2fa504a87486db29c73b5ab397d2c3.png", "created_at": "2023-10-13T10:50:41+09:00", "attended_event_count": 6, "organize_event_count": 27, "presenter_event_count": 1, "bookmark_event_count": 1}]}
Status Code: 200, Response: {"results_start": 1, "results_returned": 1, "results_available": 1, "users": [{"user_id": 578232, "nickname": "kashew_local", "display_name": "kashewkashewkashewkashewkashew", "description": "\u4eba\u751f\u3068\u3044\u3046\u58c1\u3092\u767b\u308bSoftwareDeveloper. \u4ed5\u4e8b\u3067\u306fPython\u3068\u304bDjango\u3068\u304b. \u6687\u3055\u3048\u3042\u308c\u3070Vim\u3063\u305f\u308a\u3001\u30dc\u30eb\u30c0\u30ea\u30f3\u30b0\u3057\u305f\u308a\u3059\u308b. Wish List: https://t.co/tA2Psk0Ot3", "user_url": "https://example.com/user/kashew_local/", "user_image_url": "https://media.dev-connpass.owo3.net/thumbs/4c/2f/4c2fa504a87486db29c73b5ab397d2c3.png", "created_at": "2023-10-13T10:50:41+09:00", "attended_event_count": 6, "organize_event_count": 27, "presenter_event_count": 1, "bookmark_event_count": 1}]}
Status Code: 200, Response: {"results_start": 1, "results_returned": 1, "results_available": 1, "users": [{"user_id": 578232, "nickname": "kashew_local", "display_name": "kashewkashewkashewkashewkashew", "description": "\u4eba\u751f\u3068\u3044\u3046\u58c1\u3092\u767b\u308bSoftwareDeveloper. \u4ed5\u4e8b\u3067\u306fPython\u3068\u304bDjango\u3068\u304b. \u6687\u3055\u3048\u3042\u308c\u3070Vim\u3063\u305f\u308a\u3001\u30dc\u30eb\u30c0\u30ea\u30f3\u30b0\u3057\u305f\u308a\u3059\u308b. Wish List: https://t.co/tA2Psk0Ot3", "user_url": "https://example.com/user/kashew_local/", "user_image_url": "https://media.dev-connpass.owo3.net/thumbs/4c/2f/4c2fa504a87486db29c73b5ab397d2c3.png", "created_at": "2023-10-13T10:50:41+09:00", "attended_event_count": 6, "organize_event_count": 27, "presenter_event_count": 1, "bookmark_event_count": 1}]}
Status Code: 200, Response: {"results_start": 1, "results_returned": 1, "results_available": 1, "users": [{"user_id": 578232, "nickname": "kashew_local", "display_name": "kashewkashewkashewkashewkashew", "description": "\u4eba\u751f\u3068\u3044\u3046\u58c1\u3092\u767b\u308bSoftwareDeveloper. \u4ed5\u4e8b\u3067\u306fPython\u3068\u304bDjango\u3068\u304b. \u6687\u3055\u3048\u3042\u308c\u3070Vim\u3063\u305f\u308a\u3001\u30dc\u30eb\u30c0\u30ea\u30f3\u30b0\u3057\u305f\u308a\u3059\u308b. Wish List: https://t.co/tA2Psk0Ot3", "user_url": "https://example.com/user/kashew_local/", "user_image_url": "https://media.dev-connpass.owo3.net/thumbs/4c/2f/4c2fa504a87486db29c73b5ab397d2c3.png", "created_at": "2023-10-13T10:50:41+09:00", "attended_event_count": 6, "organize_event_count": 27, "presenter_event_count": 1, "bookmark_event_count": 1}]}
Status Code: 429, Response: {"detail": "Request was throttled. Expected available in 1 second."}
Status Code: 429, Response: {"detail": "Request was throttled. Expected available in 1 second."}
Status Code: 429, Response: {"detail": "Request was throttled. Expected available in 1 second."}
Status Code: 429, Response: {"detail": "Request was throttled. Expected available in 1 second."}
....
- デフォルトでLimitOffsetPaginationとPageNumberPaginationがあり、カスタマイズもできる。
- 入力値をlimitやoffsetでなく、startとcountにしたり、出力するフィールドを変更したい場合はカスタマイズする。
- 関数ベースの場合、django-ninjaで個別にrouterを定義してけば解決しそう。
- クラスベースの場合、Django Ninja Extraを使って、基本はapi_controllerデコレーターをつけていき、routeを定義していけば解決しそう。
- urls.pyでの定義を楽にするために api.add_routerはDjango-ninja-extraで使えるか?
- https://django-ninja.dev/guides/routers/
- api_controllerで定義していく仕組みなので使えない eadwinCode/django-ninja-extra#163 (comment)
api.auto_discover_controllers()
なるものを使えば自動でrouteの定義を拾ってくるようだが、対象のモジュール名が固定(apiとcontrollers)で入っているため後からいれると怖いかも。そしてドキュメントには乗ってないなぞ。
- cache
- https://django-ninja.dev/whatsnew_v1/?h=cache#decorators によると
from ninja.decorators import decorate_view
を使うとできるが、extraでも付き合えるかは要確認
- https://django-ninja.dev/whatsnew_v1/?h=cache#decorators によると
- 既存の処理が壊れていないか?
- DjangoのViewクラスを使った実装から乗せかえる場合、呼び出し方が変わるのでテストクライアントベースにテストコードを書き換える必要がある。
- 内部向けと外部向けをどうやってわけるか→異なるバージョンとURLを指すNinjaAPI インスタンスを立てればよい
from __future__ import annotation
はPydanticで問題が発生するために使えない。X | Y
などを使いたい場合はPython3.10以上にアップデートをしたい。- PEP563はPython3.11で無期限延期になっているので、バージョンアップした後は
from __future__ import annotation
の記述は不要になる。 - 参考: `from __future__ import annotations` がPython 3.10でデフォルトにならなくなりました
- PEP563はPython3.11で無期限延期になっているので、バージョンアップした後は