Archive one or more experiments or a project instance

        This method may be used to archive experiments in order to minimize the
        amount of necessary configuration files

        Parameters
        ----------
        odir: str
            The path where to store the archive
        aname: str
            The name of the archive (minus any format-specific extension). If
            None, defaults to the projectname
        fmt: { 'gztar' | 'bztar' | 'tar' | 'zip' }
            The format of the archive. If None, it is tested whether an
            archived with the name specified by `aname` already exists and if
            yes, the format is inferred, otherwise ``'tar'`` is used
        projectname: str
            If provided, the entire project is archived
        experiments: str
            If provided, the given experiments are archived. Note that an error
            is raised if they belong to multiple project instances
        current_project: bool
            If True, `projectname` is set to the current project
        no_append: bool
            It True and the archive already exists, it is deleted
        no_project_paths: bool
            If True, paths outside the experiment directories are neglected
        exclude: list of str
            Filename patterns to ignore (see :func:`glob.fnmatch.fnmatch`)
        keep_exp: bool
            If True, the experiment directories are not removed and no
            modification is made in the configuration
        rm_project: bool
            If True, remove all the project files
        dry_run: bool
            If True, set, do not actually make anything
        dereference: bool
            If set, dereference symbolic links. Note: This is automatically set
            for ``fmt=='zip'``

