コンテンツにスキップ

pytilpack.fastapi

必要なextra

pip install pytilpack[fastapi]

pytilpack.fastapi

FastAPI関連のユーティリティ。

I18nMiddleware(app, state)

Accept-Languageからロケールを自動設定するASGIミドルウェア。

ソースコード位置: pytilpack/fastapi/i18n.py
def __init__(self, app: starlette.types.ASGIApp, state: pytilpack.i18n.I18nState) -> None:
    self.app = app
    self.state = state

__call__(scope, receive, send) async

ASGIインターフェース。

ソースコード位置: pytilpack/fastapi/i18n.py
async def __call__(
    self,
    scope: starlette.types.Scope,
    receive: starlette.types.Receive,
    send: starlette.types.Send,
) -> None:
    """ASGIインターフェース。"""
    if scope["type"] not in ("http", "websocket"):
        await self.app(scope, receive, send)
        return
    # Accept-Languageからロケールを決定
    headers = starlette.datastructures.Headers(scope=scope)
    accept_lang = headers.get("accept-language", "")
    locale = pytilpack.http.select_accept_language(
        accept_lang,
        self.state.supported_locales,
        default=self.state.default_locale,
    )
    # activate → try/finally → deactivate でリクエストスコープに閉じ込める
    tokens = pytilpack.i18n.activate(self.state, locale)
    try:
        await self.app(scope, receive, send)
    finally:
        pytilpack.i18n.deactivate(tokens)

JSONResponse

Bases: Response

インデント付きJSONを返すFastAPIのresponse_class。

デフォルトのJSONResponseはインデントなしでJSONを返すが、 このクラスはindent=2のインデント付きで返す。 Pydanticモデルやdatetime等もfastapi.encoders.jsonable_encoderで変換する。

Usage

@app.get("/pretty", response_class=pytilpack.fastapi.JSONResponse) def pretty(): return {"message": "indented JSON"}

@app.get("/model", response_model=MyModel, response_class=pytilpack.fastapi.JSONResponse) def model(): return MyModel(field="value")

render(content)

コンテンツをインデント付きJSONのバイト列に変換する。

ソースコード位置: pytilpack/fastapi/misc.py
def render(self, content: typing.Any) -> bytes:
    """コンテンツをインデント付きJSONのバイト列に変換する。"""
    encoded = fastapi.encoders.jsonable_encoder(content)
    return json.dumps(encoded, ensure_ascii=False, indent=2, separators=(", ", ": ")).encode("utf-8")

assert_bytes(response, status_code=200, content_type=None)

テストコード用。

引数:

名前 タイプ デスクリプション デフォルト
response Response

レスポンス

必須
status_code int

期待するステータスコード

200
content_type str | Iterable[str] | None

期待するContent-Type

None

発生:

タイプ デスクリプション
AssertionError

ステータスコードが異なる場合

戻り値:

タイプ デスクリプション
bytes

レスポンスボディ

ソースコード位置: pytilpack/fastapi/asserts.py
def assert_bytes(
    response: httpx.Response,
    status_code: int = 200,
    content_type: str | typing.Iterable[str] | None = None,
) -> bytes:
    """テストコード用。

    Args:
        response: レスポンス
        status_code: 期待するステータスコード
        content_type: 期待するContent-Type

    Raises:
        AssertionError: ステータスコードが異なる場合

    Returns:
        レスポンスボディ

    """
    response_body = response.content

    with pytilpack.pytest.AssertBlock(response_body, suffix=".txt"):  # bin では開けないため txt として扱う
        # ステータスコードチェック
        pytilpack.web.check_status_code(response.status_code, status_code)

        # Content-Typeチェック
        content_type_value = response.headers.get("content-type")
        pytilpack.web.check_content_type(content_type_value, content_type)

    return response_body

assert_html(response, status_code=200, content_type='__default__', tmp_path=None, strict=False)

テストコード用。

html5libが必要なので注意。

引数:

名前 タイプ デスクリプション デフォルト
response Response

レスポンス

必須
status_code int

期待するステータスコード

200
content_type str | Iterable[str] | None

