<commit_before>

from collections.abc import Iterable
import logging
import math
import numpy as np
import os
import six
import warnings

from astropy.io import fits
from astropy.table import Table
from astropy.logger import AstropyUserWarning

import stingray.utils as utils
from .utils import order_list_of_arrays, is_string
from .utils import assign_value_if_none

from .gti import get_gti_from_all_extensions, load_gtis

try:
    # Python 2
    import cPickle as pickle
except:
    # Python 3
    import pickle

_H5PY_INSTALLED = True

try:
    import h5py
except:
    _H5PY_INSTALLED = False


def rough_calibration(pis, mission):
    """Make a rough conversion betwenn PI channel and energy.

    Only works for NICER, NuSTAR, and XMM.

    Parameters
    ----------
    pis: float or array of floats
        PI channels in data
    mission: str
        Mission name

    Returns
    -------
    energies : float or array of floats
        Energy values

    Examples
    --------
    >>> rough_calibration(0, 'nustar')
    1.6
    >>> # It's case-insensitive
    >>> rough_calibration(1200, 'XMm')
    1.2
    >>> rough_calibration(10, 'asDf')
    Traceback (most recent call last):
        ...
    ValueError: Mission asdf not recognized
    >>> rough_calibration(100, 'nicer')
    1.0
    """
    if mission.lower() == "nustar":
        return pis * 0.04 + 1.6
    elif mission.lower() == "xmm":
        return pis * 0.001
    elif mission.lower() == "nicer":
        return pis * 0.01
    raise ValueError(f"Mission {mission.lower()} not recognized")


def get_file_extension(fname):
    """Get the extension from the file name.

    If g-zipped, add '.gz' to extension.

    Examples
    --------
    >>> get_file_extension('ciao.tar')
    '.tar'
    >>> get_file_extension('ciao.tar.gz')
    '.tar.gz'
    >>> get_file_extension('ciao.evt.gz')
    '.evt.gz'
    >>> get_file_extension('ciao.a.tutti.evt.gz')
    '.evt.gz'
    """
    fname_root = fname.replace('.gz', '')
    fname_root = os.path.splitext(fname_root)[0]

    return fname.replace(fname_root, '')


def high_precision_keyword_read(hdr, keyword):
    """Read FITS header keywords, also if split in two.

    In the case where the keyword is split in two, like

        MJDREF = MJDREFI + MJDREFF

    in some missions, this function returns the summed value. Otherwise, the
    content of the single keyword

    Parameters
    ----------
    hdr : dict_like
        The FITS header structure, or a dictionary

    keyword : str
        The key to read in the header

    Returns
    -------
    value : long double
        The value of the key, or ``None`` if something went wrong

    """
    try:
        value = np.longdouble(hdr[keyword])
        return value
    except:
        pass
    try:
        if len(keyword) == 8:
            keyword = keyword[:7]
        value = np.longdouble(hdr[keyword + 'I'])
        value += np.longdouble(hdr[keyword + 'F'])
        return value
    except:
        return None


def _get_additional_data(lctable, additional_columns):
    additional_data = {}
    if additional_columns is not None:
        for a in additional_columns:
            try:
                additional_data[a] = np.array(lctable.field(a))
            except:  # pragma: no cover
                if a == 'PI':
                    logging.warning('Column PI not found. Trying with PHA')
                    additional_data[a] = np.array(lctable.field('PHA'))
                else:
                    raise Exception('Column' + a + 'not found')

    return additional_data


def _get_detector_id(lctable):
    """Multi-mission detector id finder.

    Given a FITS table, look for known columns containing detector ID numbers.
    This is relevant to a few missions, like XMM, Chandra, XTE.

    Examples
    --------
    >>> from astropy.io import fits
    >>> import numpy as np
    >>> a = fits.Column(name='CCDNR', array=np.array([1, 2]), format='K')
    >>> t = fits.TableHDU.from_columns([a])
    >>> det_id1 = _get_detector_id(t.data)
    >>> a = fits.Column(name='pcuid', array=np.array([1, 2]), format='K')
    >>> t = fits.TableHDU.from_columns([a])
    >>> det_id2 = _get_detector_id(t.data)
    >>> np.all(det_id1 == det_id2)
    True
    >>> a = fits.Column(name='asdfasdf', array=np.array([1, 2]), format='K')
    >>> t = fits.TableHDU.from_columns([a])
    >>> _get_detector_id(t.data) is None
    True
    """
    for column in ["CCDNR", "ccd_id", "PCUID"]:  # XMM  # Chandra  # XTE
        for name in lctable.columns.names:
            if column.lower() == name.lower():
                return np.array(lctable.field(name), dtype=np.int)

    return None


