From 86e2f1ef5fd6b949e85fc29db5e4b9633d5c7a68 Mon Sep 17 00:00:00 2001 From: Alexander Schlemmer <alexander@mail-schlemmer.de> Date: Thu, 3 Mar 2022 14:19:11 +0100 Subject: [PATCH] ENH: implemented support of lists of references --- src/caosdb/high_level_api.py | 129 +++++++++++++++++++++++++------ unittests/test_high_level_api.py | 115 ++++++++++++++++++++++++++- 2 files changed, 221 insertions(+), 23 deletions(-) diff --git a/src/caosdb/high_level_api.py b/src/caosdb/high_level_api.py index c2f42595..66e07a62 100644 --- a/src/caosdb/high_level_api.py +++ b/src/caosdb/high_level_api.py @@ -243,6 +243,12 @@ class CaosDBPythonEntity(object): ent : db.Entity The entity to be set. """ + + if ent.name is None: + raise RuntimeError("Setting properties without name is impossible.") + + if ent.name in self.get_properties(): + raise RuntimeError("Multiproperty not implemented yet.") val = self._type_converted_value(ent.value, ent.datatype, references) @@ -357,7 +363,7 @@ class CaosDBPythonEntity(object): pr: str The datatype according to the database entry. """ - if not is_list_datatype(pr): + if not is_list_datatype(pr) and not isinstance(val, list): raise RuntimeError("Not a list.") return [ @@ -382,6 +388,8 @@ class CaosDBPythonEntity(object): # case which does not depend on pr # TODO: we might need to pass through the reference container return convert_to_python_object(val, references) + elif isinstance(val, list): + return self._type_converted_list(val, pr, references) elif pr is None: return val elif pr == DOUBLE: @@ -491,6 +499,30 @@ class CaosDBPythonEntity(object): return True return False + def _resolve_caosdb_python_unresolved_reference(self, propval, deep, + references, visited): + # This does not make sense for unset ids: + if propval.id is None: + raise RuntimeError("Unresolved property reference without an ID.") + # have we encountered this id before: + if propval.id in visited: + # self.__setattr__(prop, visited[propval.id]) + # don't do the lookup in the references container + return visited[propval.id] + + # lookup in container: + for ent in references: + # Entities in container without an ID will be skipped: + if ent.id is not None and ent.id == propval.id: + # resolve this entity: + obj = convert_to_python_object(ent, references) + visited[propval.id] = obj + # self.__setattr__(prop, visited[propval.id]) + if deep: + obj.resolve_references(deep, references, visited) + return obj + return propval + def resolve_references(self, deep: bool, references: db.Container, visited: dict[Union[str, int], "CaosDBPythonEntity"] = None): @@ -520,27 +552,24 @@ class CaosDBPythonEntity(object): # Resolve all previously unresolved attributes that are entities: if deep and isinstance(propval, CaosDBPythonEntity): propval.resolve_references(deep, references) + elif isinstance(propval, list): + resolvedelements = [] + for element in propval: + if deep and isinstance(element, CaosDBPythonEntity): + element.resolve_references(deep, references) + resolvedelements.append(element) + if isinstance(element, CaosDBPythonUnresolvedReference): + resolvedelements.append( + self._resolve_caosdb_python_unresolved_reference(element, deep, + references, visited)) + else: + resolvedelements.append(element) + self.__setattr__(prop, resolvedelements) + elif isinstance(propval, CaosDBPythonUnresolvedReference): - # This does not make sense for unset ids: - if propval.id is None: - raise RuntimeError("Unresolved property reference without an ID.") - # have we encountered this id before: - if propval.id in visited: - self.__setattr__(prop, visited[propval.id]) - # don't do the lookup in the references container - continue - - # lookup in container: - for ent in references: - # Entities in container without an ID will be skipped: - if ent.id is not None and ent.id == propval.id: - # resolve this entity: - obj = convert_to_python_object(ent, references) - visited[propval.id] = obj - self.__setattr__(prop, visited[propval.id]) - if deep: - obj.resolve_references(deep, references, visited) - break + val = self._resolve_caosdb_python_unresolved_reference(propval, deep, + references, visited) + self.__setattr__(prop, val) def get_properties(self): """ @@ -599,6 +628,19 @@ class CaosDBPythonEntity(object): properties[p] = {"id": val.id, "unresolved": True} elif isinstance(val, CaosDBPythonEntity): properties[p] = val.serialize(without_metadata) + elif isinstance(val, list): + serializedelements = [] + for element in val: + if isinstance(element, CaosDBPythonUnresolvedReference): + elm = dict() + elm["id"] = element.id + elm["unresolved"] = True + serializedelements.append(elm) + elif isinstance(element, CaosDBPythonEntity): + serializedelements.append(element.serialize(without_metadata)) + else: + serializedelements.append(element) + properties[p] = serializedelements else: properties[p] = val @@ -717,7 +759,9 @@ def _single_convert_to_entity(entity: db.Entity, propval = _single_convert_to_entity( standard_type_for_high_level_type(propval)(), propval) elif isinstance(propval, list): - raise NotImplementedError() + # propval = [] + if not isinstance(propval[0], int): + raise NotImplementedError() entity.add_property( name=prop, @@ -768,3 +812,44 @@ def convert_to_python_object(entity: Union[db.Container, db.Entity], return _single_convert_to_python_object( high_level_type_for_standard_type(entity)(), entity, references) + +def new_high_level_entity_for_record_type(entity: db.RecordType, + importance_level: str, + name: str = None, + deep: bool = True, + references: Optional[db.Container] = None): + """ + Create an new record in high level format based on a record type in standard format. + + entity: db.RecordType + The record type to initialize the new record from. + + importance_level: str + None, obligatory, recommended or suggested + Initialize new properties up to this level. + Properties in the record type with no importance will be added + regardless of the importance_level. + + name: str + Name of the new record. + """ + + r = db.Record(name=name) + r.add_parent(entity) + + impmap = { + None: 0, "SUGGESTED": 3, "RECOMMENDED": 2, "OBLIGATORY": 1} + + for prop in entity.properties: + imp = entity.get_importance(prop) + if imp is not None and impmap[importance_level] < impmap[imp]: + continue + + r.add_property(prop) + + if deep: + raise NotImplementedError("Recursive creation is not possible at the moment.") + + return convert_to_python_object(r) + + diff --git a/unittests/test_high_level_api.py b/unittests/test_high_level_api.py index 2ca57c11..3e7a7369 100644 --- a/unittests/test_high_level_api.py +++ b/unittests/test_high_level_api.py @@ -25,12 +25,17 @@ import caosdb as db -from caosdb.high_level_api import (convert_to_entity, convert_to_python_object) +from caosdb.high_level_api import (convert_to_entity, convert_to_python_object, + new_high_level_entity_for_record_type) from caosdb.high_level_api import (CaosDBPythonUnresolvedParent, CaosDBPythonUnresolvedReference, CaosDBPythonRecord, CaosDBPythonFile) from caosdb.apiutils import compare_entities +from caosdb.common.datatype import (is_list_datatype, + get_list_datatype, + is_reference) + import pytest from lxml import etree import os @@ -396,3 +401,111 @@ def test_files(): assert p.datatype == db.FILE assert p.value.file == "/local/path/test.dat" assert p.value.path == "test.dat" + + +@pytest.mark.xfail +def test_record_generator(): + rt = db.RecordType(name="Simulation") + rt.add_property(name="a", datatype=db.INTEGER) + rt.add_property(name="b", datatype=db.DOUBLE) + rt.add_property(name="inputfile", datatype=db.FILE) + + simrt = db.RecordType(name="SimOutput") + rt.add_property(name="outputfile", datatype="SimOutput") + + obj = new_high_level_entity_for_record_type( + rt, "SUGGESTED", "", True) + print(obj) + assert False + + +def test_list_types(): + r = db.Record() + r.add_property(name="a", value=[1, 2, 4]) + + assert get_list_datatype(r.get_property("a").datatype) is None + + obj = convert_to_python_object(r) + assert type(obj.a) == list + assert len(obj.a) == 3 + assert 4 in obj.a + assert obj.get_property_metadata("a").datatype is None + + conv = convert_to_entity(obj) + prop = r.get_property("a") + assert prop.value == [1, 2, 4] + assert prop.datatype is None + + r.get_property("a").datatype = db.LIST(db.INTEGER) + assert r.get_property("a").datatype == "LIST<INTEGER>" + obj = convert_to_python_object(r) + assert type(obj.a) == list + assert len(obj.a) == 3 + assert 4 in obj.a + assert obj.get_property_metadata("a").datatype == "LIST<INTEGER>" + + conv = convert_to_entity(obj) + prop = r.get_property("a") + assert prop.value == [1, 2, 4] + assert obj.get_property_metadata("a").datatype == "LIST<INTEGER>" + + # List of referenced objects: + r = db.Record() + r.add_property(name="a", value=[1, 2, 4], datatype="LIST<TestReference>") + obj = convert_to_python_object(r) + assert type(obj.a) == list + assert len(obj.a) == 3 + assert obj.get_property_metadata("a").datatype == "LIST<TestReference>" + for i in range(3): + assert type(obj.a[i]) == CaosDBPythonUnresolvedReference + assert obj.a == [CaosDBPythonUnresolvedReference(id=i) for i in [1, 2, 4]] + + # Try resolving: + + # Should not work: + obj.resolve_references(False, db.Container()) + assert type(obj.a) == list + assert len(obj.a) == 3 + assert obj.get_property_metadata("a").datatype == "LIST<TestReference>" + for i in range(3): + assert type(obj.a[i]) == CaosDBPythonUnresolvedReference + assert obj.a == [CaosDBPythonUnresolvedReference(id=i) for i in [1, 2, 4]] + + references = db.Container() + for i in [1, 2, 4]: + ref = db.Record(id=i) + ref.add_property(name="val", value=str(i) + " bla") + references.append(ref) + + obj.resolve_references(False, references) + assert type(obj.a) == list + assert len(obj.a) == 3 + assert obj.get_property_metadata("a").datatype == "LIST<TestReference>" + for i in range(3): + assert type(obj.a[i]) == CaosDBPythonRecord + + assert obj.a[0].val == "1 bla" + + # Conversion with embedded records: + r2 = db.Record() + r2.add_property(name="a", value=4) + r3 = db.Record() + r3.add_property(name="b", value=8) + + r = db.Record() + r.add_property(name="a", value=[r2, r3]) + + obj = convert_to_python_object(r) + assert type(obj.a) == list + assert len(obj.a) == 2 + assert obj.a[0].a == 4 + assert obj.a[1].b == 8 + + # Serialization + text = str(obj) + text2 = str(convert_to_python_object(r2)).split("\n") + print(text) + # cut away first two characters in text + text = [line[4:] for line in text.split("\n")] + for line in text2: + assert line in text -- GitLab