期待するContent-Type

'__default__'
tmp_path Path | None

一時ファイルを保存するディレクトリ

None
strict bool

HTML解析を厳格に行うかどうか

False

発生:

タイプ デスクリプション
AssertionError

ステータスコードが異なる場合

戻り値:

タイプ デスクリプション
str

レスポンスボディ (bs4.BeautifulSoup)

ソースコード位置: pytilpack/fastapi/asserts.py
def assert_html(
    response: httpx.Response,
    status_code: int = 200,
    content_type: str | typing.Iterable[str] | None = "__default__",
    tmp_path: pathlib.Path | None = None,
    strict: bool = False,
) -> str:
    """テストコード用。

    html5libが必要なので注意。

    Args:
        response: レスポンス
        status_code: 期待するステータスコード
        content_type: 期待するContent-Type
        tmp_path: 一時ファイルを保存するディレクトリ
        strict: HTML解析を厳格に行うかどうか

    Raises:
        AssertionError: ステータスコードが異なる場合

    Returns:
        レスポンスボディ (bs4.BeautifulSoup)

    """
    response_body = response.text

    if content_type == "__default__":
        content_type = ["text/html", "application/xhtml+xml"]

    with pytilpack.pytest.AssertBlock(response_body, suffix=".html", tmp_path=tmp_path):
        # ステータスコードチェック
        pytilpack.web.check_status_code(response.status_code, status_code)

        # Content-Typeチェック
        content_type_value = response.headers.get("content-type")
        pytilpack.web.check_content_type(content_type_value, content_type)

        # HTMLのチェック
        pytilpack.web.check_html(response.content, strict=strict)

    return response_body

assert_json(response, status_code=200, content_type='application/json')

テストコード用。

引数:

名前 タイプ デスクリプション デフォルト
response Response

レスポンス

必須
status_code int

期待するステータスコード

200
content_type str | Iterable[str] | None

期待するContent-Type

'application/json'

発生:

タイプ デスクリプション
AssertionError

ステータスコードが異なる場合

戻り値:

タイプ デスクリプション
Any

レスポンスのjson

ソースコード位置: pytilpack/fastapi/asserts.py
def assert_json(
    response: httpx.Response,
    status_code: int = 200,
    content_type: str | typing.Iterable[str] | None = "application/json",
) -> typing.Any:
    """テストコード用。

    Args:
        response: レスポンス
        status_code: 期待するステータスコード
        content_type: 期待するContent-Type

    Raises:
        AssertionError: ステータスコードが異なる場合

    Returns:
        レスポンスのjson

    """
    response_body = response.text
    data: typing.Any

    with pytilpack.pytest.AssertBlock(response_body, suffix=".json"):
        # ステータスコードチェック
        pytilpack.web.check_status_code(response.status_code, status_code)

        # Content-Typeチェック
        content_type_value = response.headers.get("content-type")
        pytilpack.web.check_content_type(content_type_value, content_type)

        # JSONのチェック
        try:
            data = json.loads(response_body)
        except Exception as e:
            raise AssertionError(f"JSONエラー: {e}") from e

    return data

assert_xml(response, status_code=200, content_type='__default__')

テストコード用。

引数:

名前 タイプ デスクリプション デフォルト
response Response

レスポンス

必須
status_code int

期待するステータスコード

200
content_type str | Iterable[str] | None

期待するContent-Type

'__default__'

発生:

タイプ デスクリプション
AssertionError

ステータスコードが異なる場合

戻り値:

タイプ デスクリプション
str

レスポンスのxml

ソースコード位置: pytilpack/fastapi/asserts.py
def assert_xml(
    response: httpx.Response,
    status_code: int = 200,
    content_type: str | typing.Iterable[str] | None = "__default__",
) -> str:
    """テストコード用。

    Args:
        response: レスポンス
        status_code: 期待するステータスコード
        content_type: 期待するContent-Type

    Raises:
        AssertionError: ステータスコードが異なる場合

    Returns:
        レスポンスのxml

    """
    response_body = response.text

    if content_type == "__default__":
        content_type = ["text/xml", "application/xml"]

    with pytilpack.pytest.AssertBlock(response_body, suffix=".xml"):
        # ステータスコードチェック
        pytilpack.web.check_status_code(response.status_code, status_code)

        # Content-Typeチェック
        content_type_value = response.headers.get("content-type")
        pytilpack.web.check_content_type(content_type_value, content_type)

        # XMLのチェック
        try:
            _ = ET.fromstring(response_body)
        except Exception as e:
            raise AssertionError(f"XMLエラー: {e}") from e

    return response_body