def load_events_and_gtis(
    fits_file,
    additional_columns=None,
    gtistring="GTI,GTI0,STDGTI",
    gti_file=None,
    hduname="EVENTS",
    column="TIME",
):
    """Load event lists and GTIs from one or more files.

    Loads event list from HDU EVENTS of file fits_file, with Good Time
    intervals. Optionally, returns additional columns of data from the same
    HDU of the events.

    Parameters
    ----------
    fits_file : str

    Other parameters
    ----------------
    additional_columns: list of str, optional
        A list of keys corresponding to the additional columns to extract from
        the event HDU (ex.: ['PI', 'X'])
    gtistring : str
        Comma-separated list of accepted GTI extensions (default GTI,STDGTI),
        with or without appended integer number denoting the detector
    gti_file : str, default None
        External GTI file
    hduname : str, default 'EVENTS'
        Name of the HDU containing the event list
    return_limits: bool, optional
        Return the TSTART and TSTOP keyword values

    Returns
    -------
    retvals : Object with the following attributes:
        ev_list : array-like
            Event times in Mission Epoch Time
        gti_list: [[gti0_0, gti0_1], [gti1_0, gti1_1], ...]
            GTIs in Mission Epoch Time
        additional_data: dict
            A dictionary, where each key is the one specified in additional_colums.
            The data are an array with the values of the specified column in the
            fits file.
        t_start : float
            Start time in Mission Epoch Time
        t_stop : float
            Stop time in Mission Epoch Time
        pi_list : array-like
            Raw Instrument energy channels
        cal_pi_list : array-like
            Calibrated PI channels (those that can be easily converted to energy
            values, regardless of the instrument setup.)
        energy_list : array-like
            Energy of each photon in keV (only for NuSTAR, NICER, XMM)
        instr : str
            Name of the instrument (e.g. EPIC-pn or FPMA)
        mission : str
            Name of the instrument (e.g. XMM or NuSTAR)
        mjdref : float
            MJD reference time for the mission
        header : str
            Full header of the FITS file, for debugging purposes
        detector_id : array-like, int
            Detector id for each photon (e.g. each of the CCDs composing XMM's or
            Chandra's instruments)
    """
    from astropy.io import fits as pf

    gtistring = assign_value_if_none(gtistring, "GTI,GTI0,STDGTI")
    lchdulist = pf.open(fits_file)

    # Load data table
    try:
        lctable = lchdulist[hduname].data
    except Exception:  # pragma: no cover
        warnings.warn("HDU %s not found. Trying first extension" % hduname)
        lctable = lchdulist[1].data
        hduname = 1

    # Read event list
    ev_list = np.array(lctable.field(column), dtype=np.longdouble)
    detector_id = _get_detector_id(lctable)
    det_number = None if detector_id is None else list(set(detector_id))
    header = lchdulist[1].header
    # Read TIMEZERO keyword and apply it to events
    try:
        timezero = np.longdouble(header["TIMEZERO"])
    except Exception:  # pragma: no cover
        timezero = np.longdouble(0.0)

    instr = mission = 'unknown'
    if "INSTRUME" in header:
        instr = header["INSTRUME"].lower()
    if "TELESCOP" in header:
        mission = header["TELESCOP"].strip().lower()

    ev_list += timezero

    # Read TSTART, TSTOP from header
    try:
        t_start = np.longdouble(header["TSTART"])
        t_stop = np.longdouble(header["TSTOP"])
    except Exception:  # pragma: no cover
        warnings.warn(
            "Tstart and Tstop error. using defaults", AstropyUserWarning
        )
        t_start = ev_list[0]
        t_stop = ev_list[-1]

    mjdref = np.longdouble(high_precision_keyword_read(header, "MJDREF"))

    # Read and handle GTI extension
    accepted_gtistrings = gtistring.split(",")

    if gti_file is None:
        # Select first GTI with accepted name
        try:
            gti_list = get_gti_from_all_extensions(
                lchdulist,
                accepted_gtistrings=accepted_gtistrings,
                det_numbers=det_number,
            )
        except Exception:  # pragma: no cover
            warnings.warn(
                "No extensions found with a valid name. "
                "Please check the `accepted_gtistrings` values.",
                AstropyUserWarning,
            )
            gti_list = np.array([[t_start, t_stop]], dtype=np.longdouble)
    else:
        gti_list = load_gtis(gti_file, gtistring)

    if additional_columns is None:
        additional_columns = ["PI"]
    if "PI" not in additional_columns:
        additional_columns.append("PI")
    if "PHA" not in additional_columns and mission.lower() in ["swift", "xmm"]:
        additional_columns.append("PHA")

    additional_data = _get_additional_data(lctable, additional_columns)

    lchdulist.close()

    # Sort event list
    order = np.argsort(ev_list)
    ev_list = ev_list[order]
    if detector_id is not None:
        detector_id = detector_id[order]

    additional_data = order_list_of_arrays(additional_data, order)

    pi = additional_data["PI"].astype(np.float32)
    cal_pi = pi

    if mission.lower() in ["xmm", "swift"]:
        pi = additional_data["PHA"].astype(np.float32)
        cal_pi = additional_data["PI"]
        # additional_data.pop("PHA")

    # additional_data.pop("PI")
    # EventReadOutput() is an empty class. We will assign a number of attributes to
    # it, like the arrival times of photons, the energies, and some information
    # from the header.
    returns = EventReadOutput()

    returns.ev_list = ev_list
    returns.gti_list = gti_list
    returns.pi_list = pi
    returns.cal_pi_list = cal_pi
    try:
        returns.energy_list = rough_calibration(cal_pi, mission)
    except ValueError:
        returns.energy_list = None
    returns.instr = instr
    returns.mission = mission
    returns.mjdref = mjdref
    returns.header = header.tostring()
    returns.additional_data = additional_data
    returns.t_start = t_start
    returns.t_stop = t_stop
    returns.detector_id = detector_id

    return returns


