Django Ninja 설정 방법

Wagtail은 Django REST Framework 기반의 내장 API 모듈을 제공하지만, 다른 API 프레임워크를 사용하는 것도 가능합니다. 여기서는 Python 타입 힌트와 Pydantic을 기반으로 구축된 API 프레임워크인 Django Ninja 사용법에 대한 정보를 제공하며, OpenAPI 스키마에 대한 내장 지원을 포함합니다.

기본 설정

django-ninja 를 설치합니다. 선택적으로 INSTALLED_APPSninja 를 추가하여 OpenAPI 문서 뷰어를 사용할 때 외부에서 정적 파일을 로드하지 않도록 할 수 있습니다.

API 생성

프로젝트 루트의 기존 urls.py 파일 옆에 새로운 api.py 모듈을 만들고 라우터를 인스턴스화합니다.

# api.py
from typing import Literal
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from ninja import Field, ModelSchema, NinjaAPI
from wagtail.models import Page

api = NinjaAPI()

다음으로, Django가 요청을 API로 라우팅할 수 있도록 URL을 등록합니다. 이것이 작동하는지 테스트하려면 /api/docs 로 이동하여 OpenAPI 문서를 표시합니다 (아직 사용 가능한 엔드포인트는 없음).

# urls.py

from .api import api

urlpatterns = [
    ...

    path("api/", api.urls),

    ...

    # api 라인이 기본 Wagtail 페이지 제공 경로 위에 오도록 합니다.
    path("", include(wagtail_urls)),
]

첫 번째 엔드포인트

사이트의 모든 페이지 목록을 반환하는 간단한 엔드포인트를 만듭니다. @api.get 작업 데코레이터를 사용하여 엔드포인트가 사용 가능한 경로와 응답 형식을 정의합니다. 여기서는 우리가 만드는 사용자 정의 스키마를 사용합니다.

# api.py


class BasePageSchema(ModelSchema):
    url: str = Field(None, alias="get_url")

    class Config:
        model = Page
        model_fields = [
            "id",
            "title",
            "slug",
        ]


@api.get("/pages/", response=list[BasePageSchema])
def list_pages(request: "HttpRequest"):
    return Page.objects.live().public().exclude(id=1)

우리의 사용자 정의 BasePageSchema 는 두 가지 기술을 결합합니다: Django 모델에서 스키마 생성별칭을 사용한 계산된 필드. 여기서는 별칭을 사용하여 페이지 URL을 검색합니다.

또한 엔드포인트에 child_of: int = None 매개변수를 추가하여 부모별로 페이지를 필터링할 수 있습니다.

@api.get("/pages/", response=list[BasePageSchema])
def list_pages(request: "HttpRequest", child_of: int = None):
    if child_of:
        return get_object_or_404(Page, id=child_of).get_children().live().public()
    # 페이지 트리 루트를 제외합니다.
    return Page.objects.live().public().exclude(id=1)

Ninja는 list_pages 함수의 모든 매개변수를 쿼리 매개변수로 처리합니다. 제공된 타입 힌트를 사용하여 값을 구문 분석하고 유효성을 검사하며 OpenAPI 스키마를 생성합니다.

사용자 정의 페이지 필드 추가

다음으로, 특정 유형의 단일 페이지를 반환하는 “상세” API 엔드포인트를 추가해 보겠습니다. Ninja의 경로 매개변수를 사용하여 page_id 를 검색할 수 있습니다. 또한 특정 페이지 유형에 대한 새 스키마를 만듭니다. 여기서는 BasePageSchema 를 기반으로 하는 BlogPage 입니다.

from blog.models import BlogPage


class BlogPageSchema(BasePageSchema, ModelSchema):
    class Config(BasePageSchema.Config):
        model = BlogPage
        model_fields = [
            "intro",
        ]


@api.get("/pages/{page_id}/", response=BlogPageSchema)
def get_page(request: "HttpRequest", page_id: int):
    return get_object_or_404(Page, id=page_id).specific

이것은 잘 작동하며, 엔드포인트는 이제 일반 Page 필드와 BlogPage 소개를 반환합니다. 그러나 모든 페이지 콘텐츠가 API를 통해 제공되는 사이트의 경우 모든 페이지 유형에 대해 새 엔드포인트를 만드는 것이 지루해질 수 있습니다.

여러 스키마 결합

응답이 여러 페이지 유형을 반환할 수 있음을 반영하기 위해 타입 힌트 유니온 구문을 사용하여 여러 스키마를 결합합니다. 이를 통해 동일한 엔드포인트에서 다른 페이지 유형을 반환할 수 있습니다. 다음은 HomePage 유형에 대한 추가 스키마가 있는 예입니다.

from home.models import HomePage


class HomePageSchema(BasePageSchema, ModelSchema):
    class Config(BasePageSchema.Config):
        model = HomePage

@api.get("/pages/{page_id}/", response=BlogPageSchema | HomePageSchema)
def get_page(request: "HttpRequest", page_id: int):
    return get_object_or_404(Page, id=page_id).specific