assert_sse(response, status_code=200)

テストコード用。

引数:

名前 タイプ デスクリプション デフォルト
response Response

レスポンス

必須
status_code int

期待するステータスコード

200

発生:

タイプ デスクリプション
AssertionError

ステータスコードが異なる場合、またはContent-Typeが異なる場合

戻り値:

タイプ デスクリプション
Response

レスポンス

ソースコード位置: pytilpack/fastapi/asserts.py
def assert_sse(
    response: httpx.Response,
    status_code: int = 200,
) -> httpx.Response:
    """テストコード用。

    Args:
        response: レスポンス
        status_code: 期待するステータスコード

    Raises:
        AssertionError: ステータスコードが異なる場合、またはContent-Typeが異なる場合

    Returns:
        レスポンス

    """
    pytilpack.web.check_status_code(response.status_code, status_code)
    content_type_value = response.headers.get("content-type")
    pytilpack.web.check_content_type(content_type_value, "text/event-stream")
    return response

assert_response(response, status_code=200)

テストコード用。

引数:

名前 タイプ デスクリプション デフォルト
response Response

レスポンス

必須
status_code int

期待するステータスコード

200

発生:

タイプ デスクリプション
AssertionError

ステータスコードが異なる場合

戻り値:

タイプ デスクリプション
Response

レスポンス

ソースコード位置: pytilpack/fastapi/asserts.py
def assert_response(
    response: httpx.Response,
    status_code: int = 200,
) -> httpx.Response:
    """テストコード用。

    Args:
        response: レスポンス
        status_code: 期待するステータスコード

    Raises:
        AssertionError: ステータスコードが異なる場合

    Returns:
        レスポンス

    """
    pytilpack.web.check_status_code(response.status_code, status_code)
    return response

init_app(app, locale_dir, domain='messages', supported_locales=None, default_locale='en', fallback=True)

FastAPI/Starletteアプリにi18nを統合する。

引数:

名前 タイプ デスクリプション デフォルト
app ASGIApp

FastAPI/Starletteアプリ

必須
locale_dir str | Path

localeディレクトリのパス

必須
domain str

gettextドメイン名

'messages'
supported_locales list[str] | None

サポートするロケール一覧

None
default_locale str

デフォルトロケール

'en'
fallback bool

フォールバック有無

True

戻り値:

タイプ デスクリプション
I18nMiddleware

I18nMiddlewareインスタンス(app.add_middlewareを使う場合はこの関数ではなく

I18nMiddleware

直接I18nMiddlewareを使用する)

ソースコード位置: pytilpack/fastapi/i18n.py
def init_app(
    app: starlette.types.ASGIApp,
    locale_dir: str | pathlib.Path,
    domain: str = "messages",
    supported_locales: list[str] | None = None,
    default_locale: str = "en",
    fallback: bool = True,
) -> I18nMiddleware:
    """FastAPI/Starletteアプリにi18nを統合する。

    Args:
        app: FastAPI/Starletteアプリ
        locale_dir: localeディレクトリのパス
        domain: gettextドメイン名
        supported_locales: サポートするロケール一覧
        default_locale: デフォルトロケール
        fallback: フォールバック有無

    Returns:
        I18nMiddlewareインスタンス(app.add_middlewareを使う場合はこの関数ではなく
        直接I18nMiddlewareを使用する)

    """
    state = pytilpack.i18n.I18nState(
        locale_dir=locale_dir,
        domain=domain,
        locales=supported_locales,
        default_locale=default_locale,
        fallback=fallback,
    )
    return I18nMiddleware(app, state)