class EventReadOutput():
    def __init__(self):
        pass


def mkdir_p(path):  # pragma: no cover
    """Safe ``mkdir`` function, found at [so-mkdir]_.

    Parameters
    ----------
    path : str
        The absolute path to the directory to be created

    Notes
    -----
    .. [so-mkdir] http://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python
    """
    import os
    import errno
    try:
        os.makedirs(path)
    except OSError as exc:  # Python >2.5
        if exc.errno == errno.EEXIST and os.path.isdir(path):
            pass
        else:
            raise


def read_header_key(fits_file, key, hdu=1):
    """Read the header key key from HDU hdu of the file ``fits_file``.

    Parameters
    ----------
    fits_file: str
        The file name and absolute path to the event file.

    key: str
        The keyword to be read

    Other Parameters
    ----------------
    hdu : int
        Index of the HDU extension from which the header key to be read.

    Returns
    -------
    value : object
        The value stored under ``key`` in ``fits_file``
    """

    hdulist = fits.open(fits_file, ignore_missing_end=True)
    try:
        value = hdulist[hdu].header[key]
    except:  # pragma: no cover
        value = ''
    hdulist.close()
    return value


def ref_mjd(fits_file, hdu=1):
    """Read ``MJDREFF``, ``MJDREFI`` or, if failed, ``MJDREF``, from the FITS header.

    Parameters
    ----------
    fits_file : str
        The file name and absolute path to the event file.

    Other Parameters
    ----------------
    hdu : int
        Index of the HDU extension from which the header key to be read.

    Returns
    -------
    mjdref : numpy.longdouble
        the reference MJD
    """

    if isinstance(fits_file, Iterable) and\
            not is_string(fits_file):  # pragma: no cover
        fits_file = fits_file[0]
        logging.info("opening %s" % fits_file)

    hdulist = fits.open(fits_file, ignore_missing_end=True)

    ref_mjd_val = high_precision_keyword_read(hdulist[hdu].header, "MJDREF")

    hdulist.close()
    return ref_mjd_val


