はじめに
「DjangoとDjango REST frameworkで定義されているエンドポイントを全て取得する」という内容で記事を書こうと考えていたところ、Django Ninjaを発見してしまいました。
Django Ninjaは2023/06現在、まだメジャーバージョンが出ておりません。
ただ、結構イケていそうで触ってみたい気持ちが勝ったのと、Django NinjaでもDRFでも記事にしたかった部分は変わらないので、「Django Ninjaで定義されているエンドポイントを全て取得する」方法について書いていきます。
よろしくお願いします。
ディレクトリ構成
基本的にはDjango Ninjaのチュートリアルに沿って定義します。
今回は account
と item
という抽象的なアプリケーション2つと、エンドポイント全てを取得するアプリケーション all_url
を定義しています。
環境はpoetryで作っちゃいました。
.
├── django_project
│ ├── apps
│ │ ├── __init__.py
│ │ ├── account
│ │ │ ├── __init__.py
│ │ │ ├── apps.py
│ │ │ ├── schema.py
│ │ │ └── views.py
│ │ ├── all_url
│ │ │ ├── __init__.py
│ │ │ ├── apps.py
│ │ │ └── views.py
│ │ └── item
│ │ ├── __init__.py
│ │ ├── apps.py
│ │ ├── schema.py
│ │ └── views.py
│ ├── django_project
│ │ ├── __init__.py
│ │ ├── asgi.py
│ │ ├── settings.py
│ │ ├── urls.py
│ │ └── wsgi.py
│ └── manage.py
├── poetry.lock
└── pyproject.toml
エンドポイント定義
それぞれviewsを定義します。
返り値とかリクエストボディとかは今回は何でもいいので深掘りません。
全てのエンドポイントを取得するにあたって、url名がほしいので url_name
を定義しています。(参考)
Djangoではdjango.urls.pathの引数に name
があるので、ここで定義します。
- account.views
from ninja import Router
from .schema import Account
router = Router()
@router.get('/', url_name='account_list')
def account_list(request):
return [
'account_1',
'account_2',
'account_3'
]
@router.get('/{int:account_id}', url_name='account_detail')
def account_detail(request, account_id: int):
return {'item_id': account_id}
@router.post('/', url_name='account_create')
def create(request, item: Account):
return item
- item.views
from ninja import Router
from .schema import Item
router = Router()
@router.get('/', url_name='item_list')
def item_list(request):
return [
'item_1',
'item_2',
'item_3'
]
@router.get('/{int:item_id}', url_name='item_detail')
def item_detail(request, item_id: int):
return {'item_id': item_id}
@router.post('/', url_name='item_create')
def create(request, item: Item):
return item
そして今回の肝となるエンドポイント全取得APIです。
responseにオブジェクトは含められないので、 url_name
を返却しています。
- all_url.view
from ninja import Router
from django.urls import get_resolver, URLResolver, URLPattern
router = Router()
@router.get('/', url_name='all_url_list')
def all_url_list(request):
targets = [
url
for url in get_resolver().url_patterns
]
all_url = list()
def dfs_urls(url_patterns):
for url in url_patterns:
if isinstance(url, URLResolver):
dfs_urls(url.url_patterns)
elif isinstance(url, URLPattern):
all_url.append(url.name)
dfs_urls(targets)
return all_url
get_resolver()
でエンドポイント解決周りのあれこれが取ってこれるので、 url_patterns
で一覧を取得。URLResolver
または URLPattern
のユニオン型リストとなっています。
URLResolver
はincludeで定義されたもの、URLPattern
はpathで定義されたものを指します。
深さ優先探索を用いて再帰的に潜ってやることでエンドポイントを全て取得します。
これでAPIはすべて定義しました。
今度はこれらを urlpatterns
に登録していきます。
urlpatterns
django_project.urls.py
に記載します。
Reverse Resolution of URLSに従うがまま書きました。
from django.contrib import admin
from django.urls import path
from ninja import NinjaAPI
from apps.item.views import router as item_router
from apps.account.views import router as account_router
from apps.all_url.views import router as all_url_router
api = NinjaAPI()
api.add_router('/items/', item_router)
api.add_router('/accounts/', account_router)
api.add_router('/endpoint/', all_url_router)
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', api.urls),
]
動かしてみる
poetry shell
cd path/to/dir
python manage.py runserver 8080
- response
[
"index",
"login",
"logout",
"password_change",
"password_change_done",
"autocomplete",
"jsi18n",
"view_on_site",
"auth_group_changelist",
"auth_group_add",
"auth_group_history",
"auth_group_delete",
"auth_group_change",
null,
"auth_user_password_change",
"auth_user_changelist",
"auth_user_add",
"auth_user_history",
"auth_user_delete",
"auth_user_change",
null,
"app_list",
null,
"openapi-json",
"openapi-view",
"item_list",
"item_create",
"item_detail",
"account_list",
"account_create",
"account_detail",
"all_url_list",
"api-root"
]
下の方にviewで定義した url_name
が見受けられます。
でも余計なのも多い。
よけいなのを消す
if url.app_name not in ['admin']
を all_url.view
に追加します。
- all_url.view
from ninja import Router
from django.urls import get_resolver, URLResolver, URLPattern
router = Router()
@router.get('/', url_name='all_url_list')
def all_url_list(request):
targets = [
url
for url in get_resolver().url_patterns
+ if url.app_name not in ['admin']
]
all_url = list()
def dfs_urls(url_patterns):
for url in url_patterns:
if isinstance(url, URLResolver):
dfs_urls(url.url_patterns)
elif isinstance(url, URLPattern):
all_url.append(url.name)
dfs_urls(targets)
return all_url
かなりすっきり
url.app_name
というのはDjangoのURL名の名前空間で定義されたものになります。
urlpatterns
の最初にある path('admin/', admin.site.urls),
が名前空間 admin
で定義されているため、リスト内包表記内で弾くことができました。
[
"openapi-json",
"openapi-view",
"item_list",
"item_create",
"item_detail",
"account_list",
"account_create",
"account_detail",
"all_url_list",
"api-root"
]
ただまだ自分で定義していないのがいます。
Django NinjaではOpen APIの生成もよしなにやってくれているのでそれっぽいですね。
ちなみにDjango Ninjaみたいなのを噛ませず、pathとincludeでやる分にはこれで自分が定義したpath_nameのみ全て取ってこれます。
ルーティングがネストしていても問題ありません。
ここまででninjaもninjaでURLの名前空間が定義されているのでは? と考えられます。
確認してみましょう。
- all_url.view
from ninja import Router
from django.urls import get_resolver, URLResolver, URLPattern
router = Router()
@router.get('/', url_name='all_url_list')
def all_url_list(request):
targets = [
url
for url in get_resolver().url_patterns
if url.app_name not in ['admin']
]
all_url = list()
+ print([t.app_name for t in targets])
def dfs_urls(url_patterns):
for url in url_patterns:
if isinstance(url, URLResolver):
dfs_urls(url.url_patterns)
elif isinstance(url, URLPattern):
all_url.append(url.name)
dfs_urls(targets)
return all_url
- 実行結果
["ninja"]
なるほど。ninjaを弾いてみます。
- all_url.view
from ninja import Router
from django.urls import get_resolver, URLResolver, URLPattern
router = Router()
@router.get('/', url_name='all_url_list')
def all_url_list(request):
targets = [
url
for url in get_resolver().url_patterns
+ if url.app_name not in ['admin', 'ninja']
]
all_url = list()
def dfs_urls(url_patterns):
for url in url_patterns:
if isinstance(url, URLResolver):
dfs_urls(url.url_patterns)
elif isinstance(url, URLPattern):
all_url.append(url.name)
dfs_urls(targets)
return all_url
- 実行結果
[]
なにもなくなりました。
routerのパス定義がDjango Ninjaに乗っかっているので当然っちゃ当然ですね。
なんとか完璧に取り出せないか試してみたのですが、ソースコードを読みに行ったら無理そうでした。
ちなみにどうしても出てきてしまう openapi-***
はopenAPIのパスで、デフォルトで生成されますがオフにすることもできます。
おわりに
Django Ninjaみたいなのを噛ませず、pathとincludeでやる分にはここで紹介した方法でキレイに取れます。
それでじゃあこれの使いどころは何処かと言うと……テストとかですかね?
終わります。