コンテンツにスキップ

pytilpack.flask

必要なextra

pip install pytilpack[flask]

pytilpack.flask

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

ResponseType = flask.Response | werkzeug.test.TestResponse module-attribute

レスポンスの型。

RouteInfo

Bases: NamedTuple

ルーティング情報を保持するクラス。

属性:

名前 タイプ デスクリプション
endpoint str

エンドポイント名

url_parts list[str]

URLのパーツのリスト

arg_names list[str]

URLパーツの引数名のリスト

ProxyFix(flaskapp, x_for=1, x_proto=1, x_host=0, x_port=0, x_prefix=1)

Bases: ProxyFix

リバースプロキシ対応。

nginx.conf設定例:: proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Prefix $http_x_forwarded_prefix;

ソースコード位置: pytilpack/flask/proxy_fix.py
def __init__(
    self,
    flaskapp: flask.Flask,
    x_for: int = 1,
    x_proto: int = 1,
    x_host: int = 0,
    x_port: int = 0,
    x_prefix: int = 1,
):
    super().__init__(
        flaskapp.wsgi_app,
        x_for=x_for,
        x_proto=x_proto,
        x_host=x_host,
        x_port=x_port,
        x_prefix=x_prefix,
    )
    self.flaskapp = flaskapp

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

テストコード用。

引数:

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

レスポンス

必須
status_code int

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

200
content_type str | Iterable[str] | None

期待するContent-Type

None

発生:

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

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

戻り値:

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

レスポンスボディ

