From fd55ecfdefeb8ffb7096312bd9832b40b6644865 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Henrik=20tom=20W=C3=B6rden?= <h.tomwoerden@indiscale.com>
Date: Thu, 14 Mar 2024 20:08:46 +0100
Subject: [PATCH 01/10] MAINT: remove error when comparing different roles

---
 src/linkahead/apiutils.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/linkahead/apiutils.py b/src/linkahead/apiutils.py
index e2ed0fac..06547198 100644
--- a/src/linkahead/apiutils.py
+++ b/src/linkahead/apiutils.py
@@ -215,9 +215,9 @@ def compare_entities(old_entity: Entity, new_entity: Entity, compare_referenced_
     if old_entity is new_entity:
         return (olddiff, newdiff)
 
-    if type(old_entity) is not type(new_entity):
-        raise ValueError(
-            "Comparison of different Entity types is not supported.")
+    #if type(old_entity) is not type(new_entity):
+    #    raise ValueError(
+    #        "Comparison of different Entity types is not supported.")
 
     for attr in SPECIAL_ATTRIBUTES:
         try:
-- 
GitLab


From 5f1b683541119becabfffb44a0dd7be3b58f5458 Mon Sep 17 00:00:00 2001
From: Daniel Hornung <d.hornung@indiscale.com>
Date: Mon, 25 Mar 2024 10:13:51 +0100
Subject: [PATCH 02/10] DOC: Typo

---
 src/linkahead/common/models.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py
index 6f6e4c8f..d7bc7a47 100644
--- a/src/linkahead/common/models.py
+++ b/src/linkahead/common/models.py
@@ -2286,7 +2286,7 @@ class _Properties(list):
 
 
 class _ParentList(list):
-    # TODO unclear why this class is private. Isn't it use full for users?
+    # TODO unclear why this class is private. Isn't it useful for users?
 
     def _get_entity_by_cuid(self, cuid):
         '''
-- 
GitLab


From 6a39b7348ef05682e98e2aa6a7a7506c63d66bcf Mon Sep 17 00:00:00 2001
From: Daniel <d.hornung@indiscale.com>
Date: Thu, 2 May 2024 14:22:22 +0200
Subject: [PATCH 03/10] WIP: Plain json serialization for high level api.

---
 src/linkahead/high_level_api.py | 91 ++++++++++++++++++++-------------
 1 file changed, 56 insertions(+), 35 deletions(-)

diff --git a/src/linkahead/high_level_api.py b/src/linkahead/high_level_api.py
index 18d219c7..5b1682f3 100644
--- a/src/linkahead/high_level_api.py
+++ b/src/linkahead/high_level_api.py
@@ -471,8 +471,7 @@ class CaosDBPythonEntity(object):
 
         if isinstance(att, list):
             return att
-        else:
-            return [att]
+        return [att]
 
     def add_parent(self, parent: Union[
             CaosDBPythonUnresolvedParent, "CaosDBPythonRecordType", str]):
@@ -679,53 +678,68 @@ class CaosDBPythonEntity(object):
 
         return entity
 
-    def serialize(self, without_metadata: bool = False, visited: dict = None):
-        """
-        Serialize necessary information into a dict.
-
-        without_metadata: bool
-                          If True don't set the metadata field in order to increase
-                          readability. Not recommended if deserialization is needed.
+    def serialize(self, without_metadata: bool = None, plain_json: bool = False,
+                  visited: dict = None):
+        """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.  This implies ``without_metadata = True``.
         """
+        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:
@@ -735,30 +749,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):
-- 
GitLab


From da7a10d5124a02fee57ee86cef9fac4db626206f Mon Sep 17 00:00:00 2001
From: Daniel <d.hornung@indiscale.com>
Date: Fri, 3 May 2024 13:16:55 +0200
Subject: [PATCH 04/10] MAINT: Formatting.

---
 src/linkahead/high_level_api.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/linkahead/high_level_api.py b/src/linkahead/high_level_api.py
index 5b1682f3..cdbd81e3 100644
--- a/src/linkahead/high_level_api.py
+++ b/src/linkahead/high_level_api.py
@@ -44,7 +44,8 @@ from .common.datatype import (BOOLEAN, DATETIME, DOUBLE, FILE, INTEGER,
                               REFERENCE, TEXT, get_list_datatype,
                               is_list_datatype, is_reference)
 
-warnings.warn("""EXPERIMENTAL! The high_level_api module is experimental and may be changed or
+warnings.warn("""
+EXPERIMENTAL! The high_level_api module is experimental and may be changed or
 removed in the future. Its purpose is to give an impression on how the Python client user interface
 might be changed.""")
 
-- 
GitLab


From fd0650aef3de47be12112937a09ca0380c8c1bb2 Mon Sep 17 00:00:00 2001
From: Daniel <d.hornung@indiscale.com>
Date: Wed, 22 May 2024 14:00:29 +0200
Subject: [PATCH 05/10] FIX: No infinite recursion for 1-character strings.

---
 src/linkahead/common/models.py | 4 ++--
 unittests/test_issues.py       | 6 ++++++
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py
index 483c5231..9df9788e 100644
--- a/src/linkahead/common/models.py
+++ b/src/linkahead/common/models.py
@@ -1709,8 +1709,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/unittests/test_issues.py b/unittests/test_issues.py
index 7472f710..ba934009 100644
--- a/unittests/test_issues.py
+++ b/unittests/test_issues.py
@@ -64,3 +64,9 @@ def test_issue_156():
     # </ParentList>
     assert value is project
     assert parents[0].name == "RTName"
+
+
+def test_parse_datatype():
+    """No infinite recursion."""
+    from linkahead.common.models import _parse_value
+    assert 1 == _parse_value("labels0", "1")
-- 
GitLab


From b1a00123707c72e256f97fb7ca34aff7ee97401c Mon Sep 17 00:00:00 2001
From: Daniel <d.hornung@indiscale.com>
Date: Mon, 9 Dec 2024 16:59:32 +0100
Subject: [PATCH 06/10] WIP: Better diff in case of different types.

---
 src/linkahead/apiutils.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/linkahead/apiutils.py b/src/linkahead/apiutils.py
index 1aa127d3..b0b65d8d 100644
--- a/src/linkahead/apiutils.py
+++ b/src/linkahead/apiutils.py
@@ -291,6 +291,7 @@ 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?g
     if type(entity0) is not type(entity1):
         raise ValueError(
             "Comparison of different Entity types is not supported.")
-- 
GitLab


From b70d0f98bce6f7accfaffb9a2f159605d5d710df Mon Sep 17 00:00:00 2001
From: Daniel <d.hornung@indiscale.com>
Date: Mon, 9 Dec 2024 17:04:46 +0100
Subject: [PATCH 07/10] WIP: Better diff in case of different types.

---
 src/linkahead/apiutils.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/linkahead/apiutils.py b/src/linkahead/apiutils.py
index b0b65d8d..53336a4a 100644
--- a/src/linkahead/apiutils.py
+++ b/src/linkahead/apiutils.py
@@ -291,10 +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?g
+    # 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:
-- 
GitLab


From 49382a8fa5f0be460fd3fe34b7718021d25cf336 Mon Sep 17 00:00:00 2001
From: Daniel <d.hornung@indiscale.com>
Date: Tue, 8 Apr 2025 11:13:20 +0200
Subject: [PATCH 08/10] STYLE: Whitespace only.

---
 src/linkahead/high_level_api.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/linkahead/high_level_api.py b/src/linkahead/high_level_api.py
index 44686a42..a835fbb3 100644
--- a/src/linkahead/high_level_api.py
+++ b/src/linkahead/high_level_api.py
@@ -688,9 +688,11 @@ class CaosDBPythonEntity(object):
 
 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
-- 
GitLab


From af843d65c6de11fea91f01728d2659c18d312b8f Mon Sep 17 00:00:00 2001
From: Daniel <d.hornung@indiscale.com>
Date: Tue, 8 Apr 2025 14:36:30 +0200
Subject: [PATCH 09/10] TEST: Added unit test for high level api serialization.

---
 unittests/test_high_level_api.py | 29 +++++++++++++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/unittests/test_high_level_api.py b/unittests/test_high_level_api.py
index 82c1a5ca..e35dc678 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:
-- 
GitLab


From 70588ba60165370b1262581b1fc620e3bbde3d94 Mon Sep 17 00:00:00 2001
From: Daniel <d.hornung@indiscale.com>
Date: Tue, 8 Apr 2025 14:40:34 +0200
Subject: [PATCH 10/10] DOCS: More documentation for the high level api
 serialization.

---
 CHANGELOG.md                    |  2 ++
 src/linkahead/high_level_api.py | 28 ++++++++++++++++++----------
 2 files changed, 20 insertions(+), 10 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 20f2498a..33142766 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/high_level_api.py b/src/linkahead/high_level_api.py
index a835fbb3..a0137dac 100644
--- a/src/linkahead/high_level_api.py
+++ b/src/linkahead/high_level_api.py
@@ -683,20 +683,28 @@ class CaosDBPythonEntity(object):
         return entity
 
     def serialize(self, without_metadata: bool = None, plain_json: bool = False,
-                  visited: dict = None):
+                  visited: dict = None) -> dict:
         """Serialize necessary information into a dict.
 
-Parameters
-----------
+        Parameters
+        ----------
 
-without_metadata: bool, optional
-  If True don't set the metadata field in order to increase
-  readability. Not recommended if deserialization is needed.
+        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.  This implies ``without_metadata = True``.
+        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
+        -------
+
+        out: dict
+          A dict corresponding to this entity.
+        ``.
         """
         if plain_json:
             if without_metadata is None:
-- 
GitLab