Source code for highcharts_core.headless_export

try:
    from dotenv import load_dotenv

    load_dotenv()
except ImportError:
    pass

import json
import os
from typing import Optional, Dict, List

import requests
from validator_collection import validators, checkers

from highcharts_core import __version__ as highcharts_version
from highcharts_core import errors, constants
from highcharts_core.decorators import class_sensitive
from highcharts_core.metaclasses import HighchartsMeta
from highcharts_core.utility_classes.javascript_functions import CallbackFunction
from highcharts_core.options import HighchartsOptions
from highcharts_core.options.data import Data


[docs]class ExportServer(HighchartsMeta): """Class that provides methods for interacting with the Highcharts `Export Server <https://github.com/highcharts/node-export-server>`_. .. note:: By default, the :class:`ExportServer` class operates using the Highcharts-provided export server. If you wish to use your own (or a custom) export server, you can configure the class using either the :meth:`url <ExportServer.url>`, :meth:`port <ExportServer.port>`, and :meth:`path <ExportServer.path>` properties explicitly or by setting the ``HIGHCHARTS_EXPORT_SERVER_DOMAIN``, ``HIGHCHARTS_EXPORT_SERVER_PORT``, or ``HIGHCHARTS_EXPORT_SERVER_PATH`` environment variables. """ def __init__(self, **kwargs): self._url = None self._port = None self._path = None self._options = None self._format_ = None self._scale = None self._width = None self._height = None self._callback = None self._constructor = None self._use_base64 = None self._no_download = None self._async_rendering = None self._global_options = None self._data_options = None self._custom_code = None self._resources = None self._files = None self._css = None self._js = None self._referer = None self._user_agent = None self.protocol = kwargs.get( "protocol", os.getenv("HIGHCHARTS_EXPORT_SERVER_PROTOCOL", "https") ) self.domain = kwargs.get( "domain", os.getenv("HIGHCHARTS_EXPORT_SERVER_DOMAIN", "export.highcharts.com"), ) self.port = kwargs.get("port", os.getenv("HIGHCHARTS_EXPORT_SERVER_PORT", "")) self.path = kwargs.get("path", os.getenv("HIGHCHARTS_EXPORT_SERVER_PATH", "")) self.options = kwargs.get("options", None) self.format_ = kwargs.get("format_", kwargs.get("type", "png")) self.scale = kwargs.get("scale", 1) self.width = kwargs.get("width", None) self.height = kwargs.get("height", None) self.callback = kwargs.get("callback", None) self.constructor = kwargs.get("constructor", "chart") self.use_base64 = kwargs.get("use_base64", False) self.no_download = kwargs.get("no_download", False) self.async_rendering = kwargs.get("async_rendering", False) self.global_options = kwargs.get("global_options", None) self.data_options = kwargs.get("data_options", None) self.custom_code = kwargs.get("custom_code", None) files = kwargs.get("files", None) css = kwargs.get("css", None) js = kwargs.get("js", None) resources = kwargs.get("resources", None) self.referer = kwargs.get( "referer", os.getenv( "HIGHCHARTS_EXPORT_SERVER_REFERER", "https://www.highchartspython.com" ), ) self.user_agent = kwargs.get( "user_agent", os.getenv("HIGHCHARTS_EXPORT_SERVER_USER_AGENT", None) ) if resources: self.resources = kwargs.get("resources", None) else: self.files = files self.css = css self.js = js super().__init__(**kwargs) @property def referer(self) -> Optional[str]: """The referer to use when making requests to the export server. Defaults to the ``HIGHCHARTS_EXPORT_SERVER_REFERER`` environment variable if present, otherwise defaults to ``'https://www.highchartspython.com'``. :rtype: :class:`str <python:str>` or :obj:`None <python:None>` """ if not self._referer: return os.getenv( "HIGHCHARTS_EXPORT_SERVER_REFERER", "https://www.highchartspython.com" ) return self._referer @referer.setter def referer(self, value): value = validators.url(value, allow_empty=True) self._referer = value @property def user_agent(self) -> Optional[str]: """The user agent to use when making requests to the export server. Defaults to the ``HIGHCHARTS_EXPORT_SERVER_USER_AGENT`` environment variable if present, otherwise defaults to ``Highcharts Core for Python / v.<VERSION NUMBER>``. :rtype: :class:`str <python:str>` or :obj:`None <python:None>` """ if self._user_agent: return self._user_agent user_agent = os.getenv( "HIGHCHARTS_EXPORT_SERVER_USER_AGENT", f"HighchartsCoreforPy/{highcharts_version.__version__}", ) return user_agent @user_agent.setter def user_agent(self, value): value = validators.string(value, allow_empty=True) if not value: value = None self._user_agent = value @property def protocol(self) -> Optional[str]: """The protocol over which the Highcharts for Python library should communicate with the :term:`Export Server`. Accepts either ``'https'`` or ``'http'``. Defaults to the ``HIGHCHARTS_EXPORT_SERVER_PROTOCOL`` environment variable if present, otherwise falls back to default of ``'https'``. .. tip:: This property is set automatically by the ``HIGHCHARTS_EXPORT_SERVER_PROTOCOL`` environment variable, if present. .. warning:: If set to :obj:`None <python:None>`, will fall back to the ``HIGHCHARTS_EXPORT_SERVER_PROTOCOL`` value if available, and the Highsoft- provided server (``'export.highcharts.com'``) if not. :rtype: :class:`str <python:str>` """ return self._protocol @protocol.setter def protocol(self, value): value = validators.string(value, allow_empty=True) if not value: value = os.getenv("HIGHCHARTS_EXPORT_SERVER_PROTOCOL", "https") value = value.lower() if value not in ["https", "http"]: raise errors.HighchartsUnsupportedProtocolError( f"protocol expects either " f'"https" or "http". ' f'Received: "{value}"' ) self._protocol = value self._url = None @property def domain(self) -> Optional[str]: """The domain where the :term:`Export Server` can be found. Defaults to the Highsoft-provided Export Server at ``'export.highcharts.com'``, unless over-ridden by the ``HIGHCHARTS_EXPORT_SERVER_DOMAIN`` environment variable. .. tip:: This property is set automatically by the ``HIGHCHARTS_EXPORT_SERVER_DOMAIN`` environment variable, if present. .. warning:: If set to :obj:`None <python:None>`, will fall back to the ``HIGHCHARTS_EXPORT_SERVER_DOMAIN`` value if available, and the Highsoft- provided server (``'export.highcharts.com'``) if not. :rtype: :class:`str <pythoon:str>` """ return self._domain @domain.setter def domain(self, value): value = validators.domain(value, allow_empty=True) if not value: value = os.getenv( "HIGHCHARTS_EXPORT_SERVER_DOMAIN", "export.highcharts.com" ) self._domain = value self._url = None @property def port(self) -> Optional[int]: """The port on which the :term:`Export Server` can be found. Defaults to :obj:`None <python:None>` (for the Highsoft-provided export server), unless over-ridden by the ``HIGHCHARTS_EXPORT_SERVER_PORT`` environment variable. .. tip:: This property is set automatically by the ``HIGHCHARTS_EXPORT_SERVER_PORT`` environment variable, if present. .. warning:: If set to :obj:`None <python:None>`, will fall back to the ``HIGHCHARTS_EXPORT_SERVER_PORT`` value if available. If unavailable, will revert to :obj:`None <python:None>`. :rtype: :class:`str <pythoon:str>` """ return self._port @port.setter def port(self, value): if value or value == 0: value = validators.integer( value, allow_empty=True, minimum=0, maximum=65536 ) else: value = os.getenv("HIGHCHARTS_EXPORT_SERVER_PORT", None) self._port = value self._url = None @property def path(self) -> Optional[str]: """The path (at the :meth:`ExportServer.url`) where the :term:`Export Server` can be reached. Defaults to :obj:`None <python:None>` (for the Highsoft-provided export server), unless over-ridden by the ``HIGHCHARTS_EXPORT_SERVER_PATH`` environment variable. .. tip:: This property is set automatically by the ``HIGHCHARTS_EXPORT_SERVER_PATH`` environment variable, if present. .. warning:: If set to :obj:`None <python:None>`, will fall back to the ``HIGHCHARTS_EXPORT_SERVER_PATH`` value if available. If unavailable, will revert to :obj:`None <python:None>`. :rtype: :class:`str <pythoon:str>` """ return self._path @path.setter def path(self, value): value = validators.path(value, allow_empty=True) if value is None: value = os.getenv("HIGHCHARTS_EXPORT_SERVER_PATH", None) self._path = value self._url = None @property def url(self) -> Optional[str]: """The fully-formed URL for the :term:`Export Server`, consisting of a :meth:`protocol <ExportServer.protocol>`, a :meth:`domain <ExportServer.domain>`, and optional :meth:`port <ExportServer.port>` and :meth:`path <ExportServer.path>`. .. note:: If explicitly set, will override the values in related properties: * :meth:`protocol <ExportServer.protocol>`, * :meth:`domain <ExportServer.domain>`, * :meth:`port <ExportServer.port>`, and * :meth:`path <ExportServer.path>` :rtype: :class:`str <python:str>` """ if self._url: return self._url else: return_value = f"{self.protocol}://{self.domain}" if self.port is not None: return_value += f":{self.port}/" if self.path is not None: return_value += self.path return return_value @url.setter def url(self, value): value = validators.url( value, allow_empty=True, allow_special_ips=os.getenv("HCP_ALLOW_SPECIAL_IPS", False), ) if not value: self.protocol = None self.domain = None self.port = None self.path = None else: original_value = value self.protocol = value[: value.index(":")] protocol = self.protocol + "://" value = value.replace(protocol, "") no_port = False try: end_of_domain = value.index(":") self.domain = value[:end_of_domain] except ValueError: no_port = True try: end_of_domain = value.index("/") self.domain = value[:end_of_domain] except ValueError: self.domain = value domain = self.domain + "/" if domain in value: value = value.replace(domain, "") elif self.domain in value: value = value.replace(self.domain, "") if value and no_port: if value.startswith("/"): self.path = value[1:] else: self.path = value else: if value.startswith(":"): start_of_port = 1 else: start_of_port = 0 try: end_of_port = value.index("/") except ValueError: end_of_port = None if end_of_port: self.port = value[start_of_port:end_of_port] else: self.port = value[start_of_port:] port = f":{self.port}" value = value.replace(port, "") if value.startswith("/"): self.path = value[1:] elif value: self.path = value else: self.path = None self._url = original_value @property def options(self) -> Optional[HighchartsOptions]: """The :class:`HighchartsOptions` which should be applied to render the exported chart. Defaults to :obj:`None <python:None>`. :rtype: :class:`HighchartsOptions` or :obj:`None <pythoN:None>` """ return self._options @options.setter @class_sensitive(HighchartsOptions) def options(self, value): self._options = value @property def format_(self) -> Optional[str]: """The format in which the exported chart should be returned. Defaults to ``'png'``. Accepts: * ``'png'`` * ``'jpeg'`` * ``'pdf'`` * ``'svg'`` * ``'image/svg+xml'`` :rtype: :class:`str <python:str>` or :obj:`None <python:None>` """ return self._format_ @format_.setter def format_(self, value): value = validators.string(value, allow_empty=True) if not value: self._format_ = None else: value = value.lower() if value not in ["png", "jpeg", "pdf", "svg", "image/svg+xml"]: raise errors.HighchartsUnsupportedExportTypeError( f"format_ expects either " f'"png", "jpeg", "pdf", "svg", or ' f'"image/svg+xml". Received: {value}' ) if value == "svg": value = "image/svg+xml" self._format_ = value @property def scale(self) -> Optional[int | float]: """The scale factor by which the exported chart image should be scaled. Defaults to ``1``. .. tip:: Use this setting to improve resolution when exporting PNG or JPEG images. For example, setting ``.scale = 2`` on a chart whose width is 600px will produce an image with a width of 1200px. .. warning:: If :meth:`width <ExportServer.width>` is explicitly set, this setting will be overridden. :rtype: numeric """ return self._scale @scale.setter def scale(self, value): value = validators.numeric(value, allow_empty=True, minimum=0) if not value: value = 1 self._scale = value @property def width(self) -> Optional[int | float]: """The width that the exported chart should have. Defaults to :obj:`None <python:None>`. .. warning:: If explicitly set, this setting will override :meth:`scale <ExportServer.scale>`. :rtype: numeric or :obj:`None <python:None>` """ return self._width @width.setter def width(self, value): value = validators.numeric(value, allow_empty=True, minimum=0) if not value: value = None self._width = value @property def height(self) -> Optional[int | float]: """The height that the exported chart should have. Defaults to :obj:`None <python:None>`. .. warning:: If explicitly set, this setting will override :meth:`scale <ExportServer.scale>`. :rtype: numeric or :obj:`None <python:None>` """ return self._height @height.setter def height(self, value): value = validators.numeric(value, allow_empty=True, minimum=0) if not value: value = None self._height = value @property def callback(self) -> Optional[CallbackFunction]: """A JavaScript function to execute in the (JavaScript) Highcharts constructor. .. note:: This setting is equivalent to providing the :meth:`Chart.callback` setting. :rtype: :class:`CallbackFunction` or :obj:`None <pythoN:None>` """ return self._callback @callback.setter @class_sensitive(CallbackFunction) def callback(self, value): self._callback = value @property def constructor(self) -> Optional[str]: """The (JavaScript) constructor to use when generating the exported chart. Defaults to :obj:`None <python:None>`. Accepts: * ``'Chart'`` * ``'chart'`` * ``'Stock'`` * ``'stockChart'`` * ``'Map'`` * ``'mapChart'`` * ``'Gantt'`` * ``'ganttChart'`` :rtype: :class:`str <python:str>` or :obj:`None <python:None>` """ return self._constructor @constructor.setter def constructor(self, value): value = validators.string(value, allow_empty=True) if not value: self._constructor = None else: if value not in [ "Chart", "Stock", "Map", "Gantt", "chart", "stockChart", "mapChart", "ganttChart", ]: raise errors.HighchartsUnsupportedConstructorError( f"constructor expects " f'"Chart", "Stock", "Map", "Gantt", "chart", ' f'"stockChart", "mapChart", or "ganttChart", but ' f'received: "{value}"' ) if value == "Chart": value = "chart" elif value == "Stock": value = "stockChart" elif value == "Map": value = "mapChart" elif value == "Gantt": value = "ganttChart" self._constructor = value @property def use_base64(self) -> bool: """If ``True``, returns the exported chart in base64 encoding. If ``False``, returns the exported chart in binary. Defaults to ``False``. :rtype: :class:`bool <python:bool>` """ return self._use_base64 @use_base64.setter def use_base64(self, value): self._use_base64 = bool(value) @property def no_download(self) -> bool: """If ``True``, will not send attachment headers in the HTTP response when exporting a chart. Defaults to ``False``. :rtype: :class:`bool <python:bool>` """ return self._no_download @no_download.setter def no_download(self, value): self._no_download = bool(value) @property def async_rendering(self) -> bool: """If ``True``, will delay the (server-side) rendering of the exported chart until all scripts, functions, and event handlers provided have been executed and the (JavaScript) method ``highexp.done()`` is called. Defaults to ``False``. :rtype: :class:`bool <python:bool>` """ return self._async_rendering @async_rendering.setter def async_rendering(self, value): self._async_rendering = bool(value) @property def global_options(self) -> Optional[HighchartsOptions]: """The global options which will be passed to the (JavaScript) ``Highcharts.setOptions()`` method, and which will be applied to the exported chart. Defaults to :obj:`None <python:None>`. :rtype: :class:`HighchartsOptions` """ return self._global_options @global_options.setter @class_sensitive(HighchartsOptions) def global_options(self, value): self._global_options = value @property def data_options(self) -> Optional[Data]: """Configuration of data options to add data to the chart from sources like CSV. Defaults to :obj:`None <python:None>`. :rtype: :class:`Data` or :obj:`None <python:None>` """ return self._data_options @data_options.setter @class_sensitive(Data) def data_options(self, value): self._data_options = value @property def custom_code(self) -> Optional[CallbackFunction]: """When :meth:`data_options <ExportServer.data_options>` is not :obj:`None <python:None>`, this (JavaScript) callback function is executed after the data options are applied. The only argument it receives is the complete set of :class:`HighchartsOptions` (as a JS literal object), which will be passed to the Highcharts constructor on return. Defaults to :obj:`None <python:None>`. :rtype: :class:`CallbackFunction` or :obj:`None <python:None>` """ return self._custom_code @custom_code.setter @class_sensitive(CallbackFunction) def custom_code(self, value): self._custom_code = value @property def resources(self) -> Optional[Dict]: """A dictionary of resources to be used in the export server. Expects to contain up to three keys: * ``files`` which contains an array of JS filenames * ``js`` which contains a string representation of JS code * ``css`` which contains a string representation of CSS code that will applied to the chart on export Defaults to :obj:`None <python:None>`. :rtype: :class:`dict <python:dict>` or :obj:`None <python:None>` """ resources = {} if self.files: resources["files"] = self.files if self.js: resources["js"] = self.js if self.css: resources["css"] = self.css if not resources: return None return resources @resources.setter def resources(self, value): if not value: self.files = None self.js = None self.css = None elif not isinstance(value, dict): raise errors.HighchartsValueError( 'resources expects a dictionary with keys "files", "js", and "css"' ) else: self.files = value.get("files", None) self.js = value.get("js", None) self.css = value.get("css", None) @property def files(self) -> Optional[List[str]]: """Collection of files that will be loaded into context for the export. Defaults to :obj:`None <python:None>`. :rtype: :class:`list <python:list>` of :class:`str <python:str>` or :obj:`None <python:None>` """ return self._files @files.setter def files(self, value): if not value: self._files = None else: if isinstance(value, str): value = [value] elif not checkers.is_iterable(value): raise errors.HighchartsValueError("files expects a list of strings") self._files = value @property def js(self) -> Optional[str]: """JavaScript code that will be loaded into context for the exported chart. Defaults to :obj:`None <python:None>`. :rtype: :class:`str <python:str>` or :obj:`None <python:None>` """ return self._js @js.setter def js(self, value): if not value: self._js = None else: if not isinstance(value, str): raise errors.HighchartsValueError("js expects a string") self._js = value @property def css(self) -> Optional[str]: """CSS code that will be loaded into context for the exported chart. Defaults to :obj:`None <python:None>`. :rtype: :class:`str <python:str>` or :obj:`None <python:None>` """ return self._css @css.setter def css(self, value): if not value: self._css = None else: if not isinstance(value, str): raise errors.HighchartsValueError("css expects a string") self._css = value
[docs] @classmethod def is_export_supported(cls, options) -> bool: """Evaluates whether the Highcharts Export Server supports exporting the series types in ``options``. :rtype: :class:`bool <python:bool>` """ if not isinstance(options, HighchartsOptions): return False if not options.series: return True series_types = [x.type for x in options.series] for item in series_types: if item in constants.EXPORT_SERVER_UNSUPPORTED_SERIES_TYPES: return False return True
@classmethod def _get_kwargs_from_dict(cls, as_dict): url = as_dict.get("url", None) protocol = None domain = None port = None path = None if not url: protocol = as_dict.get("protocol", None) domain = as_dict.get("domain", None) port = as_dict.get("port", None) path = as_dict.get("path", None) kwargs = { "options": as_dict.get("options", None), "format_": as_dict.get("type", as_dict.get("format_", "png")), "scale": as_dict.get("scale", 1), "width": as_dict.get("width", None), "callback": as_dict.get("callback", None), "constructor": as_dict.get("constructor", None) or as_dict.get("constr", None), "use_base64": as_dict.get("use_base64", None) or as_dict.get("b64", False), "no_download": as_dict.get("noDownload", None) or as_dict.get("no_download", None), "async_rendering": as_dict.get("asyncRendering", False) or as_dict.get("async_rendering", False), "global_options": as_dict.get("global_options", None) or as_dict.get("globalOptions", None), "data_options": as_dict.get("data_options", None) or as_dict.get("dataOptions", None), "custom_code": as_dict.get("custom_code", None) or as_dict.get("customCode", None), } if url: kwargs["url"] = url if protocol: kwargs["protocol"] = protocol if domain: kwargs["domain"] = domain if port: kwargs["port"] = port if path: kwargs["path"] = path return kwargs def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { "url": self.url, "options": self.options, "type": self.format_, "scale": self.scale, "width": self.width, "callback": self.callback, "constr": self.constructor, "b64": self.use_base64, "noDownload": self.no_download, "asyncRendering": self.async_rendering, "globalOptions": self.global_options, "dataOptions": self.data_options, "customCode": self.custom_code, "resources": self.resources, } return untrimmed
[docs] def request_chart( self, filename=None, auth_user=None, auth_password=None, timeout=3, **kwargs ): """Execute a request against the export server based on the configuration in the instance. :param filename: The name of the file where the exported chart should (optionally) be persisted. Defaults to :obj:`None <python:None>`. :type filename: Path-like or :obj:`None <python:None>` :param auth_user: The username to use to authenticate against the Export Server, using :term:`basic authentication`. Defaults to :obj:`None <python:None>`. :type auth_user: :class:`str <python:str>` or :obj:`None <python:None>` :param auth_password: The password to use to authenticate against the Export Server (using :term:`basic authentication`). Defaults to :obj:`None <python:None>`. :type auth_password: :class:`str <python:str>` or :obj:`None <python:None>` :param timeout: The number of seconds to wait before issuing a timeout error. The timeout check is passed if bytes have been received on the socket in less than the ``timeout`` value. Defaults to ``3``. :type timeout: numeric or :obj:`None <python:None>` .. note:: All other keyword arguments are as per the :class:`ExportServer` constructor :meth:`ExportServer.__init__() <highcharts_core.headless_export.ExportServer.__init__>` :returns: The exported chart image, either as a :class:`bytes <python:bytes>` binary object or as a base-64 encoded string (depending on the :meth:`use_base64 <ExportServer.use_base64>` property). :rtype: :class:`bytes <python:bytes>` or :class:`str <python:str>` """ self.options = kwargs.get("options", self.options) self.format_ = kwargs.get("format_", kwargs.get("type", self.format_)) self.scale = kwargs.get("scale", self.scale) self.width = kwargs.get("width", self.width) self.callback = kwargs.get("callback", self.callback) self.constructor = kwargs.get("constructor", self.constructor) self.use_base64 = kwargs.get("use_base64", self.use_base64) self.no_download = kwargs.get("no_download", self.no_download) self.async_rendering = kwargs.get("async_rendering", self.async_rendering) self.global_options = kwargs.get("global_options", self.global_options) self.custom_code = kwargs.get("custom_code", self.custom_code) missing_details = [] if not self.options: missing_details.append("options") if not self.format_: missing_details.append("format_") if not self.constructor: missing_details.append("constructor") if not self.url: missing_details.append("url") if missing_details: raise errors.HighchartsMissingExportSettingsError( f"Unable to export a chart." f"ExportServer was missing " f" following settings: " f"{missing_details}" ) basic_auth = None if auth_user and auth_password: basic_auth = requests.HTTPBasicAuth(auth_user, auth_password) payload = { "infile": "HIGHCHARTS FOR PYTHON: REPLACE WITH OPTIONS", "type": self.format_, "scale": self.scale, "constr": self.constructor, "b64": self.use_base64, "noDownload": self.no_download, } if self.width: payload["width"] = self.width if self.height: payload["height"] = self.height if self.callback: payload["callback"] = "HIGHCHARTS FOR PYTHON: REPLACE WITH CALLBACK" if self.global_options: payload["globalOptions"] = "HIGHCHARTS FOR PYTHON: REPLACE WITH GLOBAL" if self.custom_code: payload["customCode"] = "HIGHCHARTS FOR PYTHON: REPLACE WITH CUSTOM" if self.resources: payload["resources"] = self.resources as_json = json.dumps(payload) if not self.is_export_supported(self.options): raise errors.HighchartsUnsupportedExportError( "The Highcharts Export Server currently only supports " "exports from Highcharts (Javascript) v.10. You are " "using a series type introduced in v.11. Sorry, but " "that functionality is still forthcoming." ) options_as_json = self.options.to_json(for_export=True) if isinstance(options_as_json, bytes): options_as_str = str(options_as_json, encoding="utf-8") else: options_as_str = options_as_json as_json = as_json.replace( '"HIGHCHARTS FOR PYTHON: REPLACE WITH OPTIONS"', options_as_str ) if self.callback: callback_as_json = self.callback.to_json(for_export=True) if isinstance(callback_as_json, bytes): callback_as_str = str(callback_as_json, encoding="utf-8") else: callback_as_str = callback_as_json as_json = as_json.replace( '"HIGHCHARTS FOR PYTHON: REPLACE WITH CALLBACK"', callback_as_str ) if self.global_options: global_as_json = self.global_options.to_json(for_export=True) if isinstance(global_as_json, bytes): global_as_str = str(global_as_json, encoding="utf-8") else: global_as_str = global_as_json as_json = as_json.replace( '"HIGHCHARTS FOR PYTHON: REPLACE WITH GLOBAL"', global_as_str ) if self.data_options: data_as_json = self.data_options.to_json(for_export=True) if isinstance(data_as_json, bytes): data_as_str = str(data_as_json, encoding="utf-8") else: data_as_str = data_as_json as_json = as_json.replace( '"HIGHCHARTS FOR PYTHON: REPLACE WITH DATA"', data_as_str ) if self.custom_code: code_as_json = self.custom_code.to_json(for_export=True) if isinstance(code_as_json, bytes): code_as_str = str(code_as_json, encoding="utf-8") else: code_as_str = code_as_json as_json = as_json.replace( '"HIGHCHARTS FOR PYTHON: REPLACE WITH CUSTOM"', code_as_str ) headers = { "Content-Type": "application/json", "Origin": self.referer, "Referer": self.referer, "User-Agent": self.user_agent, } result = requests.post( self.url, data=as_json.encode("utf-8"), headers=headers, auth=basic_auth, timeout=timeout, ) result.raise_for_status() if filename and self.format_ != "svg": with open(filename, "wb") as file_: file_.write(result.content) elif filename and self.format_ == "svg": content = str(result.content, encoding="utf-8").replace("\u200b", " ") with open(filename, "wt") as file_: file_.write(content) return result.content
[docs] @classmethod def get_chart( cls, filename=None, auth_user=None, auth_password=None, timeout=3, **kwargs ): """Produce an exported chart image. :param filename: The name of the file where the exported chart should (optionally) be persisted. Defaults to :obj:`None <python:None>`. :type filename: Path-like or :obj:`None <python:None>` :param auth_user: The username to use to authenticate against the Export Server, using :term:`basic authentication`. Defaults to :obj:`None <python:None>`. :type auth_user: :class:`str <python:str>` or :obj:`None <python:None>` :param auth_password: The password to use to authenticate against the Export Server (using :term:`basic authentication`). Defaults to :obj:`None <python:None>`. :type auth_password: :class:`str <python:str>` or :obj:`None <python:None>` :param timeout: The number of seconds to wait before issuing a timeout error. The timeout check is passed if bytes have been received on the socket in less than the ``timeout`` value. Defaults to ``3``. :type timeout: numeric or :obj:`None <python:None>` .. note:: All other keyword arguments are as per the :class:`ExportServer` constructor :meth:`ExportServer.__init__() <highcharts_core.headless_export.ExportServer.__init__>` :returns: The exported chart image, either as a :class:`bytes <python:bytes>` binary object or as a base-64 encoded string (depending on the ``use_base64`` keyword argument). :rtype: :class:`bytes <python:bytes>` or :class:`str <python:str>` """ instance = cls(**kwargs) exported_chart = instance.request_chart( filename=filename, auth_user=auth_user, auth_password=auth_password, timeout=timeout, ) return exported_chart