aboutsummaryrefslogtreecommitdiff
path: root/venv/lib/python3.8/site-packages/plotly/io/_json.py
diff options
context:
space:
mode:
Diffstat (limited to 'venv/lib/python3.8/site-packages/plotly/io/_json.py')
-rw-r--r--venv/lib/python3.8/site-packages/plotly/io/_json.py594
1 files changed, 594 insertions, 0 deletions
diff --git a/venv/lib/python3.8/site-packages/plotly/io/_json.py b/venv/lib/python3.8/site-packages/plotly/io/_json.py
new file mode 100644
index 0000000..e4324d1
--- /dev/null
+++ b/venv/lib/python3.8/site-packages/plotly/io/_json.py
@@ -0,0 +1,594 @@
+import json
+import decimal
+import datetime
+import warnings
+from pathlib import Path
+
+from plotly.io._utils import validate_coerce_fig_to_dict, validate_coerce_output_type
+from _plotly_utils.optional_imports import get_module
+from _plotly_utils.basevalidators import ImageUriValidator
+
+
+# Orca configuration class
+# ------------------------
+class JsonConfig(object):
+ _valid_engines = ("json", "orjson", "auto")
+
+ def __init__(self):
+ self._default_engine = "auto"
+
+ @property
+ def default_engine(self):
+ return self._default_engine
+
+ @default_engine.setter
+ def default_engine(self, val):
+ if val not in JsonConfig._valid_engines:
+ raise ValueError(
+ "Supported JSON engines include {valid}\n Received {val}".format(
+ valid=JsonConfig._valid_engines, val=val
+ )
+ )
+
+ if val == "orjson":
+ self.validate_orjson()
+
+ self._default_engine = val
+
+ @classmethod
+ def validate_orjson(cls):
+ orjson = get_module("orjson")
+ if orjson is None:
+ raise ValueError("The orjson engine requires the orjson package")
+
+
+config = JsonConfig()
+
+
+def coerce_to_strict(const):
+ """
+ This is used to ultimately *encode* into strict JSON, see `encode`
+
+ """
+ # before python 2.7, 'true', 'false', 'null', were include here.
+ if const in ("Infinity", "-Infinity", "NaN"):
+ return None
+ else:
+ return const
+
+
+_swap_json = (
+ ("<", "\\u003c"),
+ (">", "\\u003e"),
+ ("/", "\\u002f"),
+)
+_swap_orjson = _swap_json + (
+ ("\u2028", "\\u2028"),
+ ("\u2029", "\\u2029"),
+)
+
+
+def _safe(json_str, _swap):
+ out = json_str
+ for unsafe_char, safe_char in _swap:
+ if unsafe_char in out:
+ out = out.replace(unsafe_char, safe_char)
+ return out
+
+
+def to_json_plotly(plotly_object, pretty=False, engine=None):
+ """
+ Convert a plotly/Dash object to a JSON string representation
+
+ Parameters
+ ----------
+ plotly_object:
+ A plotly/Dash object represented as a dict, graph_object, or Dash component
+
+ pretty: bool (default False)
+ True if JSON representation should be pretty-printed, False if
+ representation should be as compact as possible.
+
+ engine: str (default None)
+ The JSON encoding engine to use. One of:
+ - "json" for an engine based on the built-in Python json module
+ - "orjson" for a faster engine that requires the orjson package
+ - "auto" for the "orjson" engine if available, otherwise "json"
+ If not specified, the default engine is set to the current value of
+ plotly.io.json.config.default_engine.
+
+ Returns
+ -------
+ str
+ Representation of input object as a JSON string
+
+ See Also
+ --------
+ to_json : Convert a plotly Figure to JSON with validation
+ """
+ orjson = get_module("orjson", should_load=True)
+
+ # Determine json engine
+ if engine is None:
+ engine = config.default_engine
+
+ if engine == "auto":
+ if orjson is not None:
+ engine = "orjson"
+ else:
+ engine = "json"
+ elif engine not in ["orjson", "json"]:
+ raise ValueError("Invalid json engine: %s" % engine)
+
+ modules = {
+ "sage_all": get_module("sage.all", should_load=False),
+ "np": get_module("numpy", should_load=False),
+ "pd": get_module("pandas", should_load=False),
+ "image": get_module("PIL.Image", should_load=False),
+ }
+
+ # Dump to a JSON string and return
+ # --------------------------------
+ if engine == "json":
+ opts = {}
+ if pretty:
+ opts["indent"] = 2
+ else:
+ # Remove all whitespace
+ opts["separators"] = (",", ":")
+
+ from _plotly_utils.utils import PlotlyJSONEncoder
+
+ return _safe(
+ json.dumps(plotly_object, cls=PlotlyJSONEncoder, **opts), _swap_json
+ )
+ elif engine == "orjson":
+ JsonConfig.validate_orjson()
+ opts = orjson.OPT_NON_STR_KEYS | orjson.OPT_SERIALIZE_NUMPY
+
+ if pretty:
+ opts |= orjson.OPT_INDENT_2
+
+ # Plotly
+ try:
+ plotly_object = plotly_object.to_plotly_json()
+ except AttributeError:
+ pass
+
+ # Try without cleaning
+ try:
+ return _safe(
+ orjson.dumps(plotly_object, option=opts).decode("utf8"), _swap_orjson
+ )
+ except TypeError:
+ pass
+
+ cleaned = clean_to_json_compatible(
+ plotly_object,
+ numpy_allowed=True,
+ datetime_allowed=True,
+ modules=modules,
+ )
+ return _safe(orjson.dumps(cleaned, option=opts).decode("utf8"), _swap_orjson)
+
+
+def to_json(fig, validate=True, pretty=False, remove_uids=True, engine=None):
+ """
+ Convert a figure to a JSON string representation
+
+ Parameters
+ ----------
+ fig:
+ Figure object or dict representing a figure
+
+ validate: bool (default True)
+ True if the figure should be validated before being converted to
+ JSON, False otherwise.
+
+ pretty: bool (default False)
+ True if JSON representation should be pretty-printed, False if
+ representation should be as compact as possible.
+
+ remove_uids: bool (default True)
+ True if trace UIDs should be omitted from the JSON representation
+
+ engine: str (default None)
+ The JSON encoding engine to use. One of:
+ - "json" for an engine based on the built-in Python json module
+ - "orjson" for a faster engine that requires the orjson package
+ - "auto" for the "orjson" engine if available, otherwise "json"
+ If not specified, the default engine is set to the current value of
+ plotly.io.json.config.default_engine.
+
+ Returns
+ -------
+ str
+ Representation of figure as a JSON string
+
+ See Also
+ --------
+ to_json_plotly : Convert an arbitrary plotly graph_object or Dash component to JSON
+ """
+ # Validate figure
+ # ---------------
+ fig_dict = validate_coerce_fig_to_dict(fig, validate)
+
+ # Remove trace uid
+ # ----------------
+ if remove_uids:
+ for trace in fig_dict.get("data", []):
+ trace.pop("uid", None)
+
+ return to_json_plotly(fig_dict, pretty=pretty, engine=engine)
+
+
+def write_json(fig, file, validate=True, pretty=False, remove_uids=True, engine=None):
+ """
+ Convert a figure to JSON and write it to a file or writeable
+ object
+
+ Parameters
+ ----------
+ fig:
+ Figure object or dict representing a figure
+
+ file: str or writeable
+ A string representing a local file path or a writeable object
+ (e.g. a pathlib.Path object or an open file descriptor)
+
+ pretty: bool (default False)
+ True if JSON representation should be pretty-printed, False if
+ representation should be as compact as possible.
+
+ remove_uids: bool (default True)
+ True if trace UIDs should be omitted from the JSON representation
+
+ engine: str (default None)
+ The JSON encoding engine to use. One of:
+ - "json" for an engine based on the built-in Python json module
+ - "orjson" for a faster engine that requires the orjson package
+ - "auto" for the "orjson" engine if available, otherwise "json"
+ If not specified, the default engine is set to the current value of
+ plotly.io.json.config.default_engine.
+ Returns
+ -------
+ None
+ """
+
+ # Get JSON string
+ # ---------------
+ # Pass through validate argument and let to_json handle validation logic
+ json_str = to_json(
+ fig, validate=validate, pretty=pretty, remove_uids=remove_uids, engine=engine
+ )
+
+ # Try to cast `file` as a pathlib object `path`.
+ # ----------------------------------------------
+ if isinstance(file, str):
+ # Use the standard Path constructor to make a pathlib object.
+ path = Path(file)
+ elif isinstance(file, Path):
+ # `file` is already a Path object.
+ path = file
+ else:
+ # We could not make a Path object out of file. Either `file` is an open file
+ # descriptor with a `write()` method or it's an invalid object.
+ path = None
+
+ # Open file
+ # ---------
+ if path is None:
+ # We previously failed to make sense of `file` as a pathlib object.
+ # Attempt to write to `file` as an open file descriptor.
+ try:
+ file.write(json_str)
+ return
+ except AttributeError:
+ pass
+ raise ValueError(
+ """
+The 'file' argument '{file}' is not a string, pathlib.Path object, or file descriptor.
+""".format(file=file)
+ )
+ else:
+ # We previously succeeded in interpreting `file` as a pathlib object.
+ # Now we can use `write_bytes()`.
+ path.write_text(json_str)
+
+
+def from_json_plotly(value, engine=None):
+ """
+ Parse JSON string using the specified JSON engine
+
+ Parameters
+ ----------
+ value: str or bytes
+ A JSON string or bytes object
+
+ engine: str (default None)
+ The JSON decoding engine to use. One of:
+ - if "json", parse JSON using built in json module
+ - if "orjson", parse using the faster orjson module, requires the orjson
+ package
+ - if "auto" use orjson module if available, otherwise use the json module
+
+ If not specified, the default engine is set to the current value of
+ plotly.io.json.config.default_engine.
+
+ Returns
+ -------
+ dict
+
+ See Also
+ --------
+ from_json_plotly : Parse JSON with plotly conventions into a dict
+ """
+ orjson = get_module("orjson", should_load=True)
+
+ # Validate value
+ # --------------
+ if not isinstance(value, (str, bytes)):
+ raise ValueError(
+ """
+from_json_plotly requires a string or bytes argument but received value of type {typ}
+ Received value: {value}""".format(typ=type(value), value=value)
+ )
+
+ # Determine json engine
+ if engine is None:
+ engine = config.default_engine
+
+ if engine == "auto":
+ if orjson is not None:
+ engine = "orjson"
+ else:
+ engine = "json"
+ elif engine not in ["orjson", "json"]:
+ raise ValueError("Invalid json engine: %s" % engine)
+
+ if engine == "orjson":
+ JsonConfig.validate_orjson()
+ # orjson handles bytes input natively
+ value_dict = orjson.loads(value)
+ else:
+ # decode bytes to str for built-in json module
+ if isinstance(value, bytes):
+ value = value.decode("utf-8")
+ value_dict = json.loads(value)
+
+ return value_dict
+
+
+def from_json(value, output_type="Figure", skip_invalid=False, engine=None):
+ """
+ Construct a figure from a JSON string
+
+ Parameters
+ ----------
+ value: str or bytes
+ String or bytes object containing the JSON representation of a figure
+
+ output_type: type or str (default 'Figure')
+ The output figure type or type name.
+ One of: graph_objs.Figure, 'Figure', graph_objs.FigureWidget, 'FigureWidget'
+
+ skip_invalid: bool (default False)
+ False if invalid figure properties should result in an exception.
+ True if invalid figure properties should be silently ignored.
+
+ engine: str (default None)
+ The JSON decoding engine to use. One of:
+ - if "json", parse JSON using built in json module
+ - if "orjson", parse using the faster orjson module, requires the orjson
+ package
+ - if "auto" use orjson module if available, otherwise use the json module
+
+ If not specified, the default engine is set to the current value of
+ plotly.io.json.config.default_engine.
+
+ Raises
+ ------
+ ValueError
+ if value is not a string, or if skip_invalid=False and value contains
+ invalid figure properties
+
+ Returns
+ -------
+ Figure or FigureWidget
+ """
+
+ # Decode JSON
+ # -----------
+ fig_dict = from_json_plotly(value, engine=engine)
+
+ # Validate coerce output type
+ # ---------------------------
+ cls = validate_coerce_output_type(output_type)
+
+ # Create and return figure
+ # ------------------------
+ fig = cls(fig_dict, skip_invalid=skip_invalid)
+ return fig
+
+
+def read_json(file, output_type="Figure", skip_invalid=False, engine=None):
+ """
+ Construct a figure from the JSON contents of a local file or readable
+ Python object
+
+ Parameters
+ ----------
+ file: str or readable
+ A string containing the path to a local file or a read-able Python
+ object (e.g. a pathlib.Path object or an open file descriptor)
+
+ output_type: type or str (default 'Figure')
+ The output figure type or type name.
+ One of: graph_objs.Figure, 'Figure', graph_objs.FigureWidget, 'FigureWidget'
+
+ skip_invalid: bool (default False)
+ False if invalid figure properties should result in an exception.
+ True if invalid figure properties should be silently ignored.
+
+ engine: str (default None)
+ The JSON decoding engine to use. One of:
+ - if "json", parse JSON using built in json module
+ - if "orjson", parse using the faster orjson module, requires the orjson
+ package
+ - if "auto" use orjson module if available, otherwise use the json module
+
+ If not specified, the default engine is set to the current value of
+ plotly.io.json.config.default_engine.
+
+ Returns
+ -------
+ Figure or FigureWidget
+ """
+
+ # Try to cast `file` as a pathlib object `path`.
+ if isinstance(file, str):
+ # Use the standard Path constructor to make a pathlib object.
+ path = Path(file)
+ elif isinstance(file, Path):
+ # `file` is already a Path object.
+ path = file
+ else:
+ # We could not make a Path object out of file. Either `file` is an open file
+ # descriptor with a `write()` method or it's an invalid object.
+ path = None
+
+ # Read file contents into JSON string
+ # -----------------------------------
+ if path is not None:
+ json_str = path.read_text()
+ else:
+ json_str = file.read()
+
+ # Construct and return figure
+ # ---------------------------
+ return from_json(
+ json_str, skip_invalid=skip_invalid, output_type=output_type, engine=engine
+ )
+
+
+def clean_to_json_compatible(obj, **kwargs):
+ # Try handling value as a scalar value that we have a conversion for.
+ # Return immediately if we know we've hit a primitive value
+
+ # Bail out fast for simple scalar types
+ if isinstance(obj, (int, float, str)):
+ return obj
+
+ if isinstance(obj, dict):
+ return {k: clean_to_json_compatible(v, **kwargs) for k, v in obj.items()}
+ elif isinstance(obj, (list, tuple)):
+ if obj:
+ # Must process list recursively even though it may be slow
+ return [clean_to_json_compatible(v, **kwargs) for v in obj]
+
+ # unpack kwargs
+ numpy_allowed = kwargs.get("numpy_allowed", False)
+ datetime_allowed = kwargs.get("datetime_allowed", False)
+
+ modules = kwargs.get("modules", {})
+ sage_all = modules["sage_all"]
+ np = modules["np"]
+ pd = modules["pd"]
+ image = modules["image"]
+
+ # Sage
+ if sage_all is not None:
+ if obj in sage_all.RR:
+ return float(obj)
+ elif obj in sage_all.ZZ:
+ return int(obj)
+
+ # numpy
+ if np is not None:
+ if obj is np.ma.core.masked:
+ return float("nan")
+ elif isinstance(obj, np.ndarray):
+ if numpy_allowed and obj.dtype.kind in ("b", "i", "u", "f"):
+ return np.ascontiguousarray(obj)
+ elif obj.dtype.kind == "M":
+ # datetime64 array
+ return np.datetime_as_string(obj).tolist()
+ elif obj.dtype.kind == "U":
+ return obj.tolist()
+ elif obj.dtype.kind == "O":
+ # Treat object array as a lists, continue processing
+ obj = obj.tolist()
+ elif isinstance(obj, np.datetime64):
+ return str(obj)
+
+ # pandas
+ if pd is not None:
+ if obj is pd.NaT or obj is pd.NA:
+ return None
+ elif isinstance(obj, (pd.Series, pd.DatetimeIndex)):
+ if numpy_allowed and obj.dtype.kind in ("b", "i", "u", "f"):
+ return np.ascontiguousarray(obj.values)
+ elif obj.dtype.kind == "M":
+ if isinstance(obj, pd.Series):
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", FutureWarning)
+ # Series.dt.to_pydatetime will return Index[object]
+ # https://github.com/pandas-dev/pandas/pull/52459
+ dt_values = np.array(obj.dt.to_pydatetime()).tolist()
+ else: # DatetimeIndex
+ dt_values = obj.to_pydatetime().tolist()
+
+ if not datetime_allowed:
+ # Note: We don't need to handle dropping timezones here because
+ # numpy's datetime64 doesn't support them and pandas's tz_localize
+ # above drops them.
+ for i in range(len(dt_values)):
+ dt_values[i] = dt_values[i].isoformat()
+
+ return dt_values
+
+ # datetime and date
+ try:
+ # Need to drop timezone for scalar datetimes. Don't need to convert
+ # to string since engine can do that
+ obj = obj.to_pydatetime()
+ except (TypeError, AttributeError):
+ pass
+
+ if not datetime_allowed:
+ try:
+ return obj.isoformat()
+ except (TypeError, AttributeError):
+ pass
+ elif isinstance(obj, datetime.datetime):
+ return obj
+
+ # Try .tolist() convertible, do not recurse inside
+ try:
+ return obj.tolist()
+ except AttributeError:
+ pass
+
+ # Do best we can with decimal
+ if isinstance(obj, decimal.Decimal):
+ return float(obj)
+
+ # PIL
+ if image is not None and isinstance(obj, image.Image):
+ return ImageUriValidator.pil_image_to_uri(obj)
+
+ # Plotly
+ try:
+ obj = obj.to_plotly_json()
+ except AttributeError:
+ pass
+
+ # Recurse into lists and dictionaries
+ if isinstance(obj, dict):
+ return {k: clean_to_json_compatible(v, **kwargs) for k, v in obj.items()}
+ elif isinstance(obj, (list, tuple)):
+ if obj:
+ # Must process list recursively even though it may be slow
+ return [clean_to_json_compatible(v, **kwargs) for v in obj]
+
+ return obj