def archive(self, odir=None, aname=None, fmt=None, projectname=None,
                experiments=None, current_project=False, no_append=False,
                no_project_paths=False, exclude=None, keep_exp=False,
                rm_project=False, dry_run=False, dereference=False, **kwargs):
        """
        Archive one or more experiments or a project instance

        This method may be used to archive experiments in order to minimize the
        amount of necessary configuration files

        Parameters
        ----------
        odir: str
            The path where to store the archive
        aname: str
            The name of the archive (minus any format-specific extension). If
            None, defaults to the projectname
        fmt: { 'gztar' | 'bztar' | 'tar' | 'zip' }
            The format of the archive. If None, it is tested whether an
            archived with the name specified by `aname` already exists and if
            yes, the format is inferred, otherwise ``'tar'`` is used
        projectname: str
            If provided, the entire project is archived
        experiments: str
            If provided, the given experiments are archived. Note that an error
            is raised if they belong to multiple project instances
        current_project: bool
            If True, `projectname` is set to the current project
        no_append: bool
            It True and the archive already exists, it is deleted
        no_project_paths: bool
            If True, paths outside the experiment directories are neglected
        exclude: list of str
            Filename patterns to ignore (see :func:`glob.fnmatch.fnmatch`)
        keep_exp: bool
            If True, the experiment directories are not removed and no
            modification is made in the configuration
        rm_project: bool
            If True, remove all the project files
        dry_run: bool
            If True, set, do not actually make anything
        dereference: bool
            If set, dereference symbolic links. Note: This is automatically set
            for ``fmt=='zip'``
        """
        fnmatch = glob.fnmatch.fnmatch

        def to_exclude(fname):
            if exclude and (fnmatch(exclude, fname) or
                            fnmatch(exclude, osp.basename(fname))):
                return True

        def do_nothing(path, file_obj):
            return

        def tar_add(path, file_obj):
            if sys.version_info[:2] < (3, 7):
                file_obj.add(path, self.relpath(path), exclude=to_exclude)
            else:
                file_obj.add(path, self.relpath(path),
                             filter=lambda f: None if to_exclude(f) else f)

        def zip_add(path, file_obj):
            # ziph is zipfile handle
            for root, dirs, files in os.walk(path):
                for f in files:
                    abs_file = os.path.join(root, f)
                    if not to_exclude(abs_file):
                        file_obj.write(abs_file, self.relpath(abs_file))

        self.app_main(**kwargs)
        logger = self.logger
        all_exps = self.config.experiments
        if current_project or projectname is not None:
            if current_project:
                projectname = self.projectname
            experiments = list(
                self.config.experiments.project_map[projectname])
            if not experiments:
                raise ValueError(
                    "Could not find any unarchived experiment for %s" % (
                        projectname))
        elif experiments is None:
            experiments = [self.experiment]
        already_archived = list(filter(self.is_archived, experiments))
        if already_archived:
            raise ValueError(
                "The experiments %s have already been archived or are not "
                "existent!" % ', '.join(
                    already_archived))
        if projectname is None:
            projectnames = {all_exps[exp]['project'] for exp in experiments}
            if len(projectnames) > 1:
                raise ValueError(
                    "Experiments belong to multiple projects: %s" % (
                        ', '.join(projectnames)))
            projectname = next(iter(projectnames))

        self.projectname = projectname
        self.experiment = experiments[-1]

        exps2archive = OrderedDict(
            (exp, all_exps[exp]) for exp in experiments)
        project_config = self.config.projects[projectname]

        ext_map, fmt_map = self._archive_extensions()
        if aname is None:
            aname = projectname
        if fmt is None:
            ext, fmt = next(
                (t for t in fmt_map.items() if osp.exists(aname + t[0])),
                ['.tar', 'tar'])
        else:
            ext = fmt_map[fmt]
        if odir is None:
            odir = getcwd()
        archive_name = osp.join(odir, aname + ext)
        exists = osp.exists(archive_name)
        if exists and no_append:
            logger.debug('Removing existing archive %s' % archive_name)
            os.remove(archive_name)
            exists = False
        elif exists and fmt not in ['tar', 'zip']:
            raise ValueError(
                "'Cannot append to %s because this is only possible for 'tar' "
                "and 'zip' extension. Not %s" % (archive_name, fmt))
        logger.info('Archiving to %s', archive_name)

        paths = self._get_all_paths(exps2archive)
        root_dir = self.config.projects[projectname]['root']
        check_path = partial(utils.dir_contains, root_dir)
        not_included = OrderedDict([
            (key, list(filterfalse(check_path, utils.safe_list(val))))
            for key, val in paths.items()])

        for key, key_paths in not_included.items():
            for p in key_paths:
                logger.warn(
                    '%s for key %s lies outside the project directory and '
                    'will not be included in the archive!', p, key)
        modes = {'bztar': 'w:bz2', 'gztar': 'w:gz', 'tar': 'w', 'zip': 'w'}
        mode = 'a' if exists else modes[fmt]
        atype = 'zip' if fmt == 'zip' else 'tar'
        if dry_run:
            add_dir = do_nothing
            file_obj = None
        elif atype == 'zip':
            import zipfile
            add_dir = zip_add
            file_obj = zipfile.ZipFile(archive_name, mode)
        else:
            import tarfile
            add_dir = tar_add
            file_obj = tarfile.open(archive_name, mode,
                                    dereference=dereference)
        for exp in experiments:
            exp_dir = exps2archive[exp]['expdir']
            logger.debug('Adding %s', exp_dir)
            add_dir(exp_dir, file_obj)

        now = str(dt.datetime.now())  # current time

        # configuration directory
        config_dir = osp.join(root_dir, '.project')
        if not dry_run and not osp.exists(config_dir):
            os.makedirs(config_dir)
        for exp in experiments:
            conf_file = osp.join(config_dir, exp + '.yml')
            logger.debug('Store %s experiment config to %s', exp, conf_file)
            if not dry_run:
                exps2archive[exp].setdefault('timestamps', {})
                exps2archive[exp]['timestamps']['archive'] = now
                with open(osp.join(config_dir, exp + '.yml'), 'w') as f:
                    ordered_yaml_dump(self.rel_paths(
                        copy.deepcopy(exps2archive[exp])), f)
        # project configuration file
        conf_file = osp.join(config_dir, '.project.yml')
        logger.debug('Store %s project config to %s', projectname, conf_file)
        if not dry_run:
            safe_dump(project_config, conf_file)

        logger.debug('Add %s to archive', config_dir)
        add_dir(config_dir, file_obj)

        if not no_project_paths:
            for dirname in os.listdir(root_dir):
                if osp.basename(dirname) not in ['experiments', '.project']:
                    logger.debug('Adding %s', osp.join(root_dir, dirname))
                    add_dir(osp.join(root_dir, dirname), file_obj)
        if not keep_exp:
            for exp in experiments:
                exp_dir = exps2archive[exp]['expdir']
                logger.debug('Removing %s', exp_dir)
                if not dry_run:
                    all_exps[exp] = a = Archive(archive_name)
                    a.project = projectname
                    a.time = now
                    shutil.rmtree(exp_dir)
        if rm_project:
            logger.debug('Removing %s', root_dir)
            if not dry_run:
                shutil.rmtree(root_dir)
        if not dry_run:
            file_obj.close()