From c7363305c70d4d21c46e5c9fdafe8197404ef596 Mon Sep 17 00:00:00 2001 From: Samuel Gaist <samuel.gaist@idiap.ch> Date: Wed, 10 Apr 2019 16:38:03 +0200 Subject: [PATCH] [protocoltemplate] Implement ProtocolTemplate object This object define the design of a database protocol. --- beat/backend/python/protocoltemplate.py | 316 ++++++++++++++++++ .../different_frequencies/1.json | 14 + .../prefix/protocoltemplates/double/1.json | 15 + .../prefix/protocoltemplates/labelled/1.json | 14 + .../prefix/protocoltemplates/triple/1.json | 16 + .../prefix/protocoltemplates/two_sets/1.json | 26 ++ .../python/test/test_protocoltemplate.py | 113 +++++++ 7 files changed, 514 insertions(+) create mode 100644 beat/backend/python/protocoltemplate.py create mode 100644 beat/backend/python/test/prefix/protocoltemplates/different_frequencies/1.json create mode 100644 beat/backend/python/test/prefix/protocoltemplates/double/1.json create mode 100644 beat/backend/python/test/prefix/protocoltemplates/labelled/1.json create mode 100644 beat/backend/python/test/prefix/protocoltemplates/triple/1.json create mode 100644 beat/backend/python/test/prefix/protocoltemplates/two_sets/1.json create mode 100644 beat/backend/python/test/test_protocoltemplate.py diff --git a/beat/backend/python/protocoltemplate.py b/beat/backend/python/protocoltemplate.py new file mode 100644 index 0000000..a98ffb1 --- /dev/null +++ b/beat/backend/python/protocoltemplate.py @@ -0,0 +1,316 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : + +################################################################################### +# # +# Copyright (c) 2019 Idiap Research Institute, http://www.idiap.ch/ # +# Contact: beat.support@idiap.ch # +# # +# Redistribution and use in source and binary forms, with or without # +# modification, are permitted provided that the following conditions are met: # +# # +# 1. Redistributions of source code must retain the above copyright notice, this # +# list of conditions and the following disclaimer. # +# # +# 2. Redistributions in binary form must reproduce the above copyright notice, # +# this list of conditions and the following disclaimer in the documentation # +# and/or other materials provided with the distribution. # +# # +# 3. Neither the name of the copyright holder nor the names of its contributors # +# may be used to endorse or promote products derived from this software without # +# specific prior written permission. # +# # +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # +# # +################################################################################### + + +""" +======== +protocoltemplates +======== + +Validation of database protocol templates +""" + +import simplejson + +from .dataformat import DataFormat + +from . import utils + + +# ---------------------------------------------------------- + + +class Storage(utils.Storage): + """Resolves paths for protocol templates + + Parameters: + + prefix (str): Establishes the prefix of your installation. + + name (str): The name of the protocol template object in the format + ``<name>/<version>``. + + """ + + def __init__(self, prefix, name): + + if name.count("/") != 1: + raise RuntimeError("invalid protocol template name: `%s'" % name) + + self.name, self.version = name.split("/") + self.fullname = name + self.prefix = prefix + + path = utils.hashed_or_simple( + self.prefix, "protocoltemplates", name, suffix=".json" + ) + path = path[:-5] + super(Storage, self).__init__(path) + + +# ---------------------------------------------------------- + + +class ProtocolTemplate(object): + """Protocol template define the design of the database. + + + Parameters: + + prefix (str): Establishes the prefix of your installation. + + name (str): The fully qualified protocol template name (e.g. ``db/1``) + + dataformat_cache (:py:class:`dict`, Optional): A dictionary mapping + dataformat names to loaded dataformats. This parameter is optional and, + if passed, may greatly speed-up database loading times as dataformats + that are already loaded may be re-used. If you use this parameter, you + must guarantee that the cache is refreshed as appropriate in case the + underlying dataformats change. + + + Attributes: + + name (str): The full, valid name of this database + + data (dict): The original data for this database, as loaded by our JSON + decoder. + + """ + + def __init__(self, prefix, name, dataformat_cache=None): + + self._name = None + self.prefix = prefix + self.dataformats = {} # preloaded dataformats + self.storage = None + + self.errors = [] + self.data = None + + # if the user has not provided a cache, still use one for performance + dataformat_cache = dataformat_cache if dataformat_cache is not None else {} + + self._load(name, dataformat_cache) + + def _load(self, data, dataformat_cache): + """Loads the protocol template""" + + self._name = data + + self.storage = Storage(self.prefix, self._name) + json_path = self.storage.json.path + if not self.storage.json.exists(): + self.errors.append( + "Protocol template declaration file not found: %s" % json_path + ) + return + + with open(json_path, "rt") as f: + self.data = simplejson.loads(f.read()) + + for set_ in self.data["sets"]: + + for key, value in set_["outputs"].items(): + + if value in self.dataformats: + continue + + if value in dataformat_cache: + dataformat = dataformat_cache[value] + else: + dataformat = DataFormat(self.prefix, value) + dataformat_cache[value] = dataformat + + self.dataformats[value] = dataformat + + @property + def name(self): + """Returns the name of this object + """ + return self._name or "__unnamed_protocoltemplate__" + + @name.setter + def name(self, value): + self._name = value + self.storage = Storage(self.prefix, value) + + @property + def description(self): + """The short description for this object""" + return self.data.get("description", None) + + @description.setter + def description(self, value): + """Sets the short description for this object""" + self.data["description"] = value + + @property + def documentation(self): + """The full-length description for this object""" + + if not self._name: + raise RuntimeError("database has no name") + + if self.storage.doc.exists(): + return self.storage.doc.load() + return None + + @documentation.setter + def documentation(self, value): + """Sets the full-length description for this object""" + + if not self._name: + raise RuntimeError("protocol template has no name") + + if hasattr(value, "read"): + self.storage.doc.save(value.read()) + else: + self.storage.doc.save(value) + + def hash(self): + """Returns the hexadecimal hash for its declaration""" + + if not self._name: + raise RuntimeError("protocol template has no name") + + return self.storage.hash() + + @property + def schema_version(self): + """Returns the schema version""" + return self.data.get("schema_version", 1) + + @property + def valid(self): + """A boolean that indicates if this database is valid or not""" + + return not bool(self.errors) + + def json_dumps(self, indent=4): + """Dumps the JSON declaration of this object in a string + + + Parameters: + + indent (int): The number of indentation spaces at every indentation + level + + + Returns: + + str: The JSON representation for this object + + """ + + return simplejson.dumps(self.data, indent=indent, cls=utils.NumpyJSONEncoder) + + def __str__(self): + return self.json_dumps() + + def write(self, storage=None): + """Writes contents to prefix location + + Parameters: + + storage (:py:class:`.Storage`, Optional): If you pass a new storage, + then this object will be written to that storage point rather than + its default. + + """ + + if storage is None: + if not self._name: + raise RuntimeError("protocol template has no name") + storage = self.storage # overwrite + + storage.save(str(self), self.description) + + def export(self, prefix): + """Recursively exports itself into another prefix + + Dataformats associated are also exported recursively + + + Parameters: + + prefix (str): A path to a prefix that must different then my own. + + + Returns: + + None + + + Raises: + + RuntimeError: If prefix and self.prefix point to the same directory. + + """ + + if not self._name: + raise RuntimeError("protocol template has no name") + + if not self.valid: + raise RuntimeError("protocol template is not valid") + + if prefix == self.prefix: + raise RuntimeError( + "Cannot export protocol template to the same prefix (" "%s)" % prefix + ) + + for k in self.dataformats.values(): + k.export(prefix) + + self.write(Storage(prefix, self.name)) + + def sets(self): + """Returns all the sets available in this protocol template""" + + return self.data["sets"] + + def set(self, name): + """Returns the set requested + + Parameters: + name (str): name of the set to retrieve + """ + + set_ = None + for item in self.data["sets"]: + if item["name"] == name: + set_ = item + break + return set_ diff --git a/beat/backend/python/test/prefix/protocoltemplates/different_frequencies/1.json b/beat/backend/python/test/prefix/protocoltemplates/different_frequencies/1.json new file mode 100644 index 0000000..fa1659c --- /dev/null +++ b/beat/backend/python/test/prefix/protocoltemplates/different_frequencies/1.json @@ -0,0 +1,14 @@ +{ + "schema_version": 1, + "sets": [ + { + "name": "double", + "template": "double", + "view": "DifferentFrequencies", + "outputs": { + "a": "user/single_integer/1", + "b": "user/single_integer/1" + } + } + ] +} diff --git a/beat/backend/python/test/prefix/protocoltemplates/double/1.json b/beat/backend/python/test/prefix/protocoltemplates/double/1.json new file mode 100644 index 0000000..7cd58dd --- /dev/null +++ b/beat/backend/python/test/prefix/protocoltemplates/double/1.json @@ -0,0 +1,15 @@ +{ + "schema_version": 1, + "sets": [ + { + "name": "double", + "template": "double", + "view": "Double", + "outputs": { + "a": "user/single_integer/1", + "b": "user/single_integer/1", + "sum": "user/single_integer/1" + } + } + ] +} diff --git a/beat/backend/python/test/prefix/protocoltemplates/labelled/1.json b/beat/backend/python/test/prefix/protocoltemplates/labelled/1.json new file mode 100644 index 0000000..ac3b936 --- /dev/null +++ b/beat/backend/python/test/prefix/protocoltemplates/labelled/1.json @@ -0,0 +1,14 @@ +{ + "schema_version": 1, + "sets": [ + { + "name": "labelled", + "template": "labelled", + "view": "Labelled", + "outputs": { + "value": "user/single_integer/1", + "label": "user/single_string/1" + } + } + ] +} diff --git a/beat/backend/python/test/prefix/protocoltemplates/triple/1.json b/beat/backend/python/test/prefix/protocoltemplates/triple/1.json new file mode 100644 index 0000000..f5b67ab --- /dev/null +++ b/beat/backend/python/test/prefix/protocoltemplates/triple/1.json @@ -0,0 +1,16 @@ +{ + "schema_version": 1, + "sets": [ + { + "name": "triple", + "view": "Triple", + "template": "triple", + "outputs": { + "a": "user/single_integer/1", + "b": "user/single_integer/1", + "c": "user/single_integer/1", + "sum": "user/single_integer/1" + } + } + ] +} diff --git a/beat/backend/python/test/prefix/protocoltemplates/two_sets/1.json b/beat/backend/python/test/prefix/protocoltemplates/two_sets/1.json new file mode 100644 index 0000000..0daf32c --- /dev/null +++ b/beat/backend/python/test/prefix/protocoltemplates/two_sets/1.json @@ -0,0 +1,26 @@ +{ + "schema_version": 1, + "sets": [ + { + "name": "double", + "template": "double", + "view": "Double", + "outputs": { + "a": "user/single_integer/1", + "b": "user/single_integer/1", + "sum": "user/single_integer/1" + } + }, + { + "name": "triple", + "template": "triple", + "view": "Triple", + "outputs": { + "a": "user/single_integer/1", + "b": "user/single_integer/1", + "c": "user/single_integer/1", + "sum": "user/single_integer/1" + } + } + ] +} diff --git a/beat/backend/python/test/test_protocoltemplate.py b/beat/backend/python/test/test_protocoltemplate.py new file mode 100644 index 0000000..6f1a931 --- /dev/null +++ b/beat/backend/python/test/test_protocoltemplate.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : + +################################################################################### +# # +# Copyright (c) 2019 Idiap Research Institute, http://www.idiap.ch/ # +# Contact: beat.support@idiap.ch # +# # +# Redistribution and use in source and binary forms, with or without # +# modification, are permitted provided that the following conditions are met: # +# # +# 1. Redistributions of source code must retain the above copyright notice, this # +# list of conditions and the following disclaimer. # +# # +# 2. Redistributions in binary form must reproduce the above copyright notice, # +# this list of conditions and the following disclaimer in the documentation # +# and/or other materials provided with the distribution. # +# # +# 3. Neither the name of the copyright holder nor the names of its contributors # +# may be used to endorse or promote products derived from this software without # +# specific prior written permission. # +# # +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # +# # +################################################################################### + + +import nose.tools +from ..protocoltemplate import ProtocolTemplate + +from . import prefix + + +# ---------------------------------------------------------- + + +def load(pt_name): + + protocol_template = ProtocolTemplate(prefix, pt_name) + nose.tools.assert_true( + protocol_template.valid, "\n * %s" % "\n * ".join(protocol_template.errors) + ) + return protocol_template + + +# ---------------------------------------------------------- + + +def test_load_valid_protocol_template(): + for name, set_size in [("double/1", 1), ("triple/1", 1), ("two_sets/1", 2)]: + yield load_valid_protocol_template, name, set_size + + +def load_valid_protocol_template(name, set_size): + + protocol_template = load(name) + + nose.tools.eq_(len(protocol_template.sets()), set_size) + + +# ---------------------------------------------------------- + + +def test_load_protocol_with_one_set(): + + protocol_template = load("double/1") + nose.tools.eq_(len(protocol_template.sets()), 1) + + set_ = protocol_template.set("double") + + nose.tools.eq_(set_["name"], "double") + nose.tools.eq_(len(set_["outputs"]), 3) + + nose.tools.assert_is_not_none(set_["outputs"]["a"]) + nose.tools.assert_is_not_none(set_["outputs"]["b"]) + nose.tools.assert_is_not_none(set_["outputs"]["sum"]) + + +# ---------------------------------------------------------- + + +def test_load_protocol_with_two_sets(): + + protocol_template = load("two_sets/1") + nose.tools.eq_(len(protocol_template.sets()), 2) + + set_ = protocol_template.set("double") + + nose.tools.eq_(set_["name"], "double") + nose.tools.eq_(len(set_["outputs"]), 3) + + nose.tools.assert_is_not_none(set_["outputs"]["a"]) + nose.tools.assert_is_not_none(set_["outputs"]["b"]) + nose.tools.assert_is_not_none(set_["outputs"]["sum"]) + + set_ = protocol_template.set("triple") + + nose.tools.eq_(set_["name"], "triple") + nose.tools.eq_(len(set_["outputs"]), 4) + + nose.tools.assert_is_not_none(set_["outputs"]["a"]) + nose.tools.assert_is_not_none(set_["outputs"]["b"]) + nose.tools.assert_is_not_none(set_["outputs"]["c"]) + nose.tools.assert_is_not_none(set_["outputs"]["sum"]) -- GitLab