Source code for fitrequest.response_formatter
from functools import cached_property
from xml.etree.ElementTree import Element as XmlElement
import defusedxml.ElementTree as XmlTree
import httpx
import jsonpath_ng
import orjson
from pydantic import BaseModel
[docs]
class ResponseFormatter:
def __init__(
self,
httpx_response: httpx.Response,
json_path: str | None = None,
response_model: type[BaseModel] | None = None,
) -> None:
self.httpx_response = httpx_response
self.json_path = json_path
self.response_model = response_model
self.content_type = self.httpx_response.headers.get('Content-Type', '').split(';')[0].strip().lower()
[docs]
def extract_json_path(self, data: dict | list | None) -> dict | list | None:
if not data or not self.json_path:
return data
json_expr = jsonpath_ng.parse(self.json_path)
matches = [match.value for match in json_expr.find(data)]
if len(matches) == 0:
return None
if len(matches) == 1:
return matches[0]
return matches
[docs]
def format_json(self) -> dict | list | None:
data = self.httpx_response.json() if self.httpx_response.content else None
return self.extract_json_path(data)
[docs]
def format_json_lines(self) -> dict | list | None:
data = [orjson.loads(line) for line in self.httpx_response.content.splitlines()] or None
return self.extract_json_path(data)
[docs]
def format_html(self) -> str | None:
# For now html formatting is not handled, the raw string is returned
return self.httpx_response.text or None
[docs]
def format_default(self) -> bytes | None:
# By default, the response content bytes is returned
return self.httpx_response.content or None
[docs]
def format_pydantic(
self,
data: dict | list | bytes | XmlElement | str | None,
) -> BaseModel | dict | list | bytes | XmlElement | str | None:
if not self.response_model or not isinstance(self.response_model, type(BaseModel)):
return data
if isinstance(data, dict):
return self.response_model(**data)
if isinstance(data, list):
return [self.response_model(**elem) for elem in data]
return data
@cached_property
def data(self) -> BaseModel | dict | list | bytes | XmlElement | str | None:
if self.content_type.endswith('json'):
data = self.format_json()
elif self.content_type.endswith(('json-l', 'jsonlines')):
data = self.format_json_lines()
elif self.content_type.endswith(('plain', 'html')):
data = self.format_html()
elif self.content_type.endswith('xml'):
data = self.format_xml()
else:
data = self.format_default()
return self.format_pydantic(data)
@cached_property
def data_bytes(self) -> bytes:
if self.data is None:
return b''
if isinstance(self.data, bytes):
return self.data
if isinstance(self.data, XmlElement):
return XmlTree.tostring(self.data, xml_declaration=True, encoding='utf-8')
if isinstance(self.data, BaseModel):
return bytes(self.data.model_dump_json(indent=2), encoding='utf-8')
if isinstance(self.data, list) and len(self.data) > 0 and isinstance(self.data[0], BaseModel):
dumped_list = [elem.model_dump() for elem in self.data]
return orjson.dumps(dumped_list, option=orjson.OPT_INDENT_2)
return orjson.dumps(self.data, option=orjson.OPT_INDENT_2)