From 6d7af13499add7a23eabd896b5e803d00ea8966b Mon Sep 17 00:00:00 2001 From: Demian Wassermann Date: Mon, 19 May 2014 14:10:14 +0200 Subject: [PATCH 1/9] ENH: Added a datagrabber capable of grabbing files from an SSH access --- nipype/interfaces/io.py | 274 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 273 insertions(+), 1 deletion(-) diff --git a/nipype/interfaces/io.py b/nipype/interfaces/io.py index 7f14b0e796..03497a1c77 100644 --- a/nipype/interfaces/io.py +++ b/nipype/interfaces/io.py @@ -18,10 +18,12 @@ """ import glob +import fnmatch import string import os import os.path as op import shutil +import subprocess import re import tempfile from warnings import warn @@ -34,6 +36,11 @@ except: pass +try: + import paramiko +except: + pass + from nipype.interfaces.base import (TraitedSpec, traits, File, Directory, BaseInterface, InputMultiPath, isdefined, OutputMultiPath, DynamicTraitedSpec, @@ -750,7 +757,7 @@ class DataFinder(IOBase): '013-ep2d_fid_T1_pre'] >>> print result.outputs.basename # doctest: +SKIP ['acquisition', - 'acquisition', + 'acquisition' 'acquisition', 'acquisition'] @@ -1539,3 +1546,268 @@ def _list_outputs(self): conn.commit() c.close() return None + +class SSHDataGrabberInputSpec(DynamicTraitedSpec, BaseInterfaceInputSpec): + hostname = traits.Str(mandatory=True, + desc='Server hostname.') + download_files = traits.Bool(True, usedefault=True, + desc='If false it will return the file names without downloading them') + base_directory = traits.Str(mandatory=True, + desc='Path to the base directory consisting of subject data.') + raise_on_empty = traits.Bool(True, usedefault=True, + desc='Generate exception if list is empty for a given field') + sort_filelist = traits.Bool(mandatory=True, + desc='Sort the filelist that matches the template') + template = traits.Str(mandatory=True, + desc='Layout used to get files. relative to base directory if defined') + template_args = traits.Dict(key_trait=traits.Str, + value_trait=traits.List(traits.List), + desc='Information to plug into template') + template_expression = traits.Enum(['fnmatch', 'regexp'], usedefault=True, + desc='Use either fnmatch or regexp to express templates') + + ssh_log_to_file = traits.Str('', usedefault=True, + desc='If set SSH commands will be logged to the given file') + + +class SSHDataGrabber(IOBase): + """ Datagrabber module that downloads the file list and optionally + the files from a SSH server. The SSH operation must not need + user and password so an SSH agent must be active in where this + module is being run. + + + .. attention:: + + Doesn't support directories currently + + Examples + -------- + + >>> from nipype.interfaces.io import SSHDataGrabber + >>> dg = SSHDataGrabber() + >>> dg.inputs.hostname = 'myhost.com' + >>> dg.inputs.base_directory = '/main_folder/my_remote_dir' + + Pick all files from the base directory + + >>> dg.inputs.template = '*' + + Pick all files starting with "s" and a number from current directory + + >>> dg.inputs.template_expression = 'regexp' + >>> dg.inputs.template = 's[0-9].*' + + Same thing but with dynamically created fields + + >>> dg = SSHDataGrabber(infields=['arg1','arg2']) + >>> dg.inputs.hostname = 'myhost.com' + >>> dg.inputs.base_directory = '~/my_remote_dir' + >>> dg.inputs.template = '%s/%s.nii' + >>> dg.inputs.arg1 = 'foo' + >>> dg.inputs.arg2 = 'foo' + + however this latter form can be used with iterables and iterfield in a + pipeline. + + Dynamically created, user-defined input and output fields + + >>> dg = SSHDataGrabber(infields=['sid'], outfields=['func','struct','ref']) + >>> dg.inputs.hostname = 'myhost.com' + >>> dg.inputs.base_directory = '/main_folder/my_remote_dir' + >>> dg.inputs.template_args['func'] = [['sid',['f3','f5']]] + >>> dg.inputs.template_args['struct'] = [['sid',['struct']]] + >>> dg.inputs.template_args['ref'] = [['sid','ref']] + >>> dg.inputs.sid = 's1' + + Change the template only for output field struct. The rest use the + general template + + >>> dg.inputs.field_template = dict(struct='%s/struct.nii') + >>> dg.inputs.template_args['struct'] = [['sid']] + + """ + input_spec = SSHDataGrabberInputSpec + output_spec = DynamicTraitedSpec + _always_run = True + + def __init__(self, infields=None, outfields=None, **kwargs): + """ + Parameters + ---------- + infields : list of str + Indicates the input fields to be dynamically created + + outfields: list of str + Indicates output fields to be dynamically created + + See class examples for usage + + """ + if not outfields: + outfields = ['outfiles'] + super(SSHDataGrabber, self).__init__(**kwargs) + undefined_traits = {} + # used for mandatory inputs check + self._infields = infields + self._outfields = outfields + if infields: + for key in infields: + self.inputs.add_trait(key, traits.Any) + undefined_traits[key] = Undefined + # add ability to insert field specific templates + self.inputs.add_trait('field_template', + traits.Dict(traits.Enum(outfields), + desc="arguments that fit into template")) + undefined_traits['field_template'] = Undefined + if not isdefined(self.inputs.template_args): + self.inputs.template_args = {} + for key in outfields: + if not key in self.inputs.template_args: + if infields: + self.inputs.template_args[key] = [infields] + else: + self.inputs.template_args[key] = [] + + self.inputs.trait_set(trait_change_notify=False, **undefined_traits) + + if ( + self.inputs.template_expression == 'regexp' and + self.inputs.template[-1] != '$' + ): + self.inputs.template += '$' + + + + def _add_output_traits(self, base): + """ + + Using traits.Any instead out OutputMultiPath till add_trait bug + is fixed. + """ + return add_traits(base, self.inputs.template_args.keys()) + + def _list_outputs(self): + if len(self.inputs.ssh_log_to_file) > 0: + paramiko.util.log_to_file(self.inputs.ssh_log_to_file) + # infields are mandatory, however I could not figure out how to set 'mandatory' flag dynamically + # hence manual check + if self._infields: + for key in self._infields: + value = getattr(self.inputs, key) + if not isdefined(value): + msg = "%s requires a value for input '%s' because it was listed in 'infields'" % \ + (self.__class__.__name__, key) + raise ValueError(msg) + + outputs = {} + for key, args in self.inputs.template_args.items(): + outputs[key] = [] + template = self.inputs.template + if hasattr(self.inputs, 'field_template') and \ + isdefined(self.inputs.field_template) and \ + key in self.inputs.field_template: + template = self.inputs.field_template[key] + #template = os.path.join( + # os.path.abspath(self.inputs.base_directory), template) + if not args: + client = self._get_ssh_client() + sftp = client.open_sftp() + sftp.chdir(self.inputs.base_directory) + filelist = sftp.listdir() + if self.inputs.template_expression == 'fnmatch': + filelist = fnmatch.filter(filelist, template) + elif self.inputs.template_expression == 'regexp': + regexp = re.compile(template) + filelist = filter(regexp.match, filelist) + else: + raise ValueError('template_expression value invalid') + if len(filelist) == 0: + msg = 'Output key: %s Template: %s returned no files' % ( + key, template) + if self.inputs.raise_on_empty: + raise IOError(msg) + else: + warn(msg) + else: + if self.inputs.sort_filelist: + filelist = human_order_sorted(filelist) + outputs[key] = list_to_filename(filelist) + if self.inputs.download_files: + for f in filelist: + sftp.get(f, f) + for argnum, arglist in enumerate(args): + maxlen = 1 + for arg in arglist: + if isinstance(arg, str) and hasattr(self.inputs, arg): + arg = getattr(self.inputs, arg) + if isinstance(arg, list): + if (maxlen > 1) and (len(arg) != maxlen): + raise ValueError('incompatible number of arguments for %s' % key) + if len(arg) > maxlen: + maxlen = len(arg) + outfiles = [] + for i in range(maxlen): + argtuple = [] + for arg in arglist: + if isinstance(arg, str) and hasattr(self.inputs, arg): + arg = getattr(self.inputs, arg) + if isinstance(arg, list): + argtuple.append(arg[i]) + else: + argtuple.append(arg) + filledtemplate = template + if argtuple: + try: + filledtemplate = template % tuple(argtuple) + except TypeError as e: + raise TypeError(e.message + ": Template %s failed to convert with args %s" % (template, str(tuple(argtuple)))) + client = self._get_ssh_client() + sftp = client.open_sftp() + sftp.chdir(self.inputs.base_directory) + filledtemplate_dir = os.path.dirname(filledtemplate) + filledtemplate_base = os.path.basename(filledtemplate) + filelist = sftp.listdir(filledtemplate_dir) + if self.inputs.template_expression == 'fnmatch': + outfiles = fnmatch.filter(filelist, filledtemplate_base) + elif self.inputs.template_expression == 'regexp': + regexp = re.compile(filledtemplate_base) + outfiles = filter(regexp.match, filelist) + else: + raise ValueError('template_expression value invalid') + if len(outfiles) == 0: + msg = 'Output key: %s Template: %s returned no files' % (key, filledtemplate) + if self.inputs.raise_on_empty: + raise IOError(msg) + else: + warn(msg) + outputs[key].append(None) + else: + if self.inputs.sort_filelist: + outfiles = human_order_sorted(outfiles) + outputs[key].append(list_to_filename(outfiles)) + if self.inputs.download_files: + for f in outfiles: + sftp.get(os.path.join(filledtemplate_dir, f), f) + if any([val is None for val in outputs[key]]): + outputs[key] = [] + if len(outputs[key]) == 0: + outputs[key] = None + elif len(outputs[key]) == 1: + outputs[key] = outputs[key][0] + return outputs + + def _get_ssh_client(self): + config = paramiko.SSHConfig() + config.parse(open(os.path.expanduser('~/.ssh/config'))) + host = config.lookup(self.inputs.hostname) + proxy = paramiko.ProxyCommand( + subprocess.check_output( + [os.environ['SHELL'], '-c', 'echo %s' % host['proxycommand']] + ).strip() + ) + client = paramiko.SSHClient() + client.load_system_host_keys() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + client.connect(host['hostname'], username=host['user'], sock=proxy) + return client From 7252f2ebcba117f9d42b7653663283574a6b0051 Mon Sep 17 00:00:00 2001 From: Demian Wassermann Date: Mon, 19 May 2014 14:24:11 +0200 Subject: [PATCH 2/9] ENH: Improved style taking advantage of inheritance --- nipype/interfaces/io.py | 37 +------------------------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/nipype/interfaces/io.py b/nipype/interfaces/io.py index 03497a1c77..6f64639d3c 100644 --- a/nipype/interfaces/io.py +++ b/nipype/interfaces/io.py @@ -1570,7 +1570,7 @@ class SSHDataGrabberInputSpec(DynamicTraitedSpec, BaseInterfaceInputSpec): desc='If set SSH commands will be logged to the given file') -class SSHDataGrabber(IOBase): +class SSHDataGrabber(Datagrabber): """ Datagrabber module that downloads the file list and optionally the files from a SSH server. The SSH operation must not need user and password so an SSH agent must be active in where this @@ -1647,30 +1647,6 @@ def __init__(self, infields=None, outfields=None, **kwargs): if not outfields: outfields = ['outfiles'] super(SSHDataGrabber, self).__init__(**kwargs) - undefined_traits = {} - # used for mandatory inputs check - self._infields = infields - self._outfields = outfields - if infields: - for key in infields: - self.inputs.add_trait(key, traits.Any) - undefined_traits[key] = Undefined - # add ability to insert field specific templates - self.inputs.add_trait('field_template', - traits.Dict(traits.Enum(outfields), - desc="arguments that fit into template")) - undefined_traits['field_template'] = Undefined - if not isdefined(self.inputs.template_args): - self.inputs.template_args = {} - for key in outfields: - if not key in self.inputs.template_args: - if infields: - self.inputs.template_args[key] = [infields] - else: - self.inputs.template_args[key] = [] - - self.inputs.trait_set(trait_change_notify=False, **undefined_traits) - if ( self.inputs.template_expression == 'regexp' and self.inputs.template[-1] != '$' @@ -1678,15 +1654,6 @@ def __init__(self, infields=None, outfields=None, **kwargs): self.inputs.template += '$' - - def _add_output_traits(self, base): - """ - - Using traits.Any instead out OutputMultiPath till add_trait bug - is fixed. - """ - return add_traits(base, self.inputs.template_args.keys()) - def _list_outputs(self): if len(self.inputs.ssh_log_to_file) > 0: paramiko.util.log_to_file(self.inputs.ssh_log_to_file) @@ -1708,8 +1675,6 @@ def _list_outputs(self): isdefined(self.inputs.field_template) and \ key in self.inputs.field_template: template = self.inputs.field_template[key] - #template = os.path.join( - # os.path.abspath(self.inputs.base_directory), template) if not args: client = self._get_ssh_client() sftp = client.open_sftp() From 33d92175a85188313d3010e0b6b2e139d4f65fd0 Mon Sep 17 00:00:00 2001 From: Demian Wassermann Date: Mon, 19 May 2014 14:39:14 +0200 Subject: [PATCH 3/9] BUG: Corrected typo --- nipype/interfaces/io.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nipype/interfaces/io.py b/nipype/interfaces/io.py index 6f64639d3c..ce4363b18c 100644 --- a/nipype/interfaces/io.py +++ b/nipype/interfaces/io.py @@ -1570,11 +1570,11 @@ class SSHDataGrabberInputSpec(DynamicTraitedSpec, BaseInterfaceInputSpec): desc='If set SSH commands will be logged to the given file') -class SSHDataGrabber(Datagrabber): - """ Datagrabber module that downloads the file list and optionally - the files from a SSH server. The SSH operation must not need - user and password so an SSH agent must be active in where this - module is being run. +class SSHDataGrabber(DataGrabber): + """ Extension of DataGrabber module that downloads the file list and + optionally the files from a SSH server. The SSH operation must + not need user and password so an SSH agent must be active in + where this module is being run. .. attention:: From 303ba3025d26330687ad7778a625421392ad45fe Mon Sep 17 00:00:00 2001 From: Demian Wassermann Date: Mon, 19 May 2014 14:45:10 +0200 Subject: [PATCH 4/9] ENH: Now the case when there is no SSH forwarding is well handled --- nipype/interfaces/io.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/nipype/interfaces/io.py b/nipype/interfaces/io.py index ce4363b18c..e1388afcce 100644 --- a/nipype/interfaces/io.py +++ b/nipype/interfaces/io.py @@ -1766,11 +1766,14 @@ def _get_ssh_client(self): config = paramiko.SSHConfig() config.parse(open(os.path.expanduser('~/.ssh/config'))) host = config.lookup(self.inputs.hostname) - proxy = paramiko.ProxyCommand( - subprocess.check_output( - [os.environ['SHELL'], '-c', 'echo %s' % host['proxycommand']] - ).strip() - ) + if 'proxycommand' in host: + proxy = paramiko.ProxyCommand( + subprocess.check_output( + [os.environ['SHELL'], '-c', 'echo %s' % host['proxycommand']] + ).strip() + ) + else: + proxy = None client = paramiko.SSHClient() client.load_system_host_keys() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) From 652c1391efc4ff53c76acbf06b7fbef7a0d9e2c7 Mon Sep 17 00:00:00 2001 From: Demian Wassermann Date: Mon, 19 May 2014 22:36:09 +0200 Subject: [PATCH 5/9] ENH: Incorporated mwaskom's suggestions --- nipype/interfaces/io.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/nipype/interfaces/io.py b/nipype/interfaces/io.py index e1388afcce..41b7a84118 100644 --- a/nipype/interfaces/io.py +++ b/nipype/interfaces/io.py @@ -1547,25 +1547,15 @@ def _list_outputs(self): c.close() return None -class SSHDataGrabberInputSpec(DynamicTraitedSpec, BaseInterfaceInputSpec): +class SSHDataGrabberInputSpec(DataGrabberInputSpec): hostname = traits.Str(mandatory=True, desc='Server hostname.') download_files = traits.Bool(True, usedefault=True, desc='If false it will return the file names without downloading them') base_directory = traits.Str(mandatory=True, desc='Path to the base directory consisting of subject data.') - raise_on_empty = traits.Bool(True, usedefault=True, - desc='Generate exception if list is empty for a given field') - sort_filelist = traits.Bool(mandatory=True, - desc='Sort the filelist that matches the template') - template = traits.Str(mandatory=True, - desc='Layout used to get files. relative to base directory if defined') - template_args = traits.Dict(key_trait=traits.Str, - value_trait=traits.List(traits.List), - desc='Information to plug into template') template_expression = traits.Enum(['fnmatch', 'regexp'], usedefault=True, desc='Use either fnmatch or regexp to express templates') - ssh_log_to_file = traits.Str('', usedefault=True, desc='If set SSH commands will be logged to the given file') @@ -1629,7 +1619,7 @@ class SSHDataGrabber(DataGrabber): """ input_spec = SSHDataGrabberInputSpec output_spec = DynamicTraitedSpec - _always_run = True + _always_run = False def __init__(self, infields=None, outfields=None, **kwargs): """ @@ -1644,6 +1634,13 @@ def __init__(self, infields=None, outfields=None, **kwargs): See class examples for usage """ + try: + paramiko + except NameError: + raise ImportError( + "The library parmiko needs to be installed" + " for this module to run." + ) if not outfields: outfields = ['outfiles'] super(SSHDataGrabber, self).__init__(**kwargs) From 7e24f288d7d778321af5b369a59a1509e2fdc859 Mon Sep 17 00:00:00 2001 From: Demian Wassermann Date: Thu, 22 May 2014 13:18:17 +0200 Subject: [PATCH 6/9] ENH: Changed the checks for the paramiko library so doctests pass even if the library is not there --- nipype/interfaces/io.py | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/nipype/interfaces/io.py b/nipype/interfaces/io.py index 41b7a84118..ea46eb6b64 100644 --- a/nipype/interfaces/io.py +++ b/nipype/interfaces/io.py @@ -1550,6 +1550,10 @@ def _list_outputs(self): class SSHDataGrabberInputSpec(DataGrabberInputSpec): hostname = traits.Str(mandatory=True, desc='Server hostname.') + username = traits.Str(mandatory=False, + desc='Server username.') + password = traits.Password(mandatory=False, + desc='Server password.') download_files = traits.Bool(True, usedefault=True, desc='If false it will return the file names without downloading them') base_directory = traits.Str(mandatory=True, @@ -1576,8 +1580,10 @@ class SSHDataGrabber(DataGrabber): >>> from nipype.interfaces.io import SSHDataGrabber >>> dg = SSHDataGrabber() - >>> dg.inputs.hostname = 'myhost.com' - >>> dg.inputs.base_directory = '/main_folder/my_remote_dir' + >>> dg.inputs.hostname = 'test.rebex.net' + >>> dg.inputs.user = 'demo' + >>> dg.inputs.password = 'password' + >>> dg.inputs.base_directory = 'pub/example' Pick all files from the base directory @@ -1586,15 +1592,17 @@ class SSHDataGrabber(DataGrabber): Pick all files starting with "s" and a number from current directory >>> dg.inputs.template_expression = 'regexp' - >>> dg.inputs.template = 's[0-9].*' + >>> dg.inputs.template = 'pop[0-9].*' Same thing but with dynamically created fields >>> dg = SSHDataGrabber(infields=['arg1','arg2']) - >>> dg.inputs.hostname = 'myhost.com' - >>> dg.inputs.base_directory = '~/my_remote_dir' - >>> dg.inputs.template = '%s/%s.nii' - >>> dg.inputs.arg1 = 'foo' + >>> dg.inputs.hostname = 'test.rebex.net' + >>> dg.inputs.user = 'demo' + >>> dg.inputs.password = 'password' + >>> dg.inputs.base_directory = 'pub' + >>> dg.inputs.template = '%s/%s.txt' + >>> dg.inputs.arg1 = 'example' >>> dg.inputs.arg2 = 'foo' however this latter form can be used with iterables and iterfield in a @@ -1637,13 +1645,21 @@ def __init__(self, infields=None, outfields=None, **kwargs): try: paramiko except NameError: - raise ImportError( + warn( "The library parmiko needs to be installed" " for this module to run." ) if not outfields: outfields = ['outfiles'] super(SSHDataGrabber, self).__init__(**kwargs) + if ( + None in (self.inputs.username or self.inputs.password) + ): + raise ValueError( + "either both username and password " + "are provided or none of them" + ) + if ( self.inputs.template_expression == 'regexp' and self.inputs.template[-1] != '$' @@ -1652,6 +1668,14 @@ def __init__(self, infields=None, outfields=None, **kwargs): def _list_outputs(self): + try: + paramiko + except NameError: + raise ImportError( + "The library parmiko needs to be installed" + " for this module to run." + ) + if len(self.inputs.ssh_log_to_file) > 0: paramiko.util.log_to_file(self.inputs.ssh_log_to_file) # infields are mandatory, however I could not figure out how to set 'mandatory' flag dynamically From 5156fa44d9200b6a545aef0f22e847a5ccb990ea Mon Sep 17 00:00:00 2001 From: Demian Wassermann Date: Thu, 22 May 2014 13:35:11 +0200 Subject: [PATCH 7/9] BUG: Corrected inheritance bug --- nipype/interfaces/io.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nipype/interfaces/io.py b/nipype/interfaces/io.py index ea46eb6b64..1f5e508abb 100644 --- a/nipype/interfaces/io.py +++ b/nipype/interfaces/io.py @@ -1651,9 +1651,12 @@ def __init__(self, infields=None, outfields=None, **kwargs): ) if not outfields: outfields = ['outfiles'] + kwargs = kwargs.copy() + kwargs['infields'] = infields + kwargs['outfields'] = outfields super(SSHDataGrabber, self).__init__(**kwargs) if ( - None in (self.inputs.username or self.inputs.password) + None in (self.inputs.username, self.inputs.password) ): raise ValueError( "either both username and password " From 4934023f8f6a9aec095036054c24040b514dd995 Mon Sep 17 00:00:00 2001 From: Demian Wassermann Date: Fri, 6 Jun 2014 10:56:31 +0200 Subject: [PATCH 8/9] ENH: Added a description of the new interface in the CHANGES file --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index b21025bfdb..c63fff963c 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,7 @@ Next Release * API: Interfaces to external packages are no longer available in the top-level ``nipype`` namespace, and must be imported directly (e.g. ``from nipype.interfaces import fsl``). * ENH: New ANTs interface: ApplyTransformsToPoints * ENH: New FreeSurfer workflow: create_skullstripped_recon_flow() +* ENH: Added SSHDataGrabber, a data grabbing interface that works over SSH connections * FIX: MRTrix tracking algorithms were ignoring mask parameters. Release 0.9.2 (January 31, 2014) From 854e510193fbb85bd776fe2eb36e2005c32c7847 Mon Sep 17 00:00:00 2001 From: Demian Wassermann Date: Fri, 6 Jun 2014 10:57:52 +0200 Subject: [PATCH 9/9] ENH: Added a description of the new interface in the CHANGES file, more consistent phrasing --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index c63fff963c..f4ebb73be0 100644 --- a/CHANGES +++ b/CHANGES @@ -4,7 +4,7 @@ Next Release * API: Interfaces to external packages are no longer available in the top-level ``nipype`` namespace, and must be imported directly (e.g. ``from nipype.interfaces import fsl``). * ENH: New ANTs interface: ApplyTransformsToPoints * ENH: New FreeSurfer workflow: create_skullstripped_recon_flow() -* ENH: Added SSHDataGrabber, a data grabbing interface that works over SSH connections +* ENH: New data grabbing interface that works over SSH connections, SSHDataGrabber * FIX: MRTrix tracking algorithms were ignoring mask parameters. Release 0.9.2 (January 31, 2014)