diff --git a/beat/backend/python/algorithm.py b/beat/backend/python/algorithm.py index 08b8128825584c265a9a94c9a266049c67238e04..27573eb2466fad96a56f65eddee5d9c0fbaca626 100644 --- a/beat/backend/python/algorithm.py +++ b/beat/backend/python/algorithm.py @@ -50,13 +50,17 @@ import numpy import simplejson as json import six +from . import config from . import dataformat -from . import library from . import loader from . import utils +from .library import Library logger = logging.getLogger(__name__) +ALGORITHM_TYPE = "algorithm" +ALGORITHM_FOLDER = "algorithms" + # ---------------------------------------------------------- @@ -66,28 +70,26 @@ class Storage(utils.CodeStorage): Parameters: - prefix (str): Establishes the prefix of your installation. - name (str): The name of the algorithm object in the format ``<user>/<name>/<version>``. """ - asset_type = "algorithm" - asset_folder = "algorithms" + asset_type = ALGORITHM_TYPE + asset_folder = ALGORITHM_FOLDER - def __init__(self, prefix, name, language=None): + def __init__(self, name, language=None, prefix=None): if name.count("/") != 2: raise RuntimeError("invalid algorithm name: `%s'" % name) self.username, self.name, self.version = name.split("/") self.fullname = name + if prefix is None: + prefix = config.get_config()["prefix"] self.prefix = prefix - path = utils.hashed_or_simple( - self.prefix, self.asset_folder, name, suffix=".json" - ) + path = utils.hashed_or_simple(prefix, self.asset_folder, name, suffix=".json") path = path[:-5] super(Storage, self).__init__(path, language) @@ -375,7 +377,16 @@ class Runner(object): # ---------------------------------------------------------- -class Algorithm(object): +def _to_name(v, convert_basic_types=False): + if hasattr(v, "name"): + v = v.name + if convert_basic_types: + if not isinstance(v, str): + v = numpy.dtype(v).name + return v + + +class Algorithm(metaclass=config.PrefixMeta): """Algorithms represent runnable components within the platform. This class can only parse the meta-parameters of the algorithm (i.e., input @@ -387,20 +398,8 @@ class Algorithm(object): Parameters: - prefix (str): Establishes the prefix of your installation. - name (str): The fully qualified algorithm name (e.g. ``user/algo/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 algorithm loading times as dataformats - that are already loaded may be re-used. - - library_cache (:py:class:`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: @@ -449,6 +448,9 @@ class Algorithm(object): """ + asset_type = ALGORITHM_TYPE + asset_folder = ALGORITHM_FOLDER + LEGACY = "legacy" SEQUENTIAL = "sequential" AUTONOMOUS = "autonomous" @@ -459,27 +461,83 @@ class Algorithm(object): dataformat_klass = dataformat.DataFormat - def __init__(self, prefix, name, dataformat_cache=None, library_cache=None): - + def _init(self): self._name = None self.storage = None - self.prefix = prefix self.dataformats = {} self.libraries = {} self.groups = [] self.errors = [] + return self + + def __init__(self, name): + + self._init() + self._load(name) + + @classmethod + def new( + cls, + code_path, + name, + groups, + parameters=None, + description=None, + type="autonomous", + splittable=False, + api_version=2, + language="python", + uses=None, + schema_version=2, + **kwargs, + ): + self = cls.__new__(cls)._init() + + uses = {k: _to_name(v) for k, v in (uses or {}).items()} or None + # convert dataformats to their names in groups + for i, grp in enumerate(groups): + for key, value in grp.items(): + if key not in ("inputs", "outputs"): + continue + for node_details in value.values(): + node_details["type"] = _to_name(node_details["type"]) + + data = dict( + api_version=api_version, + description=description, + groups=groups, + language=language, + parameters=parameters, + schema_version=schema_version, + splittable=splittable, + type=type, + uses=uses, + ) + data = {k: v for k, v in data.items() if v is not None} + + self.data = data + + if not name: + raise ValueError(f"Invalid {name}. The name should be a non-empty string!") - 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 - self._load(name, dataformat_cache, library_cache) + self.storage = Storage(self.name) + self.code_path = self.storage.code.path = code_path + with open(code_path, "rt") as f: + self.code = self.storage.code.contents = f.read() + self.storage.json.contents = str(self) - def _load(self, data, dataformat_cache, library_cache): + self._resolve() + + return self + + def _load(self, name): """Loads the algorithm""" - self._name = data + self.name = name - self.storage = Storage(self.prefix, data) + self.storage = Storage(self.name) json_path = self.storage.json.path if not self.storage.exists(): self.errors.append("Algorithm declaration file not found: %s" % json_path) @@ -497,7 +555,9 @@ class Algorithm(object): self.code_path = self.storage.code.path self.code = self.storage.code.load() + self._resolve() + def _resolve(self): self.groups = self.data["groups"] # create maps for easy access to data @@ -511,46 +571,46 @@ class Algorithm(object): for k, v in g.get("outputs", {}).items() ] ) + # find the group name of outputs + self.output_group = None + for g in self.groups: + for k, v in g.get("outputs", {}).items(): + self.output_group = g + break + break + self.loop_map = dict( [(k, v["type"]) for g in self.groups for k, v in g.get("loop", {}).items()] ) - self._load_dataformats(dataformat_cache) + self._load_dataformats() self._convert_parameter_types() - self._load_libraries(library_cache) + self._load_libraries() - def _update_dataformat_cache(self, type_name, dataformat_cache): + def _update_dataformat_cache(self, type_name): """Update the data format cache based on the type name""" if type_name not in self.dataformats: - if dataformat_cache and type_name in dataformat_cache: # reuse - thisformat = dataformat_cache[type_name] - else: # load it - thisformat = self.dataformat_klass(self.prefix, type_name) - if dataformat_cache is not None: # update it - dataformat_cache[type_name] = thisformat - + thisformat = self.dataformat_klass[type_name] self.dataformats[type_name] = thisformat return self.dataformats[type_name] - def _update_dataformat_cache_for_group(self, group, dataformat_cache): + def _update_dataformat_cache_for_group(self, group): for _, entry in group.items(): - self._update_dataformat_cache(entry["type"], dataformat_cache) + self._update_dataformat_cache(entry["type"]) - def _load_dataformats(self, dataformat_cache): + def _load_dataformats(self): """Makes sure we can load all requested formats """ for group in self.groups: - self._update_dataformat_cache_for_group(group["inputs"], dataformat_cache) + self._update_dataformat_cache_for_group(group["inputs"]) if "outputs" in group: - self._update_dataformat_cache_for_group( - group["outputs"], dataformat_cache - ) + self._update_dataformat_cache_for_group(group["outputs"]) if "loop" in group: - self._update_dataformat_cache_for_group(group["loop"], dataformat_cache) + self._update_dataformat_cache_for_group(group["loop"]) if self.results: @@ -559,7 +619,7 @@ class Algorithm(object): # results can only contain base types and plots therefore, only # process plots if result_type.find("/") != -1: - self._update_dataformat_cache(result_type, dataformat_cache) + self._update_dataformat_cache(result_type) def _convert_parameter_types(self): """Converts types to numpy equivalents, checks defaults, ranges and choices @@ -639,7 +699,7 @@ class Algorithm(object): ) ) - def _load_libraries(self, library_cache): + def _load_libraries(self): # all used libraries must be loadable; cannot use self as a library @@ -647,9 +707,7 @@ class Algorithm(object): for name, value in self.uses.items(): - self.libraries[value] = library_cache.setdefault( - value, library.Library(self.prefix, value, library_cache) - ) + self.libraries[value] = Library[value] @property def name(self): @@ -668,8 +726,11 @@ class Algorithm(object): if self.data["language"] == "unknown": raise RuntimeError("algorithm has no programming language set") + if "/" not in value: + value = f"{config.get_config()['user']}/{value}/1" + self._name = value - self.storage = Storage(self.prefix, value, self.data["language"]) + self.storage = Storage(value, self.data["language"]) @property def schema_version(self): @@ -898,7 +959,7 @@ class Algorithm(object): ) format = dataformat.DataFormat( - self.prefix, dict([(k, v["type"]) for k, v in self.results.items()]) + dict([(k, v["type"]) for k, v in self.results.items()]) ) format.name = "analysis:" + self.name @@ -1034,7 +1095,7 @@ class Algorithm(object): Raises: - RuntimeError: If prefix and self.prefix point to the same directory. + RuntimeError: If prefix and prefix point to the same directory. """ @@ -1044,19 +1105,53 @@ class Algorithm(object): if not self.valid: raise RuntimeError("algorithm is not valid:\n%s" % "\n".join(self.errors)) - if prefix == self.prefix: - raise RuntimeError( - "Cannot export algorithm to the same prefix (" "%s)" % prefix - ) - for k in self.libraries.values(): k.export(prefix) for k in self.dataformats.values(): k.export(prefix) - self.write(Storage(prefix, self.name, self.language)) + self.write(Storage(self.name, self.language, prefix=prefix)) class Analyzer(Algorithm): """docstring for Analyzer""" + + @classmethod + def new( + cls, + code_path, + name, + groups, + results, + description=None, + type="autonomous", + api_version=2, + language="python", + uses=None, + schema_version=2, + **kwargs, + ): + + self = super().new( + code_path=code_path, + name=name, + groups=groups, + description=description, + type=type, + api_version=api_version, + language=language, + uses=uses, + schema_version=schema_version, + # no parameters or splittable for analyzers + parameters=None, + splittable=None, + **kwargs, + ) + # convert dataformats to their names in results + for name, details in results.items(): + details["type"] = _to_name(details["type"], convert_basic_types=True) + + self.data["results"] = results + self._resolve() + return self diff --git a/beat/backend/python/config.py b/beat/backend/python/config.py index 0a04f6c7c2110cad9edf0180b97579b2b526e0fa..3ef4df2438472d5f8b2a99eb675c197565b70639 100644 --- a/beat/backend/python/config.py +++ b/beat/backend/python/config.py @@ -99,7 +99,7 @@ def set_config(**kwargs): """ supported_keys = set(DEFAULTS.keys()) set_keys = set(kwargs.keys()) - if set_keys not in supported_keys: + if not set_keys.issubset(supported_keys): raise ValueError( f"Only {supported_keys} are valid configurations. " f"Got these extra values: {set_keys - supported_keys}" @@ -137,14 +137,14 @@ def config_context(**new_config): """ old_config = get_config().copy() # also backup prefix - old_prefix = Prefix().copy() + prefix = Prefix() + old_prefix = prefix.copy() set_config(**new_config) try: yield finally: set_config(**old_config) - prefix = Prefix() prefix.clear() prefix.update(old_prefix) @@ -154,7 +154,6 @@ def config_context(**new_config): class Singleton(type): """A Singleton metaclass - The singleton class calls the __init__ method each time the instance is requested. From: https://stackoverflow.com/a/6798042/1286165 """ @@ -167,11 +166,55 @@ class Singleton(type): class Prefix(dict, metaclass=Singleton): - def __init__(self, path=None, *args, **kwargs): - super().__init__(*args, **kwargs) + pass class PrefixMeta(type): + # cache instances when __init__ is called + def __call__(cls, *args, **kwargs): + instance = super().__call__(*args, **kwargs) + + folder = f"{cls.asset_folder}/{instance.name}" + prefix = Prefix() + prefix[folder] = instance + print(f"caching {cls.__name__}/{instance.name} in __init__ calls") + + return instance + + def __init__(cls, name, bases, clsdict): + # cache instances when new is called + if "new" in clsdict: + + def caching_new(*args, **kwargs): + instance = clsdict["new"].__func__(cls, *args, **kwargs) + folder = f"{cls.asset_folder}/{instance.name}" + prefix = Prefix() + prefix[folder] = instance + print(f"caching {cls.__name__}/{instance.name} in new calls") + return instance + + setattr(cls, "new", caching_new) + + # change the key of instance when its name is changed + if "name" in clsdict: + name_property = clsdict["name"] + actual_fset = name_property.fset + + def updating_fset(self, value): + print(f"updating name of {cls.__name__}/{self.name} in cached prefix") + if self.name in cls: + del cls[self.name] + actual_fset(self, value) + cls[self.name] = self + + name_property = property( + name_property.fget, + updating_fset, + name_property.fdel, + name_property.__doc__, + ) + setattr(cls, "name", name_property) + def __contains__(cls, key): return f"{cls.asset_folder}/{key}" in Prefix() @@ -191,3 +234,8 @@ class PrefixMeta(type): folder = f"{cls.asset_folder}/{key}" prefix = Prefix() prefix[folder] = value + + def __delitem__(cls, key): + folder = f"{cls.asset_folder}/{key}" + prefix = Prefix() + del prefix[folder] diff --git a/beat/backend/python/data.py b/beat/backend/python/data.py index b38b8b1b668abafde6a8902c941f22344308a735..432358ed633f49282188b396846944fe6c1fc64e 100644 --- a/beat/backend/python/data.py +++ b/beat/backend/python/data.py @@ -351,7 +351,7 @@ class CachedDataSource(DataSource): ) self.dataformat = algo.result_dataformat() else: - self.dataformat = DataFormat(self.prefix, dataformat_name) + self.dataformat = DataFormat[dataformat_name] if not self.dataformat.valid: raise RuntimeError( @@ -614,7 +614,7 @@ class DatabaseOutputDataSource(DataSource): self.output_name = output_name self.pack = pack - self.dataformat = DataFormat(self.prefix, dataformat_name) + self.dataformat = DataFormat[dataformat_name] if not self.dataformat.valid: raise RuntimeError("the dataformat `%s' is not valid" % dataformat_name) @@ -731,7 +731,7 @@ class RemoteDataSource(DataSource): self.input_name = input_name self.unpack = unpack - self.dataformat = DataFormat(prefix, dataformat_name) + self.dataformat = DataFormat[dataformat_name] if not self.dataformat.valid: raise RuntimeError("the dataformat `%s' is not valid" % dataformat_name) diff --git a/beat/backend/python/database.py b/beat/backend/python/database.py index 2383c0602c6cea3e9884770aae758a341e966571..bf8c784ffd80e59c7d6e357afd8edfa0c41ff90a 100644 --- a/beat/backend/python/database.py +++ b/beat/backend/python/database.py @@ -60,6 +60,9 @@ from .exceptions import OutputError from .outputs import OutputList from .protocoltemplate import ProtocolTemplate +DATABASE_TYPE = "database" +DATABASE_FOLDER = "databases" + # ---------------------------------------------------------- @@ -73,20 +76,21 @@ class Storage(utils.CodeStorage): """ - asset_type = "database" - asset_folder = "databases" + asset_type = DATABASE_TYPE + asset_folder = DATABASE_FOLDER - def __init__(self, name): + def __init__(self, name, prefix=None): if name.count("/") != 1: raise RuntimeError("invalid database name: `%s'" % name) self.name, self.version = name.split("/") self.fullname = name + if prefix is None: + prefix = config.get_config()["prefix"] + self.prefix = prefix - path = os.path.join( - config.get_config()["prefix"], self.asset_folder, name + ".json" - ) + path = os.path.join(prefix, self.asset_folder, name + ".json") path = path[:-5] # views are coded in Python super(Storage, self).__init__(path, "python") @@ -217,7 +221,7 @@ class Runner(object): # ---------------------------------------------------------- -class Database(object): +class Database(metaclass=config.PrefixMeta): """Databases define the start point of the dataflow in an experiment. @@ -234,12 +238,16 @@ class Database(object): """ + asset_type = DATABASE_TYPE + asset_folder = DATABASE_FOLDER + def _init(self): self._name = None self.dataformats = {} # preloaded dataformats self.storage = None self.errors = [] self.data = None + return self def __init__(self, name): @@ -256,8 +264,7 @@ class Database(object): schema_version=2, root_folder="/foo/bar", ): - self = cls.__new__(cls) - self._init() + self = cls.__new__(cls)._init() if not name: raise ValueError(f"Invalid {name}. The name should be a non-empty string!") @@ -275,7 +282,7 @@ class Database(object): for i, proto in enumerate(protocols): protocols[i]["template"] = protocoltemplate_name(proto["template"]) - data = dict(protocols=protocols) + data = dict(protocols=protocols, root_folder=root_folder) if description is not None: data["description"] = description if schema_version is not None: @@ -283,12 +290,11 @@ class Database(object): self.data = data - self.storage = Storage(name) - # save the code into storage + self.storage = Storage(self.name) + self.code_path = self.storage.code.path = code_path with open(code_path, "rt") as f: - self.storage.code.save(f.read()) - self.code_path = self.storage.code.path - self.code = self.storage.code.load() + self.code = self.storage.code.contents = f.read() + self.storage.json.contents = str(self) self._load_v2() @@ -496,7 +502,8 @@ class Database(object): protocol = self.protocol(protocol_name) template_name = protocol["template"] protocol_template = ProtocolTemplate[template_name] - view_definition = protocol_template.set(set_name) + # copy the set so we don't modify the original set + view_definition = dict(protocol_template.set(set_name)) view_definition["view"] = protocol["views"][set_name]["view"] parameters = protocol["views"][set_name].get("parameters") if parameters is not None: @@ -629,11 +636,6 @@ class Database(object): if not self.valid: raise RuntimeError("database is not valid") - if prefix == config.get_config()["prefix"]: - raise RuntimeError( - "Cannot export database to the same prefix (" "%s)" % prefix - ) - for k in self.dataformats.values(): k.export(prefix) @@ -642,7 +644,7 @@ class Database(object): protocol_template = ProtocolTemplate[protocol["template"]] protocol_template.export(prefix) - self.write(Storage(self.name)) + self.write(Storage(self.name, prefix=prefix)) # ---------------------------------------------------------- diff --git a/beat/backend/python/dataformat.py b/beat/backend/python/dataformat.py index f5774ae6c37ad7a10a3ba83b8c63aa588479513c..a8ad589089742778c7fadb60d0b9e7ed0caa0ac1 100644 --- a/beat/backend/python/dataformat.py +++ b/beat/backend/python/dataformat.py @@ -54,8 +54,8 @@ from . import config from . import utils from .baseformat import baseformat -DATA_FORMAT_TYPE = "dataformat" -DATA_FORMAT_FOLDER = "dataformats" +DATAFORMAT_TYPE = "dataformat" +DATAFORMAT_FOLDER = "dataformats" # ---------------------------------------------------------- @@ -70,25 +70,27 @@ class Storage(utils.Storage): """ - asset_type = DATA_FORMAT_TYPE - asset_folder = DATA_FORMAT_FOLDER + asset_type = DATAFORMAT_TYPE + asset_folder = DATAFORMAT_FOLDER - def __init__(self, name): + def __init__(self, name, prefix=None): if name.count("/") != 2: raise RuntimeError("invalid dataformat name: `%s'" % name) self.username, self.name, self.version = name.split("/") self.fullname = name - prefix = config.get_config()["prefix"] + if prefix is None: + prefix = config.get_config()["prefix"] + self.prefix = prefix path = utils.hashed_or_simple(prefix, self.asset_folder, name, suffix=".json") path = path[:-5] - super(Storage, self).__init__(path) + super().__init__(path) def hash(self): """The 64-character hash of the database declaration JSON""" - return super(Storage, self).hash("#description") + return super().hash("#description") # ---------------------------------------------------------- @@ -141,8 +143,8 @@ class DataFormat(metaclass=config.PrefixMeta): """ - asset_type = DATA_FORMAT_TYPE - asset_folder = DATA_FORMAT_FOLDER + asset_type = DATAFORMAT_TYPE + asset_folder = DATAFORMAT_FOLDER def _init(self): self._name = None @@ -153,24 +155,23 @@ class DataFormat(metaclass=config.PrefixMeta): self.resolved = None self.referenced = {} self.parent = None + return self def __init__(self, data, parent=None): self._init() self.parent = parent self._load(data) - # cache in prefix - DataFormat[self.name] = self def _load(self, data): """Loads the dataformat""" if isinstance(data, dict): - self._name = "analysis:result" + self.name = "analysis:result" self.data = data else: - self._name = data - self.storage = Storage(data) + self.name = data + self.storage = Storage(self.name) json_path = self.storage.json.path if not self.storage.exists(): self.errors.append( @@ -264,8 +265,7 @@ class DataFormat(metaclass=config.PrefixMeta): schema_version=None, parent=None, ): - self = cls.__new__(cls) - self._init() + self = cls.__new__(cls)._init() def str_or_dtype_or_type(v): # if it is a dict @@ -301,21 +301,16 @@ class DataFormat(metaclass=config.PrefixMeta): if not name: raise ValueError(f"Invalid {name}. The name should be a non-empty string!") - if name != "analysis:result" and "/" not in name: - name = f"{config.get_config()['user']}/{name}/1" - - self._name = name + self.name = name self.parent = parent - if name != "analysis:result": - self.storage = Storage(name) + if self.name != "analysis:result": + self.storage = Storage(self.name) + self.storage.json.contents = str(self) self._resolve() - # cache in prefix - DataFormat[self.name] = self - return self @property @@ -330,8 +325,11 @@ class DataFormat(metaclass=config.PrefixMeta): @name.setter def name(self, value): + if value != "analysis:result" and "/" not in value: + value = f"{config.get_config()['user']}/{value}/1" self._name = value - self.storage = Storage(value) + if value != "analysis:result": + self.storage = Storage(value) @property def schema_version(self): @@ -578,12 +576,7 @@ class DataFormat(metaclass=config.PrefixMeta): if not self.valid: raise RuntimeError("dataformat is not valid:\n{}".format(self.errors)) - if prefix == prefix: - raise RuntimeError( - "Cannot export dataformat to the same prefix (" "%s)" % prefix - ) - for k in self.referenced.values(): k.export(prefix) - self.write(Storage(prefix, self.name)) + self.write(Storage(self.name, prefix=prefix)) diff --git a/beat/backend/python/execution/algorithm.py b/beat/backend/python/execution/algorithm.py index abda1faee489875756dfd08e92ce6d8d1a5ae55d..99a1fb37ec7ab5c8927ab8dc5d1478a7f8d5f34d 100644 --- a/beat/backend/python/execution/algorithm.py +++ b/beat/backend/python/execution/algorithm.py @@ -181,6 +181,61 @@ class AlgorithmExecutor(object): self.loop_channel = LoopChannel(self.loop_socket) self.loop_channel.setup(self.algorithm, self.prefix) + @classmethod + def new( + cls, + data, + socket, + db_socket=None, + loop_socket=None, + databases=None, + cache_root="/cache", + ): + self = cls.__new__(cls) + self.data = data + self.socket = socket + self.db_socket = db_socket + self.loop_socket = loop_socket + self.loop_channel = None + self._runner = None + self.prefix = None + + # Load the algorithm + self.algorithm = self.data["algorithm"] + + if db_socket: + db_access_mode = AccessMode.REMOTE + else: + db_access_mode = AccessMode.LOCAL + + (self.input_list, self.data_loaders) = create_inputs_from_configuration( + self.data, + self.algorithm, + prefix=self.prefix, + cache_root=cache_root, + cache_access=AccessMode.LOCAL, + db_access=db_access_mode, + socket=self.db_socket, + databases=databases, + ) + + # Loads algorithm outputs + (self.output_list, _) = create_outputs_from_configuration( + self.data, + self.algorithm, + prefix=self.prefix, + cache_root=cache_root, + input_list=self.input_list, + data_loaders=self.data_loaders, + loop_socket=self.loop_socket, + ) + + if self.loop_socket: + self.loop_channel = LoopChannel(self.loop_socket) + self.loop_channel.setup(self.algorithm, self.prefix) + + return self + @property def runner(self): """Returns the algorithm runner diff --git a/beat/backend/python/hash.py b/beat/backend/python/hash.py index c6a483bccf997a8fcb40132c49562575bb009312..10c0d80f9fe1110bda86ee6b5100c2b40470c718 100644 --- a/beat/backend/python/hash.py +++ b/beat/backend/python/hash.py @@ -48,7 +48,6 @@ import hashlib import os import simplejson -import six # ---------------------------------------------------------- @@ -57,11 +56,8 @@ def _sha256(s): """A python2/3 shortcut for :py:func:`haslib.sha256.hexdigest` to will ensure that the given string is unicode before going further. """ - if isinstance(s, six.string_types): - try: - s = six.u(s).encode("utf-8") - except Exception: - s = s.encode("utf-8") + if isinstance(s, str): + s = s.encode("utf-8") return hashlib.sha256(s).hexdigest() diff --git a/beat/backend/python/library.py b/beat/backend/python/library.py index 22bd721bc20215f061a12d465bc2a4da31de3785..5f50b863969b125c2326d4c3fd8718884aca4182 100644 --- a/beat/backend/python/library.py +++ b/beat/backend/python/library.py @@ -46,9 +46,13 @@ import os import simplejson as json +from . import config from . import loader from . import utils +LIBRARY_TYPE = "library" +LIBRARY_FOLDER = "libraries" + # ---------------------------------------------------------- @@ -57,29 +61,26 @@ class Storage(utils.CodeStorage): Parameters: - prefix (str): Establishes the prefix of - your installation. - name (str): The name of the library object in the format ``<user>/<name>/<version>``. """ - asset_type = "library" - asset_folder = "libraries" + asset_type = LIBRARY_TYPE + asset_folder = LIBRARY_FOLDER - def __init__(self, prefix, name, language=None): + def __init__(self, name, language=None, prefix=None): if name.count("/") != 2: raise RuntimeError("invalid library name: `%s'" % name) self.username, self.name, self.version = name.split("/") self.fullname = name + if prefix is None: + prefix = config.get_config()["prefix"] self.prefix = prefix - path = utils.hashed_or_simple( - self.prefix, self.asset_folder, name, suffix=".json" - ) + path = utils.hashed_or_simple(prefix, self.asset_folder, name, suffix=".json") path = path[:-5] super(Storage, self).__init__(path, language) @@ -87,7 +88,7 @@ class Storage(utils.CodeStorage): # ---------------------------------------------------------- -class Library(object): +class Library(metaclass=config.PrefixMeta): """Librarys represent independent algorithm components within the platform. This class can only parse the meta-parameters of the library. The actual @@ -97,15 +98,8 @@ class Library(object): Parameters: - prefix (str): Establishes the prefix of your installation. - name (str): The fully qualified algorithm name (e.g. ``user/algo/1``) - library_cache (:py:class:`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: @@ -136,28 +130,58 @@ class Library(object): """ - def __init__(self, prefix, name, library_cache=None): + asset_type = LIBRARY_TYPE + asset_folder = LIBRARY_FOLDER + def _init(self): self._name = None self.storage = None - self.prefix = prefix self.errors = [] self.libraries = {} + return self + + def __init__(self, name): + self._init() + self._load(name) + + @classmethod + def new( + cls, code_path, name, description=None, language="python", uses=None, + ): + self = cls.__new__(cls)._init() + + def lib_name(v): + if hasattr(v, "name"): + v = v.name + return v - library_cache = library_cache if library_cache is not None else {} + uses = {k: lib_name(v) for k, v in (uses or {}).items()} or None + data = dict(language=language, description=description, uses=uses) + data = {k: v for k, v in data.items() if v is not None} - try: - self._load(name, library_cache) - finally: - if self._name is not None: # registers it into the cache, even if failed - library_cache[self._name] = self + self.data = data - def _load(self, data, library_cache): + if not name: + raise ValueError(f"Invalid {name}. The name should be a non-empty string!") + + self.name = name + + self.storage = Storage(self.name) + self.code_path = self.storage.code.path = code_path + with open(code_path, "rt") as f: + self.code = self.storage.code.contents = f.read() + self.storage.json.contents = str(self) + + self._resolve() + + return self + + def _load(self, data): """Loads the library""" - self._name = data + self.name = data - self.storage = Storage(self.prefix, data) + self.storage = Storage(self.name) json_path = self.storage.json.path if not self.storage.exists(): self.errors.append("Library declaration file not found: %s" % json_path) @@ -178,11 +202,12 @@ class Library(object): # if no errors so far, make sense out of the library data self.data.setdefault("uses", {}) + self._resolve() + + def _resolve(self): if self.uses is not None: for name, value in self.uses.items(): - self.libraries[value] = Library(self.prefix, value, library_cache) - - self.libraries[self._name] = self + self.libraries[value] = Library[value] def uses_dict(self): """Returns the usage dictionary for all dependent modules""" @@ -233,8 +258,11 @@ class Library(object): if self.data["language"] == "unknown": raise RuntimeError("library has no programming language set") + if "/" not in value: + value = f"{config.get_config()['user']}/{value}/1" + self._name = value - self.storage = Storage(self.prefix, value, self.data["language"]) + self.storage = Storage(value, self.data["language"]) @property def schema_version(self): @@ -252,7 +280,6 @@ class Library(object): if self.storage: self.storage.language = value self.data["language"] = value - self._check_language_consistence() @property def valid(self): @@ -369,7 +396,7 @@ class Library(object): Raises: - RuntimeError: If prefix and self.prefix point to the same directory. + RuntimeError: If prefix and prefix point to the same directory. """ @@ -379,12 +406,7 @@ class Library(object): if not self.valid: raise RuntimeError("library is not valid") - if prefix == self.prefix: - raise RuntimeError( - "Cannot export library to the same prefix (" "%s)" % (prefix) - ) - for k in self.libraries.values(): k.export(prefix) - self.write(Storage(prefix, self.name)) + self.write(Storage(self.name, prefix=prefix)) diff --git a/beat/backend/python/protocoltemplate.py b/beat/backend/python/protocoltemplate.py index e14b1d9ecfd254c0562563b2f55435d8e8020e33..720bb111c0d3919d3770b1cef637c8d64680bb37 100644 --- a/beat/backend/python/protocoltemplate.py +++ b/beat/backend/python/protocoltemplate.py @@ -48,8 +48,8 @@ from . import config from . import utils from .dataformat import DataFormat -PROTOCOL_TEMPLATE_TYPE = "protocoltemplate" -PROTOCOL_TEMPLATE_FOLDER = "protocoltemplates" +PROTOCOLTEMPLATE_TYPE = "protocoltemplate" +PROTOCOLTEMPLATE_FOLDER = "protocoltemplates" # ---------------------------------------------------------- @@ -63,20 +63,21 @@ class Storage(utils.Storage): """ - asset_type = PROTOCOL_TEMPLATE_TYPE - asset_folder = PROTOCOL_TEMPLATE_FOLDER + asset_type = PROTOCOLTEMPLATE_TYPE + asset_folder = PROTOCOLTEMPLATE_FOLDER - def __init__(self, name): + def __init__(self, name, prefix=None): if name.count("/") != 1: raise RuntimeError("invalid protocol template name: `%s'" % name) self.name, self.version = name.split("/") self.fullname = name + if prefix is None: + prefix = config.get_config()["prefix"] + self.prefix = prefix - path = utils.hashed_or_simple( - config.get_config()["prefix"], self.asset_folder, name, suffix=".json" - ) + path = utils.hashed_or_simple(prefix, self.asset_folder, name, suffix=".json") path = path[:-5] super(Storage, self).__init__(path) @@ -101,8 +102,8 @@ class ProtocolTemplate(metaclass=config.PrefixMeta): """ - asset_type = PROTOCOL_TEMPLATE_TYPE - asset_folder = PROTOCOL_TEMPLATE_FOLDER + asset_type = PROTOCOLTEMPLATE_TYPE + asset_folder = PROTOCOLTEMPLATE_FOLDER def _init(self): self._name = None @@ -110,21 +111,18 @@ class ProtocolTemplate(metaclass=config.PrefixMeta): self.storage = None self.errors = [] self.data = None + return self def __init__(self, name): self._init() self._load(name) - # cache in prefix - ProtocolTemplate[self.name] = self - @classmethod def new( - cls, sets, name, description=None, schema_version=None, + cls, sets, name, description=None, schema_version=1, ): - self = cls.__new__(cls) - self._init() + self = cls.__new__(cls)._init() if not name: raise ValueError(f"Invalid {name}. The name should be a non-empty string!") @@ -144,21 +142,17 @@ class ProtocolTemplate(metaclass=config.PrefixMeta): k: dataformat_name(v) for k, v in set_["outputs"].items() } - data = dict(sets=sets) + data = dict(sets=sets, schema_version=schema_version) if description is not None: data["description"] = description - if schema_version is not None: - data["schema_version"] = schema_version - + print("ProtocolTemplate data", data) self.data = data - self.storage = Storage(name) + self.storage = Storage(self.name) + self.storage.json.contents = str(self) self._resolve() - # cache in prefix - ProtocolTemplate[self.name] = self - return self def _resolve(self): @@ -327,15 +321,11 @@ class ProtocolTemplate(metaclass=config.PrefixMeta): if not self.valid: raise RuntimeError("protocol template is not valid") - if prefix == 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)) + print("ProtocolTemplate data at export", self.data) + self.write(Storage(self.name, prefix=prefix)) def sets(self): """Returns all the sets available in this protocol template""" diff --git a/beat/backend/python/test/test_interactive.py b/beat/backend/python/test/test_interactive.py index 4f2a1da8e9ba407d65e058e35a5025eb0ca6a0ec..c45ab3f46e71bd48e0855afc10368b5cbacaf2b6 100644 --- a/beat/backend/python/test/test_interactive.py +++ b/beat/backend/python/test/test_interactive.py @@ -111,6 +111,18 @@ class DataFormatTest(unittest.TestCase): with self.assertRaises(ValueError): DataFormat.new({"value": int}, name="") + # test name change + df = DataFormat.new({"value": int}, name="int") + old_name = df.name + df.name = "int2" + new_name = df.name + self.assertTrue(old_name not in DataFormat) + self.assertTrue(new_name in DataFormat, new_name) + + # test creation with init + df = DataFormat({"value": int}) + self.assertTrue(len(df.errors) == 0, df.errors) + # class AlgorithmTest(unittest.TestCase): # """docstring for AlgorithmTest""" diff --git a/beat/backend/python/utils.py b/beat/backend/python/utils.py index 4468a3f92499b645068181dc133415e1d0c46100..04662c89ab721ec0967629d4667613a17d4e19eb 100644 --- a/beat/backend/python/utils.py +++ b/beat/backend/python/utils.py @@ -48,7 +48,6 @@ import shutil import numpy import simplejson -import six from . import hash @@ -132,6 +131,7 @@ class File(object): self.path = path self.binary = binary + self.contents = None def exists(self): @@ -139,12 +139,18 @@ class File(object): def load(self): - mode = "rb" if self.binary else "rt" - with open(self.path, mode) as f: - return f.read() + if self.contents is None: + mode = "rb" if self.binary else "rt" + with open(self.path, mode) as f: + self.contents = f.read() + + return self.contents def try_load(self): + if self.contents is not None: + return self.contents + if os.path.exists(self.path): return self.load() return None @@ -172,11 +178,11 @@ class File(object): mode = "wb" if self.binary else "wt" if self.binary: mode = "wb" - if isinstance(contents, six.string_types): + if isinstance(contents, str): contents = contents.encode("utf-8") else: mode = "wt" - if not isinstance(contents, six.string_types): + if not isinstance(contents, str): contents = contents.decode("utf-8") with open(self.path, mode) as f: @@ -247,8 +253,8 @@ class Storage(AbstractStorage): def hash(self, description="description"): """Re-imp""" - - return hash.hashJSONFile(self.json.path, description) + contents = simplejson.loads(self.json.load()) + return hash.hashJSON(contents=contents, description=description) def load(self): """Re-imp""" @@ -261,7 +267,7 @@ class Storage(AbstractStorage): if description: self.doc.save(description.encode("utf8")) - if not isinstance(declaration, six.string_types): + if not isinstance(declaration, str): declaration = simplejson.dumps(declaration, indent=4) self.json.save(declaration) @@ -309,10 +315,11 @@ class CodeStorage(AbstractStorage): def hash(self): """Re-imp""" - declaration_hash = hash.hashJSONFile(self.json.path, "description") + contents = simplejson.loads(self.json.load()) + declaration_hash = hash.hashJSON(contents=contents, description="description") if self.code.exists(): - code_hash = hash.hashFileContents(self.code.path) + code_hash = hash.hash(self.code.load()) return hash.hash(dict(declaration=declaration_hash, code=code_hash)) else: return declaration_hash @@ -336,7 +343,7 @@ class CodeStorage(AbstractStorage): if description: self.doc.save(description.encode("utf8")) - if not isinstance(declaration, six.string_types): + if not isinstance(declaration, str): declaration = simplejson.dumps(declaration, indent=4) self.json.save(declaration)