Commit 91996aac authored by André Anjos's avatar André Anjos 💬
Browse files

Open-source release

parents
*~
*.swp
*.pyc
*.so
bin
eggs
parts
.installed.cfg
.mr.developer.cfg
*.egg-info
develop-eggs
sphinx
doc/api
dist
.nfs*
.gdb_history
build
*.egg
opsnr.stt
.coverage
.DS_Store
html/
.. vim: set fileencoding=utf-8 :
.. Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ ..
.. Contact: beat.support@idiap.ch ..
.. ..
.. This file is part of the beat.web module of the BEAT platform. ..
.. ..
.. Commercial License Usage ..
.. Licensees holding valid commercial BEAT licenses may use this file in ..
.. accordance with the terms contained in a written agreement between you ..
.. and Idiap. For further information contact tto@idiap.ch ..
.. ..
.. Alternatively, this file may be used under the terms of the GNU Affero ..
.. Public License version 3 as published by the Free Software and appearing ..
.. in the file LICENSE.AGPL included in the packaging of this file. ..
.. The BEAT platform is distributed in the hope that it will be useful, but ..
.. WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY ..
.. or FITNESS FOR A PARTICULAR PURPOSE. ..
.. ..
.. You should have received a copy of the GNU Affero Public License along ..
.. with the BEAT platform. If not, see http://www.gnu.org/licenses/. ..
===========================================================
Authors of the Biometrics Evaluation and Testing Platform
===========================================================
Andre Anjos <andre.anjos@idiap.ch>
Flavio Tarsetti <flavio.tarsetti@idiap.ch>
Laurent El-Shafey <laurent.el-shafey@idiap.ch>
Philip Abbet <philip.abbet@idiap.ch>
Samuel Gaist <samuel.gaist@idiap.ch>
Sebastien Marcel <sebastien.marcel@idiap.ch>
This diff is collapsed.
include LICENSE.AGPL README.rst buildout.cfg bootstrap-buildout.py
recursive-include doc conf.py *.rst *.png *.svg *.ico
.. vim: set fileencoding=utf-8 :
.. Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ ..
.. Contact: beat.support@idiap.ch ..
.. ..
.. This file is part of the beat.backend.python module of the BEAT platform. ..
.. ..
.. Commercial License Usage ..
.. Licensees holding valid commercial BEAT licenses may use this file in ..
.. accordance with the terms contained in a written agreement between you ..
.. and Idiap. For further information contact tto@idiap.ch ..
.. ..
.. Alternatively, this file may be used under the terms of the GNU Affero ..
.. Public License version 3 as published by the Free Software and appearing ..
.. in the file LICENSE.AGPL included in the packaging of this file. ..
.. The BEAT platform is distributed in the hope that it will be useful, but ..
.. WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY ..
.. or FITNESS FOR A PARTICULAR PURPOSE. ..
.. ..
.. You should have received a copy of the GNU Affero Public License along ..
.. with the BEAT platform. If not, see http://www.gnu.org/licenses/. ..
============================================
Biometrics Evaluation and Testing Platform
============================================
This package contains the source code for a python-based backend for the BEAT
platform.
Installation
------------
Really easy, with ``zc.buildout``::
$ python bootstrap-buildout.py
$ ./bin/buildout
These 2 commands should download and install all non-installed dependencies and
get you a fully operational test and development environment.
.. note::
If you are on the Idiap filesystem, you may use
``/idiap/project/beat/environments/staging/usr/bin/python`` to bootstrap this
package instead. It contains the same setup deployed at the final BEAT
machinery.
Documentation
-------------
To build the documentation, just do::
$ ./bin/sphinx-apidoc --separate -d 2 --output=doc/api beat/backend/python
$ ./bin/sphinx-build doc sphinx
Testing
-------
After installation, it is possible to run our suite of unit tests. To do so,
use ``nose``::
$ ./bin/nosetests -sv
If you want to skip slow tests (at least those pulling stuff from our servers)
or executing lengthy operations, just do::
$ ./bin/nosetests -sv -a '!slow'
To measure the test coverage, do the following::
$ ./bin/nosetests -sv --with-coverage --cover-package=beat.backend.python
To produce an HTML test coverage report, at the directory `./htmlcov`, do the
following::
$ ./bin/nosetests -sv --with-coverage --cover-package=beat.backend.python --cover-html --cover-html-dir=htmlcov
Our documentation is also interspersed with test units. You can run them using
sphinx::
$ ./bin/sphinx -b doctest doc sphinx
#!/usr/bin/env python
# vim: set fileencoding=utf-8 :
###############################################################################
# #
# Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ #
# Contact: beat.support@idiap.ch #
# #
# This file is part of the beat.backend.python module of the BEAT platform. #
# #
# Commercial License Usage #
# Licensees holding valid commercial BEAT licenses may use this file in #
# accordance with the terms contained in a written agreement between you #
# and Idiap. For further information contact tto@idiap.ch #
# #
# Alternatively, this file may be used under the terms of the GNU Affero #
# Public License version 3 as published by the Free Software and appearing #
# in the file LICENSE.AGPL included in the packaging of this file. #
# The BEAT platform is distributed in the hope that it will be useful, but #
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY #
# or FITNESS FOR A PARTICULAR PURPOSE. #
# #
# You should have received a copy of the GNU Affero Public License along #
# with the BEAT platform. If not, see http://www.gnu.org/licenses/. #
# #
###############################################################################
#see http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
__import__('pkg_resources').declare_namespace(__name__)
#!/usr/bin/env python
# vim: set fileencoding=utf-8 :
###############################################################################
# #
# Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ #
# Contact: beat.support@idiap.ch #
# #
# This file is part of the beat.backend.python module of the BEAT platform. #
# #
# Commercial License Usage #
# Licensees holding valid commercial BEAT licenses may use this file in #
# accordance with the terms contained in a written agreement between you #
# and Idiap. For further information contact tto@idiap.ch #
# #
# Alternatively, this file may be used under the terms of the GNU Affero #
# Public License version 3 as published by the Free Software and appearing #
# in the file LICENSE.AGPL included in the packaging of this file. #
# The BEAT platform is distributed in the hope that it will be useful, but #
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY #
# or FITNESS FOR A PARTICULAR PURPOSE. #
# #
# You should have received a copy of the GNU Affero Public License along #
# with the BEAT platform. If not, see http://www.gnu.org/licenses/. #
# #
###############################################################################
#see http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
__import__('pkg_resources').declare_namespace(__name__)
#!/usr/bin/env python
# vim: set fileencoding=utf-8 :
#!/usr/bin/env python
# vim: set fileencoding=utf-8 :
###############################################################################
# #
# Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ #
# Contact: beat.support@idiap.ch #
# #
# This file is part of the beat.backend.python module of the BEAT platform. #
# #
# Commercial License Usage #
# Licensees holding valid commercial BEAT licenses may use this file in #
# accordance with the terms contained in a written agreement between you #
# and Idiap. For further information contact tto@idiap.ch #
# #
# Alternatively, this file may be used under the terms of the GNU Affero #
# Public License version 3 as published by the Free Software and appearing #
# in the file LICENSE.AGPL included in the packaging of this file. #
# The BEAT platform is distributed in the hope that it will be useful, but #
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY #
# or FITNESS FOR A PARTICULAR PURPOSE. #
# #
# You should have received a copy of the GNU Affero Public License along #
# with the BEAT platform. If not, see http://www.gnu.org/licenses/. #
# #
###############################################################################
"""Validation for algorithms"""
import os
import sys
import six
import numpy
import simplejson
from . import dataformat
from . import library
from . import loader
class Runner(object):
'''A special loader class for algorithms, with specialized methods
Parameters:
module (module): The preloaded module containing the algorithm as
returned by :py:func:`beat.core.loader.load_module`.
obj_name (str): The name of the object within the module you're interested
on
exc (class): The class to use as base exception when translating the
exception from the user code. Read the documention of :py:func:`run`
for more details.
algorithm (object): The algorithm instance that is used for parameter
checking.
*args: Constructor parameters for the algorithm (normally none)
**kwargs: Constructor parameters for the algorithm (normally none)
'''
def __init__(self, module, obj_name, algorithm, exc=None, *args,
**kwargs):
try:
class_ = getattr(module, obj_name)
except Exception as e:
if exc is not None:
type, value, traceback = sys.exc_info()
six.reraise(exc, exc(value), traceback)
else:
raise #just re-raise the user exception
self.obj = loader.run(class_, '__new__', exc, *args, **kwargs)
self.name = module.__name__
self.algorithm = algorithm
self.exc = exc
# if the algorithm does not have a 'setup' method, it is ready by default
self.ready = not hasattr(self.obj, 'setup')
def _check_parameters(self, parameters):
"""Checks input parameters from the user and fill defaults"""
user_keys = set(parameters.keys())
algo_parameters = self.algorithm.parameters or {}
valid_keys = set(algo_parameters.keys())
# checks the user is not trying to set an undeclared parameter
if not user_keys.issubset(valid_keys):
err_keys = user_keys - valid_keys
message = "parameters `%s' are not declared for algorithm `%s' - " \
"valid parameters are `%s'" % (
','.join(err_keys),
self.name,
','.join(valid_keys),
)
exc = self.exc or KeyError
raise exc(message)
# checks all values set by the user are in range (if a range is set)
retval = dict() #dictionary with checked user parameters and defaults
for key, definition in algo_parameters.items():
if key in parameters:
try:
value = self.algorithm.clean_parameter(key, parameters[key])
except Exception as e:
message = "parameter `%s' cannot be safely cast to the declared " \
"type on algorithm `%s': %s" % (key, self.name, e)
exc = self.exc or ValueError
raise exc(message)
else: #user has not set a value, use the default
value = algo_parameters[key]['default']
# in the end, set the value on the dictionary to be returned
retval[key] = value
return retval
def setup(self, parameters, *args, **kwargs):
'''Sets up the algorithm, only effective on the first call'''
if self.ready: return self.ready
completed_parameters = self._check_parameters(parameters) #may raise
kwargs['parameters'] = completed_parameters
if hasattr(self.obj, 'setup'):
self.ready = loader.run(self.obj, 'setup', self.exc, *args, **kwargs)
return self.ready
else:
return True
def process(self, *args, **kwargs):
'''Runs through data'''
if not self.ready:
message = "algorithm `%s' is not yet setup" % (self.name,)
exc = self.exc or RuntimeError
raise self.exc(message)
return loader.run(self.obj, 'process', self.exc, *args, **kwargs)
def __getattr__(self, key):
'''Returns an attribute of the algorithm - only called at last resort'''
return getattr(self.obj, key)
class Algorithm(object):
"""Algorithms represent runnable components within the platform.
This class can only parse the meta-parameters of the algorithm (i.e., input
and output declaration, grouping, synchronization details, parameters and
splittability). The actual algorithm is not directly treated by this class -
it can, however, provide you with a loader for actually running the
algorithmic code (see :py:meth:`Algorithm.runner`).
Parameters:
prefix (str): Establishes the prefix of your installation.
name (str): The fully qualified algorithm name (e.g. ``user/algo/1``)
dataformat_cache (dict, optional): A dictionary mapping dataformat names to
loaded dataformats. This parameter is optional and, if passed, may
greatly speed-up algorithm loading times as dataformats that are already
loaded may be re-used.
library_cache (dict, optional): A dictionary mapping library names to
loaded libraries. This parameter is optional and, if passed, may greatly
speed-up library loading times as libraries that are already loaded may
be re-used.
Attributes:
name (str): The algorithm name
dataformats (dict): A dictionary containing all pre-loaded dataformats used
by this algorithm. Data format objects will be of type
:py:class:`beat.core.dataformat.DataFormat`.
libraries (dict): A mapping object defining other libraries this algorithm
needs to load so it can work properly.
uses (dict): A mapping object defining the required library import name
(keys) and the full-names (values).
parameters (dict): A dictionary containing all pre-defined parameters that
this algorithm accepts.
splittable (bool): A boolean value that indicates if this algorithm is
automatically parallelizeable by our backend.
input_map (dict): A dictionary where the key is the input name and the
value, its type. All input names (potentially from different groups) are
comprised in this dictionary.
output_map (dict): A dictionary where the key is the output name and the
value, its type. All output names (potentially from different groups) are
comprised in this dictionary.
results (dict): If this algorithm is actually an analyzer (i.e., there are
no formal outputs, but results that must be saved by the platform), then
this dictionary contains the names and data types of those elements.
groups (dict): A list containing dictionaries with inputs and outputs
belonging to the same synchronization group.
data (dict): The original data for this algorithm, as loaded by our JSON
decoder.
code (str): The code that is associated with this algorithm, loaded as a
text (or binary) file.
"""
def __init__(self, prefix, name, dataformat_cache=None, library_cache=None):
self.prefix = prefix
self.dataformats = {}
self.libraries = {}
self.groups = []
dataformat_cache = dataformat_cache if dataformat_cache is not None else {}
library_cache = library_cache if library_cache is not None else {}
self.name = name
json_path = os.path.join(prefix, 'algorithms', name + '.json')
with open(json_path, 'rb') as f: self.data = simplejson.load(f)
self.code_path = os.path.join(prefix, 'algorithms', name + '.py')
self.groups = self.data['groups']
# create maps for easy access to data
self.input_map = dict([(k,v['type']) for g in self.groups \
for k,v in g['inputs'].items()])
self.output_map = dict([(k,v['type']) for g in self.groups \
for k,v in g.get('outputs', {}).items()])
self._load_dataformats(dataformat_cache)
self._convert_parameter_types()
self._load_libraries(library_cache)
def _load_dataformats(self, dataformat_cache):
"""Makes sure we can load all requested formats
"""
for group in self.groups:
for name, input in group['inputs'].items():
if input['type'] in self.dataformats: continue
if dataformat_cache and input['type'] in dataformat_cache: #reuse
thisformat = dataformat_cache[input['type']]
else: #load it
thisformat = dataformat.DataFormat(self.prefix, input['type'])
if dataformat_cache is not None: #update it
dataformat_cache[input['type']] = thisformat
self.dataformats[input['type']] = thisformat
if 'outputs' not in group: continue
for name, output in group['outputs'].items():
if output['type'] in self.dataformats: continue
if dataformat_cache and output['type'] in dataformat_cache: #reuse
thisformat = dataformat_cache[output['type']]
else: #load it
thisformat = dataformat.DataFormat(self.prefix, output['type'])
if dataformat_cache is not None: #update it
dataformat_cache[output['type']] = thisformat
self.dataformats[output['type']] = thisformat
if self.results:
for name, result in self.results.items():
if result['type'].find('/') != -1:
if result['type'] in self.dataformats: continue
if dataformat_cache and result['type'] in dataformat_cache: #reuse
thisformat = dataformat_cache[result['type']]
else:
thisformat = dataformat.DataFormat(self.prefix, result['type'])
if dataformat_cache is not None: #update it
dataformat_cache[result['type']] = thisformat
self.dataformats[result['type']] = thisformat
def _convert_parameter_types(self):
"""Converts types to numpy equivalents, checks defaults, ranges and choices
"""
def _try_convert(name, tp, value, desc):
try:
return tp.type(value)
except Exception as e:
self.errors.append("%s for parameter `%s' cannot be cast to type " \
"`%s': %s" % (desc, name, tp.name, e))
if self.parameters is None: return
for name, parameter in self.parameters.items():
if parameter['type'] == 'string':
parameter['type'] = numpy.dtype('str')
else:
parameter['type'] = numpy.dtype(parameter['type'])
if 'range' in parameter:
parameter['range'][0] = _try_convert(name, parameter['type'],
parameter['range'][0], 'start of range')
parameter['range'][1] = _try_convert(name, parameter['type'],
parameter['range'][1], 'end of range')
if parameter['range'][0] >= parameter['range'][1]:
self.errors.append("range for parameter `%s' has a start greater " \
"then the end value (%r >= %r)" % \
(name, parameter['range'][0], parameter['range'][1]))
if 'choice' in parameter:
for i, choice in enumerate(parameter['choice']):
parameter['choice'][i] = _try_convert(name, parameter['type'],
parameter['choice'][i], 'choice[%d]' % i)
if 'default' in parameter:
parameter['default'] = _try_convert(name, parameter['type'],
parameter['default'], 'default')
if 'range' in parameter: #check range
if parameter['default'] < parameter['range'][0] or \
parameter['default'] > parameter['range'][1]:
self.errors.append("default for parameter `%s' (%r) is not " \
"within parameter range [%r, %r]" % (name, parameter['default'],
parameter['range'][0], parameter['range'][1]))
if 'choice' in parameter: #check choices
if parameter['default'] not in parameter['choice']:
self.errors.append("default for parameter `%s' (%r) is not " \
"a valid choice `[%s]'" % (name, parameter['default'],
', '.join(['%r' % k for k in parameter['choice']])))
def _load_libraries(self, library_cache):
# all used libraries must be loadable; cannot use self as a library
if self.uses:
for name, value in self.uses.items():
self.libraries[value] = library_cache.setdefault(value,
library.Library(self.prefix, value, library_cache))
@property
def schema_version(self):
"""Returns the schema version"""
return self.data.get('schema_version', 1)
def clean_parameter(self, parameter, value):
"""Checks if a given value against a declared parameter
This method checks if the provided user value can be safe-cast to the
parameter type as defined on its specification and that it conforms to any
parameter-imposed restrictions.
Parameters:
parameter (str): The name of the parameter to check the value against
value (object): An object that will be safe cast into the defined
parameter type.