def common_name(str1, str2, default='common'):
    """Strip two strings of the letters not in common.

    Filenames must be of same length and only differ by a few letters.

    Parameters
    ----------
    str1 : str
    str2 : str

    Other Parameters
    ----------------
    default : str
        The string to return if ``common_str`` is empty

    Returns
    -------
    common_str : str
        A string containing the parts of the two names in common

    """
    if not len(str1) == len(str2):
        return default
    common_str = ''
    # Extract the MP root of the name (in case they're event files)

    for i, letter in enumerate(str1):
        if str2[i] == letter:
            common_str += letter
    # Remove leading and trailing underscores and dashes
    common_str = common_str.rstrip('_').rstrip('-')
    common_str = common_str.lstrip('_').lstrip('-')
    if common_str == '':
        common_str = default
    logging.debug('common_name: %s %s -> %s' % (str1, str2, common_str))
    return common_str


def split_numbers(number, shift=0):
    """
    Split high precision number(s) into doubles. You can specify the number of shifts to move the decimal point.

    Parameters
    ----------
    number: long double
        The input high precision number which is to be split

    Other parameters
    ----------------
    shift: integer
        Number of places to move the decimal point to the right

    Returns
    -------
    number_I: double
        First part of high precision number

    number_F: double
        Second part of high precision number

    Examples
    --------
    n = 12.34
    >>> split_numbers(n)
    (12.0, 0.33999999999999986)
    >>> split_numbers(n, 2)
    (1234.0, 0.0)
    >>> split_numbers(n, -2)
    (0.0, 0.1234)
    """
    if isinstance(number, Iterable):
        mods = [math.modf(n) for n in number]
        number_F = [f for f, _ in mods]
        number_I = [i for _, i in mods]
    else:
        number *= 10**shift
        number_F, number_I = math.modf(number)

    return np.double(number_I), np.double(number_F)


