Writes data into an HDF5 file (high level).

    High level function to store one or more Python types (data) to
    specified pathes in an HDF5 file. The paths are specified as POSIX
    style paths where the directory name is the Group to put it in and
    the basename is the name to write it to.

    There are various options that can be used to influence how the data
    is written. They can be passed as an already constructed ``Options``
    into `options` or as additional keywords that will be used to make
    one by ``options = Options(**keywords)``.

    Two very important options are ``store_python_metadata`` and
    ``matlab_compatible``, which are ``bool``. The first makes it so
    that enough metadata (HDF5 Attributes) are written that `data` can
    be read back accurately without it (or its contents if it is a
    container type) ending up different types, transposed in the case of
    numpy arrays, etc. The latter makes it so that the appropriate
    metadata is written, string and bool and complex types are converted
    properly, and numpy arrays are transposed; which is needed to make
    sure that MATLAB can import `data` correctly (the HDF5 header is
    also set so MATLAB will recognize it).

    Paths are POSIX style and can either be given directly as ``str`` or
    ``bytes``, or the separated path can be given as an iterable of
    ``str`` and ``bytes``. Each part of a separated path is escaped
    using ``utilities.escape_path``. Otherwise, the path is assumed to
    be already escaped. Escaping is done so that targets with a part
    that starts with one or more periods, contain slashes, and/or
    contain nulls can be used without causing the wrong Group to be
    looked in or the wrong target to be looked at. It essentially allows
    one to make a Dataset named ``'..'`` or ``'a/a'`` instead of moving
    around in the Dataset hierarchy.

    Parameters
    ----------
    mdict : dict, dict like
        The ``dict`` or other dictionary type object of paths
        and data to write to the file. The paths, the keys, must be
        POSIX style paths where the directory name is the Group to put
        it in and the basename is the name to write it to. The values
        are the data to write.
    filename : str, optional
        The name of the HDF5 file to write `data` to.
    truncate_existing : bool, optional
        Whether to truncate the file if it already exists before writing
        to it.
    truncate_invalid_matlab : bool, optional
        Whether to truncate a file if matlab_compatibility is being
        done and the file doesn't have the proper header (userblock in
        HDF5 terms) setup for MATLAB metadata to be placed.
    options : Options, optional
        The options to use when writing. Is mutually exclusive with any
        additional keyword arguments given (set to ``None`` or don't
        provide to use them).
    **keywords :
        If `options` was not provided or was ``None``, these are used as
        arguments to make a ``Options``.

    Raises
    ------
    TypeError
        If a path is of an invalid type.
    NotImplementedError
        If writing `data` is not supported.
    exceptions.TypeNotMatlabCompatibleError
        If writing a type not compatible with MATLAB and
        `options.action_for_matlab_incompatible` is set to ``'error'``.

    See Also
    --------
    utilities.process_path
    utilities.escape_path
    write : Writes just a single piece of data
    reads
    read
    Options
    utilities.write_data : Low level version

