utils.py 23.1 KB
Newer Older
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed
# coding=utf-8
import hashlib
import shutil
import tarfile
import netCDF4
import re
import tempfile
from autosubmit.config.log import Log
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed
from earthdiagnostics.constants import Basins
    """
    Container class for miscellaneous utility methods
    """
    """An instance of Nco class ready to be used"""
    """An instance of Cdo class ready to be used"""
    @staticmethod
    def get_mask(basin):
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed
        """
        Returns a numpy array containing the mask for the given basin

        :param basin: basin to retrieve
        :type basin: Basin
        :return: mask
        :rtype: numpy.array
        """
        basin = Basins.parse(basin)
        if basin != Basins.Global:
            mask_handler = Utils.openCdf('mask_regions.nc')
            mask = mask_handler.variables[basin.fullname][:, 0, :]
            mask_handler.close()
        else:
            mask_handler = Utils.openCdf('mask.nc')
            mask = np.asfortranarray(mask_handler.variables['tmask'][0, 0, :])
            mask_handler.close()
        return mask

    @staticmethod
    def setminmax(filename, variable_list):
        """
        Sets the valid_max and valid_min values to the current max and min values on the file
        :param filename: path to file
        :type filename: str
        :param variable_list: list of variables in which valid_min and valid_max will be set
        :type variable_list: str | list
        """
        if isinstance(variable_list, basestring):
            variable_list = variable_list.split()

        Log.info('Getting max and min values for {0}', ' '.join(variable_list))
        handler = Utils.openCdf(filename)
            var = handler.variables[variable]
            values = [np.max(var), np.min(var)]
            Utils.nco.ncatted(input=filename, output=filename,
                              options='-h -a valid_max,{0},m,f,{1}'.format(variable, values[0]))
            Utils.nco.ncatted(input=filename, output=filename,
                              options='-h -a valid_min,{0},m,f,{1}'.format(variable, values[1]))
        handler.close()
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed
    @staticmethod
    def rename_variable(filepath, old_name, new_name, must_exist=True, rename_dimension=False):
        """
        Rename multiple variables from a NetCDF file
        :param filepath: path to file
        :type filepath: str
        :param old_name: variable's name to change
        :type old_name: str
        :param new_name: new name
        :type new_name: str
        :param must_exist: if True, the function will raise an exception if the variable name does not exist
        :type must_exist: bool
        :param rename_dimension: if True, also rename dimensions with the same name
        :type rename_dimension: bool
        """
        Utils.rename_variables(filepath, {old_name: new_name}, must_exist, rename_dimension)
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed
    def rename_variables(filepath, dic_names, must_exist=True, rename_dimension=False):
        """
        Rename multiple variables from a NetCDF file
        :param filepath: path to file
        :type filepath: str
        :param dic_names: dictionary containing old names as keys and new names as values
        :type dic_names: dict
        :param must_exist: if True, the function will raise an exception if the variable name does not exist
        :type must_exist: bool
        :param rename_dimension: if True, also rename dimensions with the same name
        :type rename_dimension: bool
        """
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed
        for old, new in dic_names.iteritems():
            if old == new:
                raise ValueError('{0} original name is the same as the new')
        handler = Utils.openCdf(filepath)
        original_names = set(handler.variables.keys()).union(handler.dimensions.keys())
        if not any((True for x in dic_names.keys() if x in original_names)):
            handler.close()
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed
            if must_exist:
                raise Exception("Variables {0} does not exist in file {1}".format(','.join(dic_names.keys()), filepath))
            return
        handler.close()

        temp = TempFile.get()
        shutil.copyfile(filepath, temp)

        handler = Utils.openCdf(temp)
            Utils._rename_vars_directly(dic_names, filepath, handler, must_exist, rename_dimension)
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed
        except RuntimeError:

        if not Utils.check_netcdf_file(temp):
            Log.info('First attemp to rename failed. Using secondary rename method for netCDF')
            Utils._rename_vars_by_creating_new_file(dic_names, filepath, temp)
            Log.info('Rename done')
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed

    @staticmethod
    def check_netcdf_file(filepath):
        try:
            Utils.cdo.showvar(input=filepath)
        except CDOException:
            return False
        return True

    @staticmethod
    def get_file_variables(filename):
        handler = Utils.openCdf(filename)
        variables = handler.variables.keys()
        handler.close()
        return variables

    @staticmethod
    def _rename_vars_by_creating_new_file(dic_names, filepath, temp):
        original_handler = Utils.openCdf(filepath)
        new_handler = Utils.openCdf(temp, 'w')
        for attribute in original_handler.ncattrs():
            original = getattr(original_handler, attribute)
            setattr(new_handler, attribute, Utils.convert_to_ASCII_if_possible(original))
        for dimension in original_handler.dimensions.keys():
            Utils.copy_dimension(original_handler, new_handler, dimension, new_names=dic_names)
        for variable in original_handler.variables.keys():
            Utils.copy_variable(original_handler, new_handler, variable, new_names=dic_names)
        original_handler.close()
        new_handler.close()

    # noinspection PyPep8Naming
    @staticmethod
    def convert_to_ASCII_if_possible(string, encoding='ascii'):
        if isinstance(string, basestring):
            try:
                return string.encode(encoding)
            except UnicodeEncodeError:
                if u'Bretonnière' in string:
                    string = string.replace(u'Bretonnière', 'Bretonniere')
                    return Utils.convert_to_ASCII_if_possible(string, encoding)
    @staticmethod
    def _rename_vars_directly(dic_names, filepath, handler, must_exist, rename_dimension):
        for old_name, new_name in dic_names.items():
            if rename_dimension:
                if old_name in handler.dimensions:
                    handler.renameDimension(old_name, new_name)
                elif must_exist:
                    raise Exception("Dimension {0} does not exist in file {1}".format(old_name, filepath))

            if old_name in handler.variables:
                if new_name not in handler.variables:
                    handler.renameVariable(old_name, new_name)
            elif must_exist:
                raise Exception("Variable {0} does not exist in file {1}".format(old_name, filepath))
            handler.sync()

    @staticmethod
    def copy_file(source, destiny):
        Copies a file from source to destiny, creating dirs if necessary

        :param source: path to source
        :type source: str
        :param destiny:  path to destiny
        :type destiny: str
        """
        dirname_path = os.path.dirname(destiny)
        if dirname_path and not os.path.exists(dirname_path):
            except OSError as ex:
                # This can be due to a race condition. If directory already exists, we don have to do nothing
                if not os.path.exists(dirname_path):
        hash_destiny = None
        hash_original = Utils.get_file_hash(source)

        retrials = 5
        while hash_original != hash_destiny:
            if retrials == 0:
                raise Exception('Can not move {0} to {1}'.format(source, destiny))
            shutil.copyfile(source, destiny)
            hash_destiny = Utils.get_file_hash(destiny)

    @staticmethod
    def move_file(source, destiny):
        """
        Moves a file from source to destiny, creating dirs if necessary

        :param source: path to source
        :type source: str
        :param destiny:  path to destiny
        :type destiny: str
        """
        Utils.copy_file(source, destiny)
        os.remove(source)

    @staticmethod
    def remove_file(path):
        """
        Removes a file, checking before if its exists

        :param path: path to file
        :type path: str
        """
        if os.path.isfile(path):
            os.remove(path)

    @staticmethod
    def get_file_hash(filepath):
        """
        Returns the MD5 hash for the given filepath
        :param filepath: path to the file to compute hash on
        :type filepath:str
        :return: file's MD5 hash
        :rtype: str
        """
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed
        blocksize = 65536
        hasher = hashlib.md5()
        with open(filepath, 'rb') as afile:
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed
            buf = afile.read(blocksize)
            while len(buf) > 0:
                hasher.update(buf)
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed
                buf = afile.read(blocksize)
        return hasher.hexdigest()
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed

    def execute_shell_command(command, log_level=Log.DEBUG):
        """
        Executes a sheel command
        :param command: command to execute

        Log.info('Detailed time for diagnostic class')
        :param log_level: log level to use for command output
        :type log_level: int
        :return: command output
        :rtype: list
        """
        if isinstance(command, basestring):
            command = command.split()
        process = subprocess.Popen(command, stdout=subprocess.PIPE)
        output = list()
        comunicate = process.communicate()
        for line in comunicate:
            if not line:
                continue
            if log_level != Log.NO_LOG:
                Log.log.log(log_level, line)
            raise Utils.ExecutionError('Error executing {0}\n Return code: {1}'.format(' '.join(command),
                                                                                       process.returncode))
        return output
    _cpu_count = None

    @staticmethod
    def available_cpu_count():
        """
        Number of available virtual or physical CPUs on this systemx
        """
        if Utils._cpu_count is None:
            try:
                m = re.search(r'(?m)^Cpus_allowed:\s*(.*)$',
                              open('/proc/self/status').read())
                if m:
                    res = bin(int(m.group(1).replace(',', ''), 16)).count('1')
                    if res > 0:
                        Utils._cpu_count = res
            except IOError:
                try:
                    import multiprocessing
                    Utils._cpu_count = multiprocessing.cpu_count()
                    return Utils._cpu_count
                except (ImportError, NotImplementedError):
                    Utils._cpu_count = -1
        return Utils._cpu_count

    def convert2netcdf4(filetoconvert):
        """
        Checks if a file is in netCDF4 format and converts to netCDF4 if not

        :param filetoconvert: file to convert
        :type filetoconvert: str
        """

        if Utils._is_compressed_netcdf4(filetoconvert):
            return

        Log.debug('Reformatting to netCDF-4')
        Utils.execute_shell_command(["nccopy", "-4", "-d4", "-s", filetoconvert, temp])
    @classmethod
    def _is_compressed_netcdf4(cls, filetoconvert):
        is_compressed = True
        handler = Utils.openCdf(filetoconvert)
        if not handler.file_format == 'NETCDF4':
            is_compressed = False
        else:
            ncdump_result = Utils.execute_shell_command('ncdump -hs {0}'.format(filetoconvert), Log.NO_LOG)
            ncdump_result = ncdump_result[0].replace('\t', '').split('\n')
            for var in handler.variables:
                if not '{0}:_DeflateLevel = 4 ;'.format(var) in ncdump_result:
                    is_compressed = False
                    break
                if not '{0}:_Shuffle = "true" ;'.format(var) in ncdump_result:
                    is_compressed = False
                    break

        handler.close()
        return is_compressed


# noinspection PyPep8Naming
    @staticmethod
    def openCdf(filepath, mode='a'):
        """
        Opens a netCDF file and returns a handler to it

        :param filepath: path to the file
        :type filepath: str
        :param mode: mode to open the file. By default, a (append)
        :type mode: str
        :return: handler to the file
        :rtype: netCDF4.Dataset
        """
        return netCDF4.Dataset(filepath, mode)

    @staticmethod
    def get_datetime_from_netcdf(handler, time_variable='time'):
        """
        Gets a datetime array from a netCDF file

        :param handler: file to read
        :type handler: netCDF4.Dataset
        :param time_variable: variable to read, by default 'time'
        :type time_variable: str
        :return: Datetime numpy array created from the values stored at the netCDF file
        :rtype: np.array
        """
        var_time = handler.variables[time_variable]
        nctime = var_time[:]  # get values
        units = var_time.units
            cal_temps = var_time.calendar
        except AttributeError:
            cal_temps = u"standard"
        return netCDF4.num2date(nctime, units=units, calendar=cal_temps)

    @staticmethod
    def copy_variable(source, destiny, variable, must_exist=True, add_dimensions=False, new_names=None):
        """
        Copies the given variable from source to destiny

        :param add_dimensions: if it's true, dimensions required by the variable will be automatically added to the
                               file. It will also add the dimension variable
        :type add_dimensions: bool
        :param source: origin file
        :type source: netCDF4.Dataset
        :param destiny: destiny file
        :type destiny: netCDF4.Dataset
        :param variable: variable to copy
        :type variable: str
        :param must_exist: if false, does not raise an error uf variable does not exist
        :type must_exist: bool
        :param new_names: dictionary containing variables to rename and new name as key-value pairs
        :type new_names: dict
        if not must_exist and variable not in source.variables.keys():
            return

        if not new_names:
            new_names = dict()
        if variable in new_names:
            new_name = new_names[variable]
        else:
            new_name = variable

        if new_name in destiny.variables.keys():
            return

        translated_dimensions = Utils._translate(source.variables[variable].dimensions, new_names)
        if not set(translated_dimensions).issubset(destiny.dimensions):
            if not add_dimensions:
                raise Exception('Variable {0} can not be added because dimensions does not match: '
                                '{1} {2}'.format(variable, translated_dimensions, destiny.dimensions))
            for dimension in source.variables[variable].dimensions:
                Utils.copy_dimension(source, destiny, dimension, must_exist, new_names)
            if new_name in destiny.variables.keys():
                # Just in case the variable we are copying match a dimension name
                return
        original_var = source.variables[variable]
        new_var = destiny.createVariable(new_name, original_var.datatype, translated_dimensions)
        new_var.setncatts({k: Utils.convert_to_ASCII_if_possible(original_var.getncattr(k))
                           for k in original_var.ncattrs()})
        new_var[:] = original_var[:]

    @staticmethod
    def copy_dimension(source, destiny, dimension, must_exist=True, new_names=None):
        """
        Copies the given dimension from source to destiny, including dimension variables if present

        :param new_names: dictionary containing variables to rename and new name as key-value pairs
        :type new_names: dict
        :param source: origin file
        :type source: netCDF4.Dataset
        :param destiny: destiny file
        :type destiny: netCDF4.Dataset
        :param dimension: variable to copy
        :type dimension: str
        :param must_exist: if false, does not raise an error uf variable does not exist
        :type must_exist: bool

        :return:
        """
        if not must_exist and dimension not in source.dimensions.keys():
            return
        if not new_names:
            new_names = dict()
        if dimension in new_names:
            new_name = new_names[dimension]
        else:
            new_name = dimension
        if new_name in destiny.dimensions.keys():
            return
        if not new_name:
            new_name = dimension
        destiny.createDimension(new_name, source.dimensions[dimension].size)
        if dimension in source.variables:
            Utils.copy_variable(source, destiny, dimension, new_names=new_names)

    @staticmethod
    def concat_variables(source, destiny, remove_source=False):
        """
        Add variables from a nc file to another
        :param source: path to source file
        :type source: str
        :param destiny: path to destiny file
        :type destiny: str
        :param remove_source: if True, removes source file
        :type remove_source: bool
        """
        if os.path.exists(destiny):
            handler_total = Utils.openCdf(destiny)
            handler_variable = Utils.openCdf(source)
            for var in handler_variable.variables:
                if var not in handler_total.variables:
                    Utils.copy_variable(handler_variable, handler_total, var, add_dimensions=True)
                else:
                    variable = handler_variable.variables[var]
                    if 'time' not in variable.dimensions:
                        continue
                    concatenated[var] = np.concatenate((handler_total.variables[var][:], variable[:]),
                                                       axis=variable.dimensions.index('time'))

            for var, array in concatenated.iteritems():
                handler_total.variables[var][:] = array
            handler_total.close()
            handler_variable.close()
            if remove_source:
                os.remove(source)
        else:
            if remove_source:
                Utils.move_file(source, destiny)
            else:
                shutil.copy(source, destiny)
            Utils.convert2netcdf4(destiny)
    @staticmethod
    def expand_path(path):
        """
        Expands character ~ and system variables on the given path
        :param path: path to expand
        :type path: str
        :return: path after the expansion
        """
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed
        """
        Exception to raise when a command execution fails
        """
        pass

    @classmethod
    def _translate(cls, dimensions, new_names):
        translated = list()
        for dim in dimensions:
            if dim in new_names:
                translated.append(new_names[dim])
            else:
                translated.append(dim)
        return translated

    @staticmethod
    def create_folder_tree(path):
        """
        Createas a fodle path will and parent directories if needed.
        :param path: folder's path
        :type path: str
        """
        if not os.path.exists(path):
            # This can be a race condition
            # noinspection PyBroadException
            try:
                os.makedirs(path)
            except Exception:
                pass

Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed
    def untar(files, destiny_path):
        """
        Untar files to a given destiny
        :param files: files to unzip
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed
        :param destiny_path: path to destination folder
        :type destiny_path: str
        """
        for filepath in files:
            Log.debug('Unpacking {0}', filepath)
            tar = tarfile.open(filepath)
            for file_compressed in tar.getmembers():
                if file_compressed.isdir():
                    if os.path.isdir(os.path.join(destiny_path, file_compressed.name)):
                        continue
                else:
                    if os.path.exists(os.path.join(destiny_path, file_compressed.name)):
                        os.remove(os.path.join(destiny_path, file_compressed.name))
                tar.extract(file_compressed, destiny_path)
            tar.close()

    def unzip(files, force=False):
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed
        """
        Unzip a list of files
        :param files: files to unzip
        :type files: list
        :param force: if True, it will overwrite  unzipped files
        :type force: bool
        """
        for filepath in files:
            Log.debug('Unzipping {0}', filepath)
            if force:
                option = ' -f'
            else:
                option = ''
                Utils.execute_shell_command('gunzip{1} {0}'.format(filepath, option))
            except Exception as ex:
                raise Utils.UnzipException('Can not unzip {0}: {1}'.format(filepath, ex))

    class UnzipException(Exception):
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed
        """
        Excpetion raised when unzip fails
        """
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed
class TempFile(object):
    """
    Class to manage temporal files
    """
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed

    autoclean = True
    """
    If True, new temporary files are added to the list for future cleaning
    """
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed
    files = list()
    """
    List of files to clean automatically
    """
    """
    Scratch folder to create temporary files on it
    """
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed
    prefix = 'temp'
    """
    Prefix for temporary filenames
    """
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed

    @staticmethod
    def get(filename=None, clean=None, suffix='.nc'):
        """
        Gets a new temporal filename, storing it for automated cleaning

        :param filename: if it is not none, the function will use this filename instead of a random one
        :type filename: str
        :param clean: if true, stores filename for cleaning
        :type clean: bool
        :return: path to the temporal file
        :rtype: str
        """
        if clean is None:
            clean = TempFile.autoclean

Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed
        if filename:
            path = os.path.join(TempFile.scratch_folder, filename)
        else:
            fd, path = tempfile.mkstemp(dir=TempFile.scratch_folder, prefix=TempFile.prefix, suffix=suffix)
            os.close(fd)
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed

        if clean:
            TempFile.files.append(path)

        return path

    @staticmethod
    def clean():
        """
        Removes all temporary files created with Tempfile until now
        """
Javier Vegas-Regidor's avatar
Javier Vegas-Regidor committed
        for temp_file in TempFile.files:
            if os.path.exists(temp_file):
                os.remove(temp_file)
        TempFile.files = list()