Source code for cdm_reader_mapper.common.json_dict
"""JSON dictionary manipulator."""
from __future__ import annotations
import json
from collections.abc import Sequence
from pathlib import Path
from typing import Any
from .getting_files import get_path
[docs]
def open_json_file(ifile: str | Path, encoding: str = "utf-8") -> dict[Any, Any]:
"""
Open a JSON file and return its contents as a dictionary.
Parameters
----------
ifile : str or Path
Path to the JSON file.
encoding : str, default 'utf-8'
Encoding to use when reading the file.
Returns
-------
dict
Contents of the JSON file.
"""
with Path(ifile).open(encoding=encoding) as f:
data = json.load(f)
if not isinstance(data, dict):
raise TypeError(f"Expected a JSON object, got {type(data)}.")
return data
[docs]
def collect_json_files(idir: str, *args: str, base: str | None = None, name: str | None = None) -> list[Path]:
r"""
Collect JSON files recursively based on directory and optional subdirectories.
Parameters
----------
idir : str
Base directory to search.
\*args : str
Optional subdirectory names for recursive searching.
base : str, optional
Base path to prepend to idir.
name : str, optional
Base file name to search. If None, defaults to idir.
Returns
-------
list of Path
List of matching JSON file paths.
"""
path = f"{base}.{idir}" if base else idir
data_dir = get_path(path)
ifile = name or idir
list_of_files = list(data_dir.glob(f"{ifile}.json")) if data_dir else []
for arg in args:
if name is None:
ifile = f"{ifile}_{arg}"
path = f"{path}.{arg}" if base else arg
data_dir = get_path(path)
arg_files = list(data_dir.glob(f"{ifile}.json")) if data_dir else []
list_of_files.extend(arg_files)
return list_of_files
[docs]
def combine_dicts(list_of_files: str | Path | Sequence[str | Path | dict[str, Any]], base: str | None = None) -> dict[str, Any]:
"""
Combine multiple JSON files or dictionaries into a single dictionary.
Supports nested 'substitute' references to recursively load additional JSON files.
Parameters
----------
list_of_files : str, Path, list
JSON file(s) or dictionaries to combine.
base : str, optional
Base path used when resolving substituted files.
Returns
-------
dict
Combined dictionary from all input files/dictionaries.
"""
def update_dict(old: dict[str, Any], new: dict[str, Any]) -> dict[str, Any]:
"""
Update dictionary.
Parameters
----------
old : dict
Dictionary to be updated.
new : dict
Dictionary used for updating.
Returns
-------
dict
Updated dictionary.
"""
keys = set(old.keys()) | set(new.keys())
for key in keys:
if key not in new:
continue
elif key not in old:
old[key] = new[key]
elif new[key] == "ignore":
old.pop(key, None)
elif isinstance(new[key], dict):
old[key] = update_dict(old.get(key, {}), new[key])
else:
old[key] = new[key]
return old
combined_dict: dict[str, Any] = {}
if isinstance(list_of_files, (str, Path)):
list_of_files = [list_of_files]
for item in list_of_files:
json_dict = item if isinstance(item, dict) else open_json_file(item)
# Handle recursive substitution
if "substitute" in json_dict:
sub = json_dict["substitute"]
data_model = sub.get("data_model")
release = sub.get("release")
deck = sub.get("deck")
new_files = collect_json_files(data_model, release, deck, base=base)
new_files = [f for f in new_files if f not in list_of_files]
json_dict = combine_dicts(new_files, base=base)
combined_dict = update_dict(combined_dict, json_dict)
return combined_dict