ソースコード位置: pytilpack/flask/asserts.py
def assert_bytes(
    response: ResponseType,
    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.get_data()

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

        # Content-Typeチェック
        pytilpack.web.check_content_type(response.content_type, content_type)

    return response_body

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

テストコード用。

html5libが必要なので注意。

引数:

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

レスポンス

必須
status_code int

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

200
content_type str | Iterable[str] | None

期待するContent-Type

'__default__'
strict bool

Trueの場合、HTML5の仕様に従ったパースを行う

False
tmp_path Path | None

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

None

発生:

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

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

戻り値:

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

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

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

    html5libが必要なので注意。

    Args:
        response: レスポンス
        status_code: 期待するステータスコード
        content_type: 期待するContent-Type
        strict: Trueの場合、HTML5の仕様に従ったパースを行う
        tmp_path: 一時ファイルを保存するディレクトリ

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

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

    """
    response_body = response.get_data().decode("utf-8")

    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チェック
        pytilpack.web.check_content_type(response.content_type, content_type)

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

    return response_body

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

テストコード用。

引数:

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

レスポンス

必須
status_code int

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

200
content_type str | Iterable[str] | None

期待するContent-Type

'application/json'

発生:

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

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

戻り値:

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

レスポンスのjson

ソースコード位置: pytilpack/flask/asserts.py
def assert_json(
    response: ResponseType,
    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.get_data().decode("utf-8")
    data: typing.Any

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

        # Content-Typeチェック
        pytilpack.web.check_content_type(response.content_type, 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 ResponseType

レスポンス

必須
status_code int

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

200
content_type str | Iterable[str] | None

期待するContent-Type

'__default__'

発生:

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

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

戻り値:

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

レスポンスのxml

ソースコード位置: pytilpack/flask/asserts.py
def assert_xml(
    response: ResponseType,
    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.get_data().decode("utf-8")

    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チェック
        pytilpack.web.check_content_type(response.content_type, 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 ResponseType

レスポンス

必須
status_code int

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

200

発生:

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

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

戻り値:

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

レスポンス

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

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

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

    Returns:
        レスポンス

    """
    pytilpack.web.check_status_code(response.status_code, status_code)
    pytilpack.web.check_content_type(response.content_type, "text/event-stream")
    return response

assert_response(response, status_code=200)

テストコード用。

引数:

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

レスポンス

必須
status_code int

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

200

発生:

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

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

戻り値:

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

レスポンス

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

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

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

    Returns:
        レスポンス

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

check_status_code(status_code, valid_status_code)

Deprecated.

ソースコード位置: pytilpack/flask/asserts.py
def check_status_code(status_code: int, valid_status_code: int) -> None:
    """Deprecated."""
    warnings.warn(
        "pytilpack.flask_.check_status_code is deprecated. Use pytilpack.web.check_status_code instead.",
        DeprecationWarning,
        stacklevel=2,
    )
    pytilpack.web.check_status_code(status_code, valid_status_code)

check_content_type(content_type, valid_content_types)

Deprecated.

ソースコード位置: pytilpack/flask/asserts.py
def check_content_type(content_type: str, valid_content_types: str | typing.Iterable[str] | None) -> None:
    """Deprecated."""
    warnings.warn(
        "pytilpack.flask_.check_content_type is deprecated. Use pytilpack.web.check_content_type instead.",
        DeprecationWarning,
        stacklevel=2,
    )
    pytilpack.web.check_content_type(content_type, valid_content_types)

generate_secret_key(cache_path)

Deprecated.

ソースコード位置: pytilpack/flask/misc.py
def generate_secret_key(cache_path: str | pathlib.Path) -> bytes:
    """Deprecated."""
    warnings.warn(
        "pytilpack.flask_.generate_secret_key is deprecated. Use pytilpack.secrets_.generate_secret_key instead.",
        DeprecationWarning,
        stacklevel=2,
    )
    return pytilpack.secrets.generate_secret_key(cache_path)

data_url(data, mime_type)

小さい画像などのバイナリデータをURLに埋め込んだものを作って返す。

引数:

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

埋め込むデータ

必須
mime_type str

例:'image/png'

必須
ソースコード位置: pytilpack/flask/misc.py
def data_url(data: bytes, mime_type: str) -> str:
    """小さい画像などのバイナリデータをURLに埋め込んだものを作って返す。

    Args:
        data: 埋め込むデータ
        mime_type: 例:'image/png'

    """
    b64 = base64.b64encode(data).decode("ascii")
    return f"data:{mime_type};base64,{b64}"

get_next_url()

ログイン後遷移用のnextパラメータ用のURLを返す。

ソースコード位置: pytilpack/flask/misc.py
def get_next_url() -> str:
    """ログイン後遷移用のnextパラメータ用のURLを返す。"""
    path = flask.request.script_root + flask.request.path
    query_string = flask.request.query_string.decode("utf-8")
    next_ = f"{path}?{query_string}" if query_string else path
    return next_

prefer_markdown()

AcceptヘッダーでmarkdownがHTMLより優先されているかを返す。

参考: https://vercel.com/blog/making-agent-friendly-pages-with-content-negotiation

(CDNやプロキシがAcceptヘッダーを書き換える場合があるという話もあるが…。)

戻り値:

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

markdownがHTMLより優先されている場合True、そうでなければFalse

ソースコード位置: pytilpack/flask/misc.py
def prefer_markdown() -> bool:
    """AcceptヘッダーでmarkdownがHTMLより優先されているかを返す。

    参考: <https://vercel.com/blog/making-agent-friendly-pages-with-content-negotiation>

    (CDNやプロキシがAcceptヘッダーを書き換える場合があるという話もあるが…。)

    Returns:
        markdownがHTMLより優先されている場合True、そうでなければFalse

    """
    accept_header = flask.request.headers.get("Accept", "")
    # text/htmlを先頭にすることで、同一quality時はHTMLを優先する(従来と同じ挙動)
    result = pytilpack.http.select_accept(accept_header, ["text/html", "text/markdown", "text/plain"])
    return result in ("text/markdown", "text/plain")

static_url_for(filename, cache_busting=True, cache_timestamp='when_not_debug', **kwargs)

静的ファイルのURLを生成する。

引数:

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

静的ファイルの名前

必須
cache_busting bool

キャッシュバスティングを有効にするかどうか (デフォルト: True)

True
cache_timestamp bool | Literal['when_not_debug']

キャッシュバスティングするときのファイルの最終更新日時をプロセス単位でキャッシュするか否か。 - True: プロセス単位でキャッシュする。プロセスの再起動やSIGHUPなどをしない限り更新されない。 - False: キャッシュしない。常に最新を参照する。 - "when_not_debug": デバッグモードでないときのみキャッシュする。

'when_not_debug'
**kwargs Any

flask.url_forに渡す追加の引数

{}

戻り値:

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

静的ファイルのURL

ソースコード位置: pytilpack/flask/misc.py
def static_url_for(
    filename: str,
    cache_busting: bool = True,
    cache_timestamp: bool | typing.Literal["when_not_debug"] = "when_not_debug",
    **kwargs: typing.Any,
) -> str:
    """静的ファイルのURLを生成する。

    Args:
        filename: 静的ファイルの名前
        cache_busting: キャッシュバスティングを有効にするかどうか (デフォルト: True)
        cache_timestamp: キャッシュバスティングするときのファイルの最終更新日時をプロセス単位でキャッシュするか否か。
            - True: プロセス単位でキャッシュする。プロセスの再起動やSIGHUPなどをしない限り更新されない。
            - False: キャッシュしない。常に最新を参照する。
            - "when_not_debug": デバッグモードでないときのみキャッシュする。
        **kwargs: flask.url_forに渡す追加の引数

    Returns:
        静的ファイルのURL
    """
    if not cache_busting:
        return flask.url_for("static", filename=filename, **kwargs)

    # スタティックファイルのパスを取得
    static_folder = flask.current_app.static_folder
    assert static_folder is not None, "static_folder is None"

    filepath = pathlib.Path(static_folder) / filename
    try:
        # ファイルの最終更新日時のキャッシュを利用するか否か
        if cache_timestamp is True or (cache_timestamp == "when_not_debug" and not flask.current_app.debug):
            # キャッシュを使う
            timestamp = _TIMESTAMP_CACHE.get(str(filepath))
            if timestamp is None:
                timestamp = int(filepath.stat().st_mtime)
                _TIMESTAMP_CACHE[str(filepath)] = timestamp
        else:
            # キャッシュを使わない
            timestamp = int(filepath.stat().st_mtime)

        # キャッシュバスティングありのURLを返す
        return flask.url_for("static", filename=filename, v=timestamp, **kwargs)
    except OSError:
        # ファイルが存在しない場合などは通常のURLを返す
        return flask.url_for("static", filename=filename, **kwargs)

get_safe_url(target, host_url, default_url)

Deprecated.

ソースコード位置: pytilpack/flask/misc.py
def get_safe_url(target: str | None, host_url: str, default_url: str) -> str:
    """Deprecated."""
    warnings.warn(
        "pytilpack.flask_.get_safe_url is deprecated. Use pytilpack.web.get_safe_url instead.",
        DeprecationWarning,
        stacklevel=2,
    )
    return pytilpack.web.get_safe_url(target, host_url, default_url)

get_routes(app)

ルーティング情報を取得する。

戻り値:

タイプ デスクリプション
list[RouteInfo]

ルーティング情報のリスト。

ソースコード位置: pytilpack/flask/misc.py
def get_routes(app: flask.Flask) -> list[RouteInfo]:
    """ルーティング情報を取得する。

    Returns:
        ルーティング情報のリスト。
    """
    arg_regex = re.compile(r"<([^>]+)>")  # <name> <type:name> にマッチするための正規表現
    split_regex = re.compile(r"<[^>]+>")  # re.splitのためグループ無しにした版
    output: list[RouteInfo] = []
    for r in app.url_map.iter_rules():
        endpoint = str(r.endpoint)
        rule = (
            r.rule
            if app.config["APPLICATION_ROOT"] == "/" or not app.config["APPLICATION_ROOT"]
            else f"{app.config['APPLICATION_ROOT']}{r.rule}"
        )
        url_parts = [str(part) for part in split_regex.split(rule)]
        arg_names = [str(x.split(":")[-1]) for x in arg_regex.findall(rule)]
        output.append(RouteInfo(endpoint, url_parts, arg_names))
    return sorted(output, key=lambda x: len(x[2]), reverse=True)

run(app, host='localhost', port=5000)

Flaskアプリを実行するコンテキストマネージャ。テストコードなど用。

ソースコード位置: pytilpack/flask/misc.py
@contextlib.contextmanager
def run(app: flask.Flask, host: str = "localhost", port: int = 5000):
    """Flaskアプリを実行するコンテキストマネージャ。テストコードなど用。"""
    if not any(m.endpoint == "_pytilpack_flask_dummy" for m in app.url_map.iter_rules()):

        @app.route("/_pytilpack_flask_dummy")
        def _pytilpack_flask_dummy():
            return "OK"

    server = werkzeug.serving.make_server(host, port, app, threaded=True)
    ctx = app.app_context()
    ctx.push()
    thread = threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()
    try:
        # サーバーが起動するまで待機
        while True:
            try:
                httpx.get(f"http://{host}:{port}/_pytilpack_flask_dummy").raise_for_status()
                break
            except Exception:
                pass
        # 制御を戻す
        yield
    finally:
        server.shutdown()
        thread.join()

asserts

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

ResponseType = flask.Response | werkzeug.test.TestResponse module-attribute

レスポンスの型。

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

テストコード用。

引数:

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

レスポンス

必須
status_code int

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

200
content_type str | Iterable[str] | None

期待するContent-Type

None

発生:

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

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

戻り値:

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

レスポンスボディ

ソースコード位置: pytilpack/flask/asserts.py
def assert_bytes(
    response: ResponseType,
    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.get_data()

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

        # Content-Typeチェック
        pytilpack.web.check_content_type(response.content_type, content_type)

    return response_body

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

テストコード用。

html5libが必要なので注意。

引数:

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

レスポンス

必須
status_code int

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

200
content_type str | Iterable[str] | None

期待するContent-Type

'__default__'
strict bool

Trueの場合、HTML5の仕様に従ったパースを行う

False
tmp_path Path | None

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

None

発生:

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

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

戻り値:

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

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

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

    html5libが必要なので注意。

    Args:
        response: レスポンス
        status_code: 期待するステータスコード
        content_type: 期待するContent-Type
        strict: Trueの場合、HTML5の仕様に従ったパースを行う
        tmp_path: 一時ファイルを保存するディレクトリ

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

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

    """
    response_body = response.get_data().decode("utf-8")

    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チェック
        pytilpack.web.check_content_type(response.content_type, content_type)

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

    return response_body

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

テストコード用。

引数:

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

レスポンス

必須
status_code int

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

200
content_type str | Iterable[str] | None

期待するContent-Type

'application/json'

発生:

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

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

戻り値:

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

レスポンスのjson

ソースコード位置: pytilpack/flask/asserts.py
def assert_json(
    response: ResponseType,
    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.get_data().decode("utf-8")
    data: typing.Any

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

        # Content-Typeチェック
        pytilpack.web.check_content_type(response.content_type, 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 ResponseType

レスポンス

必須
status_code int

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

200
content_type str | Iterable[str] | None

期待するContent-Type

'__default__'

発生:

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

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

戻り値:

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

レスポンスのxml

ソースコード位置: pytilpack/flask/asserts.py
def assert_xml(
    response: ResponseType,
    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.get_data().decode("utf-8")

    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チェック
        pytilpack.web.check_content_type(response.content_type, 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 ResponseType

レスポンス

必須
status_code int

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

200

発生:

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

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

戻り値:

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

レスポンス

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

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

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

    Returns:
        レスポンス

    """
    pytilpack.web.check_status_code(response.status_code, status_code)
    pytilpack.web.check_content_type(response.content_type, "text/event-stream")
    return response

assert_response(response, status_code=200)

テストコード用。

引数:

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

レスポンス

必須
status_code int

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

200

発生:

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

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

戻り値:

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

レスポンス

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

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

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

    Returns:
        レスポンス

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

check_status_code(status_code, valid_status_code)

Deprecated.

ソースコード位置: pytilpack/flask/asserts.py
def check_status_code(status_code: int, valid_status_code: int) -> None:
    """Deprecated."""
    warnings.warn(
        "pytilpack.flask_.check_status_code is deprecated. Use pytilpack.web.check_status_code instead.",
        DeprecationWarning,
        stacklevel=2,
    )
    pytilpack.web.check_status_code(status_code, valid_status_code)

check_content_type(content_type, valid_content_types)

Deprecated.

ソースコード位置: pytilpack/flask/asserts.py
def check_content_type(content_type: str, valid_content_types: str | typing.Iterable[str] | None) -> None:
    """Deprecated."""
    warnings.warn(
        "pytilpack.flask_.check_content_type is deprecated. Use pytilpack.web.check_content_type instead.",
        DeprecationWarning,
        stacklevel=2,
    )
    pytilpack.web.check_content_type(content_type, valid_content_types)

misc

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

RouteInfo

Bases: NamedTuple

ルーティング情報を保持するクラス。

属性:

名前 タイプ デスクリプション
endpoint str

エンドポイント名

url_parts list[str]

URLのパーツのリスト

arg_names list[str]

URLパーツの引数名のリスト

generate_secret_key(cache_path)

Deprecated.

ソースコード位置: pytilpack/flask/misc.py
def generate_secret_key(cache_path: str | pathlib.Path) -> bytes:
    """Deprecated."""
    warnings.warn(
        "pytilpack.flask_.generate_secret_key is deprecated. Use pytilpack.secrets_.generate_secret_key instead.",
        DeprecationWarning,
        stacklevel=2,
    )
    return pytilpack.secrets.generate_secret_key(cache_path)

data_url(data, mime_type)

小さい画像などのバイナリデータをURLに埋め込んだものを作って返す。

引数:

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

埋め込むデータ

必須
mime_type str

例:'image/png'

必須
ソースコード位置: pytilpack/flask/misc.py
def data_url(data: bytes, mime_type: str) -> str:
    """小さい画像などのバイナリデータをURLに埋め込んだものを作って返す。

    Args:
        data: 埋め込むデータ
        mime_type: 例:'image/png'

    """
    b64 = base64.b64encode(data).decode("ascii")
    return f"data:{mime_type};base64,{b64}"

get_next_url()

ログイン後遷移用のnextパラメータ用のURLを返す。

ソースコード位置: pytilpack/flask/misc.py
def get_next_url() -> str:
    """ログイン後遷移用のnextパラメータ用のURLを返す。"""
    path = flask.request.script_root + flask.request.path
    query_string = flask.request.query_string.decode("utf-8")
    next_ = f"{path}?{query_string}" if query_string else path
    return next_

prefer_markdown()

AcceptヘッダーでmarkdownがHTMLより優先されているかを返す。

参考: https://vercel.com/blog/making-agent-friendly-pages-with-content-negotiation

(CDNやプロキシがAcceptヘッダーを書き換える場合があるという話もあるが…。)

戻り値:

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

markdownがHTMLより優先されている場合True、そうでなければFalse

ソースコード位置: pytilpack/flask/misc.py
def prefer_markdown() -> bool:
    """AcceptヘッダーでmarkdownがHTMLより優先されているかを返す。

    参考: <https://vercel.com/blog/making-agent-friendly-pages-with-content-negotiation>

    (CDNやプロキシがAcceptヘッダーを書き換える場合があるという話もあるが…。)

    Returns:
        markdownがHTMLより優先されている場合True、そうでなければFalse

    """
    accept_header = flask.request.headers.get("Accept", "")
    # text/htmlを先頭にすることで、同一quality時はHTMLを優先する(従来と同じ挙動)
    result = pytilpack.http.select_accept(accept_header, ["text/html", "text/markdown", "text/plain"])
    return result in ("text/markdown", "text/plain")

static_url_for(filename, cache_busting=True, cache_timestamp='when_not_debug', **kwargs)

静的ファイルのURLを生成する。

引数:

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

静的ファイルの名前

必須
cache_busting bool

キャッシュバスティングを有効にするかどうか (デフォルト: True)

True
cache_timestamp bool | Literal['when_not_debug']

キャッシュバスティングするときのファイルの最終更新日時をプロセス単位でキャッシュするか否か。 - True: プロセス単位でキャッシュする。プロセスの再起動やSIGHUPなどをしない限り更新されない。 - False: キャッシュしない。常に最新を参照する。 - "when_not_debug": デバッグモードでないときのみキャッシュする。

'when_not_debug'
**kwargs Any

flask.url_forに渡す追加の引数

{}

戻り値:

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

静的ファイルのURL

ソースコード位置: pytilpack/flask/misc.py
def static_url_for(
    filename: str,
    cache_busting: bool = True,
    cache_timestamp: bool | typing.Literal["when_not_debug"] = "when_not_debug",
    **kwargs: typing.Any,
) -> str:
    """静的ファイルのURLを生成する。

    Args:
        filename: 静的ファイルの名前
        cache_busting: キャッシュバスティングを有効にするかどうか (デフォルト: True)
        cache_timestamp: キャッシュバスティングするときのファイルの最終更新日時をプロセス単位でキャッシュするか否か。
            - True: プロセス単位でキャッシュする。プロセスの再起動やSIGHUPなどをしない限り更新されない。
            - False: キャッシュしない。常に最新を参照する。
            - "when_not_debug": デバッグモードでないときのみキャッシュする。
        **kwargs: flask.url_forに渡す追加の引数

    Returns:
        静的ファイルのURL
    """
    if not cache_busting:
        return flask.url_for("static", filename=filename, **kwargs)

    # スタティックファイルのパスを取得
    static_folder = flask.current_app.static_folder
    assert static_folder is not None, "static_folder is None"

    filepath = pathlib.Path(static_folder) / filename
    try:
        # ファイルの最終更新日時のキャッシュを利用するか否か
        if cache_timestamp is True or (cache_timestamp == "when_not_debug" and not flask.current_app.debug):
            # キャッシュを使う
            timestamp = _TIMESTAMP_CACHE.get(str(filepath))
            if timestamp is None:
                timestamp = int(filepath.stat().st_mtime)
                _TIMESTAMP_CACHE[str(filepath)] = timestamp
        else:
            # キャッシュを使わない
            timestamp = int(filepath.stat().st_mtime)

        # キャッシュバスティングありのURLを返す
        return flask.url_for("static", filename=filename, v=timestamp, **kwargs)
    except OSError:
        # ファイルが存在しない場合などは通常のURLを返す
        return flask.url_for("static", filename=filename, **kwargs)

get_safe_url(target, host_url, default_url)

Deprecated.

ソースコード位置: pytilpack/flask/misc.py
def get_safe_url(target: str | None, host_url: str, default_url: str) -> str:
    """Deprecated."""
    warnings.warn(
        "pytilpack.flask_.get_safe_url is deprecated. Use pytilpack.web.get_safe_url instead.",
        DeprecationWarning,
        stacklevel=2,
    )
    return pytilpack.web.get_safe_url(target, host_url, default_url)

get_routes(app)

ルーティング情報を取得する。

戻り値:

タイプ デスクリプション
list[RouteInfo]

ルーティング情報のリスト。

ソースコード位置: pytilpack/flask/misc.py
def get_routes(app: flask.Flask) -> list[RouteInfo]:
    """ルーティング情報を取得する。

    Returns:
        ルーティング情報のリスト。
    """
    arg_regex = re.compile(r"<([^>]+)>")  # <name> <type:name> にマッチするための正規表現
    split_regex = re.compile(r"<[^>]+>")  # re.splitのためグループ無しにした版
    output: list[RouteInfo] = []
    for r in app.url_map.iter_rules():
        endpoint = str(r.endpoint)
        rule = (
            r.rule
            if app.config["APPLICATION_ROOT"] == "/" or not app.config["APPLICATION_ROOT"]
            else f"{app.config['APPLICATION_ROOT']}{r.rule}"
        )
        url_parts = [str(part) for part in split_regex.split(rule)]
        arg_names = [str(x.split(":")[-1]) for x in arg_regex.findall(rule)]
        output.append(RouteInfo(endpoint, url_parts, arg_names))
    return sorted(output, key=lambda x: len(x[2]), reverse=True)

run(app, host='localhost', port=5000)

Flaskアプリを実行するコンテキストマネージャ。テストコードなど用。

ソースコード位置: pytilpack/flask/misc.py
@contextlib.contextmanager
def run(app: flask.Flask, host: str = "localhost", port: int = 5000):
    """Flaskアプリを実行するコンテキストマネージャ。テストコードなど用。"""
    if not any(m.endpoint == "_pytilpack_flask_dummy" for m in app.url_map.iter_rules()):

        @app.route("/_pytilpack_flask_dummy")
        def _pytilpack_flask_dummy():
            return "OK"

    server = werkzeug.serving.make_server(host, port, app, threaded=True)
    ctx = app.app_context()
    ctx.push()
    thread = threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()
    try:
        # サーバーが起動するまで待機
        while True:
            try:
                httpx.get(f"http://{host}:{port}/_pytilpack_flask_dummy").raise_for_status()
                break
            except Exception:
                pass
        # 制御を戻す
        yield
    finally:
        server.shutdown()
        thread.join()

proxy_fix

リバースプロキシ対応。

ProxyFix(flaskapp, x_for=1, x_proto=1, x_host=0, x_port=0, x_prefix=1)

Bases: ProxyFix

リバースプロキシ対応。

nginx.conf設定例:: proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Prefix $http_x_forwarded_prefix;

ソースコード位置: pytilpack/flask/proxy_fix.py
def __init__(
    self,
    flaskapp: flask.Flask,
    x_for: int = 1,
    x_proto: int = 1,
    x_host: int = 0,
    x_port: int = 0,
    x_prefix: int = 1,
):
    super().__init__(
        flaskapp.wsgi_app,
        x_for=x_for,
        x_proto=x_proto,
        x_host=x_host,
        x_port=x_port,
        x_prefix=x_prefix,
    )
    self.flaskapp = flaskapp