2017-11-01 08:08:28 +00:00
|
|
|
"""JSON utility functions."""
|
2020-02-17 18:49:42 +00:00
|
|
|
from collections import deque
|
2017-11-01 08:08:28 +00:00
|
|
|
import json
|
2019-12-09 15:42:10 +00:00
|
|
|
import logging
|
2018-10-02 08:16:43 +00:00
|
|
|
import os
|
2018-10-23 08:54:03 +00:00
|
|
|
import tempfile
|
2020-02-19 00:06:13 +00:00
|
|
|
from typing import Any, Dict, List, Optional, Type, Union
|
2017-11-01 08:08:28 +00:00
|
|
|
|
2020-02-25 18:20:51 +00:00
|
|
|
import orjson
|
|
|
|
|
2017-11-01 08:08:28 +00:00
|
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2018-02-18 21:11:24 +00:00
|
|
|
|
2018-06-25 16:53:49 +00:00
|
|
|
class SerializationError(HomeAssistantError):
|
|
|
|
"""Error serializing the data to JSON."""
|
|
|
|
|
|
|
|
|
|
|
|
class WriteError(HomeAssistantError):
|
|
|
|
"""Error writing the data."""
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def load_json(
|
|
|
|
filename: str, default: Union[List, Dict, None] = None
|
|
|
|
) -> Union[List, Dict]:
|
2017-11-01 08:08:28 +00:00
|
|
|
"""Load JSON data from a file and return as dict or list.
|
|
|
|
|
|
|
|
Defaults to returning empty dict if file is not found.
|
|
|
|
"""
|
|
|
|
try:
|
2019-07-31 19:25:30 +00:00
|
|
|
with open(filename, encoding="utf-8") as fdesc:
|
2020-02-25 18:20:51 +00:00
|
|
|
return orjson.loads(fdesc.read()) # type: ignore
|
2017-11-01 08:08:28 +00:00
|
|
|
except FileNotFoundError:
|
|
|
|
# This is not a fatal error
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.debug("JSON file not found: %s", filename)
|
2017-11-01 08:08:28 +00:00
|
|
|
except ValueError as error:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.exception("Could not parse JSON content: %s", filename)
|
2017-11-01 08:08:28 +00:00
|
|
|
raise HomeAssistantError(error)
|
|
|
|
except OSError as error:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.exception("JSON file reading failed: %s", filename)
|
2017-11-01 08:08:28 +00:00
|
|
|
raise HomeAssistantError(error)
|
2018-07-13 10:24:51 +00:00
|
|
|
return {} if default is None else default
|
2017-11-01 08:08:28 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def save_json(
|
|
|
|
filename: str,
|
|
|
|
data: Union[List, Dict],
|
|
|
|
private: bool = False,
|
|
|
|
*,
|
|
|
|
encoder: Optional[Type[json.JSONEncoder]] = None,
|
|
|
|
) -> None:
|
2017-11-01 08:08:28 +00:00
|
|
|
"""Save JSON data to a file.
|
|
|
|
|
|
|
|
Returns True on success.
|
|
|
|
"""
|
2020-02-17 18:49:42 +00:00
|
|
|
try:
|
|
|
|
json_data = json.dumps(data, sort_keys=True, indent=4, cls=encoder)
|
|
|
|
except TypeError:
|
|
|
|
# pylint: disable=no-member
|
|
|
|
msg = f"Failed to serialize to JSON: {filename}. Bad data found at {', '.join(find_paths_unserializable_data(data))}"
|
|
|
|
_LOGGER.error(msg)
|
|
|
|
raise SerializationError(msg)
|
|
|
|
|
2018-10-23 08:54:03 +00:00
|
|
|
tmp_filename = ""
|
|
|
|
tmp_path = os.path.split(filename)[0]
|
2017-11-01 08:08:28 +00:00
|
|
|
try:
|
2018-10-23 08:54:03 +00:00
|
|
|
# Modern versions of Python tempfile create this file with mode 0o600
|
2019-07-31 19:25:30 +00:00
|
|
|
with tempfile.NamedTemporaryFile(
|
|
|
|
mode="w", encoding="utf-8", dir=tmp_path, delete=False
|
|
|
|
) as fdesc:
|
2018-07-13 10:24:51 +00:00
|
|
|
fdesc.write(json_data)
|
2018-10-23 08:54:03 +00:00
|
|
|
tmp_filename = fdesc.name
|
|
|
|
if not private:
|
|
|
|
os.chmod(tmp_filename, 0o644)
|
2018-10-02 08:16:43 +00:00
|
|
|
os.replace(tmp_filename, filename)
|
2017-11-01 08:08:28 +00:00
|
|
|
except OSError as error:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.exception("Saving JSON file failed: %s", filename)
|
2018-06-25 16:53:49 +00:00
|
|
|
raise WriteError(error)
|
2018-10-02 08:16:43 +00:00
|
|
|
finally:
|
|
|
|
if os.path.exists(tmp_filename):
|
|
|
|
try:
|
|
|
|
os.remove(tmp_filename)
|
|
|
|
except OSError as err:
|
|
|
|
# If we are cleaning up then something else went wrong, so
|
|
|
|
# we should suppress likely follow-on errors in the cleanup
|
|
|
|
_LOGGER.error("JSON replacement cleanup failed: %s", err)
|
2020-02-17 18:49:42 +00:00
|
|
|
|
|
|
|
|
2020-02-19 00:06:13 +00:00
|
|
|
def find_paths_unserializable_data(bad_data: Any) -> List[str]:
|
2020-02-17 18:49:42 +00:00
|
|
|
"""Find the paths to unserializable data.
|
|
|
|
|
|
|
|
This method is slow! Only use for error handling.
|
|
|
|
"""
|
|
|
|
to_process = deque([(bad_data, "$")])
|
|
|
|
invalid = []
|
|
|
|
|
|
|
|
while to_process:
|
|
|
|
obj, obj_path = to_process.popleft()
|
|
|
|
|
|
|
|
try:
|
2020-02-25 18:20:51 +00:00
|
|
|
orjson.dumps(obj)
|
2020-02-19 00:06:13 +00:00
|
|
|
continue
|
2020-02-17 18:49:42 +00:00
|
|
|
except TypeError:
|
2020-02-19 00:06:13 +00:00
|
|
|
pass
|
2020-02-17 18:49:42 +00:00
|
|
|
|
|
|
|
if isinstance(obj, dict):
|
|
|
|
for key, value in obj.items():
|
|
|
|
try:
|
|
|
|
# Is key valid?
|
2020-02-25 18:20:51 +00:00
|
|
|
orjson.dumps({key: None})
|
2020-02-17 18:49:42 +00:00
|
|
|
except TypeError:
|
|
|
|
invalid.append(f"{obj_path}<key: {key}>")
|
|
|
|
else:
|
|
|
|
# Process value
|
|
|
|
to_process.append((value, f"{obj_path}.{key}"))
|
|
|
|
elif isinstance(obj, list):
|
|
|
|
for idx, value in enumerate(obj):
|
|
|
|
to_process.append((value, f"{obj_path}[{idx}]"))
|
2020-02-19 00:06:13 +00:00
|
|
|
else:
|
2020-02-17 18:49:42 +00:00
|
|
|
invalid.append(obj_path)
|
|
|
|
|
|
|
|
return invalid
|