def _save_pickle_object(object, filename):
    """
    Save a class object in pickle format.
<commit_msg>Fix split numbers with wanted behavior; solve conflicts<commit_after>

from collections.abc import Iterable
import logging
import math
import numpy as np
import os
import six
import warnings

from astropy.io import fits
from astropy.table import Table
from astropy.logger import AstropyUserWarning

import stingray.utils as utils
from .utils import order_list_of_arrays, is_string
from .utils import assign_value_if_none

from .gti import get_gti_from_all_extensions, load_gtis

try:
    # Python 2
    import cPickle as pickle
except:
    # Python 3
    import pickle

_H5PY_INSTALLED = True

try:
    import h5py
except:
    _H5PY_INSTALLED = False


def rough_calibration(pis, mission):
    """Make a rough conversion betwenn PI channel and energy.

    Only works for NICER, NuSTAR, and XMM.

    Parameters
    ----------
    pis: float or array of floats
        PI channels in data
    mission: str
        Mission name

    Returns
    -------
    energies : float or array of floats
        Energy values

    Examples
    --------
    >>> rough_calibration(0, 'nustar')
    1.6
    >>> # It's case-insensitive
    >>> rough_calibration(1200, 'XMm')
    1.2
    >>> rough_calibration(10, 'asDf')
    Traceback (most recent call last):
        ...
    ValueError: Mission asdf not recognized
    >>> rough_calibration(100, 'nicer')
    1.0
    """
    if mission.lower() == "nustar":
        return pis * 0.04 + 1.6
    elif mission.lower() == "xmm":
        return pis * 0.001
    elif mission.lower() == "nicer":
        return pis * 0.01
    raise ValueError(f"Mission {mission.lower()} not recognized")


def get_file_extension(fname):
    """Get the extension from the file name.

    If g-zipped, add '.gz' to extension.

    Examples
    --------
    >>> get_file_extension('ciao.tar')
    '.tar'
    >>> get_file_extension('ciao.tar.gz')
    '.tar.gz'
    >>> get_file_extension('ciao.evt.gz')
    '.evt.gz'
    >>> get_file_extension('ciao.a.tutti.evt.gz')
    '.evt.gz'
    """
    fname_root = fname.replace('.gz', '')
    fname_root = os.path.splitext(fname_root)[0]

    return fname.replace(fname_root, '')


def high_precision_keyword_read(hdr, keyword):
    """Read FITS header keywords, also if split in two.

    In the case where the keyword is split in two, like

        MJDREF = MJDREFI + MJDREFF

    in some missions, this function returns the summed value. Otherwise, the
    content of the single keyword

    Parameters
    ----------
    hdr : dict_like
        The FITS header structure, or a dictionary

    keyword : str
        The key to read in the header

    Returns
    -------
    value : long double
        The value of the key, or ``None`` if something went wrong

    """
    try:
        value = np.longdouble(hdr[keyword])
        return value
    except:
        pass
    try:
        if len(keyword) == 8:
            keyword = keyword[:7]
        value = np.longdouble(hdr[keyword + 'I'])
        value += np.longdouble(hdr[keyword + 'F'])
        return value
    except:
        return None


def _get_additional_data(lctable, additional_columns):
    additional_data = {}
    if additional_columns is not None:
        for a in additional_columns:
            try:
                additional_data[a] = np.array(lctable.field(a))
            except:  # pragma: no cover
                if a == 'PI':
                    logging.warning('Column PI not found. Trying with PHA')
                    additional_data[a] = np.array(lctable.field('PHA'))
                else:
                    raise Exception('Column' + a + 'not found')

    return additional_data


def _get_detector_id(lctable):
    """Multi-mission detector id finder.

    Given a FITS table, look for known columns containing detector ID numbers.
    This is relevant to a few missions, like XMM, Chandra, XTE.

    Examples
    --------
    >>> from astropy.io import fits
    >>> import numpy as np
    >>> a = fits.Column(name='CCDNR', array=np.array([1, 2]), format='K')
    >>> t = fits.TableHDU.from_columns([a])
    >>> det_id1 = _get_detector_id(t.data)
    >>> a = fits.Column(name='pcuid', array=np.array([1, 2]), format='K')
    >>> t = fits.TableHDU.from_columns([a])
    >>> det_id2 = _get_detector_id(t.data)
    >>> np.all(det_id1 == det_id2)
    True
    >>> a = fits.Column(name='asdfasdf', array=np.array([1, 2]), format='K')
    >>> t = fits.TableHDU.from_columns([a])
    >>> _get_detector_id(t.data) is None
    True
    """
    for column in ["CCDNR", "ccd_id", "PCUID"]:  # XMM  # Chandra  # XTE
        for name in lctable.columns.names:
            if column.lower() == name.lower():
                return np.array(lctable.field(name), dtype=np.int)

    return None


def load_events_and_gtis(
    fits_file,
    additional_columns=None,
    gtistring="GTI,GTI0,STDGTI",
    gti_file=None,
    hduname="EVENTS",
    column="TIME",
):
    """Load event lists and GTIs from one or more files.

    Loads event list from HDU EVENTS of file fits_file, with Good Time
    intervals. Optionally, returns additional columns of data from the same
    HDU of the events.

    Parameters
    ----------
    fits_file : str

    Other parameters
    ----------------
    additional_columns: list of str, optional
        A list of keys corresponding to the additional columns to extract from
        the event HDU (ex.: ['PI', 'X'])
    gtistring : str
        Comma-separated list of accepted GTI extensions (default GTI,STDGTI),
        with or without appended integer number denoting the detector
    gti_file : str, default None
        External GTI file
    hduname : str, default 'EVENTS'
        Name of the HDU containing the event list
    return_limits: bool, optional
        Return the TSTART and TSTOP keyword values

    Returns
    -------
    retvals : Object with the following attributes:
        ev_list : array-like
            Event times in Mission Epoch Time
        gti_list: [[gti0_0, gti0_1], [gti1_0, gti1_1], ...]
            GTIs in Mission Epoch Time
        additional_data: dict
            A dictionary, where each key is the one specified in additional_colums.
            The data are an array with the values of the specified column in the
            fits file.
        t_start : float
            Start time in Mission Epoch Time
        t_stop : float
            Stop time in Mission Epoch Time
        pi_list : array-like
            Raw Instrument energy channels
        cal_pi_list : array-like
            Calibrated PI channels (those that can be easily converted to energy
            values, regardless of the instrument setup.)
        energy_list : array-like
            Energy of each photon in keV (only for NuSTAR, NICER, XMM)
        instr : str
            Name of the instrument (e.g. EPIC-pn or FPMA)
        mission : str
            Name of the instrument (e.g. XMM or NuSTAR)
        mjdref : float
            MJD reference time for the mission
        header : str
            Full header of the FITS file, for debugging purposes
        detector_id : array-like, int
            Detector id for each photon (e.g. each of the CCDs composing XMM's or
            Chandra's instruments)
    """
    from astropy.io import fits as pf

    gtistring = assign_value_if_none(gtistring, "GTI,GTI0,STDGTI")
    lchdulist = pf.open(fits_file)

    # Load data table
    try:
        lctable = lchdulist[hduname].data
    except Exception:  # pragma: no cover
        warnings.warn("HDU %s not found. Trying first extension" % hduname)
        lctable = lchdulist[1].data
        hduname = 1

    # Read event list
    ev_list = np.array(lctable.field(column), dtype=np.longdouble)
    detector_id = _get_detector_id(lctable)
    det_number = None if detector_id is None else list(set(detector_id))
    header = lchdulist[1].header
    # Read TIMEZERO keyword and apply it to events
    try:
        timezero = np.longdouble(header["TIMEZERO"])
    except Exception:  # pragma: no cover
        timezero = np.longdouble(0.0)

    instr = mission = 'unknown'
    if "INSTRUME" in header:
        instr = header["INSTRUME"].lower()
    if "TELESCOP" in header:
        mission = header["TELESCOP"].strip().lower()

    ev_list += timezero

    # Read TSTART, TSTOP from header
    try:
        t_start = np.longdouble(header["TSTART"])
        t_stop = np.longdouble(header["TSTOP"])
    except Exception:  # pragma: no cover
        warnings.warn(
            "Tstart and Tstop error. using defaults", AstropyUserWarning
        )
        t_start = ev_list[0]
        t_stop = ev_list[-1]

    mjdref = np.longdouble(high_precision_keyword_read(header, "MJDREF"))

    # Read and handle GTI extension
    accepted_gtistrings = gtistring.split(",")

    if gti_file is None:
        # Select first GTI with accepted name
        try:
            gti_list = get_gti_from_all_extensions(
                lchdulist,
                accepted_gtistrings=accepted_gtistrings,
                det_numbers=det_number,
            )
        except Exception:  # pragma: no cover
            warnings.warn(
                "No extensions found with a valid name. "
                "Please check the `accepted_gtistrings` values.",
                AstropyUserWarning,
            )
            gti_list = np.array([[t_start, t_stop]], dtype=np.longdouble)
    else:
        gti_list = load_gtis(gti_file, gtistring)

    if additional_columns is None:
        additional_columns = ["PI"]
    if "PI" not in additional_columns:
        additional_columns.append("PI")
    if "PHA" not in additional_columns and mission.lower() in ["swift", "xmm"]:
        additional_columns.append("PHA")

    additional_data = _get_additional_data(lctable, additional_columns)

    lchdulist.close()

    # Sort event list
    order = np.argsort(ev_list)
    ev_list = ev_list[order]
    if detector_id is not None:
        detector_id = detector_id[order]

    additional_data = order_list_of_arrays(additional_data, order)

    pi = additional_data["PI"].astype(np.float32)
    cal_pi = pi

    if mission.lower() in ["xmm", "swift"]:
        pi = additional_data["PHA"].astype(np.float32)
        cal_pi = additional_data["PI"]
        # additional_data.pop("PHA")

    # additional_data.pop("PI")
    # EventReadOutput() is an empty class. We will assign a number of attributes to
    # it, like the arrival times of photons, the energies, and some information
    # from the header.
    returns = EventReadOutput()

    returns.ev_list = ev_list
    returns.gti_list = gti_list
    returns.pi_list = pi
    returns.cal_pi_list = cal_pi
    try:
        returns.energy_list = rough_calibration(cal_pi, mission)
    except ValueError:
        returns.energy_list = None
    returns.instr = instr
    returns.mission = mission
    returns.mjdref = mjdref
    returns.header = header.tostring()
    returns.additional_data = additional_data
    returns.t_start = t_start
    returns.t_stop = t_stop
    returns.detector_id = detector_id

    return returns


class EventReadOutput():
    def __init__(self):
        pass


def mkdir_p(path):  # pragma: no cover
    """Safe ``mkdir`` function, found at [so-mkdir]_.

    Parameters
    ----------
    path : str
        The absolute path to the directory to be created

    Notes
    -----
    .. [so-mkdir] http://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python
    """
    import os
    import errno
    try:
        os.makedirs(path)
    except OSError as exc:  # Python >2.5
        if exc.errno == errno.EEXIST and os.path.isdir(path):
            pass
        else:
            raise


def read_header_key(fits_file, key, hdu=1):
    """Read the header key key from HDU hdu of the file ``fits_file``.

    Parameters
    ----------
    fits_file: str
        The file name and absolute path to the event file.

    key: str
        The keyword to be read

    Other Parameters
    ----------------
    hdu : int
        Index of the HDU extension from which the header key to be read.

    Returns
    -------
    value : object
        The value stored under ``key`` in ``fits_file``
    """

    hdulist = fits.open(fits_file, ignore_missing_end=True)
    try:
        value = hdulist[hdu].header[key]
    except:  # pragma: no cover
        value = ''
    hdulist.close()
    return value


def ref_mjd(fits_file, hdu=1):
    """Read ``MJDREFF``, ``MJDREFI`` or, if failed, ``MJDREF``, from the FITS header.

    Parameters
    ----------
    fits_file : str
        The file name and absolute path to the event file.

    Other Parameters
    ----------------
    hdu : int
        Index of the HDU extension from which the header key to be read.

    Returns
    -------
    mjdref : numpy.longdouble
        the reference MJD
    """

    if isinstance(fits_file, Iterable) and\
            not is_string(fits_file):  # pragma: no cover
        fits_file = fits_file[0]
        logging.info("opening %s" % fits_file)

    hdulist = fits.open(fits_file, ignore_missing_end=True)

    ref_mjd_val = high_precision_keyword_read(hdulist[hdu].header, "MJDREF")

    hdulist.close()
    return ref_mjd_val


def common_name(str1, str2, default='common'):
    """Strip two strings of the letters not in common.

    Filenames must be of same length and only differ by a few letters.

    Parameters
    ----------
    str1 : str
    str2 : str

    Other Parameters
    ----------------
    default : str
        The string to return if ``common_str`` is empty

    Returns
    -------
    common_str : str
        A string containing the parts of the two names in common

    """
    if not len(str1) == len(str2):
        return default
    common_str = ''
    # Extract the MP root of the name (in case they're event files)

    for i, letter in enumerate(str1):
        if str2[i] == letter:
            common_str += letter
    # Remove leading and trailing underscores and dashes
    common_str = common_str.rstrip('_').rstrip('-')
    common_str = common_str.lstrip('_').lstrip('-')
    if common_str == '':
        common_str = default
    logging.debug('common_name: %s %s -> %s' % (str1, str2, common_str))
    return common_str


def split_numbers(number, shift=0):
    """
    Split high precision number(s) into doubles.

    You can specify the number of shifts to move the decimal point.

    Parameters
    ----------
    number: long double
        The input high precision number which is to be split

    Other parameters
    ----------------
    shift: integer
        Move the cut by `shift` decimal points to the right (left if negative)

    Returns
    -------
    number_I: double
        First part of high precision number

    number_F: double
        Second part of high precision number

    Examples
    --------
    >>> n = 12.34
    >>> i, f = split_numbers(n)
    >>> i == 12
    True
    >>> np.isclose(f, 0.34)
    True
    >>> split_numbers(n, 2)
    (12.34, 0.0)
    >>> split_numbers(n, -1)
    (10.0, 2.34)
    """
    if isinstance(number, Iterable):
        mods = [math.modf(n) for n in number]
        number_F = [f for f, _ in mods]
        number_I = [i for _, i in mods]
    else:
        number *= 10**shift
        number_F, number_I = math.modf(number)

    return np.double(number_I) / 10**shift, np.double(number_F) / 10**shift


def _save_pickle_object(object, filename):
    """
    Save a class object in pickle format.

{'fraction_non_alphanumeric': 0.10628113705187067, 'fraction_numerical': 0.008140405717820976, 'mean_word_length': 2.244453834777097, 'pattern_counts': {'":': 6, '<': 3, '<?xml version=': 0, '>': 160, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 1, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5725355', 'n_tokens_mistral': 9729, 'n_tokens_neox': 9285, 'n_words': 2873}