asserts

FastAPIのテストコード用アサーション関数。

assert_bytes(response, status_code=200, content_type=None)

テストコード用。

引数:

名前 タイプ デスクリプション デフォルト
response Response

レスポンス

必須
status_code int

期待するステータスコード

200
content_type str | Iterable[str] | None

期待するContent-Type

None

発生:

タイプ デスクリプション
AssertionError

ステータスコードが異なる場合

戻り値:

タイプ デスクリプション
bytes

レスポンスボディ

ソースコード位置: pytilpack/fastapi/asserts.py
def assert_bytes(
    response: httpx.Response,
    status_code: int = 200,
    content_type: str | typing.Iterable[str] | None = None,
) -> bytes:
    """テストコード用。

    Args:
        response: レスポンス
        status_code: 期待するステータスコード
        content_type: 期待するContent-Type

    Raises:
        AssertionError: ステータスコードが異なる場合

    Returns:
        レスポンスボディ

    """
    response_body = response.content

    with pytilpack.pytest.AssertBlock(response_body, suffix=".txt"):  # bin では開けないため txt として扱う
        # ステータスコードチェック
        pytilpack.web.check_status_code(response.status_code, status_code)

        # Content-Typeチェック
        content_type_value = response.headers.get("content-type")
        pytilpack.web.check_content_type(content_type_value, content_type)

    return response_body

assert_html(response, status_code=200, content_type='__default__', tmp_path=None, strict=False)

テストコード用。

html5libが必要なので注意。

引数:

名前 タイプ デスクリプション デフォルト
response Response

レスポンス

必須
status_code int

期待するステータスコード

200
content_type str | Iterable[str] | None

期待するContent-Type

'__default__'
tmp_path Path | None

一時ファイルを保存するディレクトリ

None
strict bool

HTML解析を厳格に行うかどうか

False

発生:

タイプ デスクリプション
AssertionError

ステータスコードが異なる場合

戻り値:

タイプ デスクリプション
str

レスポンスボディ (bs4.BeautifulSoup)

ソースコード位置: pytilpack/fastapi/asserts.py
def assert_html(
    response: httpx.Response,
    status_code: int = 200,
    content_type: str | typing.Iterable[str] | None = "__default__",
    tmp_path: pathlib.Path | None = None,
    strict: bool = False,
) -> str:
    """テストコード用。

    html5libが必要なので注意。

    Args:
        response: レスポンス
        status_code: 期待するステータスコード
        content_type: 期待するContent-Type
        tmp_path: 一時ファイルを保存するディレクトリ
        strict: HTML解析を厳格に行うかどうか

    Raises:
        AssertionError: ステータスコードが異なる場合

    Returns:
        レスポンスボディ (bs4.BeautifulSoup)

    """
    response_body = response.text

    if content_type == "__default__":
        content_type = ["text/html", "application/xhtml+xml"]

    with pytilpack.pytest.AssertBlock(response_body, suffix=".html", tmp_path=tmp_path):
        # ステータスコードチェック
        pytilpack.web.check_status_code(response.status_code, status_code)

        # Content-Typeチェック
        content_type_value = response.headers.get("content-type")
        pytilpack.web.check_content_type(content_type_value, content_type)

        # HTMLのチェック
        pytilpack.web.check_html(response.content, strict=strict)

    return response_body

assert_json(response, status_code=200, content_type='application/json')

テストコード用。

引数:

名前 タイプ デスクリプション デフォルト
response Response

レスポンス

必須
status_code int

期待するステータスコード

200
content_type str | Iterable[str] | None

期待するContent-Type

'application/json'

発生:

タイプ デスクリプション
AssertionError

ステータスコードが異なる場合

戻り値:

タイプ デスクリプション
Any

レスポンスのjson

