|
"""Utilities to manipulate JSON objects.""" |
|
|
|
|
|
import math |
|
import numbers |
|
import re |
|
import types |
|
import warnings |
|
from binascii import b2a_base64 |
|
from collections.abc import Iterable |
|
from datetime import datetime |
|
from typing import Any, Optional, Union |
|
|
|
from dateutil.parser import parse as _dateutil_parse |
|
from dateutil.tz import tzlocal |
|
|
|
next_attr_name = "__next__" |
|
|
|
|
|
|
|
|
|
|
|
|
|
ISO8601 = "%Y-%m-%dT%H:%M:%S.%f" |
|
ISO8601_PAT = re.compile( |
|
r"^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(\.\d{1,6})?(Z|([\+\-]\d{2}:?\d{2}))?$" |
|
) |
|
|
|
|
|
|
|
datetime.strptime("1", "%d") |
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ensure_tzinfo(dt: datetime) -> datetime: |
|
"""Ensure a datetime object has tzinfo |
|
|
|
If no tzinfo is present, add tzlocal |
|
""" |
|
if not dt.tzinfo: |
|
|
|
warnings.warn( |
|
"Interpreting naive datetime as local %s. Please add timezone info to timestamps." % dt, |
|
DeprecationWarning, |
|
stacklevel=4, |
|
) |
|
dt = dt.replace(tzinfo=tzlocal()) |
|
return dt |
|
|
|
|
|
def parse_date(s: Optional[str]) -> Optional[Union[str, datetime]]: |
|
"""parse an ISO8601 date string |
|
|
|
If it is None or not a valid ISO8601 timestamp, |
|
it will be returned unmodified. |
|
Otherwise, it will return a datetime object. |
|
""" |
|
if s is None: |
|
return s |
|
m = ISO8601_PAT.match(s) |
|
if m: |
|
dt = _dateutil_parse(s) |
|
return _ensure_tzinfo(dt) |
|
return s |
|
|
|
|
|
def extract_dates(obj: Any) -> Any: |
|
"""extract ISO8601 dates from unpacked JSON""" |
|
if isinstance(obj, dict): |
|
new_obj = {} |
|
for k, v in obj.items(): |
|
new_obj[k] = extract_dates(v) |
|
obj = new_obj |
|
elif isinstance(obj, (list, tuple)): |
|
obj = [extract_dates(o) for o in obj] |
|
elif isinstance(obj, str): |
|
obj = parse_date(obj) |
|
return obj |
|
|
|
|
|
def squash_dates(obj: Any) -> Any: |
|
"""squash datetime objects into ISO8601 strings""" |
|
if isinstance(obj, dict): |
|
obj = dict(obj) |
|
for k, v in obj.items(): |
|
obj[k] = squash_dates(v) |
|
elif isinstance(obj, (list, tuple)): |
|
obj = [squash_dates(o) for o in obj] |
|
elif isinstance(obj, datetime): |
|
obj = obj.isoformat() |
|
return obj |
|
|
|
|
|
def date_default(obj: Any) -> Any: |
|
"""DEPRECATED: Use jupyter_client.jsonutil.json_default""" |
|
warnings.warn( |
|
"date_default is deprecated since jupyter_client 7.0.0." |
|
" Use jupyter_client.jsonutil.json_default.", |
|
stacklevel=2, |
|
) |
|
return json_default(obj) |
|
|
|
|
|
def json_default(obj: Any) -> Any: |
|
"""default function for packing objects in JSON.""" |
|
if isinstance(obj, datetime): |
|
obj = _ensure_tzinfo(obj) |
|
return obj.isoformat().replace("+00:00", "Z") |
|
|
|
if isinstance(obj, bytes): |
|
return b2a_base64(obj, newline=False).decode("ascii") |
|
|
|
if isinstance(obj, Iterable): |
|
return list(obj) |
|
|
|
if isinstance(obj, numbers.Integral): |
|
return int(obj) |
|
|
|
if isinstance(obj, numbers.Real): |
|
return float(obj) |
|
|
|
raise TypeError("%r is not JSON serializable" % obj) |
|
|
|
|
|
|
|
|
|
|
|
def json_clean(obj: Any) -> Any: |
|
|
|
atomic_ok = (str, type(None)) |
|
|
|
|
|
container_to_list = (tuple, set, types.GeneratorType) |
|
|
|
|
|
|
|
|
|
if isinstance(obj, bool): |
|
return obj |
|
|
|
if isinstance(obj, numbers.Integral): |
|
|
|
return int(obj) |
|
|
|
if isinstance(obj, numbers.Real): |
|
|
|
if math.isnan(obj) or math.isinf(obj): |
|
return repr(obj) |
|
return float(obj) |
|
|
|
if isinstance(obj, atomic_ok): |
|
return obj |
|
|
|
if isinstance(obj, bytes): |
|
|
|
|
|
return b2a_base64(obj, newline=False).decode("ascii") |
|
|
|
if isinstance(obj, container_to_list) or ( |
|
hasattr(obj, "__iter__") and hasattr(obj, next_attr_name) |
|
): |
|
obj = list(obj) |
|
|
|
if isinstance(obj, list): |
|
return [json_clean(x) for x in obj] |
|
|
|
if isinstance(obj, dict): |
|
|
|
|
|
|
|
nkeys = len(obj) |
|
nkeys_collapsed = len(set(map(str, obj))) |
|
if nkeys != nkeys_collapsed: |
|
msg = ( |
|
"dict cannot be safely converted to JSON: " |
|
"key collision would lead to dropped values" |
|
) |
|
raise ValueError(msg) |
|
|
|
out = {} |
|
for k, v in obj.items(): |
|
out[str(k)] = json_clean(v) |
|
return out |
|
|
|
if isinstance(obj, datetime): |
|
return obj.strftime(ISO8601) |
|
|
|
|
|
raise ValueError("Can't clean for JSON: %r" % obj) |
|
|