diff --git a/CHANGELOG.md b/CHANGELOG.md index 20f2498a09e5599933c18606febf8f160594a3c8..33142766ee95866ec4e3052ed3d7437fae0d2dd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ## ### Added ### + - convenience functions `value_matches_versionid`, `get_id_from_versionid` and `get_versionid` +- Parameter for high level API serialization to output a plain JSON. ### Changed ### diff --git a/src/linkahead/apiutils.py b/src/linkahead/apiutils.py index b2a612faea1616c64b7e78575156abccfdb29e61..f0fe2719a80d6a14e994cc2485baf5b3e0a5a65b 100644 --- a/src/linkahead/apiutils.py +++ b/src/linkahead/apiutils.py @@ -291,9 +291,10 @@ def compare_entities(entity0: Optional[Entity] = None, if entity0 is entity1: return diff + # FIXME Why not simply return a diff which says that the types are different? if type(entity0) is not type(entity1): - raise ValueError( - "Comparison of different Entity types is not supported.") + diff[0]["type"] = type(entity0) + diff[1]["type"] = type(entity1) # compare special attributes for attr in SPECIAL_ATTRIBUTES: diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py index 8b8141bea1a3dc5dba28257136fe03fdea2f6ba0..6de271535a04f82dcf65492d810d43eca7496e1d 100644 --- a/src/linkahead/common/models.py +++ b/src/linkahead/common/models.py @@ -1738,8 +1738,8 @@ def _parse_value(datatype, value): return ret # This is for a special case, where the xml parser could not differentiate - # between single values and lists with one element. As - if hasattr(value, "__len__") and len(value) == 1: + # between single values and lists with one element. + if hasattr(value, "__len__") and not isinstance(value, str) and len(value) == 1: return _parse_value(datatype, value[0]) # deal with references diff --git a/src/linkahead/high_level_api.py b/src/linkahead/high_level_api.py index 9aa59fb9187ff47e71c412568af50e1031c42fb7..a0137dacf048238fe7f72dcc13469c77829c1a28 100644 --- a/src/linkahead/high_level_api.py +++ b/src/linkahead/high_level_api.py @@ -475,8 +475,7 @@ class CaosDBPythonEntity(object): if isinstance(att, list): return att - else: - return [att] + return [att] def add_parent(self, parent: Union[ CaosDBPythonUnresolvedParent, "CaosDBPythonRecordType", str]): @@ -683,53 +682,78 @@ class CaosDBPythonEntity(object): return entity - def serialize(self, without_metadata: bool = False, visited: dict = None): - """ - Serialize necessary information into a dict. + def serialize(self, without_metadata: bool = None, plain_json: bool = False, + visited: dict = None) -> dict: + """Serialize necessary information into a dict. + + Parameters + ---------- + + without_metadata: bool, optional + If True don't set the metadata field in order to increase + readability. Not recommended if deserialization is needed. + + plain_json: bool, optional + If True, serialize to a plain dict without any additional information besides the property values, + name and id. This should conform to the format as specified by the json schema generated by the + advanced user tools. It also sets all properties as top level items of the resulting dict. This + implies ``without_metadata = True + + Returns + ------- - without_metadata: bool - If True don't set the metadata field in order to increase - readability. Not recommended if deserialization is needed. + out: dict + A dict corresponding to this entity. + ``. """ + if plain_json: + if without_metadata is None: + without_metadata = True + if not without_metadata: + raise ValueError("`plain_json` implies `without_metadata`.") + if without_metadata is None: + without_metadata = False if visited is None: - visited = dict() + visited = {} if self in visited: return visited[self] - metadata: Dict[str, Any] = dict() - properties = dict() - parents = list() + metadata: Dict[str, Any] = {} + properties = {} + parents = [] # The full information to be returned: - fulldict = dict() + fulldict = {} visited[self] = fulldict - # Add CaosDB role: - fulldict["role"] = standard_type_for_high_level_type(self, True) - for parent in self._parents: if isinstance(parent, CaosDBPythonEntity): - parents.append(parent.serialize(without_metadata, visited)) + parents.append(parent.serialize(without_metadata=without_metadata, + plain_json=plain_json, + visited=visited)) elif isinstance(parent, CaosDBPythonUnresolvedParent): parents.append({"name": parent.name, "id": parent.id, "unresolved": True}) else: raise RuntimeError("Incompatible class used as parent.") - for baseprop in ("name", "id", "description", "version"): - val = self.__getattribute__(baseprop) - if val is not None: - fulldict[baseprop] = val + if not plain_json: + # Add LinkAhead role: + fulldict["role"] = standard_type_for_high_level_type(self, True) + for baseprop in ("name", "id", "description", "version"): + val = self.__getattribute__(baseprop) + if val is not None: + fulldict[baseprop] = val - if type(self) == CaosDBPythonFile: - fulldict["file"] = self.file - fulldict["path"] = self.path + if isinstance(self, CaosDBPythonFile): + fulldict["file"] = self.file + fulldict["path"] = self.path for p in self.get_properties(): m = self.get_property_metadata(p) - metadata[p] = dict() + metadata[p] = {} for f in fields(m): val = m.__getattribute__(f.name) if val is not None: @@ -739,30 +763,37 @@ class CaosDBPythonEntity(object): if isinstance(val, CaosDBPythonUnresolvedReference): properties[p] = {"id": val.id, "unresolved": True} elif isinstance(val, CaosDBPythonEntity): - properties[p] = val.serialize(without_metadata, visited) + properties[p] = val.serialize(without_metadata=without_metadata, + plain_json=plain_json, + visited=visited) elif isinstance(val, list): serializedelements = [] for element in val: if isinstance(element, CaosDBPythonUnresolvedReference): - elm = dict() + elm = {} elm["id"] = element.id elm["unresolved"] = True serializedelements.append(elm) elif isinstance(element, CaosDBPythonEntity): serializedelements.append( - element.serialize(without_metadata, - visited)) + element.serialize(without_metadata=without_metadata, + plain_json=plain_json, + visited=visited)) else: serializedelements.append(element) properties[p] = serializedelements else: properties[p] = val - fulldict["properties"] = properties - fulldict["parents"] = parents - - if not without_metadata: - fulldict["metadata"] = metadata + if plain_json: + fulldict["id"] = getattr(self, "id") + fulldict["name"] = getattr(self, "name") + fulldict.update(properties) + else: + fulldict["properties"] = properties + fulldict["parents"] = parents + if not without_metadata: + fulldict["metadata"] = metadata return fulldict def __str__(self): diff --git a/unittests/test_high_level_api.py b/unittests/test_high_level_api.py index 82c1a5caf0f0719b5946ecd6749b4079bb6794bc..e35dc678f7d0f44d1bb8fa763cf8dfc8225e3aee 100644 --- a/unittests/test_high_level_api.py +++ b/unittests/test_high_level_api.py @@ -322,6 +322,7 @@ def test_wrong_entity_for_file(): def test_serialization(): + # With ID r = db.Record(id=5, name="test", description="ok") r.add_property(name="v", value=15, datatype=db.INTEGER, unit="kpx", importance="RECOMMENDED") @@ -333,6 +334,22 @@ def test_serialization(): for teststr in teststrs: assert teststr in text + serialized = convert_to_python_object(r).serialize() + assert serialized == {'role': 'Record', + 'name': 'test', + 'id': 5, + 'description': 'ok', + 'properties': {'v': 15}, + 'parents': [], + 'metadata': {'v': {'unit': 'kpx', + 'datatype': 'INTEGER', + 'importance': 'RECOMMENDED'}}} + + serialized_plain = convert_to_python_object(r).serialize(plain_json=True) + assert serialized_plain == {'id': 5, 'name': 'test', 'v': 15} + + # Without ID + r = db.Record(description="ok") r.add_property(name="v", value=15, datatype=db.INTEGER, unit="kpx", importance="RECOMMENDED") @@ -341,6 +358,18 @@ def test_serialization(): assert "name" not in text assert "id" not in text + serialized = convert_to_python_object(r).serialize() + assert serialized == {'role': 'Record', + 'description': 'ok', + 'properties': {'v': 15}, + 'parents': [], + 'metadata': {'v': {'unit': 'kpx', + 'datatype': 'INTEGER', + 'importance': 'RECOMMENDED'}}} + + serialized_plain = convert_to_python_object(r).serialize(plain_json=True) + assert serialized_plain == {'id': None, 'name': None, 'v': 15} + def test_files(): # empty file: diff --git a/unittests/test_issues.py b/unittests/test_issues.py index 3b0117b28c1300ea1eb0919fce02e3881c2ab025..ed125df9103c8f9c9a69fe8632265d3f38c377dc 100644 --- a/unittests/test_issues.py +++ b/unittests/test_issues.py @@ -93,6 +93,12 @@ def test_issue_128(): assert prop_list.value == [now, now] +def test_parse_datatype(): + """No infinite recursion.""" + from linkahead.common.models import _parse_value + assert 1 == _parse_value("labels0", "1") + + def test_issue_73(): """ Test to_xml infinite recursion handling with cross- and self-references.