ソースコード位置: pytilpack/fastapi/asserts.py
def assert_json(
    response: httpx.Response,
    status_code: int = 200,
    content_type: str | typing.Iterable[str] | None = "application/json",
) -> typing.Any:
    """テストコード用。

    Args:
        response: レスポンス
        status_code: 期待するステータスコード
        content_type: 期待するContent-Type

    Raises:
        AssertionError: ステータスコードが異なる場合

    Returns:
        レスポンスのjson

    """
    response_body = response.text
    data: typing.Any

    with pytilpack.pytest.AssertBlock(response_body, suffix=".json"):
        # ステータスコードチェック
        pytilpack.web.check_status_code(response.status_code, status_code)

        # Content-Typeチェック
        content_type_value = response.headers.get("content-type")
        pytilpack.web.check_content_type(content_type_value, content_type)

        # JSONのチェック
        try:
            data = json.loads(response_body)
        except Exception as e:
            raise AssertionError(f"JSONエラー: {e}") from e

    return data

assert_xml(response, status_code=200, content_type='__default__')

テストコード用。

引数:

名前 タイプ デスクリプション デフォルト
response Response

レスポンス

必須
status_code int

期待するステータスコード

200
content_type str | Iterable[str] | None

期待するContent-Type

'__default__'

発生:

タイプ デスクリプション
AssertionError

ステータスコードが異なる場合

戻り値:

タイプ デスクリプション
str

レスポンスのxml

ソースコード位置: pytilpack/fastapi/asserts.py
def assert_xml(
    response: httpx.Response,
    status_code: int = 200,
    content_type: str | typing.Iterable[str] | None = "__default__",
) -> str:
    """テストコード用。

    Args:
        response: レスポンス
        status_code: 期待するステータスコード
        content_type: 期待するContent-Type

    Raises:
        AssertionError: ステータスコードが異なる場合

    Returns:
        レスポンスのxml

    """
    response_body = response.text

    if content_type == "__default__":
        content_type = ["text/xml", "application/xml"]

    with pytilpack.pytest.AssertBlock(response_body, suffix=".xml"):
        # ステータスコードチェック
        pytilpack.web.check_status_code(response.status_code, status_code)

        # Content-Typeチェック
        content_type_value = response.headers.get("content-type")
        pytilpack.web.check_content_type(content_type_value, content_type)

        # XMLのチェック
        try:
            _ = ET.fromstring(response_body)
        except Exception as e:
            raise AssertionError(f"XMLエラー: {e}") from e

    return response_body

assert_sse(response, status_code=200)

テストコード用。

引数:

名前 タイプ デスクリプション デフォルト
response Response

レスポンス

必須
status_code int

期待するステータスコード

200

発生:

タイプ デスクリプション
AssertionError

ステータスコードが異なる場合、またはContent-Typeが異なる場合

戻り値:

タイプ デスクリプション
Response

レスポンス

ソースコード位置: pytilpack/fastapi/asserts.py
def assert_sse(
    response: httpx.Response,
    status_code: int = 200,
) -> httpx.Response:
    """テストコード用。

    Args:
        response: レスポンス
        status_code: 期待するステータスコード

    Raises:
        AssertionError: ステータスコードが異なる場合、またはContent-Typeが異なる場合

    Returns:
        レスポンス

    """
    pytilpack.web.check_status_code(response.status_code, status_code)
    content_type_value = response.headers.get("content-type")
    pytilpack.web.check_content_type(content_type_value, "text/event-stream")
    return response

assert_response(response, status_code=200)

テストコード用。

引数:

名前 タイプ デスクリプション デフォルト
response Response

レスポンス

必須
status_code int

期待するステータスコード

200

発生:

タイプ デスクリプション
AssertionError

ステータスコードが異なる場合

戻り値:

タイプ デスクリプション
Response

レスポンス

ソースコード位置: pytilpack/fastapi/asserts.py
def assert_response(
    response: httpx.Response,
    status_code: int = 200,
) -> httpx.Response:
    """テストコード用。

    Args:
        response: レスポンス
        status_code: 期待するステータスコード

    Raises:
        AssertionError: ステータスコードが異なる場合

    Returns:
        レスポンス

    """
    pytilpack.web.check_status_code(response.status_code, status_code)
    return response

i18n

FastAPI用i18n統合。

I18nMiddleware(app, state)

Accept-Languageからロケールを自動設定するASGIミドルウェア。