def writes(mdict, filename='data.h5', truncate_existing=False,
           truncate_invalid_matlab=False, options=None, **keywords):
    """ Writes data into an HDF5 file (high level).

    High level function to store one or more Python types (data) to
    specified pathes in an HDF5 file. The paths are specified as POSIX
    style paths where the directory name is the Group to put it in and
    the basename is the name to write it to.

    There are various options that can be used to influence how the data
    is written. They can be passed as an already constructed ``Options``
    into `options` or as additional keywords that will be used to make
    one by ``options = Options(**keywords)``.

    Two very important options are ``store_python_metadata`` and
    ``matlab_compatible``, which are ``bool``. The first makes it so
    that enough metadata (HDF5 Attributes) are written that `data` can
    be read back accurately without it (or its contents if it is a
    container type) ending up different types, transposed in the case of
    numpy arrays, etc. The latter makes it so that the appropriate
    metadata is written, string and bool and complex types are converted
    properly, and numpy arrays are transposed; which is needed to make
    sure that MATLAB can import `data` correctly (the HDF5 header is
    also set so MATLAB will recognize it).

    Paths are POSIX style and can either be given directly as ``str`` or
    ``bytes``, or the separated path can be given as an iterable of
    ``str`` and ``bytes``. Each part of a separated path is escaped
    using ``utilities.escape_path``. Otherwise, the path is assumed to
    be already escaped. Escaping is done so that targets with a part
    that starts with one or more periods, contain slashes, and/or
    contain nulls can be used without causing the wrong Group to be
    looked in or the wrong target to be looked at. It essentially allows
    one to make a Dataset named ``'..'`` or ``'a/a'`` instead of moving
    around in the Dataset hierarchy.

    Parameters
    ----------
    mdict : dict, dict like
        The ``dict`` or other dictionary type object of paths
        and data to write to the file. The paths, the keys, must be
        POSIX style paths where the directory name is the Group to put
        it in and the basename is the name to write it to. The values
        are the data to write.
    filename : str, optional
        The name of the HDF5 file to write `data` to.
    truncate_existing : bool, optional
        Whether to truncate the file if it already exists before writing
        to it.
    truncate_invalid_matlab : bool, optional
        Whether to truncate a file if matlab_compatibility is being
        done and the file doesn't have the proper header (userblock in
        HDF5 terms) setup for MATLAB metadata to be placed.
    options : Options, optional
        The options to use when writing. Is mutually exclusive with any
        additional keyword arguments given (set to ``None`` or don't
        provide to use them).
    **keywords :
        If `options` was not provided or was ``None``, these are used as
        arguments to make a ``Options``.

    Raises
    ------
    TypeError
        If a path is of an invalid type.
    NotImplementedError
        If writing `data` is not supported.
    exceptions.TypeNotMatlabCompatibleError
        If writing a type not compatible with MATLAB and
        `options.action_for_matlab_incompatible` is set to ``'error'``.

    See Also
    --------
    utilities.process_path
    utilities.escape_path
    write : Writes just a single piece of data
    reads
    read
    Options
    utilities.write_data : Low level version

    """
    # Pack the different options into an Options class if an Options was
    # not given.
    if not isinstance(options, Options):
        options = Options(**keywords)

    # Go through mdict, extract the paths and data, and process the
    # paths. A list of tulpes for each piece of data to write will be
    # constructed where he first element is the group name, the second
    # the target name (name of the Dataset/Group holding the data), and
    # the third element the data to write.
    towrite = []
    for p, v in mdict.items():
        groupname, targetname = utilities.process_path(p)

        # Pack into towrite.
        towrite.append((groupname, targetname, v))

    # Open/create the hdf5 file but don't write the data yet since the
    # userblock still needs to be set. This is all wrapped in a try
    # block, so that the file can be closed if any errors happen (the
    # error is re-raised).
    f = None
    try:

        # If the file doesn't already exist or the option is set to
        # truncate it if it does, just open it truncating whatever is
        # there. Otherwise, open it for read/write access without
        # truncating. Now, if we are doing matlab compatibility and it
        # doesn't have a big enough userblock (for metadata for MATLAB
        # to be able to tell it is a valid .mat file) and the
        # truncate_invalid_matlab is set, then it needs to be closed and
        # re-opened with truncation. Whenever we create the file from
        # scratch, even if matlab compatibility isn't being done, a
        # sufficiently sized userblock is going to be allocated
        # (smallest size is 512) for future use (after all, someone
        # might want to turn it to a .mat file later and need it and it
        # is only 512 bytes).

        if truncate_existing or not os.path.isfile(filename):
            f = h5py.File(filename, mode='w', userblock_size=512)
        else:
            f = h5py.File(filename)
            if options.matlab_compatible and truncate_invalid_matlab \
                    and f.userblock_size < 128:
                f.close()
                f = h5py.File(filename, mode='w', userblock_size=512)
    except:
        raise
    finally:
        # If the hdf5 file was opened at all, get the userblock size and
        # close it since we need to set the userblock.
        if isinstance(f, h5py.File):
            userblock_size = f.userblock_size
            f.close()
        else:
            raise IOError('Unable to create or open file.')

    # If we are doing MATLAB formatting and there is a sufficiently
    # large userblock, write the new userblock. The same sort of error
    # handling is used.
    if options.matlab_compatible and userblock_size >= 128:
        # Get the time.
        now = datetime.datetime.now()

        # Construct the leading string. The MATLAB one looks like
        #
        # s = 'MATLAB 7.3 MAT-file, Platform: GLNXA64, Created on: ' \
        #     + now.strftime('%a %b %d %H:%M:%S %Y') \
        #     + ' HDF5 schema 1.00 .'
        #
        # Platform is going to be changed to hdf5storage version.

        s = 'MATLAB 7.3 MAT-file, Platform: hdf5storage ' \
            + __version__ + ', Created on: ' \
            + now.strftime('%a %b %d %H:%M:%S %Y') \
            + ' HDF5 schema 1.00 .'

        # Make the bytearray while padding with spaces up to 128-12
        # (the minus 12 is there since the last 12 bytes are special.

        b = bytearray(s + (128-12-len(s))*' ', encoding='utf-8')

        # Add 8 nulls (0) and the magic number (or something) that
        # MATLAB uses.

        b.extend(bytearray.fromhex('00000000 00000000 0002494D'))

        # Now, write it to the beginning of the file.

        try:
            fd = open(filename, 'r+b')
            fd.write(b)
        except:
            raise
        finally:
            fd.close()

    # Open the hdf5 file again and write the data, making the Group if
    # necessary. This is all wrapped in a try block, so that the file
    # can be closed if any errors happen (the error is re-raised).
    f = None
    try:
        f = h5py.File(filename)

        # Go through each element of towrite and write them.
        for groupname, targetname, data in towrite:
            # Need to make sure groupname is a valid group in f and grab its
            # handle to pass on to the low level function.
            if groupname not in f:
                grp = f.require_group(groupname)
            else:
                grp = f[groupname]

            # Hand off to the low level function.
            utilities.write_data(f, grp, targetname, data,
                                 None, options)
    except:
        raise
    finally:
        if isinstance(f, h5py.File):
            f.close()