2023年6月15日木曜日

Flask でレスポンス情報を Accept ヘッダに応じて xml or json に変換して返却する方法

Flask でレスポンス情報を Accept ヘッダに応じて xml or json に変換して返却する方法

概要

Flask でレスポンスを返却する際に xml or json を返却する方法を考えます

flask-restful という拡張を使うと簡単にできそうですが今回は素の flask だけを使って考えます

環境

  • macOS 13.4
  • Python 3.11.3
  • Flask 2.3.2
    • xmltodict 0.13.0
    • dicttoxml 1.7.16

方針

  • Accept ヘッダの内容に応じて返却するタイプを変更する
    • Accept: application/xml -> xml を返却
    • Accept: applicaton/json -> json を返却
  • すべてのルーティングに対応するため after_request を使う
  • 特に指定がない場合は json を返却する

サンプルコード

一部前回のコードを含みます
また xml レスポンスの情報を少し加工するための処理も入っています
ポイントはコード下部にある after_request の部分になります

  • vim app.py
import copy
import json
import traceback
from json.decoder import JSONDecodeError
from xml.parsers.expat import ExpatError

import dicttoxml
import xmltodict
from flask import Flask, Response, jsonify, request

app = Flask(__name__)


class Profile:
    def __init__(self) -> None:
        self.langs = ["ruby", "python", "swift"]

    def to_dict(self):
        return self.langs


# 各 langs の xml 変換時の属性名を設定
def set_item_name(_: str) -> str:
    return "lang"


class User:
    def __init__(self, name: str = "default_user", **_) -> None:
        self.name = name
        self.age = 10
        self.profile = Profile()

    def to_dict(self):
        return {"name": self.name, "age": self.age, "profile": self.profile.to_dict()}


# dict から User オブジェクトへの変換
def convert_obj(func):
    def wrapper(*args, content_dict: dict, **kwargs):
        try:
            user = User(**content_dict["xml"])
        except KeyError:
            # データ構造が正しくない場合はエラー
            raise ValueError("Root attribute must start with 'xml'.")

        return func(*args, user=user, **kwargs)

    return wrapper


# xml から dict への変換、すでに dict の場合は何もしない
def convert_dict(func):
    def wrapper(*args, **kwargs):
        data = copy.copy(request.data)
        try:
            data = xmltodict.parse(data)
        except ExpatError:
            try:
                data = json.loads(data)
            except JSONDecodeError:
                # xml じゃないかつ dict じゃない場合はエラー
                raise ValueError("You xml or dict must be specified.")

        return func(*args, content_dict=data, **kwargs)

    return wrapper


@app.route("/", methods=["GET", "POST"])
@convert_dict
@convert_obj
def parse_xml(user: User):
    # デフォルトは json
    return jsonify(user.to_dict())


@app.after_request
def after_request(response: Response):
    accept_header = request.headers.get("Accept")
    if accept_header == "application/xml":
        # xml に変換
        xml = dicttoxml.dicttoxml(
            response.get_json(),
            attr_type=True,
            root=True,
            custom_root="orgRoot",
            item_func=set_item_name,  # type: ignore
        )
        response.headers["Content-Type"] = "applicatoin/xml"
        response.data = xml
    elif accept_header == "application/json":
        # json に変換 (そのまま返却)
        pass
    return response


@app.errorhandler(ValueError)
def handle_bad_request(e):
    return traceback.format_exception_only(e), 400

動作確認

  • pipenv run python flask run
# xml
curl localhost:5000 -XPOST -d '<xml><name>hawksnowlog</name></xml>' -H 'content-type: application/xml' -H 'Accept: application/xml' 
<?xml version="1.0" encoding="UTF-8" ?><root><age type="int">10</age><name type="str">hawksnowlog</name></root>

# json
curl localhost:5000 -XPOST -d '{"xml": {"name":"hawksnowlog"}}' -H 'content-type: application/xml' -H 'Accept: application/json'
{
  "age": 10,
  "name": "hawksnowlog"
}

# json (指定なし)
curl localhost:5000 -XPOST -d '<xml><name>hawksnowlog</name></xml>' -H 'content-type: application/xml' 
{
  "age": 10,
  "name": "hawksnowlog"
}

最後に

Flask で Accept ヘッダの内容に応じてレスポンスの形式を変更するような API を考えてみました
やっていることは非常に単純ですがうまく作らないとコードが大変なことになりそうなので注意が必要そうです

(こういったマルチメディアタイプの API は実際に活用されているケースがあるのだろうか)

0 件のコメント:

コメントを投稿