이것으로, 우리는 여전히 주어진 페이지에 사용할 스키마를 결정할 방법이 없습니다. 페이지 유형별로 이 작업을 수행하고 싶으므로 스키마에 추가 content_type 클래스 속성 주석을 추가합니다.

  • BasePageSchema 의 경우, 모든 페이지 유형이 이 기반을 사용할 수 있으므로 content_type: str 을 정의합니다.

  • HomePageSchema 의 경우, content_type: Literal["homepage"] 를 설정합니다.

  • 그리고 BlogPageSchema 의 경우, content_type: Literal["blogpage"] 를 설정합니다.

이제 남은 것은 BasePageSchema리졸버 계산 필드를 추가하여 각 페이지에 대한 올바른 콘텐츠 유형을 반환하는 것입니다. 다음은 BasePageSchema 의 최종 버전입니다.

class BasePageSchema(ModelSchema):
    url: str = Field(None, alias="get_url")
    content_type: str

    @staticmethod
    def resolve_content_type(page: Page) -> str:
        return page.specific_class._meta.model_name

    class Config:
        model = Page
        model_fields = [
            "id",
            "title",
            "slug",
        ]

이것으로 Pydantic은 get_page 에서 반환된 페이지 데이터를 response 유니온의 스키마 중 하나에 따라 유효성을 검사할 수 있습니다. 그런 다음 특정 스키마에 따라 데이터를 직렬화합니다.

중첩된 데이터

페이지 스키마가 별도의 모델에 있는 데이터를 참조하는 경우 새 엔드포인트를 만드는 대신 페이지 스키마에 직접 데이터를 추가할 수 있습니다. 다음은 블로그 페이지 작성자(ParentalManyToManyField 가 있는 스니펫)를 추가하는 예입니다.

class BlogPageSchema(BasePageSchema, ModelSchema):
    content_type: Literal["blogpage"]
    authors: list[str] = []

    class Config(BasePageSchema.Config):
        model = BlogPage
        model_fields = [
            "intro",
        ]

    @staticmethod
    def resolve_authors(page: BlogPage, context) -> list[str]:
        return [author.name for author in page.authors.all()]

BlogPage 클래스에 작성자 이름을 직접 검색하는 메서드가 있는 경우 Field 클래스를 사용하여 이 작업을 수행할 수도 있습니다: authors: list[str] = Field([], alias="get_author_names").

API의 리치 텍스트

Wagtail의 리치 텍스트 필드는 리치 텍스트 내부에 설명된 특정 내부 형식을 사용합니다. 스키마에 str 로 추가할 수 있지만, API가 페이지 및 이미지에 대한 참조가 URL로 대체되는 “표시” 표현을 제공하는 것이 더 유용한 경우가 많습니다. 이것은 Ninja 리졸버로도 수행할 수 있습니다. 다음은 HomePageSchema 의 예입니다.

from wagtail.rich_text import expand_db_html


class HomePageSchema(BasePageSchema, ModelSchema):
    content_type: Literal["homepage"]
    body: str

    class Config(BasePageSchema.Config):
        model = HomePage

    @staticmethod
    def resolve_body(page: HomePage, context) -> str:
        return expand_db_html(page.body)

여기서 bodystr 로 정의되고 리졸버는 expand_db_html 함수를 사용하여 내부 표현을 HTML로 변환합니다.

API의 이미지

이미지에 대해서도 비슷한 기술을 사용할 수 있으며, 리졸버와 별칭을 결합하여 데이터를 생성합니다. get_renditions() 메서드를 사용하여 형식이 지정된 이미지를 검색하고 사용자 정의 RenditionSchema 를 사용하여 API 표현을 정의합니다.

from wagtail.images.models import AbstractRendition


class RenditionSchema(ModelSchema):
    # 속성에 대해 Field / alias API를 사용해야 합니다.
    url: str = Field(None, alias="file.url")
    alt: str = Field(None, alias="alt")

    class Config:
        model = AbstractRendition
        model_fields = [
            "width",
            "height",
        ]

BlogPageSchema 에서 이미지 필드를 main_image: list[RenditionSchema] = [] 로 정의합니다. 그런 다음 이에 대한 리졸버를 추가합니다.

    @staticmethod
    def resolve_main_image(page: BlogPage) -> list[AbstractRendition]:
        filters = [
            "fill-800x600|format-webp",
            "fill-800x600",
        ]
        if image := page.main_image():
            return image.get_renditions(*filters).values()
        return []

JSON에서 main_image 는 이제 배열로 표시되며, 각 항목은 url, alt, widthheight 속성을 가진 객체입니다.

OpenAPI 문서

Django Ninja는 정의된 작업 및 스키마를 기반으로 OpenAPI 문서를 자동으로 생성합니다. 여기에는 브라우저에서 직접 API를 시험해 볼 수 있는 지원 기능이 있는 문서 뷰어도 포함됩니다. 위의 예제를 사용하여 /api/docs 에서 문서에 액세스해 볼 수 있습니다.

이 기능을 최대한 활용하려면 지원되는 작업 매개변수를 고려하십시오.