概要
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 件のコメント:
コメントを投稿