ソースコード位置: pytilpack/fastapi/i18n.py
def __init__(self, app: starlette.types.ASGIApp, state: pytilpack.i18n.I18nState) -> None:
    self.app = app
    self.state = state
__call__(scope, receive, send) async

ASGIインターフェース。

ソースコード位置: pytilpack/fastapi/i18n.py
async def __call__(
    self,
    scope: starlette.types.Scope,
    receive: starlette.types.Receive,
    send: starlette.types.Send,
) -> None:
    """ASGIインターフェース。"""
    if scope["type"] not in ("http", "websocket"):
        await self.app(scope, receive, send)
        return
    # Accept-Languageからロケールを決定
    headers = starlette.datastructures.Headers(scope=scope)
    accept_lang = headers.get("accept-language", "")
    locale = pytilpack.http.select_accept_language(
        accept_lang,
        self.state.supported_locales,
        default=self.state.default_locale,
    )
    # activate → try/finally → deactivate でリクエストスコープに閉じ込める
    tokens = pytilpack.i18n.activate(self.state, locale)
    try:
        await self.app(scope, receive, send)
    finally:
        pytilpack.i18n.deactivate(tokens)

init_app(app, locale_dir, domain='messages', supported_locales=None, default_locale='en', fallback=True)

FastAPI/Starletteアプリにi18nを統合する。

引数:

名前 タイプ デスクリプション デフォルト
app ASGIApp

FastAPI/Starletteアプリ

必須
locale_dir str | Path

localeディレクトリのパス

必須
domain str

gettextドメイン名

'messages'
supported_locales list[str] | None

サポートするロケール一覧

None
default_locale str

デフォルトロケール

'en'
fallback bool

フォールバック有無

True

戻り値:

タイプ デスクリプション
I18nMiddleware

I18nMiddlewareインスタンス(app.add_middlewareを使う場合はこの関数ではなく

I18nMiddleware

直接I18nMiddlewareを使用する)

ソースコード位置: pytilpack/fastapi/i18n.py
def init_app(
    app: starlette.types.ASGIApp,
    locale_dir: str | pathlib.Path,
    domain: str = "messages",
    supported_locales: list[str] | None = None,
    default_locale: str = "en",
    fallback: bool = True,
) -> I18nMiddleware:
    """FastAPI/Starletteアプリにi18nを統合する。

    Args:
        app: FastAPI/Starletteアプリ
        locale_dir: localeディレクトリのパス
        domain: gettextドメイン名
        supported_locales: サポートするロケール一覧
        default_locale: デフォルトロケール
        fallback: フォールバック有無

    Returns:
        I18nMiddlewareインスタンス(app.add_middlewareを使う場合はこの関数ではなく
        直接I18nMiddlewareを使用する)

    """
    state = pytilpack.i18n.I18nState(
        locale_dir=locale_dir,
        domain=domain,
        locales=supported_locales,
        default_locale=default_locale,
        fallback=fallback,
    )
    return I18nMiddleware(app, state)

misc

FastAPI関連のその他のユーティリティ。

JSONResponse

Bases: Response

インデント付きJSONを返すFastAPIのresponse_class。

デフォルトのJSONResponseはインデントなしでJSONを返すが、 このクラスはindent=2のインデント付きで返す。 Pydanticモデルやdatetime等もfastapi.encoders.jsonable_encoderで変換する。

Usage

@app.get("/pretty", response_class=pytilpack.fastapi.JSONResponse) def pretty(): return {"message": "indented JSON"}

@app.get("/model", response_model=MyModel, response_class=pytilpack.fastapi.JSONResponse) def model(): return MyModel(field="value")

render(content)

コンテンツをインデント付きJSONのバイト列に変換する。

ソースコード位置: pytilpack/fastapi/misc.py
def render(self, content: typing.Any) -> bytes:
    """コンテンツをインデント付きJSONのバイト列に変換する。"""
    encoded = fastapi.encoders.jsonable_encoder(content)
    return json.dumps(encoded, ensure_ascii=False, indent=2, separators=(", ", ": ")).encode("utf-8")