1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Django Ninja】定義されているパスを全取得

Last updated at Posted at 2023-06-02

はじめに

DjangoDjango REST frameworkで定義されているエンドポイントを全て取得する」という内容で記事を書こうと考えていたところ、Django Ninjaを発見してしまいました。
Django Ninjaは2023/06現在、まだメジャーバージョンが出ておりません。
ただ、結構イケていそうで触ってみたい気持ちが勝ったのと、Django NinjaでもDRFでも記事にしたかった部分は変わらないので、「Django Ninjaで定義されているエンドポイントを全て取得する」方法について書いていきます。
よろしくお願いします。

ディレクトリ構成

基本的にはDjango Ninjaのチュートリアルに沿って定義します。
今回は accountitem という抽象的なアプリケーション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 のユニオン型リストとなっています。
URLResolverincludeで定義されたもの、URLPatternpathで定義されたものを指します。
深さ優先探索を用いて再帰的に潜ってやることでエンドポイントを全て取得します。

これで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でやる分にはここで紹介した方法でキレイに取れます。
それでじゃあこれの使いどころは何処かと言うと……テストとかですかね?
終わります。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?