Source code for highcharts_core.utility_functions

"""Collection of utility functions used across the library."""
import csv
import string
import random

from validator_collection import validators

from highcharts_core import errors


def get_random_string(length = 6):
    """Generate a short random alphanumeric string.
    
    :param length: The length of the string to generate. Defaults to ``8``.
    :type length: :class:`int <python:int>`
    
    :returns: A random alphanumeric string of length ``length``.
    :rtype: :class:`str <python:str>`
    """
    length = validators.integer(length, minimum = 1)
    result = ''.join(random.choices(string.ascii_uppercase + string.digits,
                                    k = length))

    return str(result)


[docs]def mro_to_dict(obj): """Work through ``obj``'s multiple parent classes, executing the appropriate ``to_dict()`` method for each parent and consolidaitng the results to a single :class:`dict <python:dict>`. :param obj: An object that has a ``to_dict()`` method. :rtype: :class:`dict <python:dict>` """ if not hasattr(obj, 'to_dict'): raise TypeError('obj does not have a to_dict() method.') classes = [x for x in obj.__class__.mro() if x.__name__ != 'object'] as_dict = {} for item in classes: has_to_dict = hasattr(super(item, obj), 'to_dict') if not has_to_dict: break try: item_dict = super(item, obj).to_dict() except (NotImplementedError, AttributeError): continue for key in item_dict: as_dict[key] = item_dict[key] return as_dict
[docs]def get_remaining_mro(cls, in_cls = None, method = '_to_untrimmed_dict'): """Retrieve the remaining classes that should be processed for ``method`` when traversing ``cls``. :param cls: The class whose ancestors are being traversed. :type cls: :class:`HighchartsMeta` :param in_cls: The class that the traversal currently finds itself in. Defaults to :obj:`None <python:None>` :type in_cls: ``type`` or :obj:`None <python:None>` :param method: The method to search for in the MRO. Defaults to ``'_to_untrimmed_dict'``. :type method: :class:`str <python:str>` :returns: List of classes that have ``method`` that occur *after* ``in_cls`` in the MRO for ``cls``. :rtype: :class:`list <python:list>` of ``type`` objects """ mro = [x for x in cls.mro() if hasattr(x, method) and x.__name__ != 'HighchartsMeta'] if in_cls is None: return mro[1:] else: index = mro.index(in_cls) return mro[(index + 1):]
[docs]def mro__to_untrimmed_dict(obj, in_cls = None): """Traverse the ancestor classes of ``obj`` and execute their ``_to_untrimmed_dict()`` methods. :param obj: The object to be traversed. :type obj: :class:`HighchartsMeta` :param in_cls: The class from which ``mro__to_untrimmed_dict()`` was called. :type in_cls: ``type`` or :obj:`None <python:None>` :returns: Collection of untrimmed :class:`dict <python:dict>` representations in the same order as the MRO. :rtype: :class:`list <python:list>` of :class:`dict <python:dict>` for each class in the MRO, execute _to_untrimmed_dict() do not repeat for each class """ cls = obj.__class__ remaining_mro = get_remaining_mro(cls, in_cls = in_cls, method = '_to_untrimmed_dict') ancestor_dicts = [] for x in remaining_mro: if hasattr(x, '_to_untrimmed_dict') and x != cls: ancestor_dicts.append(x._to_untrimmed_dict(obj, in_cls = x)) consolidated = {} for item in ancestor_dicts: for key in item: consolidated[key] = item[key] return consolidated
[docs]def validate_color(value): """Validate that ``value`` is either a :class:`Gradient`, :class:`Pattern`, or a :class:`str <python:str>`. :param value: The value to validate. :returns: The validated value. :rtype: :class:`str <python:str>`, :class:`Gradient`, :class:`Pattern``, or :obj:`None <python:None>` """ from highcharts_core.utility_classes.gradients import Gradient from highcharts_core.utility_classes.patterns import Pattern if not value: return None elif value.__class__.__name__ == 'EnforcedNullType': return value elif isinstance(value, (Gradient, Pattern)): return value elif isinstance(value, (dict, str)) and ('linearGradient' in value or 'radialGradient' in value): try: value = Gradient.from_json(value) except (TypeError, ValueError): if isinstance(value, dict): value = Gradient.from_dict(value) else: value = validators.string(value) elif isinstance(value, dict) and ('linear_gradient' in value or 'radial_gradient' in value): value = Gradient(**value) elif isinstance(value, (dict, str)) and ('patternOptions' in value or 'pattern' in value): try: value = Pattern.from_json(value) except (TypeError, ValueError): if isinstance(value, dict): value = Pattern.from_dict(value) else: value = validators.string(value) elif isinstance(value, dict) and 'pattern_options' in value: value = Pattern(**value) elif isinstance(value, str): value = validators.string(value) else: raise errors.HighchartsValueError(f'Unable to resolve value to a string, ' f'Gradient, or Pattern. Value received ' f'was: {value}') return value
[docs]def to_camelCase(snake_case): """Convert ``snake_case`` to ``camelCase``. :param snake_case: A :class:`str <python:str>` which is likely to contain ``snake_case``. :type snake_case: :class:`str <python:str>` :returns: A ``camelCase`` representation of ``snake_case``. :rtype: :class:`str <python:str>` """ snake_case = validators.string(snake_case) if '_' not in snake_case: return snake_case if 'url' in snake_case: snake_case = snake_case.replace('url', 'URL') elif 'utc' in snake_case: snake_case = snake_case.replace('utc', 'UTC') elif '_csv' in snake_case: snake_case = snake_case.replace('csv', 'CSV') elif '_jpeg' in snake_case: snake_case = snake_case.replace('jpeg', 'JPEG') elif '_pdf' in snake_case: snake_case = snake_case.replace('pdf', 'PDF') elif '_png' in snake_case: snake_case = snake_case.replace('png', 'PNG') elif '_svg' in snake_case: snake_case = snake_case.replace('svg', 'SVG') elif '_xls' in snake_case: snake_case = snake_case.replace('xls', 'XLS') elif '_atr' in snake_case: snake_case = snake_case.replace('atr', 'ATR') elif '_hlc' in snake_case: snake_case = snake_case.replace('hlc', 'HLC') elif '_ohlc' in snake_case: snake_case = snake_case.replace('ohlc', 'OHLC') elif '_xy' in snake_case: snake_case = snake_case.replace('xy', 'XY') elif snake_case.endswith('_x'): snake_case = snake_case.replace('_x', '_X') elif snake_case.endswith('_y'): snake_case = snake_case.replace('_y', '_Y') elif snake_case.endswith('_id'): snake_case = snake_case.replace('_id', '_ID') elif snake_case == 'drillup_text': snake_case = 'drillUpText' elif snake_case == 'drillup_button': snake_case = 'drillUpButton' elif snake_case == 'thousands_separator': snake_case = 'thousandsSep' elif snake_case == 'measure_xy': snake_case = 'measureXY' elif snake_case == 'use_gpu_translations': snake_case = 'useGPUTranslations' elif snake_case == 'label_rank': snake_case = 'labelrank' elif '_di_line' in snake_case: snake_case = snake_case.replace('_di_line', '_DILine') camel_case = '' previous_character = '' for character in snake_case: if character != '_' and previous_character != '_': camel_case += character previous_character = character elif character == '_': previous_character = character elif character != '_' and previous_character == '_': camel_case += character.upper() previous_character = character return camel_case
[docs]def parse_csv(csv_data, has_header_row = True, delimiter = ',', null_text = 'None', wrapper_character = "'", wrap_all_strings = False, double_wrapper_character_when_nested = False, escape_character = "\\", line_terminator = '\r\n'): """Parse ``csv_data`` to return a list of :class:`dict <python:dict>` objects, one for each record. :param csv_data: The CSV record expressed as a :class:`str <python:str>` :type csv_data: :class:`str <python:str>` :param delimiter: The delimiter used between columns. Defaults to ``,``. :type delimiter: :class:`str <python:str>` :param wrapper_character: The string used to wrap string values when wrapping is applied. Defaults to ``'``. :type wrapper_character: :class:`str <python:str>` :param null_text: The string used to indicate an empty value if empty values are wrapped. Defaults to `None`. :type null_text: :class:`str <python:str>` :returns: Collection of column names (or numerical keys) and CSV records as :class:`dict <python:dict>` values :rtype: :class:`tuple <python:tuple>` of a :class:`list <python:list>` of column names and :class:`list <python:list>` of :class:`dict <python:dict>` """ csv_data = validators.string(csv_data, allow_empty = True) if not csv_data: return [], [] if not wrapper_character: wrapper_character = '\'' if wrap_all_strings: quoting = csv.QUOTE_NONNUMERIC else: quoting = csv.QUOTE_MINIMAL if 'highcharts' in csv.list_dialects(): csv.unregister_dialect('highcharts') csv.register_dialect('highcharts', delimiter = delimiter, doublequote = double_wrapper_character_when_nested, escapechar = escape_character, quotechar = wrapper_character, quoting = quoting, lineterminator = line_terminator) if has_header_row: csv_reader = csv.DictReader(csv_data, dialect = 'highcharts', restkey = None, restval = None) records_as_dicts = [x for x in csv_reader] columns = csv_reader.fieldnames else: csv_reader = csv.reader(csv_data, dialect = 'highcharts') records_as_dicts = [] columns = [] for row in csv_reader: record_as_dict = {} column_counter = 0 for column in row: record_as_dict[column_counter] = column columns.append(column_counter) column_counter += 1 records_as_dicts.append(record_as_dict) return columns, records_as_dicts
def jupyter_add_script(url, is_last = False): """Generates the JavaScript code Promise which adds a <script/> tag to the Jupyter Lab environment. :param url: The URL to use for the script's source. :type url: :class:`str <python:str>` :param is_last: Whether the URL is the last of the promises. :type is_last: :class:`bool <python:bool>` :returns: The JavaScript code for adding the script. :rtype: :class:`str <python:str>` """ url = validators.url(url) if url.endswith('.css'): return jupyter_add_link(url, is_last = is_last) js_str = '' js_str += """new Promise(function(resolve, reject) {\n""" js_str += f""" var existing_tags = document.querySelectorAll("script[src='{url}']");""" js_str += """ if (existing_tags.length == 0) { var script = document.createElement("script"); script.onload = resolve; script.onerror = reject;""" js_str += f""" script.src = '{url}';""" js_str += """ document.head.appendChild(script); }; })""" return js_str def jupyter_add_link(url, is_last = False): """Generates the JavaScript code Promise which adds a <link/> tag to the Jupyter Lab environment. :param url: The URL to use for the link's source. :type url: :class:`str <python:str>` :param is_last: Whether the URL is the last of the promises. :type is_last: :class:`bool <python:bool>` :returns: The JavaScript code for adding the link. :rtype: :class:`str <python:str>` """ url = validators.url(url) js_str = '' js_str += """new Promise(function(resolve, reject) {\n""" js_str += f""" var existing_tags = document.querySelectorAll("link[href='{url}']");""" js_str += """ if (existing_tags.length == 0) { var link = document.createElement("link"); link.onload = resolve; link.onerror = reject;""" js_str += f""" link.href = '{url}';""" js_str += f""" link.rel = 'stylesheet';""" js_str += f""" link.type = 'text/css';""" js_str += """ document.head.appendChild(link); }; })""" return js_str def get_retryHighcharts(): """Retrieve the ``retryHighcharts()`` JavaScript function. :returns: The JavaScript code of the ``retryHighcharts()`` JavaScript function. :rtype: :class:`str <python:str>` """ js_str = """function retryHighcharts(fn, container = 'highcharts_target_div', retries = 3, retriesLeft = 3, interval = 1000) { return new Promise((resolve, reject) => { try { fn() return resolve(); } catch (err) { if ((err instanceof ReferenceError) || (err instanceof TypeError) || ((err instanceof Error) && (err.message.includes('#13')))) { if (retriesLeft === 0) { var target_div = document.getElementById(container); if (target_div) { var timeElapsed = (retries * interval) / 1000; var errorMessage = "<p>Something went wrong with the Highcharts.js script. It should have been automatically loaded, but it did not load for over " + timeElapsed + " seconds. Check your internet connection, and then if the problem persists please reach out for support.</p>"; target_div.innerHTML = errorMessage; } return reject(); } setTimeout(() => { retryHighcharts(fn, container, retries, retriesLeft - 1, interval).then(resolve).catch(reject); }, interval); } else { throw err; } } }); };""" return js_str def prep_js_for_jupyter(js_str, container = 'highcharts_target_div', retries = 3, interval = 1000): """Remove the JavaScript event listeners from the code in ``js_str`` and prepare the JavaScript code for rending in an IPython context. :param js_str: The JavaScript code from which the event listeners should be stripped. :type js_str: :class:`str <python:str>` :param container: The DIV where the Highcharts visualization is to be rendered. Defaults to ``'highcharts_target_div'``. :type container: :class:`str <python:str>` :param retries: The number of times to retry the rendering. Defaults to 3. :type retries: :class:`int <python:int>` :param interval: The number of milliseconds to wait between retries. Defaults to 1000 (1 second). :type interval: :class:`int <python:int>` :returns: The JavaScript code having removed the non-Jupyter compliant JS code. :rtype: :class:`str <python:str>` """ js_str = js_str.replace( """document.addEventListener('DOMContentLoaded', function() {""", '') js_str = js_str.replace('renderTo = ', '') js_str = js_str.replace(',\noptions = ', ',\n') js_str = js_str[:-3] random_slug = get_random_string() function_str = f"""function insertChart_{random_slug}() """ function_str += """{\n""" function_str += js_str function_str += """\n};\n""" function_str += f"""retryHighcharts(insertChart_{random_slug}, '{container}', {retries}, {retries}, {interval});""" return function_str