"""Collection of utility functions used across the library."""
import csv
import datetime
import os
import string
import random
import typing
from collections import UserDict
from validator_collection import validators, checkers
try:
    import numpy as np
    HAS_NUMPY = True
except ImportError:
    HAS_NUMPY = False
from highcharts_core import errors, constants
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>`
    """
    if not snake_case:
        raise errors.HighchartsValueError(f'snake_case cannot be empty')
    
    snake_case = str(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 to_snake_case(camel_case) -> str:
    """Convert ``camelCase`` to ``snake_case``.
    :param camel_case: A :class:`str <python:str>` which is likely to contain
      ``camelCase``.
    :type camel_case: :class:`str <python:str>`
    :returns: A ``snake_case`` representation of ``camel_case``.
    :rtype: :class:`str <python:str>`
    """
    camel_case = validators.string(camel_case)
    snake_case = ''
    previous_character = ''
    for character in camel_case:
        if character.isupper() and not previous_character.isupper():
            snake_case += f'_{character.lower()}'
        elif character.isupper() and previous_character.isupper():
            snake_case += character.lower()
        elif character.isupper() and not previous_character:
            snake_case += character.lower()
        else:
            snake_case += character
        previous_character = character
    return snake_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>`
    """
    if not csv_data:
        return [], []
    if isinstance(csv_data, str):
        csv_data = csv_data.split(line_terminator)
    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, use_require = 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>`
    
    :param use_require: Whether to return the script needed for RequireJS.
      Defaults to ``False``.
    :type use_require: :class:`bool <python:bool>`
    
    :returns: The JavaScript code for adding the script.
    :rtype: :class:`str <python:str>`
    """
    url = validators.url(
        url, allow_special_ips=os.getenv("HCP_ALLOW_SPECIAL_IPS", False)
    )
    if url.endswith('.css'):
        return jupyter_add_link(url, is_last = is_last)
    js_str = """"""
    
    if use_require:
        js_str += f"""require(['{url}'], function() """
        js_str += """{\n"""
        if is_last:
            js_str += """});"""
    else:
        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);
        } else { resolve() };
    })"""
    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, allow_special_ips=os.getenv("HCP_ALLOW_SPECIAL_IPS", False)
    )
    
    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);
    } else { resolve() };
})"""
    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 = 5, retriesLeft = 5, 
        interval = 1000) {
            return new Promise((resolve, reject) => {
            try {
                fn()
                return resolve();
            } catch (err) {
                if ((err instanceof ReferenceError) || (err instanceof TypeError) || (err.message.includes('#17'))) {
                    if (retriesLeft === 0) {
                        var target_div = document.getElementById(container);
                        if (target_div) {
                            var timeElapsed = (retries * interval) / 1000;
                            var errorMessage = "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. (You can also check your browser's console log for more details.)<br/><br/>Detailed Error Message:<br/>" + err.message;
                            var errorHTML = errorMessage;
                            
                            target_div.innerHTML = errorHTML;
                            console.log(errorMessage);
                            console.error(err);
                        }
                        return reject();
                    }
                    setTimeout(() => {
                        retryHighcharts(fn, container, retries, retriesLeft - 1, interval).then(resolve).catch(reject);
                    }, interval);
                } else if ((err instanceof Error) && (err.message.includes('#13'))) {
                    var errorMessage = "It looks like the container specified \'" + container + "\' was not created successfully. Please check your browser\'s console log for more details.";
                    console.error(errorMessage);
                    console.error(err);
                    
                    return reject();
                } else {
                    throw err;
                }
            }
        });
    };"""
    
    return js_str
def prep_js_for_jupyter(js_str,
                        container = 'highcharts_target_div',
                        random_slug = None,
                        retries = 5,
                        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 random_slug: The random sequence of characters to append to the container/function name to ensure uniqueness.
        Defaults to :obj:`None <python:None>`
    :type random_slug: :class:`str <python:str>` or :obj:`None <python:None>`
    :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')
    if '.setOptions(' not in js_str:
        js_str = js_str[:-3]
    if random_slug:
        function_str = f"""function insertChart_{random_slug}() """
    else:
        function_str = """function insertChart() """
    function_str += """{\n"""
    function_str += js_str
    function_str += """\n};\n"""
    if random_slug:
        function_str += f"""retryHighcharts(insertChart_{random_slug}, '{container}', {retries}, {retries}, {interval});"""
    else:
        function_str += f"""retryHighcharts(insertChart, '{container}', {retries}, {retries}, {interval});"""
    return function_str
def wrap_for_requirejs(if_require_js, if_no_requirejs = None):
    """Wrap ``if_require_js`` in a conditional JavaScript ``if ... { }`` statement
    that evalutes whether RequireJS is present in the browser.
    
    :param if_require_js: The (JavaScript) code that should be executed if RequireJS
      *is* present.
    :type if_require_js: :class:`str <python:str>`
    
    :param if_no_require_js: The (JavaScript) code that should be executed if RequireJS
      is *not* present. Defaults to :obj:`None <python:None>` (nothing gets executed).
    :type if_no_require_js: :class:`str <python:str>`
    """
    js_str = """var has_requirejs = typeof requirejs !== 'undefined';\n"""
    js_str += """if (has_requirejs) {\n"""
    js_str += if_require_js + '\n}'
    
    if if_no_requirejs:
        js_str += """ else {\n"""
        js_str += if_no_requirejs + '\n}'
        
    js_str += ';'
    
    return js_str
def to_ndarray(value):
    """Convert ``value`` to a :class:`numpy.ndarray <numpy:numpy.ndarray>`.
    
    :param value: The value to be converted. Expects the value to be an iterable.
    :type value: iterable
    
    :raises HighchartsDependencyError: if NumPy is not installed
    
    :returns: A :class:`numpy.ndarray <numpy:numpy.ndarray>` representation of 
      ``value``.
    :rtype: :class:`numpy.ndarray <numpy:numpy.ndarray>`
    
    """
    if not HAS_NUMPY:
        raise errors.HighchartsDependencyError('NumPy is required for this feature. '
                                               'It was not found in the runtime environment. '
                                               'Please install it using "pip install numpy" '
                                               'or equivalent.')
    for i, item in enumerate(value):
        is_iterable = not isinstance(item,
                                     (str, bytes, dict, UserDict)) and hasattr(item, 
                                                                               '__iter__')
        if item is None or isinstance(item, constants.EnforcedNullType):
            value[i] = np.nan
        elif is_iterable:
            for index, subitem in enumerate(item):
                if subitem is None or isinstance(subitem, constants.EnforcedNullType):
                    item[i] = np.nan
            value[i] = item
    if hasattr(value, '__array__'):
        as_array = np.array(value)
    else:
        as_array = np.asarray(value)
    return as_array
def to_ndarray_dict(keys, as_iterable):
    """Convert ``as_iterable`` into a :class:`dict <python:dict>`
    whose keys align to the values in ``keys``, and whose values
    are :class:`numpy.ndarray <numpy:numpy.ndarray>` instances
    corresponding to the index in ``as_iterable``.
    
    :param keys: The collection of keys to use for the resulting
      :class:`dict <python:dict>`.
    :type keys: iterable of :class:`str <python:str>`
    
    :param as_iterable: The collection of values to be converted
      to :class:`numpy.ndarray <numpy:numpy.ndarray>` instances
    :type as_iterable: iterable
    
    :returns: A :class:`dict <python:dict>` whose keys are values
      from ``keys``, and whose values are items from ``as_iterable``
      with each item converted to a 
      :class:`numpy.ndarray <numpy:numpy.ndarray>`
    :rtype: :class:`dict <python:dict>`
    
    :raises HighchartsValueError: if ``keys`` and ``as_iterable``
      have different lengths
    """
    keys = validators.iterable(keys,
                               allow_empty = False,
                               forbid_literals = (str, bytes, dict, UserDict))
    as_iterable = validators.iterable(as_iterable,
                                      allow_empty = False,
                                      forbid_literals = (str, bytes, dict, UserDict))
    if len(keys) != len(as_iterable):
        raise errors.HighchartsValueError(f'keys and as_iterable must have the same '
                                          f'length. Received: {len(keys)} for keys,'
                                          f'{len(as_iterable)} for as_iterable ')
        
    as_dict = {}
    for index, key in enumerate(keys):
        as_dict[key] = to_ndarray(as_iterable[index])
        
    return as_dict
def from_ndarray(as_ndarray, force_enforced_null = False):
    """Convert ``as_ndarray`` to a Python :class:`list <python:list>`.
    
    :param as_ndarray: The :class:`numpy.ndarray <numpy:numpy.ndarray>` 
      to be converted.
    :type as_ndarray: :class:`numpy.ndarray <numpy:numpy.ndarray>`
    
    :param force_enforced_null: if ``True``, converts any 
      :class:`numpy.nan <numpy:numpy.nan>` values to :obj:`EnforcedNull`.
      Otherwise, converts them to :obj:`None <python:None>`. Defaults to 
      ``False``.
    :type force_enforced_null: :class:`bool <python:bool>`
    
    :raises HighchartsDependencyError: if NumPy is not installed
    :raises HighchartsValueError: if ``as_ndarray`` is not a 
      :class:`numpy.ndarray <numpy:numpy.ndarray>`
    
    :returns: The Python :class:`list <python:list>` representation of
      ``as_ndarray``.
    :rtype: :class:`list <python:list>`
    
    """
    if not HAS_NUMPY:
        raise errors.HighchartsDependencyError('NumPy is required for this feature. '
                                               'It was not found in the runtime environment. '
                                               'Please install it using "pip install numpy" '
                                               'or equivalent.')
    if not isinstance(as_ndarray, np.ndarray):
        raise errors.HighchartsValueError(f'as_ndarray is expected to be a NumPy ndarray. '
                                          f'Received: {as_ndarray.__class__.__name__}')
    if force_enforced_null:
        nan_replacement = constants.EnforcedNull
    else:
        nan_replacement = None
    if as_ndarray.dtype.char not in ['O', 'U', 'M']:
        stripped = np.where(np.isnan(as_ndarray), nan_replacement, as_ndarray)
    elif as_ndarray.dtype.char == 'M':
        stripped = (as_ndarray.astype(np.int64) / 10**6).astype(np.int64)
    else:
        prelim_stripped = as_ndarray.tolist()
        stripped = []
        for item in prelim_stripped:
            if item == np.nan:
                stripped.append(nan_replacement)
            else:
                stripped.append(item)
                
        return stripped
    return stripped.tolist()
def get_ndarray_slice(array, index):
    """Return the slice of ``array`` at ``index``.
    
    :param array: A `NumPy <https://numpy.org>`__ :class:`ndarray <numpy:numpy.ndarray>`
      instance or a Python iterable.
    :type array: :class:`numpy.ndarray <numpy:numpy.ndarray>` or iterable
    
    :param index: The 0-based index of the column to return from ``array``.
    
      .. note::
      
        If ``index`` exceeds the number of dimensions in ``array``, then
        an empty collection of values should be returned, with the number
        of empty values matching the length of the ``array``.
        
    :type index: :class:`int <python:int>`
    
    :returns: A collection of values.
    :rtype: :class:`numpy.ndarray <numpy:numpy.ndarray>`
      or :class:`list <python:list>`
    """
    index = validators.integer(index, minimum = 0, allow_none = False)
    if HAS_NUMPY and isinstance(array, np.ndarray):
        if index < array.shape[1]:
            return array[:, index]
        else:
            len_array = array.shape[0]
    
            return np.full((len_array, 1), np.nan)
    else:
        array = validators.iterable(array, 
                                    allow_empty = True, 
                                    forbid_literals = (str, bytes, dict, UserDict)) or []
    
    return [x[index] for x in array]
def lengthen_array(value, members):
    """Create a NumPy :class:`ndarray <numpy:numpy.ndarray>` from 
    ``value`` where the result has ``members``.
    
    :param value: The array-like value to be inserted into the resulting array.
    
      .. note::
      
        If an :class:`int <python:int>` is supplied, the value will be repeated all
        ``members``.
    
    :type value: Array-like or :class:`int <python:int>`
    
    :param members: The number of members the resulting ``value`` expects.
    :type members: :class:`int <python:int>`
    
    :returns: A NumPy :class:`ndarray <numpy:numpy.ndarray>` of length ``members``.
    :rtype: :class:`numpy.ndarray <numpy:numpy.ndarray>`
    
    :raises HighchartsDependencyError: if NumPy is not available in the runtime
      environment
    :raises HighchartsValueError: if ``value`` has more members than ``members``
    """
    if not HAS_NUMPY:
        raise errors.HighchartsDependencyError('NumPy is required for this feature. '
                                               'It was not found in your runtime '
                                               'environment. Please make sure it is '
                                               'installed in your runtime '
                                               'environment.')
    is_ndarray = isinstance(value, np.ndarray)
    is_list = False
    if not is_ndarray:
        is_list = checkers.is_iterable(value,
                                       forbid_literals = (str, bytes, dict, UserDict),
                                       allow_empty = False)
    is_int = False
    if not is_ndarray and not is_list:
        value = validators.integer(value, allow_empty = None)
        is_int = True
    if is_list:
        value = np.asarray(value)
    elif is_int:
        value = np.full((members, 1), value)
    if len(value) > members:
        raise errors.HighchartsValueError(f'Value has more members than specified. '
                                          f'Received: {len(value)}. Expected up to: '
                                          f'{members}.')
    elif len(value) < members:
        members_to_add = members - len(value)
    else:
        members_to_add = 0
    if members_to_add:
        try:
            value = np.vstack((value, np.full((members_to_add, value.shape[1]), np.nan)))
        except IndexError:
            value = np.vstack((value, np.full((members_to_add, value.ndim), np.nan)))
    return value
def is_iterable(value) -> bool:
    """Evaluate whether ``value`` is iterable, with support for NumPy arrays.
    
    :param value: The value to evaluate.
    :type value: Any
    
    :returns: ``True`` if iterable, ``False`` if not
    :rtype: :class:`bool <python:bool>`
    """
    return checkers.is_type(value, 'ndarray') or \
        (not isinstance(value,
                        (str, bytes, dict, UserDict)) and hasattr(value, '__iter__'))
def is_arraylike(value) -> bool:
    """Evaluate whether ``value`` is a NumPy array or a Python iterable.
    
    :param value: The value to evaluate.
    :type value: Any
    
    :raises HighchartsDependencyError: if NumPy is not available in the runtime
      environment
    
    :returns: ``True`` if an array or array-like. ``False`` if not.
    :rtype: :class:`bool <python:bool>`
    """
    if not HAS_NUMPY:
        return is_iterable(value)
    return isinstance(value, np.ndarray) or is_iterable(value)
def is_ndarray(value) -> bool:
    """Evaluate whether ``value`` is a NumPy :class:`ndarray <numpy:numpy.ndarray>`.
    
    :param value: The value to evaluate.
    :type value: Any
    
    :returns: ``True`` if an array. ``False`` if not.
    :rtype: :class:`bool <python:bool>`
    """
    if value.__class__.__name__ == 'ndarray':
        return True
    classes = [x.__name__ for x in value.__class__.__mro__]
    
    return 'ndarray' in classes
def extend_columns(array, needed_members):
    """Extends ``array`` with additional positions for the number 
    of members to equal ``needed_members``. Additional positions recieve
    a value of :obj:`None <python:None>`.
    
    :param array: The array to extend
    :type array: iterable
    
    :param needed_members: the number of members the array should contain
    :type needed_members: :class:`int <python:int>`
    
    :returns: ``array`` with ``needed_members``
    :rtype: iterable
    """
    if not is_arraylike(array):
        raise errors.HighchartsValueError(f'array is expected to be an iterable. '
                                          f'Received a: {array.__class__.__name__}')
    needed_members = validators.integer(needed_members)
    original_length = len(array)
    if needed_members <= original_length:
        return array
    new_members = original_length - needed_members
    array.extend([None for x in range(new_members)])
    return array
def dict_to_ndarray(as_dict):
    """Convert ``as_dict`` to a :class:`numpy.ndarray <numpy:numpy.ndarray>`,
    with each key becoming a column.
    
    :param as_dict: :class:`dict <python:dict>` to be converted
    :type as_dict: :class:`dict <python:dict>`
    
    :returns: :class:`numpy.ndarray <numpy:numpy.ndarray>` with 1 column
      per key in ``as_dict``
    :rtype: :class:`numpy.ndarray <nunpy:numpy.ndarray>`
    
    """
    if not HAS_NUMPY:
        raise errors.HighchartsDependencyError('NumPy is required for this feature. '
                                               'It was not found in your runtime '
                                               'environment. Please make sure it is '
                                               'installed in your runtime '
                                               'environment.')
    as_dict = validators.dict(as_dict, allow_empty = True) or {}
    columns = [as_dict[key] for key in as_dict]
    as_ndarray = np.column_stack(columns)
    return as_ndarray
def datetime64_to_datetime(dt64):
    """Convert a NumPy :class:`datetime64 <numpy:numpy.datetime64>` to a Python 
    :class:`datetime <python:datetime.datetime>`.
    
    :param dt64: The NumPy :class:`datetime64 <numpy:numpy.datetime64>` to convert.
    :type dt64: :class:`numpy.datetime64 <numpy:numpy.datetime64>`
    
    :returns: A Python :class:`datetime <python:datetime.datetime>` instance.
    :rtype: :class:`datetime <python:datetime.datetime>`
    
    :raises HighchartsDependencyError: if NumPy is not available in the runtime
      environment
    """
    if not HAS_NUMPY:
        raise errors.HighchartsDependencyError('NumPy is required for this feature. '
                                               'It was not found in your runtime '
                                               'environment. Please make sure it is '
                                               'installed in your runtime '
                                               'environment.')
    timestamp = (dt64 - np.datetime64("1970-01-01T00:00:00")) / np.timedelta64(1, "s")
    
    return datetime.datetime.fromtimestamp(timestamp, datetime.timezone.utc)