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/33] 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/33] 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/33] 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/33] 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/33] 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 e7efcb316825a0b788cb8dc375792372aeaf7424 Mon Sep 17 00:00:00 2001
From: Daniel Hornung <d.hornung@indiscale.com>
Date: Wed, 18 Sep 2024 14:43:52 +0200
Subject: [PATCH 06/33] MAINT: Added deprecation warning to DropOffBox related
 options.

---
 src/linkahead/common/models.py | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py
index a8144286..0cc0f11e 100644
--- a/src/linkahead/common/models.py
+++ b/src/linkahead/common/models.py
@@ -2279,12 +2279,12 @@ class File(Record):
         server's "caosroot" file system.
     @param file: A local path or python file object.  The file designated by
         this argument will be uploaded to the server via HTTP.
-    @param pickup: A file/folder in the DropOffBox (the server will move that
+    @param pickup: Deprecated: A file/folder in the DropOffBox (the server will move that
         file into its "caosroot" file system).
     @param thumbnail: (Local) filename to a thumbnail for this file.
     @param properties: A list of properties for this file record. @todo is this
         implemented?
-    @param from_location: Deprecated, use `pickup` instead.
+    @param from_location: Deprecated x 2, use `pickup` instead.
 
     """
 
@@ -2311,10 +2311,12 @@ class File(Record):
         self.thumbnail = thumbnail
 
         self.pickup = pickup
+        if self.pickup is not None:
+            warn(DeprecationWarning("The DropOffBox is deprecated, do not use `pickup`."))
 
         if from_location is not None:
             warn(DeprecationWarning(
-                "Param `from_location` is deprecated, use `pickup instead`."))
+                "Param `from_location` is deprecated, as everything DropOffBox related."))
 
         if self.pickup is None:
             self.pickup = from_location
-- 
GitLab


From 2600dacb6a3193f90d6174c84d29229a90bd2776 Mon Sep 17 00:00:00 2001
From: Daniel Hornung <d.hornung@indiscale.com>
Date: Wed, 18 Sep 2024 15:40:40 +0200
Subject: [PATCH 07/33] MAINT: Removced DropOffBox class.

---
 src/linkahead/__init__.py      |  2 +-
 src/linkahead/common/models.py | 36 ----------------------------------
 2 files changed, 1 insertion(+), 37 deletions(-)

diff --git a/src/linkahead/__init__.py b/src/linkahead/__init__.py
index cd54f8f4..2c34c252 100644
--- a/src/linkahead/__init__.py
+++ b/src/linkahead/__init__.py
@@ -42,7 +42,7 @@ from .common.datatype import (BOOLEAN, DATETIME, DOUBLE, FILE, INTEGER, LIST,
                               REFERENCE, TEXT)
 # Import of the basic  API classes:
 from .common.models import (ACL, ALL, FIX, NONE, OBLIGATORY, RECOMMENDED,
-                            SUGGESTED, Container, DropOffBox, Entity, File,
+                            SUGGESTED, Container, Entity, File,
                             Info, Message, Permissions, Property, Query,
                             QueryTemplate, Record, RecordType, delete,
                             execute_query, get_global_acl,
diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py
index 0cc0f11e..c4b4d15e 100644
--- a/src/linkahead/common/models.py
+++ b/src/linkahead/common/models.py
@@ -4999,42 +4999,6 @@ def execute_query(
                          cache=cache, page_length=page_length)
 
 
-class DropOffBox(list):
-    def __init__(self, *args, **kwargs):
-        warn(DeprecationWarning("The DropOffBox is deprecated and will be removed in future."))
-        super().__init__(*args, **kwargs)
-
-    path = None
-
-    def sync(self):
-        c = get_connection()
-        _log_request("GET: Info")
-        http_response = c.retrieve(["Info"])
-        body = http_response.read()
-        _log_response(body)
-
-        xml = etree.fromstring(body)
-
-        for child in xml:
-            if child.tag.lower() == "stats":
-                infoelem = child
-
-                break
-
-        for child in infoelem:
-            if child.tag.lower() == "dropoffbox":
-                dropoffboxelem = child
-
-                break
-        del self[:]
-        self.path = dropoffboxelem.get('path')
-
-        for f in dropoffboxelem:
-            self.append(f.get('path'))
-
-        return self
-
-
 class UserInfo():
     """User information from a server response.
 
-- 
GitLab


From 85d209eb825e8192c8e18527c0ea8777d1a79d8f Mon Sep 17 00:00:00 2001
From: Daniel Hornung <d.hornung@indiscale.com>
Date: Wed, 25 Sep 2024 16:09:55 +0200
Subject: [PATCH 08/33] WIP: DropOffBox removal

---
 CHANGELOG.md                   |  2 ++
 src/linkahead/common/models.py | 35 +---------------------------------
 2 files changed, 3 insertions(+), 34 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5b792cc1..78b4b206 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Removed ###
 
+- `DropOffBox` class and related parameters (`pickup` for file uploading).
+
 ### Fixed ###
 
 ### Security ###
diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py
index c4b4d15e..6683ed11 100644
--- a/src/linkahead/common/models.py
+++ b/src/linkahead/common/models.py
@@ -358,17 +358,6 @@ class Entity:
     def file(self, new_file):
         self.__file = new_file
 
-    @property
-    def pickup(self):
-        if self.__pickup is not None or self._wrapped_entity is None:
-            return self.__pickup
-
-        return self._wrapped_entity.pickup
-
-    @pickup.setter
-    def pickup(self, new_pickup):
-        self.__pickup = new_pickup
-
     def grant(
         self,
         realm: Optional[str] = None,
@@ -2263,8 +2252,7 @@ class File(Record):
     """This class represents LinkAhead's file entities.
 
     For inserting a new file to the server, `path` gives the new location, and
-    (exactly?) one of `file` and `pickup` should (must?) be given to specify the
-    source of the file.
+    `file` specifies the source of the file.
 
     Symlinking from the "extroot" file system is not supported by this API yet,
     it can be done manually using the `InsertFilesInDir` flag.  For sample code,
@@ -2279,13 +2267,9 @@ class File(Record):
         server's "caosroot" file system.
     @param file: A local path or python file object.  The file designated by
         this argument will be uploaded to the server via HTTP.
-    @param pickup: Deprecated: A file/folder in the DropOffBox (the server will move that
-        file into its "caosroot" file system).
     @param thumbnail: (Local) filename to a thumbnail for this file.
     @param properties: A list of properties for this file record. @todo is this
         implemented?
-    @param from_location: Deprecated x 2, use `pickup` instead.
-
     """
 
     def __init__(
@@ -2295,9 +2279,7 @@ class File(Record):
         description: Optional[str] = None,  # @ReservedAssignment
         path: Optional[str] = None,
         file: Union[str, TextIO, None] = None,
-        pickup: Optional[str] = None,  # @ReservedAssignment
         thumbnail: Optional[str] = None,
-        from_location=None,
     ):
         Record.__init__(self, id=id, name=name, description=description)
         self.role = "File"
@@ -2310,17 +2292,6 @@ class File(Record):
         self.file = file
         self.thumbnail = thumbnail
 
-        self.pickup = pickup
-        if self.pickup is not None:
-            warn(DeprecationWarning("The DropOffBox is deprecated, do not use `pickup`."))
-
-        if from_location is not None:
-            warn(DeprecationWarning(
-                "Param `from_location` is deprecated, as everything DropOffBox related."))
-
-        if self.pickup is None:
-            self.pickup = from_location
-
     def to_xml(
         self,
         xml: Optional[etree._Element] = None,
@@ -3975,8 +3946,6 @@ class Container(list):
 
             if hasattr(entity, '_upload') and entity._upload is not None:
                 entity_xml.set("upload", entity._upload)
-            elif hasattr(entity, 'pickup') and entity.pickup is not None:
-                entity_xml.set("pickup", entity.pickup)
 
             insert_xml.append(entity_xml)
 
@@ -4149,8 +4118,6 @@ class Container(list):
 
             if hasattr(entity, '_upload') and entity._upload is not None:
                 entity_xml.set("upload", entity._upload)
-            elif hasattr(entity, 'pickup') and entity.pickup is not None:
-                entity_xml.set("pickup", entity.pickup)
             insert_xml.append(entity_xml)
 
         if len(self) > 0 and len(insert_xml) < 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 09/33] 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 10/33] 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 0dbb1a1943ce1c6984ce296462db0578a7e0b4b9 Mon Sep 17 00:00:00 2001
From: Florian Spreckelsen <f.spreckelsen@indiscale.com>
Date: Tue, 14 Jan 2025 15:50:42 +0100
Subject: [PATCH 11/33] REL: Begin next release cycle

---
 CHANGELOG.md    | 16 ++++++++++++++++
 setup.py        |  4 ++--
 src/doc/conf.py |  4 ++--
 3 files changed, 20 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f56bc3ab..e780ce25 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [Unreleased] ##
+
+### Added ###
+
+### Changed ###
+
+### Deprecated ###
+
+### Removed ###
+
+### Fixed ###
+
+### Security ###
+
+### Documentation ###
+
 ## [0.17.0] - 2025-01-14 ##
 
 ### Added ###
diff --git a/setup.py b/setup.py
index 75bcf0c7..ab8555b8 100755
--- a/setup.py
+++ b/setup.py
@@ -46,10 +46,10 @@ from setuptools import find_packages, setup
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ########################################################################
 
-ISRELEASED = True
+ISRELEASED = False
 MAJOR = 0
 MINOR = 17
-MICRO = 0
+MICRO = 1
 # Do not tag as pre-release until this commit
 # https://github.com/pypa/packaging/pull/515
 # has made it into a release. Probably we should wait for pypa/packaging>=21.4
diff --git a/src/doc/conf.py b/src/doc/conf.py
index 65600678..ce1aa8ff 100644
--- a/src/doc/conf.py
+++ b/src/doc/conf.py
@@ -29,10 +29,10 @@ copyright = '2024, IndiScale GmbH'
 author = 'Daniel Hornung'
 
 # The short X.Y version
-version = '0.17.0'
+version = '0.17.1'
 # The full version, including alpha/beta/rc tags
 # release = '0.5.2-rc2'
-release = '0.17.0'
+release = '0.17.1-dev'
 
 
 # -- General configuration ---------------------------------------------------
-- 
GitLab


From cc8c254e8145f45720a29f9fd58f3e4f6542100a Mon Sep 17 00:00:00 2001
From: Florian Spreckelsen <f.spreckelsen@indiscale.com>
Date: Thu, 16 Jan 2025 13:05:55 +0100
Subject: [PATCH 12/33] DOC: Fix and extend docstrings of
 linkahead.utils.register_tests

---
 src/linkahead/utils/register_tests.py | 72 +++++++++++++++++----------
 1 file changed, 47 insertions(+), 25 deletions(-)

diff --git a/src/linkahead/utils/register_tests.py b/src/linkahead/utils/register_tests.py
index 6909544f..66fd4553 100644
--- a/src/linkahead/utils/register_tests.py
+++ b/src/linkahead/utils/register_tests.py
@@ -18,44 +18,62 @@
 #
 # You should have received a copy of the GNU Affero General Public License
 # along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-import linkahead as db
-from linkahead import administration as admin
-
-"""
-This module implements a registration procedure for integration tests which
+"""This module implements a registration procedure for integration tests which
 need a running LinkAhead instance.
 
-It ensures that tests do not accidentally overwrite data in real LinkAhead
-instances, as it checks whether the running LinkAhead instance is actually the
-correct one, that
-should be used for these tests.
-
-The test files have to define a global variable TEST_KEY which must be unique
-for each test using
+It ensures that tests do not accidentally overwrite data in real
+LinkAhead instances, as it checks whether the running LinkAhead
+instance is actually the correct one, that should be used for these
+tests.
 
-set_test_key("ABCDE")
+The test files have to define a global variable ``TEST_KEY`` which
+must be unique for each test using
+:py:meth:`~linkahead.utils.register_tests.set_test_key`.
 
 The test procedure (invoked by pytest) checks whether a registration
 information is stored in one of the server properties or otherwise
-- offers to register this test in the currently running database ONLY if this
-  is empty.
+
+- offers to register this test in the currently running database ONLY if this is
+  empty.
 - fails otherwise with a RuntimeError
 
-NOTE: you probably need to use pytest with the -s option to be able to
-      register the test interactively. Otherwise, the server property has to be
-      set before server start-up in the server.conf of the LinkAhead server.
+.. note::
+
+    you probably need to use pytest with the -s option to be able to
+    register the test interactively. Otherwise, the server property
+    has to be set before server start-up in the server.conf of the
+    LinkAhead server.
 
 This module is intended to be used with pytest.
 
-There is a pytest fixture "clear_database" that performs the above mentioned
-checks and clears the database in case of success.
+There is a pytest fixture
+:py:meth:`~linkahead.utils.register_tests.clear_database` that
+performs the above mentioned checks and clears the database in case of
+success.
+
 """
 
+import linkahead as db
+from linkahead import administration as admin
+
 TEST_KEY = None
 
 
-def set_test_key(KEY):
+def set_test_key(KEY: str):
+    """Set the global ``TEST_KEY`` variable to `KEY`. Afterwards, if
+    `KEY` matches the ``_CAOSDB_INTEGRATION_TEST_SUITE_KEY`` server
+    environment variable, mehtods like :py:meth:`clear_database` can
+    be used. Call this function in the beginning of your test file.
+
+    Parameters
+    ----------
+    KEY : str
+        key with which the test using this function is registered and
+        which is checked against the
+        ``_CAOSDB_INTEGRATION_TEST_SUITE_KEY`` server environment
+        variable.
+
+    """
     global TEST_KEY
     TEST_KEY = KEY
 
@@ -122,10 +140,14 @@ try:
 
     @pytest.fixture
     def clear_database():
-        """Remove Records, RecordTypes, Properties, and Files ONLY IF the LinkAhead
-        server the current connection points to was registered with the appropriate key.
+        """Remove Records, RecordTypes, Properties, and Files ONLY IF
+        the LinkAhead server the current connection points to was
+        registered with the appropriate key using
+        :py:meth:`set_test_key`.
+
+        PyTestInfo Records and the corresponding RecordType and
+        Property are preserved.
 
-        PyTestInfo Records and the corresponding RecordType and Property are preserved.
         """
         _assure_test_is_registered()
         yield _clear_database()  # called before the test function
-- 
GitLab


From 90a8df2132508069cb949cb40ba4f63e0254b4ce Mon Sep 17 00:00:00 2001
From: Florian Spreckelsen <f.spreckelsen@indiscale.com>
Date: Thu, 16 Jan 2025 13:07:55 +0100
Subject: [PATCH 13/33] DOC: Update changelog

---
 CHANGELOG.md | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e780ce25..23ddf860 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Documentation ###
 
+* [#78](https://gitlab.com/linkahead/linkahead-pylib/-/issues/78) Fix
+  and extend test-registration docstrings.
+
 ## [0.17.0] - 2025-01-14 ##
 
 ### Added ###
-- 
GitLab


From 6b4062059f34c8cdc87172487f9604a9d7b5c42d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Henrik=20tom=20W=C3=B6rden?= <h.tomwoerden@indiscale.com>
Date: Thu, 16 Jan 2025 17:12:15 +0100
Subject: [PATCH 14/33] FIX: run linting on all linkahead

---
 Makefile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Makefile b/Makefile
index 21ea40ac..7490c5d5 100644
--- a/Makefile
+++ b/Makefile
@@ -40,7 +40,7 @@ style:
 .PHONY: style
 
 lint:
-	pylint --unsafe-load-any-extension=y -d all -e E,F src/linkahead/common
+	pylint --unsafe-load-any-extension=y -d all -e E,F src/linkahead
 .PHONY: lint
 
 mypy:
-- 
GitLab


From f6c3fba01eaad2f2e282a423e775ea60b53477c4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Henrik=20tom=20W=C3=B6rden?= <h.tomwoerden@indiscale.com>
Date: Fri, 24 Jan 2025 10:52:05 +0100
Subject: [PATCH 15/33] ENH: add convenience functions

`value_matches_versionid`, `get_id_from_versionid` and `get_versionid`
---
 CHANGELOG.md                   |  1 +
 src/linkahead/common/models.py | 21 +++++++++++++++++++++
 unittests/test_entity.py       | 28 +++++++++++++++++++++++++++-
 3 files changed, 49 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 23ddf860..20f2498a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ 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`
 
 ### Changed ###
 
diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py
index 75b03b70..0912647a 100644
--- a/src/linkahead/common/models.py
+++ b/src/linkahead/common/models.py
@@ -505,6 +505,9 @@ class Entity:
 
         return self
 
+    def get_versionid(self):
+        return str(self.id)+"@"+str(self.version.id)
+
     def get_importance(self, property):  # @ReservedAssignment
         """Get the importance of a given property regarding this entity."""
 
@@ -1954,6 +1957,7 @@ class QueryTemplate():
         return len(self.get_errors()) > 0
 
 
+
 class Parent(Entity):
     """The parent entities."""
 
@@ -2126,6 +2130,12 @@ class Property(Entity):
             return is_reference(self.datatype)
 
 
+    def value_matches_versionid(self):
+        return value_matches_versionid(self.value)
+
+    def get_id_from_versionid_value(self):
+        return get_id_from_versionid(self.value)
+
 class Message(object):
 
     def __init__(
@@ -5670,3 +5680,14 @@ def _filter_entity_list_by_identity(listobject: list[Entity],
             if pid_none and name_match:
                 matches.append(candidate)
     return matches
+
+def value_matches_versionid(value: Union[int, str]):
+    if isinstance(value, int):
+        return False
+    if not isinstance(value, str):
+        raise ValueError(f"A reference value needs to be int or str. It was {type(value)}. "
+                         "Did you call value_matches_versionid on a non reference value?")
+    return "@" in value
+
+def get_id_from_versionid(versionid: str):
+    return versionid.split("@")[0]
diff --git a/unittests/test_entity.py b/unittests/test_entity.py
index 855e5a39..722930b2 100644
--- a/unittests/test_entity.py
+++ b/unittests/test_entity.py
@@ -30,7 +30,9 @@ import linkahead
 from linkahead import (INTEGER, Entity, Parent, Property, Record, RecordType,
                        configure_connection)
 import warnings
-from linkahead.common.models import SPECIAL_ATTRIBUTES
+from linkahead.common.models import (SPECIAL_ATTRIBUTES, get_id_from_versionid,
+value_matches_versionid)
+from linkahead.common.versioning import Version
 from linkahead.connection.mockup import MockUpServerConnection
 from lxml import etree
 from pytest import raises
@@ -295,3 +297,27 @@ def test_filter_by_identity():
         t.parents.filter(pid=234)
         assert issubclass(w[-1].category, DeprecationWarning)
         assert "This function was renamed" in str(w[-1].message)
+
+
+def test_value_matches_versionid():
+    assert value_matches_versionid(234) is False, "integer is no version id"
+    assert value_matches_versionid("234") is False, ("string that only contains an integer is no "
+                                                "version id")
+    assert value_matches_versionid("234@bfe1a42cb37aae8ac625a757715d38814c274158") is True, (
+        "integer is no version id") is True
+    with raises(ValueError):
+        value_matches_versionid(234.0)
+    p = Property(value=234)
+    assert p.value_matches_versionid() is False
+    p = Property(value="234@bfe1a42cb37aae8ac625a757715d38814c274158")
+    assert p.value_matches_versionid() is True
+
+def test_get_id_from_versionid():
+    assert get_id_from_versionid("234@bfe1a42cb37aae8ac625a757715d38814c274158") == "234"
+    p = Property(value="234@bfe1a42cb37aae8ac625a757715d38814c274158")
+    assert p.get_id_from_versionid_value() == "234"
+
+def test_get_versionid():
+    e = Entity(id=234)
+    e.version = Version(id="bfe1a42cb37aae8ac625a757715d38814c274158")
+    assert e.get_versionid() =="234@bfe1a42cb37aae8ac625a757715d38814c274158"
-- 
GitLab


From 85d807d332542485f12a3cdfd235a6b587f54c85 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Henrik=20tom=20W=C3=B6rden?= <h.tomwoerden@indiscale.com>
Date: Fri, 24 Jan 2025 11:05:06 +0100
Subject: [PATCH 16/33] DOC: add docstrings

---
 src/linkahead/common/models.py | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py
index 0912647a..12d36179 100644
--- a/src/linkahead/common/models.py
+++ b/src/linkahead/common/models.py
@@ -506,6 +506,7 @@ class Entity:
         return self
 
     def get_versionid(self):
+        """Returns the concatenation of ID and version"""
         return str(self.id)+"@"+str(self.version.id)
 
     def get_importance(self, property):  # @ReservedAssignment
@@ -2131,9 +2132,11 @@ class Property(Entity):
 
 
     def value_matches_versionid(self):
+        """Returns True if the value matches the pattern <id>@<version>"""
         return value_matches_versionid(self.value)
 
     def get_id_from_versionid_value(self):
+        """Returns the ID part of the versionid with the pattern <id>@<version>"""
         return get_id_from_versionid(self.value)
 
 class Message(object):
@@ -5682,6 +5685,7 @@ def _filter_entity_list_by_identity(listobject: list[Entity],
     return matches
 
 def value_matches_versionid(value: Union[int, str]):
+    """Returns True if the value matches the pattern <id>@<version>"""
     if isinstance(value, int):
         return False
     if not isinstance(value, str):
@@ -5690,4 +5694,5 @@ def value_matches_versionid(value: Union[int, str]):
     return "@" in value
 
 def get_id_from_versionid(versionid: str):
+    """Returns the ID part of the versionid with the pattern <id>@<version>"""
     return versionid.split("@")[0]
-- 
GitLab


From 1a8c63724874076b633cce93e7d636c33ff0c125 Mon Sep 17 00:00:00 2001
From: Florian Spreckelsen <f.spreckelsen@indiscale.com>
Date: Fri, 24 Jan 2025 14:08:53 +0100
Subject: [PATCH 17/33] STY: autopep8'd

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

diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py
index 12d36179..bfe43b01 100644
--- a/src/linkahead/common/models.py
+++ b/src/linkahead/common/models.py
@@ -1958,7 +1958,6 @@ class QueryTemplate():
         return len(self.get_errors()) > 0
 
 
-
 class Parent(Entity):
     """The parent entities."""
 
@@ -2130,7 +2129,6 @@ class Property(Entity):
         else:
             return is_reference(self.datatype)
 
-
     def value_matches_versionid(self):
         """Returns True if the value matches the pattern <id>@<version>"""
         return value_matches_versionid(self.value)
@@ -2139,6 +2137,7 @@ class Property(Entity):
         """Returns the ID part of the versionid with the pattern <id>@<version>"""
         return get_id_from_versionid(self.value)
 
+
 class Message(object):
 
     def __init__(
@@ -5684,6 +5683,7 @@ def _filter_entity_list_by_identity(listobject: list[Entity],
                 matches.append(candidate)
     return matches
 
+
 def value_matches_versionid(value: Union[int, str]):
     """Returns True if the value matches the pattern <id>@<version>"""
     if isinstance(value, int):
@@ -5693,6 +5693,7 @@ def value_matches_versionid(value: Union[int, str]):
                          "Did you call value_matches_versionid on a non reference value?")
     return "@" in value
 
+
 def get_id_from_versionid(versionid: str):
     """Returns the ID part of the versionid with the pattern <id>@<version>"""
     return versionid.split("@")[0]
diff --git a/unittests/test_entity.py b/unittests/test_entity.py
index 722930b2..2f413717 100644
--- a/unittests/test_entity.py
+++ b/unittests/test_entity.py
@@ -31,7 +31,7 @@ from linkahead import (INTEGER, Entity, Parent, Property, Record, RecordType,
                        configure_connection)
 import warnings
 from linkahead.common.models import (SPECIAL_ATTRIBUTES, get_id_from_versionid,
-value_matches_versionid)
+                                     value_matches_versionid)
 from linkahead.common.versioning import Version
 from linkahead.connection.mockup import MockUpServerConnection
 from lxml import etree
@@ -302,7 +302,7 @@ def test_filter_by_identity():
 def test_value_matches_versionid():
     assert value_matches_versionid(234) is False, "integer is no version id"
     assert value_matches_versionid("234") is False, ("string that only contains an integer is no "
-                                                "version id")
+                                                     "version id")
     assert value_matches_versionid("234@bfe1a42cb37aae8ac625a757715d38814c274158") is True, (
         "integer is no version id") is True
     with raises(ValueError):
@@ -312,12 +312,14 @@ def test_value_matches_versionid():
     p = Property(value="234@bfe1a42cb37aae8ac625a757715d38814c274158")
     assert p.value_matches_versionid() is True
 
+
 def test_get_id_from_versionid():
     assert get_id_from_versionid("234@bfe1a42cb37aae8ac625a757715d38814c274158") == "234"
     p = Property(value="234@bfe1a42cb37aae8ac625a757715d38814c274158")
     assert p.get_id_from_versionid_value() == "234"
 
+
 def test_get_versionid():
     e = Entity(id=234)
     e.version = Version(id="bfe1a42cb37aae8ac625a757715d38814c274158")
-    assert e.get_versionid() =="234@bfe1a42cb37aae8ac625a757715d38814c274158"
+    assert e.get_versionid() == "234@bfe1a42cb37aae8ac625a757715d38814c274158"
-- 
GitLab


From 82ce5c08c7f35dedee7622660c28e211bf17db63 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Henrik=20tom=20W=C3=B6rden?= <h.tomwoerden@indiscale.com>
Date: Fri, 24 Jan 2025 14:34:35 +0100
Subject: [PATCH 18/33] FIX: remove value_matches_versionid and
 get_id_from_versionid_value member functions

---
 src/linkahead/common/models.py | 8 --------
 unittests/test_entity.py       | 6 ------
 2 files changed, 14 deletions(-)

diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py
index bfe43b01..8b8141be 100644
--- a/src/linkahead/common/models.py
+++ b/src/linkahead/common/models.py
@@ -2129,14 +2129,6 @@ class Property(Entity):
         else:
             return is_reference(self.datatype)
 
-    def value_matches_versionid(self):
-        """Returns True if the value matches the pattern <id>@<version>"""
-        return value_matches_versionid(self.value)
-
-    def get_id_from_versionid_value(self):
-        """Returns the ID part of the versionid with the pattern <id>@<version>"""
-        return get_id_from_versionid(self.value)
-
 
 class Message(object):
 
diff --git a/unittests/test_entity.py b/unittests/test_entity.py
index 2f413717..f2164d96 100644
--- a/unittests/test_entity.py
+++ b/unittests/test_entity.py
@@ -307,16 +307,10 @@ def test_value_matches_versionid():
         "integer is no version id") is True
     with raises(ValueError):
         value_matches_versionid(234.0)
-    p = Property(value=234)
-    assert p.value_matches_versionid() is False
-    p = Property(value="234@bfe1a42cb37aae8ac625a757715d38814c274158")
-    assert p.value_matches_versionid() is True
 
 
 def test_get_id_from_versionid():
     assert get_id_from_versionid("234@bfe1a42cb37aae8ac625a757715d38814c274158") == "234"
-    p = Property(value="234@bfe1a42cb37aae8ac625a757715d38814c274158")
-    assert p.get_id_from_versionid_value() == "234"
 
 
 def test_get_versionid():
-- 
GitLab


From 1459d6c612da49d21aefb59f57ba3674626007c3 Mon Sep 17 00:00:00 2001
From: "i.nueske" <i.nueske@indiscale.com>
Date: Fri, 28 Feb 2025 11:12:49 +0100
Subject: [PATCH 19/33] CI: Add python 3.14 to tests

---
 .gitlab-ci.yml | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index db600343..a773c677 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -101,13 +101,26 @@ unittest_py3.12:
   script: *python_test_script
 
 unittest_py3.13:
-  allow_failure: true
   tags: [ docker ]
   stage: test
   needs: [ ]
   image: python:3.13
   script: *python_test_script
 
+unittest_py3.14:
+  allow_failure: true   # remove on release
+  tags: [ docker ]
+  stage: test
+  needs: [ ]
+  image: python:3.14-rc
+  script:               # replace by '*python_test_script' on release
+    # Install cargo manually, source its env, and set it to accept 3.14 as interpreter
+    - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
+    - . "$HOME/.cargo/env"
+    - export PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1
+    # Continue normally
+    - *python_test_script
+
 # Trigger building of server image and integration tests
 trigger_build:
   stage: deploy
-- 
GitLab


From d1cf1b9e3b2853c0d353c582aff5f515ea2815ed Mon Sep 17 00:00:00 2001
From: "i.nueske" <i.nueske@indiscale.com>
Date: Tue, 11 Mar 2025 15:21:41 +0100
Subject: [PATCH 20/33] MNT: Change warnings.warn to logger.warning to enable
 warning suppression, and add it to the module docstring

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

diff --git a/src/linkahead/high_level_api.py b/src/linkahead/high_level_api.py
index 18d219c7..9aa59fb9 100644
--- a/src/linkahead/high_level_api.py
+++ b/src/linkahead/high_level_api.py
@@ -26,11 +26,12 @@
 # type: ignore
 """
 A high level API for accessing LinkAhead entities from within python.
+This module is experimental, and may be changed or removed in the future.
 
 This is refactored from apiutils.
 """
 
-import warnings
+import logging
 from dataclasses import dataclass, fields
 from datetime import datetime
 from typing import Any, Dict, List, Optional, Union
@@ -44,7 +45,10 @@ 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
+logger = logging.getLogger(__name__)
+
+
+logger.warning("""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 dc339849eaedfd911ce2fd321dbd09df8a2d5f07 Mon Sep 17 00:00:00 2001
From: "i.nueske" <i.nueske@indiscale.com>
Date: Tue, 1 Apr 2025 13:15:09 +0200
Subject: [PATCH 21/33] FIX: Always import CredentialsAuthenticator, ignore
 linting errors on version import

---
 src/linkahead/__init__.py              | 2 +-
 src/linkahead/connection/connection.py | 5 +++--
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/linkahead/__init__.py b/src/linkahead/__init__.py
index 567748e3..97203a20 100644
--- a/src/linkahead/__init__.py
+++ b/src/linkahead/__init__.py
@@ -55,7 +55,7 @@ from .utils.get_entity import (get_entity_by_id, get_entity_by_name,
                                get_entity_by_path)
 
 try:
-    from .version import version as __version__
+    from .version import version as __version_   # pylint: disable=import-error
 except ModuleNotFoundError:
     version = "uninstalled"
     __version__ = version
diff --git a/src/linkahead/connection/connection.py b/src/linkahead/connection/connection.py
index 74dd2317..1bee0b77 100644
--- a/src/linkahead/connection/connection.py
+++ b/src/linkahead/connection/connection.py
@@ -47,7 +47,7 @@ from ..exceptions import (ConfigurationError, HTTPClientError,
                           LoginFailedError)
 
 try:
-    from ..version import version
+    from ..version import version               # pylint: disable=import-error
 except ModuleNotFoundError:
     version = "uninstalled"
 
@@ -56,11 +56,12 @@ from .interface import CaosDBHTTPResponse, CaosDBServerConnection
 from .utils import make_uri_path, urlencode
 
 from typing import TYPE_CHECKING
+from .authentication.interface import CredentialsAuthenticator
 if TYPE_CHECKING:
     from typing import Optional, Any, Iterator, Union
     from requests.models import Response
     from ssl import _SSLMethod
-    from .authentication.interface import AbstractAuthenticator, CredentialsAuthenticator
+    from .authentication.interface import AbstractAuthenticator
 
 
 _LOGGER = logging.getLogger(__name__)
-- 
GitLab


From dfc66218e2d5c213344093d84f376a7116eb8521 Mon Sep 17 00:00:00 2001
From: "i.nueske" <i.nueske@indiscale.com>
Date: Tue, 1 Apr 2025 13:34:33 +0200
Subject: [PATCH 22/33] FIX: Ignore false positive linting warnings

---
 src/linkahead/connection/connection.py | 2 +-
 src/linkahead/connection/encode.py     | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/linkahead/connection/connection.py b/src/linkahead/connection/connection.py
index 1bee0b77..fe99b421 100644
--- a/src/linkahead/connection/connection.py
+++ b/src/linkahead/connection/connection.py
@@ -60,7 +60,7 @@ from .authentication.interface import CredentialsAuthenticator
 if TYPE_CHECKING:
     from typing import Optional, Any, Iterator, Union
     from requests.models import Response
-    from ssl import _SSLMethod
+    from ssl import _SSLMethod              # pylint: disable=no-name-in-module
     from .authentication.interface import AbstractAuthenticator
 
 
diff --git a/src/linkahead/connection/encode.py b/src/linkahead/connection/encode.py
index a7619780..0cbb0b69 100644
--- a/src/linkahead/connection/encode.py
+++ b/src/linkahead/connection/encode.py
@@ -384,7 +384,7 @@ class MultipartYielder(object):
 
     # since python 3
     def __next__(self):
-        return self.next()
+        return self.next()                     # pylint: disable=not-callable
 
     def next(self):
         """generator function to yield multipart/form-data representation of
-- 
GitLab


From d3b90636156c8d99bf9e6357387bf42e1bd3b1e2 Mon Sep 17 00:00:00 2001
From: "i.nueske" <i.nueske@indiscale.com>
Date: Tue, 1 Apr 2025 13:45:35 +0200
Subject: [PATCH 23/33] FIX: Revert accidental rename

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

diff --git a/src/linkahead/__init__.py b/src/linkahead/__init__.py
index 97203a20..ac8df123 100644
--- a/src/linkahead/__init__.py
+++ b/src/linkahead/__init__.py
@@ -55,7 +55,7 @@ from .utils.get_entity import (get_entity_by_id, get_entity_by_name,
                                get_entity_by_path)
 
 try:
-    from .version import version as __version_   # pylint: disable=import-error
+    from .version import version as __version__  # pylint: disable=import-error
 except ModuleNotFoundError:
     version = "uninstalled"
     __version__ = version
-- 
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 24/33] 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 25/33] 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 26/33] 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


From 7934fb3a7a638fe2eb32ef36a421f7e51fd645b4 Mon Sep 17 00:00:00 2001
From: Daniel <d.hornung@indiscale.com>
Date: Wed, 9 Apr 2025 15:16:58 +0200
Subject: [PATCH 27/33] DOCS: Small docstring fix.

---
 src/linkahead/common/models.py | 32 +++++++++++++++++---------------
 1 file changed, 17 insertions(+), 15 deletions(-)

diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py
index 6de27153..a3b4683a 100644
--- a/src/linkahead/common/models.py
+++ b/src/linkahead/common/models.py
@@ -2315,21 +2315,23 @@ class File(Record):
     look at `test_files.py` in the Python integration tests of the
     `load_files.py` script in the advanced user tools.
 
-    @param name: A name for this file record (That's an entity name - not to be
-        confused with the last segment of the files path).
-    @param id: An ID.
-    @param description: A description for this file record.
-    @param path: The complete path, including the file name, of the file in the
-        server's "caosroot" file system.
-    @param file: A local path or python file object.  The file designated by
-        this argument will be uploaded to the server via HTTP.
-    @param pickup: A file/folder in the DropOffBox (the server will move that
-        file into its "caosroot" file system).
-    @param thumbnail: (Local) filename to a thumbnail for this file.
-    @param properties: A list of properties for this file record. @todo is this
-        implemented?
-    @param from_location: Deprecated, use `pickup` instead.
-
+    @param name
+        A name for this file *Record* (That's an entity name - not to be confused with the last
+        segment of the files path).
+    @param id
+        An ID.
+    @param description
+        A description for this file record.
+    @param path
+        The complete path, including the file name, of the file in the server's "caosroot" file
+        system.
+    @param file
+        A local path or python file object.  The file designated by this argument will be uploaded
+        to the server via HTTP.
+    @param thumbnail
+        (Local) filename to a thumbnail for this file.
+    @param properties
+        A list of properties for this file record. @todo is this implemented?
     """
 
     def __init__(
-- 
GitLab


From 9639455f558fbc0120c4ac9a58de6810b974d996 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Henrik=20tom=20W=C3=B6rden?= <h.tomwoerden@indiscale.com>
Date: Thu, 10 Apr 2025 09:24:11 +0200
Subject: [PATCH 28/33] FIX: treat None case in depenency search

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

diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py
index a3b4683a..69ac403b 100644
--- a/src/linkahead/common/models.py
+++ b/src/linkahead/common/models.py
@@ -3824,6 +3824,8 @@ class Container(list):
                             is_being_referenced.add(prop.value)
                         elif is_list_datatype(prop_dt):
                             for list_item in prop.value:
+                                if list_item is None:
+                                    continue
                                 if isinstance(list_item, int):
                                     is_being_referenced.add(list_item)
                                 else:
-- 
GitLab


From cdd23d0ca8a9ff54e0bd1d21f2b5b152cd222b46 Mon Sep 17 00:00:00 2001
From: Daniel <d.hornung@indiscale.com>
Date: Tue, 15 Apr 2025 17:52:13 +0200
Subject: [PATCH 29/33] FEAT: `resolve_references` parameter

In `high_level_api.convert_to_python_object()`
---
 CHANGELOG.md                    |  3 ++-
 src/linkahead/high_level_api.py | 28 ++++++++++++++++------------
 2 files changed, 18 insertions(+), 13 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 33142766..35b6a480 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,7 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added ###
 
 - convenience functions `value_matches_versionid`, `get_id_from_versionid` and `get_versionid`
-- Parameter for high level API serialization to output a plain JSON.
+- High level API: Parameter for serialization to output a plain JSON.
+- High level API: Parameter to resolve references when converting to Python object.
 
 ### Changed ###
 
diff --git a/src/linkahead/high_level_api.py b/src/linkahead/high_level_api.py
index a0137dac..45839d9b 100644
--- a/src/linkahead/high_level_api.py
+++ b/src/linkahead/high_level_api.py
@@ -4,9 +4,10 @@
 #
 # Copyright (C) 2018 Research Group Biomedical Physics,
 # Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+# Copyright (C) 2020-2022,2025 IndiScale GmbH <info@indiscale.com>
 # Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
-# Copyright (C) 2020-2022 IndiScale GmbH <info@indiscale.com>
 # Copyright (C) 2022 Alexander Schlemmer <alexander.schlemmer@ds.mpg.de>
+# Copyright (C) 2025 Daniel Hornung <d.hornung@indiscale.com>
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as
@@ -21,8 +22,7 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program. If not, see <https://www.gnu.org/licenses/>.
 #
-# ** end header
-#
+
 # type: ignore
 """
 A high level API for accessing LinkAhead entities from within python.
@@ -974,26 +974,32 @@ def convert_to_entity(python_object):
 def convert_to_python_object(entity: Union[db.Container, db.Entity],
                              references: Optional[db.Container] = None,
                              visited: Optional[Dict[int,
-                                                    "CaosDBPythonEntity"]] = None):
+                                                    "CaosDBPythonEntity"]] = None,
+                             resolve_references: Optional[bool] = False,
+                             ):
     """
     Convert either a container of CaosDB entities or a single CaosDB entity
     into the high level representation.
 
-    The optional second parameter can be used
+    The optional ``references`` parameter can be used
     to resolve references that occur in the converted entities and resolve them
     to their correct representations. (Entities that are not found remain as
-    CaosDBPythonUnresolvedReferences.)
+    CaosDBPythonUnresolvedReferences, unless ``resolve_references`` is given and True.)
     """
     if isinstance(entity, db.Container):
         # Create a list of objects:
-        return [convert_to_python_object(i, references, visited) for i in entity]
+        return [convert_to_python_object(ent, references=references, visited=visited,
+                                         resolve_references=resolve_references) for ent in entity]
 
     # TODO: recursion problems?
-    return _single_convert_to_python_object(
+    converted = _single_convert_to_python_object(
         high_level_type_for_standard_type(entity)(),
         entity,
         references,
         visited)
+    if resolve_references:
+        converted.resolve_references(True, references)
+    return converted
 
 
 def new_high_level_entity(entity: db.RecordType,
@@ -1077,8 +1083,6 @@ def query(query: str,
 
     """
     res = db.execute_query(query)
-    objects = convert_to_python_object(res)
-    if resolve_references:
-        for obj in objects:
-            obj.resolve_references(True, references)
+    objects = convert_to_python_object(res, references=references,
+                                       resolve_references=resolve_references)
     return objects
-- 
GitLab


From 34f72e35e1f4a6d9c30877360552a45b919e7b8f Mon Sep 17 00:00:00 2001
From: Daniel <d.hornung@indiscale.com>
Date: Thu, 24 Apr 2025 08:58:32 +0200
Subject: [PATCH 30/33] FIX: Reverted some changes.

---
 src/linkahead/common/models.py | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py
index 0985d8f0..da46c563 100644
--- a/src/linkahead/common/models.py
+++ b/src/linkahead/common/models.py
@@ -338,6 +338,17 @@ class Entity:
     def file(self, new_file):
         self.__file = new_file
 
+    # FIXME Add test.
+    @property   # getter for _cuid
+    def cuid(self):
+        # Set if None?
+        return self._cuid
+
+    # FIXME Add test.
+    @property   # getter for _flags
+    def flags(self):
+        return self._flags.copy()   # for dict[str, str] shallow copy is enough
+
     def grant(
         self,
         realm: Optional[str] = None,
-- 
GitLab


From 10db17e97bc78d0183c87f23f9d4b082388c4981 Mon Sep 17 00:00:00 2001
From: Daniel <d.hornung@indiscale.com>
Date: Wed, 7 May 2025 09:34:46 +0200
Subject: [PATCH 31/33] DOCS: Added note to docstring.

---
 src/linkahead/common/models.py | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py
index da46c563..8d1afbac 100644
--- a/src/linkahead/common/models.py
+++ b/src/linkahead/common/models.py
@@ -3338,6 +3338,11 @@ class Container(list):
         Returns
         -------
         xml_element : etree._Element
+
+        Note
+        ----
+        Calling this method has the side effect that all entities without ID will get a negative
+        integer ID.
         """
         tmpid = 0
 
-- 
GitLab


From 17be5bafafc45084d5fd0dd131429de22af219ee Mon Sep 17 00:00:00 2001
From: Daniel <d.hornung@indiscale.com>
Date: Fri, 9 May 2025 13:35:28 +0200
Subject: [PATCH 32/33] DOCS: Fixed typo.

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

diff --git a/src/linkahead/exceptions.py b/src/linkahead/exceptions.py
index 7d4dc085..0904929c 100644
--- a/src/linkahead/exceptions.py
+++ b/src/linkahead/exceptions.py
@@ -190,7 +190,7 @@ class QueryNotUniqueError(BadQueryError):
 
 
 class EmptyUniqueQueryError(BadQueryError):
-    """A unique query or retrieve dound no result."""
+    """A unique query or retrieve found no result."""
 
 
 # ######################### Transaction errors #########################
-- 
GitLab


From 8952c3a8ca7bdd92fba8f8e994f839002b29eb25 Mon Sep 17 00:00:00 2001
From: Florian Spreckelsen <f.spreckelsen@indiscale.com>
Date: Tue, 27 May 2025 17:37:28 +0200
Subject: [PATCH 33/33] BUILD: Bump versions for release

---
 CHANGELOG.md    | 10 +---------
 CITATION.cff    |  4 ++--
 setup.py        |  6 +++---
 src/doc/conf.py |  4 ++--
 4 files changed, 8 insertions(+), 16 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bd0c7524..790b8aba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
-## [Unreleased] ##
+## [0.18.0] - 2025-05-27 ##
 
 ### Added ###
 
@@ -13,18 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - High level API: Parameter for serialization to output a plain JSON.
 - High level API: Parameter to resolve references when converting to Python object.
 
-### Changed ###
-
-### Deprecated ###
-
 ### Removed ###
 
 - `DropOffBox` class and related parameters (`pickup` for file uploading).
 
-### Fixed ###
-
-### Security ###
-
 ### Documentation ###
 
 * [#78](https://gitlab.com/linkahead/linkahead-pylib/-/issues/78) Fix
diff --git a/CITATION.cff b/CITATION.cff
index bcecc2fd..e685ff0c 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -20,6 +20,6 @@ authors:
     given-names: Stefan
     orcid: https://orcid.org/0000-0001-7214-8125
 title: CaosDB - Pylib
-version: 0.17.0
+version: 0.18.0
 doi: 10.3390/data4020083
-date-released: 2025-01-14
+date-released: 2025-05-27
diff --git a/setup.py b/setup.py
index ab8555b8..9047cbd0 100755
--- a/setup.py
+++ b/setup.py
@@ -46,10 +46,10 @@ from setuptools import find_packages, setup
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ########################################################################
 
-ISRELEASED = False
+ISRELEASED = True
 MAJOR = 0
-MINOR = 17
-MICRO = 1
+MINOR = 18
+MICRO = 0
 # Do not tag as pre-release until this commit
 # https://github.com/pypa/packaging/pull/515
 # has made it into a release. Probably we should wait for pypa/packaging>=21.4
diff --git a/src/doc/conf.py b/src/doc/conf.py
index ce1aa8ff..46c1645c 100644
--- a/src/doc/conf.py
+++ b/src/doc/conf.py
@@ -29,10 +29,10 @@ copyright = '2024, IndiScale GmbH'
 author = 'Daniel Hornung'
 
 # The short X.Y version
-version = '0.17.1'
+version = '0.18.0'
 # The full version, including alpha/beta/rc tags
 # release = '0.5.2-rc2'
-release = '0.17.1-dev'
+release = '0.18.0'
 
 
 # -- General configuration ---------------------------------------------------
-- 
GitLab