diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8845e4070c685230a99958fbebd9377238df32de..db600343569930a436a593a8ab5d511a35bc7aca 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -57,22 +57,10 @@ mypy:
   tags: [ docker ]
   stage: linting
   script:
-    - pip install mypy types-PyYAML types-jsonschema types-requests types-setuptools types-lxml types-python-dateutil pytest
+    - pip install .[mypy,test]
     - make mypy
-  allow_failure: true
 
 # run unit tests
-unittest_py3.8:
-  tags: [ docker ]
-  stage: test
-  needs: [ ]
-  image: python:3.8
-  script: &python_test_script
-    # Python docker has problems with tox and pip so use plain pytest here
-    - touch ~/.pylinkahead.ini
-    - pip install pynose pytest pytest-cov jsonschema>=4.4.0 setuptools
-    - pip install .
-    - python -m pytest unittests
 
 # This needs to be changed once Python 3.9 isn't the standard Python in Debian
 # anymore.
@@ -92,7 +80,11 @@ unittest_py3.10:
   stage: test
   needs: [ ]
   image: python:3.10
-  script: *python_test_script
+  script: &python_test_script
+    # Python docker has problems with tox and pip so use plain pytest here
+    - touch ~/.pylinkahead.ini
+    - pip install .[test]
+    - python -m pytest unittests
 
 unittest_py3.11:
   tags: [ docker ]
@@ -160,7 +152,7 @@ build-testenv:
 pages_prepare: &pages_prepare
   tags: [ cached-dind ]
   stage: deploy
-  needs: [ code_style, pylint, unittest_py3.8, unittest_py3.9, unittest_py3.10 ]
+  needs: [ code_style, pylint, unittest_py3.9, unittest_py3.10 ]
   only:
     refs:
       - /^release-.*$/i
diff --git a/CHANGELOG.md b/CHANGELOG.md
index de8099318cdc1480f2cd2c06497a7cd374e65495..f56bc3abe12fc0dde3077e74b94472a366727074 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,49 @@ 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).
 
+## [0.17.0] - 2025-01-14 ##
+
+### Added ###
+
+* New setup extra `test` which installs the dependencies for testing.
+* The Container class has a new member function `filter_by_identity`
+  which is based on `_filter_entity_list`.
+* The `Entity` properties `_cuid` and `_flags` are now available for read-only access
+  as `cuid` and `flags`, respectively.
+
+### Changed ###
+
+* Renamed the `filter` function of Container, ParentList and
+  PropertyList to `filter_by_identity`.
+
+### Deprecated ###
+
+* `ParentList.filter` and `PropertyList.filter` functions, use
+  `filter_by_identity` instead.
+
+### Removed ###
+
+* Support for Python 3.8
+
+### Fixed ###
+
+* [#73](https://gitlab.com/linkahead/linkahead-pylib/-/issues/73)
+  `Entity.to_xml` now detects potentially infinite recursion and prevents an error
+* [#89](https://gitlab.com/linkahead/linkahead-pylib/-/issues/89)
+  `to_xml` does not add `noscript` or `TransactionBenchmark` tags anymore
+* [#103](https://gitlab.com/linkahead/linkahead-pylib/-/issues/103)
+  `authentication/interface/on_response()` does not overwrite
+  `auth_token` if new value is `None`
+* [#119](https://gitlab.com/linkahead/linkahead-pylib/-/issues/119)
+  The diff returned by compare_entities now uses id instead of name as
+  key if either property does not have a name
+* [#87](https://gitlab.com/linkahead/linkahead-pylib/-/issues/87)
+  `XMLSyntaxError` messages when parsing (incomplete) responses in
+  case of certain connection timeouts.
+  The diff returned by compare_entities now uses id instead of name as key if either property does not have a name
+* [#127](https://gitlab.com/linkahead/linkahead-pylib/-/issues/127)
+  pylinkahead.ini now supports None and tuples as values for the `timeout` keyword
+
 ## [0.16.0] - 2024-11-13 ##
 
 ### Added ###
@@ -31,7 +74,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Fixed ###
 
-* [gitlab.indiscale.com#200](https://gitlab.indiscale.com/caosdb/src/caosdb-pylib/-/merge_requests/153)
+* [gitlab.indiscale.com#200](https://gitlab.indiscale.com/caosdb/src/caosdb-pylib/-/issues/200)
   ``linkahead_admin.py`` prints reasonable error messages when users
   or roles don't exist.
 
diff --git a/CITATION.cff b/CITATION.cff
index 123289ca17e8b43446f8f368621debccd8c27469..bcecc2fdd962f4c581a2d53d5c1a324fb643a4a3 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.16.0
+version: 0.17.0
 doi: 10.3390/data4020083
-date-released: 2024-11-13
+date-released: 2025-01-14
diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md
index e2326b831a71751265c6c2d5a333ccc37145bfa5..e9bd54a1459df22afa307e256625d05e74bdc6a8 100644
--- a/DEPENDENCIES.md
+++ b/DEPENDENCIES.md
@@ -1,5 +1,5 @@
 * caosdb-server >= 0.12.0
-* Python >= 3.8
+* Python >= 3.9
 * pip >= 20.0.2
 
 Any other dependencies are defined in the setup.py and are being installed via pip
diff --git a/README.md b/README.md
index 193cb8f0cff8ff5cee36a40a78e53f070527e2e0..d630e879e9ff4781bb79b193d0240ef11ae211d2 100644
--- a/README.md
+++ b/README.md
@@ -47,7 +47,7 @@ However, you can also create an issue for it.
 
 * Copyright (C) 2018 Research Group Biomedical Physics, Max Planck Institute
   for Dynamics and Self-Organization Göttingen.
-* Copyright (C) 2020-2022 Indiscale GmbH <info@indiscale.com>
+* Copyright (C) 2020-2025 Indiscale GmbH <info@indiscale.com>
 
 All files in this repository are licensed under a [GNU Affero General Public
 License](LICENCE.md) (version 3 or later).
diff --git a/README_SETUP.md b/README_SETUP.md
index 8a32fbfacb8fd5733c65998b35e52e1c7bbceab1..f4c921382edb26776391590298faed06a5391396 100644
--- a/README_SETUP.md
+++ b/README_SETUP.md
@@ -2,24 +2,44 @@
 
 ## Installation ##
 
-### Requirements ###
+### How to install ###
 
-PyCaosDB needs at least Python 3.8.  Additionally, the following packages are required (they will
-typically be installed automatically):
+First ensure that python with at least version 3.9 is installed. Should this not be
+the case, you can use the [Installing python](#installing-python-) guide for your OS.
 
-- `lxml`
-- `PyYaml`
-- `PySocks`
+#### Generic installation ####
 
-Optional packages:
-- `keyring`
-- `jsonschema`
+To install this LinkAhead python client locally, use `pip`/`pip3`:
 
-### How to install ###
+```sh
+pip install linkahead
+```
+
+#### Additional dependencies ####
+
+To test using tox, you also need to install tox:  
+`pip install tox`  
+
+To install dependencies used by optional functionality, the following pip extras 
+keywords are defined:
+- `test` for testing with pytest
+- `mypy` for mypy and types
+- `jsonschema` 
+- `keyring` 
+
+These extras can be installed using: 
+```sh
+pip install .[KEYWORD]
+```
+A current list of the dependencies installed with this program as well as those installed with 
+the keywords can be found in `setup.py`s `setup_package()` method, in the `metadata` dictionary
+entries `install_requires` and `extras_require`.
+
+### Installing python ###
 
 #### Linux ####
 
-Make sure that Python (at least version 3.8) and pip is installed, using your system tools and
+Make sure that Python (at least version 3.9) and pip is installed, using your system tools and
 documentation.
 
 Then open a terminal and continue in the [Generic installation](#generic-installation) section.
@@ -51,34 +71,7 @@ cd /Applications/Python\ 3.9/
 sudo ./Install\ Certificates.command
 ```
 
-After these steps, you may continue with the [Generic
-installation](#generic-installation).
-
-#### Generic installation ####
-
-To install PyCaosDB locally, use `pip3` (also called `pip` on some systems):
-
-```sh
-pip3 install --user caosdb
-```
-
----
-
-Alternatively, obtain the sources from GitLab and install from there (`git` must be installed for
-this option):
-
-```sh
-git clone https://gitlab.com/caosdb/caosdb-pylib
-cd caosdb-pylib
-pip3 install --user .
-```
-
-For installation of optional packages, install with an additional option, e.g. for 
-validating with the caosdb json schema:
-
-```sh
-pip3 install --user .[jsonschema]
-```
+After these steps, you may continue with the [Generic installation](#generic-installation) section.
 
 ## Configuration ##
 
@@ -87,7 +80,7 @@ is described in detail in the [configuration section of the documentation](https
 
 ## Try it out ##
 
-Start Python and check whether the you can access the database. (You will be asked for the
+Start Python and check whether you can access the database. (You will be asked for the
 password):
 
 ```python
@@ -107,6 +100,7 @@ Now would be a good time to continue with the [tutorials](tutorials/index).
 - Run all tests: `tox` or `make unittest`
 - Run a specific test file: e.g. `tox -- unittests/test_schema.py`
 - Run a specific test function: e.g. `tox -- unittests/test_schema.py::test_config_files`
+- To run using pytest: `pytest .`
 
 ## Documentation ##
 We use sphinx to create the documentation. Docstrings in the code should comply
@@ -114,13 +108,6 @@ with the Googly style (see link below).
 
 Build documentation in `build/` with `make doc`.
 
-### Requirements ###
-
-- `sphinx`
-- `sphinx-autoapi`
-- `recommonmark`
-- `sphinx_rtd_theme`
-
 ### How to contribute ###
 
 - [Google Style Python Docstrings](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html)
@@ -128,7 +115,7 @@ Build documentation in `build/` with `make doc`.
 - [References to other documentation](https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#role-external)
 
 ### Troubleshooting ###
-If the client is to be executed directly from the `/src` folder, an initial `.\setup.py install --user` must be called.
+If the client is to be executed directly from the `/src` folder, an initial `.\setup.py install` must be called.
 
 ## Migration ##
 TODO
diff --git a/setup.py b/setup.py
index b8a04adefa9d8891c10576a733f53f2fa88b981d..75bcf0c762bc10aed857c741bcf4791f73bb461b 100755
--- a/setup.py
+++ b/setup.py
@@ -48,7 +48,7 @@ from setuptools import find_packages, setup
 
 ISRELEASED = True
 MAJOR = 0
-MINOR = 16
+MINOR = 17
 MICRO = 0
 # Do not tag as pre-release until this commit
 # https://github.com/pypa/packaging/pull/515
@@ -179,7 +179,7 @@ def setup_package():
             "Topic :: Scientific/Engineering :: Information Analysis",
         ],
         packages=find_packages('src'),
-        python_requires='>=3.8',
+        python_requires='>=3.9',
         package_dir={'': 'src'},
         install_requires=['lxml>=4.6.3',
                           "requests[socks]>=2.26",
@@ -187,11 +187,27 @@ def setup_package():
                           'PyYAML>=5.4.1',
                           'future',
                           ],
-        extras_require={'keyring': ['keyring>=13.0.0'],
-                        'jsonschema': ['jsonschema>=4.4.0']},
+        extras_require={
+            "jsonschema": ["jsonschema>=4.4.0"],
+            "keyring": ["keyring>=13.0.0"],
+            "mypy": [
+                "mypy",
+                "types-PyYAML",
+                "types-jsonschema",
+                "types-requests",
+                "types-setuptools",
+                "types-lxml",
+                "types-python-dateutil",
+            ],
+            "test": [
+                "pytest",
+                "pytest-cov",
+                "coverage>=4.4.2",
+                "jsonschema>=4.4.0",
+            ]
+
+        },
         setup_requires=["pytest-runner>=2.0,<3dev"],
-        tests_require=["pytest", "pytest-cov", "coverage>=4.4.2",
-                       "jsonschema>=4.4.0"],
         package_data={
             'linkahead': ['py.typed', 'cert/indiscale.ca.crt', 'schema-pycaosdb-ini.yml'],
         },
diff --git a/src/doc/conf.py b/src/doc/conf.py
index f25ed399575bb208e699476b06f014abe43ee967..656006787e51949ccefbf213d0a80a74df556c75 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.16.0'
+version = '0.17.0'
 # The full version, including alpha/beta/rc tags
 # release = '0.5.2-rc2'
-release = '0.16.0'
+release = '0.17.0'
 
 
 # -- General configuration ---------------------------------------------------
diff --git a/src/doc/tutorials/complex_data_models.rst b/src/doc/tutorials/complex_data_models.rst
index 168cf3b9f0d6839ed8f78beb01ae24fb9d489e88..52757c320b42f18b4b24ab9b7575e7bd0becc252 100644
--- a/src/doc/tutorials/complex_data_models.rst
+++ b/src/doc/tutorials/complex_data_models.rst
@@ -78,7 +78,7 @@ Examples
 
 
 Finding parents and properties
---------
+------------------------------
 To find a specific parent or property of an Entity, its
 ParentList or PropertyList can be filtered using names, ids, or
 entities. A short example:
@@ -100,29 +100,45 @@ entities. A short example:
    properties = r.properties
 
    # As r only has one property with id 101, this returns a list containing only p1_1
-   properties.filter(pid=101)
+   properties.filter_by_identity(pid=101)
    # Result: [p1_1]
 
    # Filtering with name="Property 1" returns both p1_1 and p1_2, as they share their name
-   properties.filter(name="Property 1")
+   properties.filter_by_identity(name="Property 1")
    # Result: [p1_1, p1_2]
 
    #  If both name and pid are given, matching is based only on pid for all entities that have an id
-   properties.filter(pid="102", name="Other Property")
+   properties.filter_by_identity(pid="102", name="Other Property")
    # Result: [p2_1, p2_2, p2_3]
 
-   # However, filtering with name="Property 1" and id=101 returns both p1_1 and p1_2, because
+   # However, filter_by_identity with name="Property 1" and id=101 returns both p1_1 and p1_2, because
    # p1_2 does not have an id and matches the name
-   properties.filter(pid="101", name="Property 1")
+   properties.filter_by_identity(pid="101", name="Property 1")
    # Result: [p1_1, p1_2]
 
    # We can also filter using an entity, in which case the name and id of the entity are used:
-   properties.filter(pid="102", name="Property 2") == properties.filter(p2_1)
+   properties.filter_by_identity(pid="102", name="Property 2") == properties.filter_by_identity(p2_1)
    # Result: True
 
    # If we only need properties that match both id and name, we can set the parameter
    # conjunction to True:
-   properties.filter(pid="102", name="Property 2", conjunction=True)
+   properties.filter_by_identity(pid="102", name="Property 2", conjunction=True)
    # Result: [p2_1]
 
 The filter function of ParentList works analogously.
+
+Finding entities in a Container
+-------------------------------
+In the same way as described above, Container can be filtered.
+A short example:
+
+.. code-block:: python3
+
+   import linkahead as db
+
+   # Setup a record with six properties
+   p1 = db.Property(id=101, name="Property 1")
+   p2 = db.Property(name="Property 2")
+   c = db.Container().extend([p1,p2])
+   c.filter_by_identity(name="Property 1")
+   # Result: [p1]
diff --git a/src/linkahead/apiutils.py b/src/linkahead/apiutils.py
index 49336aa8db24fba663337185c5c37a346330c4cd..b2a612faea1616c64b7e78575156abccfdb29e61 100644
--- a/src/linkahead/apiutils.py
+++ b/src/linkahead/apiutils.py
@@ -197,13 +197,19 @@ def compare_entities(entity0: Optional[Entity] = None,
     properties and SPECIAL_ATTRIBUTES if they are missing or different from
     their counterparts in the other entity.
 
+    The key used to represent a parent in the parent list or a
+    property in the property dictionary is the entity's name if the
+    name is present for both compared entities, the id otherwise.
+
     The value of the properties dict for each listed property is again a dict
     detailing the differences between this property and its counterpart.
     The characteristics that are checked to determine whether two properties
     match are the following:
-        - datatype
-        - importance
-        - value
+
+    - datatype
+    - importance
+    - value
+
     If any of these characteristics differ for a property, the respective
     string (datatype, importance, value) is added as a key to the dict of the
     property with its value being the characteristics value,
@@ -224,9 +230,9 @@ def compare_entities(entity0: Optional[Entity] = None,
 
     Params
     ------
-    entity0                : Entity
+    entity0:                    Entity
                                 First entity to be compared.
-    entity1                : Entity
+    entity1:                    Entity
                                 Second entity to be compared.
     compare_referenced_records: bool, default: False
                                 If set to True, values with referenced records
@@ -242,6 +248,7 @@ def compare_entities(entity0: Optional[Entity] = None,
                                 entity and an int or str also checks whether
                                 the int/str matches the name or id of the
                                 entity, so Entity(id=100) == 100 == "100".
+
     """
     # ToDo: Discuss intended behaviour
     # Questions that need clarification:
@@ -275,9 +282,11 @@ def compare_entities(entity0: Optional[Entity] = None,
         if entity1 is not None:
             raise ValueError("You cannot use both entity1 and new_entity")
         entity1 = new_entity
+    assert entity0 is not None
+    assert entity1 is not None
 
-    diff: tuple = ({"properties": {}, "parents": []},
-                   {"properties": {}, "parents": []})
+    diff: tuple[dict[str, Any], dict[str, Any]] = ({"properties": {}, "parents": []},
+                                                   {"properties": {}, "parents": []})
 
     if entity0 is entity1:
         return diff
@@ -375,15 +384,20 @@ def compare_entities(entity0: Optional[Entity] = None,
 
     # compare properties
     for prop in entity0.properties:
-        matching = entity1.properties.filter(name=prop.name, pid=prop.id)
+        # ToDo: Would making id default break anything?
+        key = prop.name if prop.name is not None else prop.id
+        matching = entity1.properties.filter_by_identity(prop)
         if len(matching) == 0:
             # entity1 has prop, entity0 does not
-            diff[0]["properties"][prop.name] = {}
+            diff[0]["properties"][key] = {}
         elif len(matching) == 1:
-            diff[0]["properties"][prop.name] = {}
-            diff[1]["properties"][prop.name] = {}
-            propdiff = (diff[0]["properties"][prop.name],
-                        diff[1]["properties"][prop.name])
+            # It's possible that prop has name and id, but match only has id
+            key = prop.name if (prop.name is not None and
+                                matching[0].name == prop.name) else prop.id
+            diff[0]["properties"][key] = {}
+            diff[1]["properties"][key] = {}
+            propdiff = (diff[0]["properties"][key],
+                        diff[1]["properties"][key])
 
             # We should compare the wrapped properties instead of the
             # wrapping entities if possible:
@@ -415,8 +429,8 @@ def compare_entities(entity0: Optional[Entity] = None,
 
             # in case there is no difference, we remove the dict keys again
             if len(propdiff[0]) == 0 and len(propdiff[1]) == 0:
-                diff[0]["properties"].pop(prop.name)
-                diff[1]["properties"].pop(prop.name)
+                diff[0]["properties"].pop(key)
+                diff[1]["properties"].pop(key)
 
         else:
             raise NotImplementedError(
@@ -424,11 +438,12 @@ def compare_entities(entity0: Optional[Entity] = None,
 
     # we have not yet compared properties that do not exist in entity0
     for prop in entity1.properties:
+        key = prop.name if prop.name is not None else prop.id
         # check how often the property appears in entity0
-        num_prop_in_ent0 = len(entity0.properties.filter(prop))
+        num_prop_in_ent0 = len(entity0.properties.filter_by_identity(prop))
         if num_prop_in_ent0 == 0:
             # property is only present in entity0 - add to diff
-            diff[1]["properties"][prop.name] = {}
+            diff[1]["properties"][key] = {}
         if num_prop_in_ent0 > 1:
             # Check whether the property is present multiple times in entity0
             # and raise error - result would be incorrect
@@ -439,9 +454,10 @@ def compare_entities(entity0: Optional[Entity] = None,
     for index, parents, other_entity in [(0, entity0.parents, entity1),
                                          (1, entity1.parents, entity0)]:
         for parent in parents:
-            matching = other_entity.parents.filter(parent)
+            key = parent.name if parent.name is not None else parent.id
+            matching = other_entity.parents.filter_by_identity(parent)
             if len(matching) == 0:
-                diff[index]["parents"].append(parent.name)
+                diff[index]["parents"].append(key)
                 continue
 
     return diff
@@ -550,9 +566,10 @@ def merge_entities(entity_a: Entity,
     """
 
     # Compare both entities:
-    diff_r1, diff_r2 = compare_entities(entity_a, entity_b,
-                                        entity_name_id_equivalency=merge_id_with_resolved_entity,
-                                        compare_referenced_records=merge_references_with_empty_diffs)
+    diff_r1, diff_r2 = compare_entities(
+        entity_a, entity_b,
+        entity_name_id_equivalency=merge_id_with_resolved_entity,
+        compare_referenced_records=merge_references_with_empty_diffs)
 
     # Go through the comparison and try to apply changes to entity_a:
     for key in diff_r2["parents"]:
diff --git a/src/linkahead/common/administration.py b/src/linkahead/common/administration.py
index 28ef107579fccb689b7337aed65e054cfbf36c05..9d9d4f013f1ad10cd0957cfb9a9e4f2f44bd6102 100644
--- a/src/linkahead/common/administration.py
+++ b/src/linkahead/common/administration.py
@@ -91,7 +91,7 @@ def get_server_properties() -> dict[str, Optional[str]]:
     props: dict[str, Optional[str]] = dict()
 
     for elem in xml.getroot():
-        props[elem.tag] = elem.text
+        props[str(elem.tag)] = str(elem.text)
 
     return props
 
@@ -156,7 +156,10 @@ def generate_password(length: int):
 def _retrieve_user(name: str, realm: Optional[str] = None, **kwargs):
     con = get_connection()
     try:
-        return con._http_request(method="GET", path="User/" + (realm + "/" + name if realm is not None else name), **kwargs).read()
+        return con._http_request(
+            method="GET",
+            path="User/" + (realm + "/" + name if realm is not None else name),
+            **kwargs).read()
     except HTTPForbiddenError as e:
         e.msg = "You are not permitted to retrieve this user."
         raise
@@ -198,7 +201,9 @@ def _update_user(name: str,
     if entity is not None:
         params["entity"] = str(entity)
     try:
-        return con.put_form_data(entity_uri_segment="User/" + (realm + "/" + name if realm is not None else name), params=params, **kwargs).read()
+        return con.put_form_data(entity_uri_segment="User/" + (realm + "/" +
+                                                               name if realm is not None else name),
+                                 params=params, **kwargs).read()
     except HTTPResourceNotFoundError as e:
         e.msg = "User does not exist."
         raise e
@@ -246,7 +251,9 @@ def _insert_user(name: str,
 def _insert_role(name, description, **kwargs):
     con = get_connection()
     try:
-        return con.post_form_data(entity_uri_segment="Role", params={"role_name": name, "role_description": description}, **kwargs).read()
+        return con.post_form_data(entity_uri_segment="Role",
+                                  params={"role_name": name, "role_description": description},
+                                  **kwargs).read()
     except HTTPForbiddenError as e:
         e.msg = "You are not permitted to insert a new role."
         raise
@@ -259,7 +266,9 @@ def _insert_role(name, description, **kwargs):
 def _update_role(name, description, **kwargs):
     con = get_connection()
     try:
-        return con.put_form_data(entity_uri_segment="Role/" + name, params={"role_description": description}, **kwargs).read()
+        return con.put_form_data(entity_uri_segment="Role/" + name,
+                                 params={"role_description": description},
+                                 **kwargs).read()
     except HTTPForbiddenError as e:
         e.msg = "You are not permitted to update this role."
         raise
@@ -301,8 +310,10 @@ def _set_roles(username, roles, realm=None, **kwargs):
     body = xml2str(xml)
     con = get_connection()
     try:
-        body = con._http_request(method="PUT", path="UserRoles/" + (realm + "/" +
-                                                                    username if realm is not None else username), body=body, **kwargs).read()
+        body = con._http_request(method="PUT",
+                                 path="UserRoles/" + (realm + "/" +
+                                                      username if realm is not None else username),
+                                 body=body, **kwargs).read()
     except HTTPForbiddenError as e:
         e.msg = "You are not permitted to set this user's roles."
         raise
@@ -369,7 +380,8 @@ def _set_permissions(role, permission_rules, **kwargs):
     body = xml2str(xml)
     con = get_connection()
     try:
-        return con._http_request(method="PUT", path="PermissionRules/" + role, body=body, **kwargs).read()
+        return con._http_request(method="PUT", path="PermissionRules/" + role, body=body,
+                                 **kwargs).read()
     except HTTPForbiddenError as e:
         e.msg = "You are not permitted to set this role's permissions."
         raise
@@ -381,7 +393,9 @@ def _set_permissions(role, permission_rules, **kwargs):
 def _get_permissions(role, **kwargs):
     con = get_connection()
     try:
-        return PermissionRule._parse_body(con._http_request(method="GET", path="PermissionRules/" + role, **kwargs).read())
+        return PermissionRule._parse_body(con._http_request(method="GET",
+                                                            path="PermissionRules/" + role,
+                                                            **kwargs).read())
     except HTTPForbiddenError as e:
         e.msg = "You are not permitted to retrieve this role's permissions."
         raise
@@ -429,7 +443,8 @@ class PermissionRule():
         if permission is None:
             raise ValueError(f"Permission is missing in PermissionRule xml: {elem}")
         priority = PermissionRule._parse_boolean(elem.get("priority"))
-        return PermissionRule(elem.tag, permission, priority if priority is not None else False)
+        return PermissionRule(str(elem.tag), permission,
+                              priority if priority is not None else False)
 
     @staticmethod
     def _parse_body(body: str):
diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py
index 1dbeb802311c7afaea2340af15e49537520ef57f..75b03b70907abdde50fb383c54a02e94cad115ad 100644
--- a/src/linkahead/common/models.py
+++ b/src/linkahead/common/models.py
@@ -41,7 +41,6 @@ import warnings
 from builtins import str
 from copy import deepcopy
 from datetime import date, datetime
-from enum import Enum
 from functools import cmp_to_key
 from hashlib import sha512
 from os import listdir
@@ -350,6 +349,15 @@ class Entity:
     def pickup(self, new_pickup):
         self.__pickup = new_pickup
 
+    @property   # getter for _cuid
+    def cuid(self):
+        # Set if None?
+        return self._cuid
+
+    @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,
@@ -1120,7 +1128,7 @@ class Entity:
         else:
             return getattr(ref, special_selector.lower())
 
-    def get_property_values(self, *selectors):
+    def get_property_values(self, *selectors) -> tuple:
         """ Return a tuple with the values described by the given selectors.
 
         This represents an entity's properties as if it was a row of a table
@@ -1233,6 +1241,7 @@ class Entity:
         xml: Optional[etree._Element] = None,
         add_properties: INHERITANCE = "ALL",
         local_serialization: bool = False,
+        visited_entities: Optional[list] = None
     ) -> etree._Element:
         """Generate an xml representation of this entity. If the parameter xml
         is given, all attributes, parents, properties, and messages of this
@@ -1240,14 +1249,25 @@ class Entity:
 
         Raise an error if xml is not a lxml.etree.Element
 
-        @param xml: an xml element to which all attributes, parents,
-            properties, and messages
-            are to be added.
-
-        FIXME: Add documentation for the add_properties parameter.
-        FIXME: Add docuemntation for the local_serialization parameter.
+        Parameters
+        ----------
+        xml : etree._Element, optional
+            an xml element to which all attributes, parents,
+            properties, and messages are to be added. Default is None.
+        visited_entities : list, optional
+            list of enties that are being printed for recursion check,
+            should never be set manually. Default is None.
+        add_properties : INHERITANCE, optional
+            FIXME: Add documentation for the add_properties
+            parameter. Default is "ALL".
+        local_serialization : bool, optional
+            FIXME: Add documentation for the local_serialization
+            parameter. Default is False.
 
-        @return: xml representation of this entity.
+        Returns
+        -------
+        xml : etree._Element
+            xml representation of this entity.
         """
 
         if xml is None:
@@ -1256,9 +1276,17 @@ class Entity:
             xml = etree.Element(elem_tag)
         assert isinstance(xml, etree._Element)
 
+        if visited_entities is None:
+            visited_entities = []
+        if self in visited_entities:
+            xml.text = xml2str(etree.Comment("Recursive reference"))
+            return xml
+        visited_entities.append(self)
+
         # unwrap wrapped entity
         if self._wrapped_entity is not None:
-            xml = self._wrapped_entity.to_xml(xml, add_properties)
+            xml = self._wrapped_entity.to_xml(xml, add_properties,
+                                              visited_entities=visited_entities.copy())
 
         if self.id is not None:
             xml.set("id", str(self.id))
@@ -1273,6 +1301,10 @@ class Entity:
             xml.set("description", str(self.description))
 
         if self.version is not None:
+            # If this ever causes problems, we might add
+            # visited_entities support here since it does have some
+            # recursion with predecessors / successors. But should be
+            # fine for now, since it is always set by the server.
             xml.append(self.version.to_xml())
 
         if self.value is not None:
@@ -1282,7 +1314,8 @@ class Entity:
                 elif self.value.name is not None:
                     xml.text = str(self.value.name)
                 else:
-                    xml.text = str(self.value)
+                    dt_str = xml2str(self.value.to_xml(visited_entities=visited_entities.copy()))
+                    xml.text = dt_str
             elif isinstance(self.value, list):
                 for v in self.value:
                     v_elem = etree.Element("Value")
@@ -1293,7 +1326,8 @@ class Entity:
                         elif v.name is not None:
                             v_elem.text = str(v.name)
                         else:
-                            v_elem.text = str(v)
+                            dt_str = xml2str(v.to_xml(visited_entities=visited_entities.copy()))
+                            v_elem.text = dt_str
                     elif v == "":
                         v_elem.append(etree.Element("EmptyString"))
                     elif v is None:
@@ -1315,7 +1349,11 @@ class Entity:
                 elif self.datatype.name is not None:
                     xml.set("datatype", str(self.datatype.name))
                 else:
-                    xml.set("datatype", str(self.datatype))
+                    dt_str = xml2str(self.datatype.to_xml(visited_entities=visited_entities.copy()))
+                    # Todo: Use for pretty-printing with calls from _repr_ only?
+                    # dt_str = dt_str.replace('<', 'ᐸ').replace('>', 'ᐳ').replace(' ', '⠀').replace(
+                    # '"', '\'').replace('\n', '')
+                    xml.set("datatype", dt_str)
             else:
                 xml.set("datatype", str(self.datatype))
 
@@ -1338,10 +1376,11 @@ class Entity:
             self.messages.to_xml(xml)
 
         if self.parents is not None:
-            self.parents.to_xml(xml)
+            self.parents.to_xml(xml, visited_entities=visited_entities.copy())
 
         if self.properties is not None:
-            self.properties.to_xml(xml, add_properties)
+            self.properties.to_xml(xml, add_properties,
+                                   visited_entities=visited_entities.copy())
 
         if len(self._flags) > 0:
             flagattr = ""
@@ -1861,12 +1900,12 @@ class QueryTemplate():
 
     @staticmethod
     def _from_xml(xml: etree._Element):
-        if xml.tag.lower() == "querytemplate":
+        if str(xml.tag).lower() == "querytemplate":
             q = QueryTemplate(name=xml.get("name"),
                               description=xml.get("description"), query=None)
 
             for e in xml:
-                if e.tag.lower() == "query":
+                if str(e.tag).lower() == "query":
                     q.query = e.text
                 else:
                     child = _parse_single_xml_element(e)
@@ -1903,7 +1942,7 @@ class QueryTemplate():
         ret = Messages()
 
         for m in self.messages:
-            if m.type.lower() == "error":
+            if str(m.type).lower() == "error":
                 ret.append(m)
 
         return ret
@@ -1949,11 +1988,16 @@ class Parent(Entity):
         xml: Optional[etree._Element] = None,
         add_properties: INHERITANCE = "NONE",
         local_serialization: bool = False,
+        visited_entities: Optional[Union[list, None]] = None,
     ):
         if xml is None:
             xml = etree.Element("Parent")
 
-        return super().to_xml(xml=xml, add_properties=add_properties)
+        if visited_entities is None:
+            visited_entities = []
+
+        return super().to_xml(xml=xml, add_properties=add_properties,
+                              visited_entities=visited_entities)
 
 
 class _EntityWrapper(object):
@@ -2024,14 +2068,19 @@ class Property(Entity):
         xml: Optional[etree._Element] = None,
         add_properties: INHERITANCE = "ALL",
         local_serialization: bool = False,
+        visited_entities: Optional[Union[list, None]] = None,
     ):
         if xml is None:
             xml = etree.Element("Property")
 
+        if visited_entities is None:
+            visited_entities = []
+
         return super(Property, self).to_xml(
             xml=xml,
             add_properties=add_properties,
             local_serialization=local_serialization,
+            visited_entities=visited_entities,
         )
 
     def is_reference(self, server_retrieval: bool = False) -> Optional[bool]:
@@ -2189,15 +2238,20 @@ class RecordType(Entity):
         xml: Optional[etree._Element] = None,
         add_properties: INHERITANCE = "ALL",
         local_serialization: bool = False,
+        visited_entities: Optional[Union[list, None]] = None,
     ) -> etree._Element:
         if xml is None:
             xml = etree.Element("RecordType")
 
+        if visited_entities is None:
+            visited_entities = []
+
         return Entity.to_xml(
             self,
             xml=xml,
             add_properties=add_properties,
             local_serialization=local_serialization,
+            visited_entities=visited_entities,
         )
 
 
@@ -2228,14 +2282,19 @@ class Record(Entity):
         xml: Optional[etree._Element] = None,
         add_properties: INHERITANCE = "ALL",
         local_serialization: bool = False,
+        visited_entities: Optional[Union[list, None]] = None,
     ):
         if xml is None:
             xml = etree.Element("Record")
 
+        if visited_entities is None:
+            visited_entities = []
+
         return super().to_xml(
             xml=xml,
             add_properties=add_properties,
             local_serialization=local_serialization,
+            visited_entities=visited_entities
         )
 
 
@@ -2305,6 +2364,7 @@ class File(Record):
         xml: Optional[etree._Element] = None,
         add_properties: INHERITANCE = "ALL",
         local_serialization: bool = False,
+        visited_entities: Optional[Union[list, None]] = None,
     ) -> etree._Element:
         """Convert this file to an xml element.
 
@@ -2314,8 +2374,12 @@ class File(Record):
         if xml is None:
             xml = etree.Element("File")
 
+        if visited_entities is None:
+            visited_entities = []
+
         return Entity.to_xml(self, xml=xml, add_properties=add_properties,
-                             local_serialization=local_serialization)
+                             local_serialization=local_serialization,
+                             visited_entities=visited_entities)
 
     def download(self, target: Optional[str] = None) -> str:
         """Download this file-entity's actual file from the file server. It
@@ -2409,7 +2473,7 @@ class PropertyList(list):
     This class provides addional functionality like get/set_importance or get_by_name.
     """
 
-    def __init__(self):
+    def __init__(self) -> None:
         super().__init__()
         self._importance: dict[Entity, IMPORTANCE] = dict()
         self._inheritance: dict[Entity, INHERITANCE] = dict()
@@ -2477,15 +2541,20 @@ class PropertyList(list):
 
         return self
 
-    def to_xml(self, add_to_element: etree._Element, add_properties: INHERITANCE):
+    def to_xml(self, add_to_element: etree._Element, add_properties: INHERITANCE,
+               visited_entities: Optional[Union[list, None]] = None):
+
+        if visited_entities is None:
+            visited_entities = []
+
         p: Property
         for p in self:
             importance = self._importance.get(p)
 
             if add_properties == FIX and not importance == FIX:
                 continue
-
-            pelem = p.to_xml(xml=etree.Element("Property"), add_properties=FIX)
+            pelem = p.to_xml(xml=etree.Element("Property"), add_properties=FIX,
+                             visited_entities=visited_entities.copy())
 
             if p in self._importance:
                 pelem.set("importance", str(importance))
@@ -2503,21 +2572,23 @@ class PropertyList(list):
 
         return xml2str(xml)
 
-    def filter(self, prop: Optional[Property] = None,
-               pid: Union[None, str, int] = None,
-               name: Optional[str] = None,
-               conjunction: bool = False) -> list:
+    def filter(self, *args, **kwargs):
+        warnings.warn(DeprecationWarning("This function was renamed to filter_by_identity."))
+        return self.filter_by_identity(*args, **kwargs)
+
+    def filter_by_identity(self, prop: Optional[Property] = None,
+                           pid: Union[None, str, int] = None,
+                           name: Optional[str] = None,
+                           conjunction: bool = False) -> list:
         """
         Return all Properties from the given PropertyList that match the
         selection criteria.
 
-        Please refer to the documentation of _filter_entity_list for a detailed
+        Please refer to the documentation of _filter_entity_list_by_identity for a detailed
         description of behaviour.
 
         Params
         ------
-        listobject        : Iterable(Property)
-                            List to be filtered
         prop              : Property
                             Property to match name and ID with. Cannot be set
                             simultaneously with ID or name.
@@ -2534,8 +2605,8 @@ class PropertyList(list):
         matches          : list
                            List containing all matching Properties
         """
-        return _filter_entity_list(self, pid=pid, name=name, entity=prop,
-                                   conjunction=conjunction)
+        return _filter_entity_list_by_identity(self, pid=pid, name=name, entity=prop,
+                                               conjunction=conjunction)
 
     def _get_entity_by_cuid(self, cuid: str):
         '''
@@ -2632,7 +2703,12 @@ class ParentList(list):
 
         return self
 
-    def to_xml(self, add_to_element: etree._Element):
+    def to_xml(self, add_to_element: etree._Element,
+               visited_entities: Optional[Union[list, None]] = None):
+
+        if visited_entities is None:
+            visited_entities = []
+
         for p in self:
             pelem = etree.Element("Parent")
 
@@ -2667,15 +2743,19 @@ class ParentList(list):
 
         return xml2str(xml)
 
-    def filter(self, parent: Optional[Parent] = None,
-               pid: Union[None, str, int] = None,
-               name: Optional[str] = None,
-               conjunction: bool = False) -> list:
+    def filter(self, *args, **kwargs):
+        warnings.warn(DeprecationWarning("This function was renamed to filter_by_identity."))
+        return self.filter_by_identity(*args, **kwargs)
+
+    def filter_by_identity(self, parent: Optional[Parent] = None,
+                           pid: Union[None, str, int] = None,
+                           name: Optional[str] = None,
+                           conjunction: bool = False) -> list:
         """
         Return all Parents from the given ParentList that match the selection
         criteria.
 
-        Please refer to the documentation of _filter_entity_list for a detailed
+        Please refer to the documentation of _filter_entity_list_by_identity for a detailed
         description of behaviour.
 
         Params
@@ -2698,8 +2778,8 @@ class ParentList(list):
         matches          : list
                            List containing all matching Parents
         """
-        return _filter_entity_list(self, pid=pid, name=name, entity=parent,
-                                   conjunction=conjunction)
+        return _filter_entity_list_by_identity(self, pid=pid, name=name, entity=parent,
+                                               conjunction=conjunction)
 
     def remove(self, parent: Union[Entity, int, str]):
         """
@@ -2733,7 +2813,7 @@ class ParentList(list):
                     # by name
 
                     for e in self:
-                        if e.name is not None and e.name.lower() == parent.name.lower():
+                        if e.name is not None and str(e.name).lower() == str(parent.name).lower():
                             list.remove(self, e)
 
                             return
@@ -3029,12 +3109,12 @@ def _basic_sync(e_local, e_remote):
     if e_local.role is None:
         e_local.role = e_remote.role
     elif e_remote.role is not None and not e_local.role.lower() == e_remote.role.lower():
-        raise ValueError("The resulting entity had a different role ({0}) "
-                         "than the local one ({1}). This probably means, that "
+        raise ValueError(f"The resulting entity had a different role ({e_remote.role}) "
+                         f"than the local one ({e_local.role}). This probably means, that "
                          "the entity was intialized with a wrong class "
                          "by this client or it has changed in the past and "
-                         "this client did't know about it yet.".format(
-                             e_remote.role, e_local.role))
+                         "this client did't know about it yet.\nThis is the local version of the"
+                         f" Entity:\n{e_local}\nThis is the remote one:\n{e_remote}")
 
     e_local.id = e_remote.id
     e_local.name = e_remote.name
@@ -3284,6 +3364,7 @@ class Container(list):
 
         if add_to_element is None:
             add_to_element = etree.Element("Entities")
+        noscript_in_supplied_xml = list(add_to_element.iter("noscript", "TransactionBenchmark"))
 
         for m in self.messages:
             add_to_element.append(m.to_xml())
@@ -3300,6 +3381,13 @@ class Container(list):
                 elem = e.to_xml()
             add_to_element.append(elem)
 
+        # remove noscript and benchmark elements added by this function
+        for elem in list(add_to_element.iter("noscript", "TransactionBenchmark")):
+            if elem not in noscript_in_supplied_xml:
+                parent = elem.getparent()
+                if parent is not None:
+                    parent.remove(elem)
+
         return add_to_element
 
     def get_errors(self):
@@ -3658,6 +3746,37 @@ class Container(list):
 
         return sync_dict
 
+    def filter_by_identity(self, entity: Optional[Entity] = None,
+                           entity_id: Union[None, str, int] = None,
+                           name: Optional[str] = None,
+                           conjunction: bool = False) -> list:
+        """
+        Return all Entities from this Container that match the selection criteria.
+
+        Please refer to the documentation of _filter_entity_list_by_identity for a detailed
+        description of behaviour.
+
+        Params
+        ------
+        entity            : Entity
+                            Entity to match name and ID with
+        entity_id         : str, int
+                            Parent ID to match
+        name              : str
+                            Parent name to match
+                            simultaneously with ID or name.
+        conjunction       : bool, defaults to False
+                            Set to return only entities that match both id and name
+                            if both are given.
+
+        Returns
+        -------
+        matches          : list
+                           List containing all matching Entities
+        """
+        return _filter_entity_list_by_identity(self, pid=entity_id, name=name, entity=entity,
+                                               conjunction=conjunction)
+
     @staticmethod
     def _find_dependencies_in_container(container: Container):
         """Find elements in a container that are a dependency of another element of the same.
@@ -4536,7 +4655,7 @@ class ACL():
         return len(self._grants) + len(self._priority_grants) + \
             len(self._priority_denials) + len(self._denials) == 0
 
-    def clear(self):
+    def clear(self) -> None:
         self._grants: set[ACI] = set()
         self._denials: set[ACI] = set()
         self._priority_grants: set[ACI] = set()
@@ -4878,7 +4997,7 @@ class Query():
             self.etag = q.get("etag")
 
             for m in q:
-                if m.tag.lower() == 'warning' or m.tag.lower() == 'error':
+                if str(m.tag).lower() == 'warning' or str(m.tag).lower() == 'error':
                     self.messages.append(_parse_single_xml_element(m))
         else:
             self.q = q
@@ -5085,13 +5204,13 @@ class DropOffBox(list):
         xml = etree.fromstring(body)
 
         for child in xml:
-            if child.tag.lower() == "stats":
+            if str(child.tag).lower() == "stats":
                 infoelem = child
 
                 break
 
         for child in infoelem:
-            if child.tag.lower() == "dropoffbox":
+            if str(child.tag).lower() == "dropoffbox":
                 dropoffboxelem = child
 
                 break
@@ -5140,7 +5259,7 @@ class Info():
 
     """
 
-    def __init__(self):
+    def __init__(self) -> None:
         self.messages = Messages()
         self.user_info: Optional[UserInfo] = None
         self.time_zone: Optional[TimeZone] = None
@@ -5266,36 +5385,36 @@ def _parse_single_xml_element(elem: etree._Element):
         "entity": Entity,
     }
 
-    if elem.tag.lower() in classmap:
-        klass = classmap.get(elem.tag.lower())
+    if str(elem.tag).lower() in classmap:
+        klass = classmap.get(str(elem.tag).lower())
         if klass is None:
-            raise LinkAheadException("No class for tag '{}' found.".format(elem.tag))
+            raise LinkAheadException("No class for tag '{}' found.".format(str(elem.tag)))
         entity = klass()
         Entity._from_xml(entity, elem)
 
         return entity
-    elif elem.tag.lower() == "version":
+    elif str(elem.tag).lower() == "version":
         return Version.from_xml(elem)
-    elif elem.tag.lower() == "state":
+    elif str(elem.tag).lower() == "state":
         return State.from_xml(elem)
-    elif elem.tag.lower() == "emptystring":
+    elif str(elem.tag).lower() == "emptystring":
         return ""
-    elif elem.tag.lower() == "value":
-        if len(elem) == 1 and elem[0].tag.lower() == "emptystring":
+    elif str(elem.tag).lower() == "value":
+        if len(elem) == 1 and str(elem[0].tag).lower() == "emptystring":
             return ""
-        elif len(elem) == 1 and elem[0].tag.lower() in classmap:
+        elif len(elem) == 1 and str(elem[0].tag).lower() in classmap:
             return _parse_single_xml_element(elem[0])
         elif elem.text is None or elem.text.strip() == "":
             return None
 
         return str(elem.text.strip())
-    elif elem.tag.lower() == "querytemplate":
+    elif str(elem.tag).lower() == "querytemplate":
         return QueryTemplate._from_xml(elem)
-    elif elem.tag.lower() == 'query':
+    elif str(elem.tag).lower() == 'query':
         return Query(elem)
-    elif elem.tag.lower() == 'history':
+    elif str(elem.tag).lower() == 'history':
         return Message(type='History', description=elem.get("transaction"))
-    elif elem.tag.lower() == 'stats':
+    elif str(elem.tag).lower() == 'stats':
         counts = elem.find("counts")
         if counts is None:
             raise LinkAheadException("'stats' element without a 'count' found.")
@@ -5315,7 +5434,7 @@ def _parse_single_xml_element(elem: etree._Element):
     else:
         code = elem.get("code")
         return Message(
-            type=elem.tag,
+            type=str(elem.tag),
             code=int(code) if code is not None else None,
             description=elem.get("description"),
             body=elem.text,
@@ -5465,11 +5584,11 @@ def delete(ids: Union[list[int], range], raise_exception_on_error: bool = True):
     return c.delete(raise_exception_on_error=raise_exception_on_error)
 
 
-def _filter_entity_list(listobject: list[Entity],
-                        entity: Optional[Entity] = None,
-                        pid: Union[None, str, int] = None,
-                        name: Optional[str] = None,
-                        conjunction: bool = False) -> list:
+def _filter_entity_list_by_identity(listobject: list[Entity],
+                                    entity: Optional[Entity] = None,
+                                    pid: Union[None, str, int] = None,
+                                    name: Optional[str] = None,
+                                    conjunction: bool = False) -> list:
     """
     Returns a subset of entities from the list based on whether their id and
     name matches the selection criterion.
diff --git a/src/linkahead/common/state.py b/src/linkahead/common/state.py
index e352f82d9820620d1692cb6337eb218210e799e6..b708ca13cb0a648aa2ca00507f39a531e4f55d14 100644
--- a/src/linkahead/common/state.py
+++ b/src/linkahead/common/state.py
@@ -20,11 +20,11 @@
 # ** end header
 
 from __future__ import annotations  # Can be removed with 3.10.
-import copy
-from lxml import etree
 
+import copy
 from typing import TYPE_CHECKING
-import sys
+
+from lxml import etree
 
 if TYPE_CHECKING:
     from typing import Optional
@@ -87,7 +87,8 @@ class Transition:
         return self._to_state
 
     def __repr__(self):
-        return f'Transition(name="{self.name}", from_state="{self.from_state}", to_state="{self.to_state}", description="{self.description}")'
+        return (f'Transition(name="{self.name}", from_state="{self.from_state}", '
+                f'to_state="{self.to_state}", description="{self.description}")')
 
     def __eq__(self, other):
         return (
@@ -103,9 +104,9 @@ class Transition:
     @staticmethod
     def from_xml(xml: etree._Element) -> "Transition":
         to_state = [to.get("name")
-                    for to in xml if to.tag.lower() == "tostate"]
+                    for to in xml if str(to.tag).lower() == "tostate"]
         from_state = [
-            from_.get("name") for from_ in xml if from_.tag.lower() == "fromstate"
+            from_.get("name") for from_ in xml if str(from_.tag).lower() == "fromstate"
         ]
         return Transition(
             name=xml.get("name"),
@@ -199,7 +200,7 @@ class State:
         result._id = xml.get("id")
         result._description = xml.get("description")
         transitions = [
-            Transition.from_xml(t) for t in xml if t.tag.lower() == "transition"
+            Transition.from_xml(t) for t in xml if str(t.tag).lower() == "transition"
         ]
         if transitions:
             result._transitions = set(transitions)
diff --git a/src/linkahead/common/versioning.py b/src/linkahead/common/versioning.py
index 11cf5f6904b02954eb0b2bddc16478590df167e7..1c2999df8174e239a470cfc637533c3c8c302c33 100644
--- a/src/linkahead/common/versioning.py
+++ b/src/linkahead/common/versioning.py
@@ -101,11 +101,14 @@ class Version():
     # pylint: disable=redefined-builtin
     def __init__(self, id: Optional[str] = None, date: Optional[str] = None,
                  username: Optional[str] = None, realm: Optional[str] = None,
-                 predecessors: Optional[List[Version]] = None, successors: Optional[List[Version]] = None,
+                 predecessors: Optional[List[Version]] = None,
+                 successors: Optional[List[Version]] = None,
                  is_head: Union[bool, str, None] = False,
                  is_complete_history: Union[bool, str, None] = False):
-        """Typically the `predecessors` or `successors` should not "link back" to an existing Version
-        object."""
+        """Typically the `predecessors` or `successors` should not "link back" to an existing
+        Version object.
+
+        """
         self.id = id
         self.date = date
         self.username = username
@@ -205,8 +208,8 @@ class Version():
         version : Version
             a new version instance
         """
-        predecessors = [Version.from_xml(p) for p in xml if p.tag.lower() == "predecessor"]
-        successors = [Version.from_xml(s) for s in xml if s.tag.lower() == "successor"]
+        predecessors = [Version.from_xml(p) for p in xml if str(p.tag).lower() == "predecessor"]
+        successors = [Version.from_xml(s) for s in xml if str(s.tag).lower() == "successor"]
         return Version(id=xml.get("id"), date=xml.get("date"),
                        is_head=xml.get("head"),
                        is_complete_history=xml.get("completeHistory"),
diff --git a/src/linkahead/configuration.py b/src/linkahead/configuration.py
index f57289d7dcb6d7ab062024dc697dbda557670d7a..5081c28af253d3da31926ab1c9449309cc171c4f 100644
--- a/src/linkahead/configuration.py
+++ b/src/linkahead/configuration.py
@@ -30,6 +30,15 @@ import yaml
 try:
     optional_jsonschema_validate: Optional[Callable] = None
     from jsonschema import validate as optional_jsonschema_validate
+
+    # Adapted from https://github.com/python-jsonschema/jsonschema/issues/148
+    # Defines Validator to allow parsing of all iterables as array in jsonschema
+    # CustomValidator can be removed if/once jsonschema allows tuples for arrays
+    from collections.abc import Iterable
+    from jsonschema import validators
+    default = validators.validator_for(True)   # Returns latest supported draft
+    t_c = (default.TYPE_CHECKER.redefine('array', lambda x, y: isinstance(y, Iterable)))
+    CustomValidator = validators.extend(default, type_checker=t_c)
 except ImportError:
     pass
 
@@ -72,14 +81,40 @@ def get_config() -> ConfigParser:
     return _pycaosdbconf
 
 
-def config_to_yaml(config: ConfigParser) -> dict[str, dict[str, Union[int, str, bool]]]:
-    valobj: dict[str, dict[str, Union[int, str, bool]]] = {}
+def config_to_yaml(config: ConfigParser) -> dict[str, dict[str, Union[int, str, bool, tuple, None]]]:
+    """
+    Generates and returns a dict with all config options and their values
+    defined in the config.
+    The values of the options 'debug', 'timeout', and 'ssl_insecure' are
+    parsed, all other values are saved as string.
+
+    Parameters
+    ----------
+    config : ConfigParser
+        The config to be converted to a dict
+
+    Returns
+    -------
+    valobj : dict
+        A dict with config options and their values as key value pairs
+    """
+    valobj: dict[str, dict[str, Union[int, str, bool, tuple, None]]] = {}
     for s in config.sections():
         valobj[s] = {}
         for key, value in config[s].items():
             # TODO: Can the type be inferred from the config object?
-            if key in ["timeout", "debug"]:
+            if key in ["debug"]:
                 valobj[s][key] = int(value)
+            elif key in ["timeout"]:
+                value = "".join(value.split())          # Remove whitespace
+                if str(value).lower() in ["none", "null"]:
+                    valobj[s][key] = None
+                elif value.startswith('(') and value.endswith(')'):
+                    content = [None if str(s).lower() in ["none", "null"] else int(s)
+                               for s in value[1:-1].split(',')]
+                    valobj[s][key] = tuple(content)
+                else:
+                    valobj[s][key] = int(value)
             elif key in ["ssl_insecure"]:
                 valobj[s][key] = bool(value)
             else:
@@ -88,11 +123,12 @@ def config_to_yaml(config: ConfigParser) -> dict[str, dict[str, Union[int, str,
     return valobj
 
 
-def validate_yaml_schema(valobj: dict[str, dict[str, Union[int, str, bool]]]):
+def validate_yaml_schema(valobj: dict[str, dict[str, Union[int, str, bool, tuple, None]]]):
     if optional_jsonschema_validate:
         with open(os.path.join(os.path.dirname(__file__), "schema-pycaosdb-ini.yml")) as f:
             schema = yaml.load(f, Loader=yaml.SafeLoader)
-        optional_jsonschema_validate(instance=valobj, schema=schema["schema-pycaosdb-ini"])
+        optional_jsonschema_validate(instance=valobj, schema=schema["schema-pycaosdb-ini"],
+                                     cls=CustomValidator)
     else:
         warnings.warn("""
             Warning: The validation could not be performed because `jsonschema` is not installed.
diff --git a/src/linkahead/connection/authentication/interface.py b/src/linkahead/connection/authentication/interface.py
index b48e27c08312bf1358d32a9a1203627a9d0007c2..8288880583dc58fc82ab03d371861f067406b3d3 100644
--- a/src/linkahead/connection/authentication/interface.py
+++ b/src/linkahead/connection/authentication/interface.py
@@ -125,8 +125,9 @@ class AbstractAuthenticator(ABC):
         Returns
         -------
         """
-        self.auth_token = parse_auth_token(
-            response.getheader("Set-Cookie"))
+        new_token = parse_auth_token(response.getheader("Set-Cookie"))
+        if new_token is not None:
+            self.auth_token = new_token
 
     def on_request(self, method: str, path: str, headers: QueryDict, **kwargs):
         # pylint: disable=unused-argument
@@ -190,7 +191,7 @@ class CredentialsAuthenticator(AbstractAuthenticator):
     def _logout(self):
         self.logger.debug("[LOGOUT]")
         if self.auth_token is not None:
-            self._connection.request(method="DELETE", path="logout")
+            self._connection.request(method="GET", path="logout")
         self.auth_token = None
 
     def _login(self):
diff --git a/src/linkahead/connection/connection.py b/src/linkahead/connection/connection.py
index c95134fed3fd6b031b01b518c6362bf3b371c960..74dd23177c548dd640c6dd1c03ce4069c366802b 100644
--- a/src/linkahead/connection/connection.py
+++ b/src/linkahead/connection/connection.py
@@ -39,7 +39,7 @@ from requests.adapters import HTTPAdapter
 from requests.exceptions import ConnectionError as HTTPConnectionError
 from urllib3.poolmanager import PoolManager
 
-from ..configuration import get_config
+from ..configuration import get_config, config_to_yaml
 from ..exceptions import (ConfigurationError, HTTPClientError,
                           HTTPForbiddenError, HTTPResourceNotFoundError,
                           HTTPServerError, HTTPURITooLongError,
@@ -83,8 +83,10 @@ class _WrappedHTTPResponse(CaosDBHTTPResponse):
         return self.response.status_code
 
     def read(self, size: Optional[int] = None):
+        # FIXME This function behaves unexpectedly if `size` is larger than in the first run.
+
         if self._stream_consumed is True:
-            raise RuntimeError("Stream is consumed")
+            raise BufferError("Stream is consumed")
 
         if self._buffer is None:
             # the buffer has been drained in the previous call.
@@ -97,14 +99,14 @@ class _WrappedHTTPResponse(CaosDBHTTPResponse):
             return self.response.content
 
         if size is None or size == 0:
-            raise RuntimeError(
-                "size parameter should not be None if the stream is not consumed yet")
+            raise BufferError(
+                "`size` parameter can not be None or zero once reading has started with a non-zero "
+                "value.")
 
         if len(self._buffer) >= size:
             # still enough bytes in the buffer
-            # FIXME: `chunk`` is used before definition
-            result = chunk[:size]
-            self._buffer = chunk[size:]
+            result = self._buffer[:size]
+            self._buffer = self._buffer[size:]
             return result
 
         if self._generator is None:
@@ -116,16 +118,16 @@ class _WrappedHTTPResponse(CaosDBHTTPResponse):
         try:
             # read new data into the buffer
             chunk = self._buffer + next(self._generator)
-            result = chunk[:size]
+            result = chunk[:size]  # FIXME what if `size` is larger than at `iter_content(size)`?
             if len(result) == 0:
                 self._stream_consumed = True
             self._buffer = chunk[size:]
             return result
         except StopIteration:
             # drain buffer
-            result = self._buffer
+            last_result = self._buffer
             self._buffer = None
-            return result
+            return last_result
 
     def getheader(self, name: str, default=None):
         return self.response.headers[name] if name in self.response.headers else default
@@ -218,7 +220,7 @@ class _DefaultCaosDBServerConnection(CaosDBServerConnection):
                 "Connection failed. Network or server down? " + str(conn_err)
             )
 
-    def configure(self, **config):
+    def configure(self, **config) -> None:
         """configure.
 
         Configure the http connection.
@@ -420,8 +422,10 @@ def configure_connection(**kwargs):
         - "keyring"  Uses the `keyring` library.
         - "auth_token" Uses only a given auth_token.
 
-    timeout : int
+    timeout : int, tuple, or None
         A connection timeout in seconds. (Default: 210)
+        If a tuple is given, they are used as connect and read timeouts
+        respectively, timeout None disables the timeout.
 
     ssl_insecure : bool
         Whether SSL certificate warnings should be ignored. Only use this for
@@ -463,21 +467,29 @@ def configure_connection(**kwargs):
     global_conf = {}
     conf = get_config()
     # Convert config to dict, with preserving types
-    int_opts = ["timeout"]
+    int_opts = []
     bool_opts = ["ssl_insecure"]
+    other_opts = ["timeout"]
 
     if conf.has_section("Connection"):
         global_conf = dict(conf.items("Connection"))
-        # Integer options
 
+        # Integer options
         for opt in int_opts:
             if opt in global_conf:
                 global_conf[opt] = conf.getint("Connection", opt)
-        # Boolean options
 
+        # Boolean options
         for opt in bool_opts:
             if opt in global_conf:
                 global_conf[opt] = conf.getboolean("Connection", opt)
+
+        # Other options, defer parsing to configuration.config_to_yaml:
+        connection_config = config_to_yaml(conf)["Connection"]
+        for opt in other_opts:
+            if opt in global_conf:
+                global_conf[opt] = connection_config[opt]
+
     local_conf = _make_conf(_DEFAULT_CONF, global_conf, kwargs)
 
     connection = _Connection.get_instance()
@@ -551,9 +563,9 @@ class _Connection(object):  # pylint: disable=useless-object-inheritance
 
     __instance = None
 
-    def __init__(self):
+    def __init__(self) -> None:
         self._delegate_connection: Optional[CaosDBServerConnection] = None
-        self._authenticator: Optional[CredentialsAuthenticator] = None
+        self._authenticator: Optional[AbstractAuthenticator] = None
         self.is_configured = False
 
     @classmethod
@@ -563,7 +575,7 @@ class _Connection(object):  # pylint: disable=useless-object-inheritance
 
         return cls.__instance
 
-    def configure(self, **config):
+    def configure(self, **config) -> _Connection:
         self.is_configured = True
 
         if "implementation" not in config:
@@ -571,8 +583,7 @@ class _Connection(object):  # pylint: disable=useless-object-inheritance
                 "Missing CaosDBServerConnection implementation. You did not "
                 "specify an `implementation` for the connection.")
         try:
-            self._delegate_connection: CaosDBServerConnection = config["implementation"](
-            )
+            self._delegate_connection = config["implementation"]()
 
             if not isinstance(self._delegate_connection,
                               CaosDBServerConnection):
@@ -762,6 +773,7 @@ class _Connection(object):  # pylint: disable=useless-object-inheritance
         if self._authenticator is None:
             raise ValueError(
                 "No authenticator set. Please call configure_connection() first.")
+        assert isinstance(self._authenticator, CredentialsAuthenticator)
         if self._authenticator._credentials_provider is None:
             raise ValueError(
                 "No credentials provider set. Please call configure_connection() first.")
diff --git a/src/linkahead/connection/mockup.py b/src/linkahead/connection/mockup.py
index 9b69971c0409708f221c402f540fac85ff9c527e..d3bc13bb474a70d48446e8532607c3e11931ff05 100644
--- a/src/linkahead/connection/mockup.py
+++ b/src/linkahead/connection/mockup.py
@@ -75,7 +75,7 @@ class MockUpServerConnection(CaosDBServerConnection):
     just returns predefined responses which mimic the LinkAhead server."""
 
     def __init__(self):
-        self.resources = [self._login]
+        self.resources = [self._login, self._logout]
 
     def _login(self, method, path, headers, body):
         if method == "POST" and path == "login":
@@ -84,6 +84,12 @@ class MockUpServerConnection(CaosDBServerConnection):
                                            "mockup-auth-token"},
                                   body="")
 
+    def _logout(self, method, path, headers, body):
+        if method in ["DELETE", "GET"] and path == "logout":
+            return MockUpResponse(200,
+                                  headers={},
+                                  body="")
+
     def configure(self, **kwargs):
         """This configure method does nothing."""
 
diff --git a/src/linkahead/exceptions.py b/src/linkahead/exceptions.py
index 609d3654ac670a993185ba1faa33db921c44409c..7d4dc0850b811c0d696cc66252aa62541c6d3029 100644
--- a/src/linkahead/exceptions.py
+++ b/src/linkahead/exceptions.py
@@ -94,12 +94,26 @@ class HTTPServerError(LinkAheadException):
     """HTTPServerError represents 5xx HTTP server errors."""
 
     def __init__(self, body):
-        xml = etree.fromstring(body)
-        error = xml.xpath('/Response/Error')[0]
-        msg = error.get("description")
-
-        if error.text is not None:
-            msg = msg + "\n\n" + error.text
+        try:
+            # This only works if the server sends a valid XML
+            # response. Then it can be parsed for more information.
+            xml = etree.fromstring(body)
+            if xml.xpath('/Response/Error'):
+                error = xml.xpath('/Response/Error')[0]
+                msg = error.get("description") if error.get("description") is not None else ""
+
+                if error.text is not None:
+                    if msg:
+                        msg = msg + "\n\n" + error.text
+                    else:
+                        msg = error.text
+            else:
+                # Valid XML, but no error information
+                msg = body
+        except etree.XMLSyntaxError:
+            # Handling of incomplete responses, e.g., due to timeouts,
+            # c.f. https://gitlab.com/linkahead/linkahead-pylib/-/issues/87.
+            msg = body
         LinkAheadException.__init__(self, msg)
 
 
diff --git a/src/linkahead/schema-pycaosdb-ini.yml b/src/linkahead/schema-pycaosdb-ini.yml
index 89ce98570738fdd29dba81de25a2c022c1581467..ae46b905c62d2ab168229d92ff138937279c7aed 100644
--- a/src/linkahead/schema-pycaosdb-ini.yml
+++ b/src/linkahead/schema-pycaosdb-ini.yml
@@ -67,7 +67,13 @@ schema-pycaosdb-ini:
           description: This option is used internally and for testing. Do not override.
           examples: [_DefaultCaosDBServerConnection]
         timeout:
-          type: integer
+          oneOf:
+            - type: [integer, "null"]
+            - type: array
+              items:
+                type: [integer, "null"]
+              minItems: 2
+              maxItems: 2
       allOf:
         - if:
             properties:
diff --git a/src/linkahead/utils/git_utils.py b/src/linkahead/utils/git_utils.py
index 7a58272a3bef1930f75a1e08364349388e2bb89f..4824d619bfc77925add0c383f72360a644dd7833 100644
--- a/src/linkahead/utils/git_utils.py
+++ b/src/linkahead/utils/git_utils.py
@@ -36,9 +36,9 @@ logger = logging.getLogger(__name__)
 
 def get_origin_url_in(folder: str):
     """return the Fetch URL of the git repository in the given folder."""
-    with tempfile.NamedTemporaryFile(delete=False, mode="w") as t:
-        call(["git", "remote", "show", "origin"], stdout=t, cwd=folder)
-    with open(t.name, "r") as t:
+    with tempfile.NamedTemporaryFile(delete=False, mode="w", encoding="utf8") as tempf:
+        call(["git", "remote", "show", "origin"], stdout=tempf, cwd=folder)
+    with open(tempf.name, "r", encoding="utf8") as t:
         urlString = "Fetch URL:"
 
         for line in t.readlines():
@@ -63,9 +63,9 @@ def get_branch_in(folder: str):
     The command "git branch" is called in the given folder and the
     output is returned
     """
-    with tempfile.NamedTemporaryFile(delete=False, mode="w") as t:
-        call(["git", "rev-parse", "--abbrev-ref", "HEAD"], stdout=t, cwd=folder)
-    with open(t.name, "r") as t:
+    with tempfile.NamedTemporaryFile(delete=False, mode="w") as tempf:
+        call(["git", "rev-parse", "--abbrev-ref", "HEAD"], stdout=tempf, cwd=folder)
+    with open(tempf.name, "r") as t:
         return t.readline().strip()
 
 
@@ -76,7 +76,7 @@ def get_commit_in(folder: str):
     and the output is returned
     """
 
-    with tempfile.NamedTemporaryFile(delete=False, mode="w") as t:
-        call(["git", "log", "-1", "--format=%h"], stdout=t, cwd=folder)
-    with open(t.name, "r") as t:
+    with tempfile.NamedTemporaryFile(delete=False, mode="w") as tempf:
+        call(["git", "log", "-1", "--format=%h"], stdout=tempf, cwd=folder)
+    with open(tempf.name, "r") as t:
         return t.readline().strip()
diff --git a/src/linkahead/utils/plantuml.py b/src/linkahead/utils/plantuml.py
index 19594d6e856e740fe2c58c5128eead31c37485ce..59e3c34dd04c2425aef46b6d9e2411f75b747aca 100644
--- a/src/linkahead/utils/plantuml.py
+++ b/src/linkahead/utils/plantuml.py
@@ -130,9 +130,9 @@ def recordtypes_to_plantuml_string(iterable,
 
     classes = [el for el in iterable
                if isinstance(el, db.RecordType)]
-    dependencies = {}
-    inheritances = {}
-    properties = [p for p in iterable if isinstance(p, db.Property)]
+    dependencies: dict = {}
+    inheritances: dict = {}
+    properties: list = [p for p in iterable if isinstance(p, db.Property)]
     grouped = [g for g in iterable if isinstance(g, Grouped)]
 
     def _add_properties(c, importance=None):
@@ -272,7 +272,8 @@ package \"The property P references an instance of D\" <<Rectangle>> {
     return result
 
 
-def retrieve_substructure(start_record_types, depth, result_id_set=None, result_container=None, cleanup=True):
+def retrieve_substructure(start_record_types, depth, result_id_set=None, result_container=None,
+                          cleanup=True):
     """Recursively retrieves LinkAhead record types and properties, starting
     from given initial types up to a specific depth.
 
diff --git a/unittests/test_apiutils.py b/unittests/test_apiutils.py
index fdd5adda065a563b15008f1b840539c110921b65..6667089abc2d16e59bd97d16f7d0fe75d07afe1b 100644
--- a/unittests/test_apiutils.py
+++ b/unittests/test_apiutils.py
@@ -991,3 +991,31 @@ def test_describe_diff():
 
     assert "first" not in diffout
     assert "second" not in diffout
+
+
+def test_diff_without_names():
+    """Test compare_entities in case of properties and parents with
+    ids and without names
+    (cf. https://gitlab.com/linkahead/linkahead-pylib/-/issues/119).
+
+    """
+
+    r1 = db.Record(name="Test").add_parent(name="TestType")
+    r2 = db.Record(name="Test").add_parent(name="TestType")
+    r2.add_property(id=123, value="Test")
+
+    diff1, diff2 = compare_entities(r1, r2)
+    assert len(diff1["properties"]) == 0
+    assert len(diff2["properties"]) == 1
+    assert 123 in diff2["properties"]
+    assert None not in diff2["properties"]
+
+    r3 = db.Record().add_parent(id=101)
+    r4 = db.Record().add_parent(id=102)
+    diff3, diff4 = compare_entities(r3, r4)
+    assert len(diff3["parents"]) == 1
+    assert 101 in diff3["parents"]
+    assert None not in diff3["parents"]
+    assert len(diff4["parents"]) == 1
+    assert 102 in diff4["parents"]
+    assert None not in diff3["parents"]
diff --git a/unittests/test_authentication_auth_token.py b/unittests/test_authentication_auth_token.py
index 3142f1f9f54230cb19666eeb8ff5809a906f9d49..4eb17bcc3892a0d0cad0f2c86289c2e8c625d426 100644
--- a/unittests/test_authentication_auth_token.py
+++ b/unittests/test_authentication_auth_token.py
@@ -96,6 +96,6 @@ def test_logout_calls_delete():
                              auth_token="[request token]",
                              implementation=MockUpServerConnection)
 
-    c._delegate_connection.resources.append(logout_resource)
+    c._delegate_connection.resources.insert(1, logout_resource)
     c._logout()
     mock.method.assert_called_once()
diff --git a/unittests/test_configs/pylinkahead-timeout1.ini b/unittests/test_configs/pylinkahead-timeout1.ini
new file mode 100644
index 0000000000000000000000000000000000000000..d9f894bfeba4f98ed30d96d8c29e057b5a1e643a
--- /dev/null
+++ b/unittests/test_configs/pylinkahead-timeout1.ini
@@ -0,0 +1,4 @@
+[Connection]
+url=https://localhost:10443/
+password_method = unauthenticated
+timeout = None
diff --git a/unittests/test_configs/pylinkahead-timeout2.ini b/unittests/test_configs/pylinkahead-timeout2.ini
new file mode 100644
index 0000000000000000000000000000000000000000..b3d3796f82148459efb8e19344fe11af9e7934ec
--- /dev/null
+++ b/unittests/test_configs/pylinkahead-timeout2.ini
@@ -0,0 +1,4 @@
+[Connection]
+url=https://localhost:10443/
+password_method = unauthenticated
+timeout = (1,20)
diff --git a/unittests/test_configuration.py b/unittests/test_configuration.py
index 95bc906c6c044c51548aa864326cc93f29a6042a..772e872c08e0a7c4aae3feffdb58244f6ad0c849 100644
--- a/unittests/test_configuration.py
+++ b/unittests/test_configuration.py
@@ -24,6 +24,7 @@
 
 from os import environ, getcwd, remove
 from os.path import expanduser, isfile, join
+from pathlib import Path
 
 import linkahead as db
 import pytest
@@ -66,3 +67,18 @@ def test_config_ini_via_envvar(temp_ini_files):
     assert expanduser("~/.pylinkahead.ini") in db.configuration._read_config_files()
     # test configuration file in cwd
     assert join(getcwd(), "pylinkahead.ini") in db.configuration._read_config_files()
+
+
+def test_config_timeout_option():
+    expected_results = [None, (1, 20)]
+    # Iterate through timeout test configs
+    test_configs = Path(__file__).parent/'test_configs'
+    for test_config in test_configs.rglob('pylinkahead-timeout*.ini'):
+        # Test that test configs can be parsed
+        db.configure(str(test_config))
+        dct = db.configuration.config_to_yaml(db.get_config())
+        # Test that resulting dict has correct content for timeout
+        assert 'Connection' in dct
+        assert 'timeout' in dct['Connection']
+        assert dct['Connection']['timeout'] in expected_results
+        expected_results.remove(dct['Connection']['timeout'])
diff --git a/unittests/test_connection.py b/unittests/test_connection.py
index a3a1eff705c64f59baec33088906bdd9a4daa14d..5d22efa46e3a6c10452085d735d1bd6f056a81fc 100644
--- a/unittests/test_connection.py
+++ b/unittests/test_connection.py
@@ -25,14 +25,18 @@
 # pylint: disable=missing-docstring
 from __future__ import print_function, unicode_literals
 
+import io
 import re
 from builtins import bytes, str  # pylint: disable=redefined-builtin
 
+import requests
+
 from linkahead import execute_query
 from linkahead.configuration import _reset_config, get_config
 from linkahead.connection.authentication.interface import CredentialsAuthenticator
 from linkahead.connection.connection import (CaosDBServerConnection,
                                              _DefaultCaosDBServerConnection,
+                                             _WrappedHTTPResponse,
                                              configure_connection)
 from linkahead.connection.mockup import (MockUpResponse, MockUpServerConnection,
                                          _request_log_message)
@@ -216,9 +220,9 @@ def test_init_connection():
 def test_resources_list():
     connection = test_init_connection()
     assert hasattr(connection, "resources")
-    assert len(connection.resources) == 1
-    connection.resources.append(lambda **kwargs: test_init_response())
     assert len(connection.resources) == 2
+    connection.resources.append(lambda **kwargs: test_init_response())
+    assert len(connection.resources) == 3
 
     return connection
 
@@ -324,3 +328,51 @@ def test_auth_token_connection():
                                 "auth_token authenticator cannot log in "
                                 "again. You must provide a new authentication "
                                 "token.")
+
+
+def test_buffer_read():
+    """Test the buffering in _WrappedHTTPResponse.read()"""
+
+    class MockResponse(requests.Response):
+        def __init__(self, content: bytes):
+            """A mock response
+
+            Parameters
+            ----------
+            content : bytes
+              The fake content.
+            """
+            super().__init__()
+            self._content = content
+            bio = io.BytesIO(expected)
+            self.raw = bio
+
+    expected = b"This response."
+    MockResponse(expected)
+
+    #############################
+    # Check for some exceptions #
+    #############################
+    resp = _WrappedHTTPResponse(response=MockResponse(expected))
+    with raises(BufferError) as rte:
+        resp.read(4)
+        resp.read()
+    assert "`size` parameter can not be None" in str(rte.value)
+
+    resp = _WrappedHTTPResponse(response=MockResponse(expected))
+    with raises(BufferError) as rte:
+        resp.read(4)
+        resp.read(0)
+    assert "`size` parameter can not be None" in str(rte.value)
+
+    print("---")
+    resp = _WrappedHTTPResponse(response=MockResponse(expected))
+    result = (
+        resp.read(4)
+        + resp.read(2)
+        + resp.read(2)  # This line failed before.
+        + resp.read(4)  # Reading the rest in two chunks, because of current limitations in read().
+        + resp.read(2)
+    )
+
+    assert result == expected
diff --git a/unittests/test_container.py b/unittests/test_container.py
index c3a60140d43383c81f03c38c9dd5cc7779bc77ba..9df40ffbbdd62b93453058993dbe64bcf3028fb5 100644
--- a/unittests/test_container.py
+++ b/unittests/test_container.py
@@ -70,7 +70,8 @@ def test_get_property_values():
                                           )
     assert len(table) == 2
     house_row = table[0]
-    assert house_row == (house.name, 40.2, "ft", window.id, None, None, None, 20.5, 20.5, "m", owner.name)
+    assert house_row == (house.name, 40.2, "ft", window.id, None, None, None, 20.5, 20.5, "m",
+                         owner.name)
 
     owner_row = table[1]
     assert owner_row == (owner.name, None, None, None, None, None, None, None, None, None, None)
@@ -199,3 +200,15 @@ def test_container_slicing():
 
     with pytest.raises(TypeError):
         cont[[0, 2, 3]]
+
+
+def test_container_filter():
+    # this is a very rudimentary test since filter_by_identity is based on
+    # _filter_entity_list_by_identity which is tested
+    # separately
+    cont = db.Container()
+    cont.extend([db.Record(name=f"TestRec{ii+1}") for ii in range(5)])
+
+    recs = cont.filter_by_identity(name="TestRec2")
+    assert len(recs) == 1
+    recs[0].name == "TestRec2"
diff --git a/unittests/test_entity.py b/unittests/test_entity.py
index 2127ce028f4de55b8ef0ca704c1e69959c24ba82..855e5a39d53180d32a40de46bc7bb43d0bbd58bc 100644
--- a/unittests/test_entity.py
+++ b/unittests/test_entity.py
@@ -29,6 +29,7 @@ import unittest
 import linkahead
 from linkahead import (INTEGER, Entity, Parent, Property, Record, RecordType,
                        configure_connection)
+import warnings
 from linkahead.common.models import SPECIAL_ATTRIBUTES
 from linkahead.connection.mockup import MockUpServerConnection
 from lxml import etree
@@ -161,7 +162,7 @@ def test_property_list():
     pl.append(p3)
 
 
-def test_filter():
+def test_filter_by_identity():
     rt1 = RecordType(id=100)
     rt2 = RecordType(id=101, name="RT")
     rt3 = RecordType(name="")
@@ -184,7 +185,7 @@ def test_filter():
         for coll in [entity.properties, entity.parents]:
             for ent in test_ents:
                 assert ent not in coll
-                assert ent not in coll.filter(ent)
+                assert ent not in coll.filter_by_identity(ent)
 
         # Checks with each type
         t, t_props, t_pars = entity, entity.properties, entity.parents
@@ -194,23 +195,23 @@ def test_filter():
         tp1 = t.properties[-1]
         t.add_property(p3)
         tp3 = t.properties[-1]
-        assert len(t_props.filter(pid=100)) == 1
-        assert tp1 in t_props.filter(pid=100)
-        assert len(t_props.filter(pid="100")) == 1
-        assert tp1 in t_props.filter(pid="100")
-        assert len(t_props.filter(pid=101, name="RT")) == 1
-        assert tp3 in t_props.filter(pid=101, name="RT")
+        assert len(t_props.filter_by_identity(pid=100)) == 1
+        assert tp1 in t_props.filter_by_identity(pid=100)
+        assert len(t_props.filter_by_identity(pid="100")) == 1
+        assert tp1 in t_props.filter_by_identity(pid="100")
+        assert len(t_props.filter_by_identity(pid=101, name="RT")) == 1
+        assert tp3 in t_props.filter_by_identity(pid=101, name="RT")
         for entity in [rt1, p2, r1, r2]:
-            assert entity not in t_props.filter(pid=100)
-            assert tp1 in t_props.filter(entity)
+            assert entity not in t_props.filter_by_identity(pid=100)
+            assert tp1 in t_props.filter_by_identity(entity)
         # Check that direct addition (not wrapped) works
         t_props.append(p2)
         tp2 = t_props[-1]
-        assert tp2 in t_props.filter(pid=100)
-        assert tp2 not in t_props.filter(pid=101, name="RT")
+        assert tp2 in t_props.filter_by_identity(pid=100)
+        assert tp2 not in t_props.filter_by_identity(pid=101, name="RT")
         for entity in [rt1, r1, r2]:
-            assert entity not in t_props.filter(pid=100)
-            assert tp2 in t_props.filter(entity)
+            assert entity not in t_props.filter_by_identity(pid=100)
+            assert tp2 in t_props.filter_by_identity(entity)
 
         # Parents
         # Filtering with both name and id
@@ -218,67 +219,79 @@ def test_filter():
         tr3 = t.parents[-1]
         t.add_parent(r5)
         tr5 = t.parents[-1]
-        assert tr3 in t_pars.filter(pid=101)
-        assert tr5 not in t_pars.filter(pid=101)
-        assert tr3 not in t_pars.filter(name="R")
-        assert tr5 in t_pars.filter(name="R")
-        assert tr3 in t_pars.filter(pid=101, name="R")
-        assert tr5 not in t_pars.filter(pid=101, name="R")
-        assert tr3 not in t_pars.filter(pid=104, name="RT")
-        assert tr5 in t_pars.filter(pid=104, name="RT")
-        assert tr3 not in t_pars.filter(pid=105, name="T")
-        assert tr5 not in t_pars.filter(pid=105, name="T")
+        assert tr3 in t_pars.filter_by_identity(pid=101)
+        assert tr5 not in t_pars.filter_by_identity(pid=101)
+        assert tr3 not in t_pars.filter_by_identity(name="R")
+        assert tr5 in t_pars.filter_by_identity(name="R")
+        assert tr3 in t_pars.filter_by_identity(pid=101, name="R")
+        assert tr5 not in t_pars.filter_by_identity(pid=101, name="R")
+        assert tr3 not in t_pars.filter_by_identity(pid=104, name="RT")
+        assert tr5 in t_pars.filter_by_identity(pid=104, name="RT")
+        assert tr3 not in t_pars.filter_by_identity(pid=105, name="T")
+        assert tr5 not in t_pars.filter_by_identity(pid=105, name="T")
         # Works also without id / name and with duplicate parents
         for ent in test_ents:
             t.add_parent(ent)
         for ent in t_pars:
-            assert ent in t_pars.filter(ent)
+            assert ent in t_pars.filter_by_identity(ent)
 
     # Grid-Based
     r7 = Record()
     r7.add_property(Property()).add_property(name="A").add_property(name="B")
     r7.add_property(id=27).add_property(id=27, name="A").add_property(id=27, name="B")
     r7.add_property(id=43).add_property(id=43, name="A").add_property(id=43, name="B")
-    assert len(r7.properties.filter(pid=27)) == 3
-    assert len(r7.properties.filter(pid=43)) == 3
-    assert len(r7.properties.filter(pid=43, conjunction=True)) == 3
-    assert len(r7.properties.filter(name="A")) == 3
-    assert len(r7.properties.filter(name="B")) == 3
-    assert len(r7.properties.filter(name="B", conjunction=True)) == 3
-    assert len(r7.properties.filter(pid=1, name="A")) == 1
-    assert len(r7.properties.filter(pid=1, name="A", conjunction=True)) == 0
-    assert len(r7.properties.filter(pid=27, name="B")) == 4
-    assert len(r7.properties.filter(pid=27, name="B", conjunction=True)) == 1
-    assert len(r7.properties.filter(pid=27, name="C")) == 3
-    assert len(r7.properties.filter(pid=27, name="C", conjunction=True)) == 0
+    assert len(r7.properties.filter_by_identity(pid=27)) == 3
+    assert len(r7.properties.filter_by_identity(pid=43)) == 3
+    assert len(r7.properties.filter_by_identity(pid=43, conjunction=True)) == 3
+    assert len(r7.properties.filter_by_identity(name="A")) == 3
+    assert len(r7.properties.filter_by_identity(name="B")) == 3
+    assert len(r7.properties.filter_by_identity(name="B", conjunction=True)) == 3
+    assert len(r7.properties.filter_by_identity(pid=1, name="A")) == 1
+    assert len(r7.properties.filter_by_identity(pid=1, name="A", conjunction=True)) == 0
+    assert len(r7.properties.filter_by_identity(pid=27, name="B")) == 4
+    assert len(r7.properties.filter_by_identity(pid=27, name="B", conjunction=True)) == 1
+    assert len(r7.properties.filter_by_identity(pid=27, name="C")) == 3
+    assert len(r7.properties.filter_by_identity(pid=27, name="C", conjunction=True)) == 0
     # Entity based filtering behaves the same
-    assert (r7.properties.filter(pid=27) ==
-            r7.properties.filter(Property(id=27)))
-    assert (r7.properties.filter(pid=43, conjunction=True) ==
-            r7.properties.filter(Property(id=43), conjunction=True))
-    assert (r7.properties.filter(name="A") ==
-            r7.properties.filter(Property(name="A")))
-    assert (r7.properties.filter(name="B") ==
-            r7.properties.filter(Property(name="B")))
-    assert (r7.properties.filter(name="B", conjunction=True) ==
-            r7.properties.filter(Property(name="B"), conjunction=True))
-    assert (r7.properties.filter(pid=1, name="A") ==
-            r7.properties.filter(Property(id=1, name="A")))
-    assert (r7.properties.filter(pid=1, name="A", conjunction=True) ==
-            r7.properties.filter(Property(id=1, name="A"), conjunction=True))
-    assert (r7.properties.filter(pid=27, name="B") ==
-            r7.properties.filter(Property(id=27, name="B")))
-    assert (r7.properties.filter(pid=27, name="B", conjunction=True) ==
-            r7.properties.filter(Property(id=27, name="B"), conjunction=True))
-    assert (r7.properties.filter(pid=27, name="C") ==
-            r7.properties.filter(Property(id=27, name="C")))
-    assert (r7.properties.filter(pid=27, name="C", conjunction=True) ==
-            r7.properties.filter(Property(id=27, name="C"), conjunction=True))
+    assert (r7.properties.filter_by_identity(pid=27) ==
+            r7.properties.filter_by_identity(Property(id=27)))
+    assert (r7.properties.filter_by_identity(pid=43, conjunction=True) ==
+            r7.properties.filter_by_identity(Property(id=43), conjunction=True))
+    assert (r7.properties.filter_by_identity(name="A") ==
+            r7.properties.filter_by_identity(Property(name="A")))
+    assert (r7.properties.filter_by_identity(name="B") ==
+            r7.properties.filter_by_identity(Property(name="B")))
+    assert (r7.properties.filter_by_identity(name="B", conjunction=True) ==
+            r7.properties.filter_by_identity(Property(name="B"), conjunction=True))
+    assert (r7.properties.filter_by_identity(pid=1, name="A") ==
+            r7.properties.filter_by_identity(Property(id=1, name="A")))
+    assert (r7.properties.filter_by_identity(pid=1, name="A", conjunction=True) ==
+            r7.properties.filter_by_identity(Property(id=1, name="A"), conjunction=True))
+    assert (r7.properties.filter_by_identity(pid=27, name="B") ==
+            r7.properties.filter_by_identity(Property(id=27, name="B")))
+    assert (r7.properties.filter_by_identity(pid=27, name="B", conjunction=True) ==
+            r7.properties.filter_by_identity(Property(id=27, name="B"), conjunction=True))
+    assert (r7.properties.filter_by_identity(pid=27, name="C") ==
+            r7.properties.filter_by_identity(Property(id=27, name="C")))
+    assert (r7.properties.filter_by_identity(pid=27, name="C", conjunction=True) ==
+            r7.properties.filter_by_identity(Property(id=27, name="C"), conjunction=True))
     # Name only matching and name overwrite
     r8 = Record().add_property(name="A").add_property(name="B").add_property(name="B")
     r8.add_property(Property(name="A"), name="B")
     r8.add_property(Property(name="A", id=12), name="C")
-    assert len(r8.properties.filter(name="A")) == 1
-    assert len(r8.properties.filter(name="B")) == 3
-    assert len(r8.properties.filter(name="C")) == 1
-    assert len(r8.properties.filter(pid=12)) == 1
+    assert len(r8.properties.filter_by_identity(name="A")) == 1
+    assert len(r8.properties.filter_by_identity(name="B")) == 3
+    assert len(r8.properties.filter_by_identity(name="C")) == 1
+    assert len(r8.properties.filter_by_identity(pid=12)) == 1
+
+    with warnings.catch_warnings(record=True) as w:
+        # Cause all warnings to always be triggered.
+        warnings.simplefilter("always")
+
+        r7.properties.filter(pid=34)
+        assert issubclass(w[-1].category, DeprecationWarning)
+        assert "This function was renamed" in str(w[-1].message)
+
+        t.parents.filter(pid=234)
+        assert issubclass(w[-1].category, DeprecationWarning)
+        assert "This function was renamed" in str(w[-1].message)
diff --git a/unittests/test_error_handling.py b/unittests/test_error_handling.py
index 3f5241466e9a8f810b581cbb587e17ccf8f123ee..64f743c85e9df554e7428cf7d8477e8c823a9758 100644
--- a/unittests/test_error_handling.py
+++ b/unittests/test_error_handling.py
@@ -30,7 +30,7 @@ import linkahead as db
 from linkahead.common.models import raise_errors
 from linkahead.exceptions import (AuthorizationError,
                                   EntityDoesNotExistError, EntityError,
-                                  EntityHasNoDatatypeError,
+                                  EntityHasNoDatatypeError, HTTPServerError,
                                   TransactionError, UniqueNamesError,
                                   UnqualifiedParentsError,
                                   UnqualifiedPropertiesError)
@@ -315,3 +315,26 @@ def test_container_with_faulty_elements():
             # record raises both of them
             assert (isinstance(err, UnqualifiedParentsError) or
                     isinstance(err, UnqualifiedPropertiesError))
+
+
+def test_incomplete_server_error_response():
+    """The reason behind https://gitlab.com/linkahead/linkahead-pylib/-/issues/87."""
+    # Case 1: Response is no XML at all
+    err = HTTPServerError("Bla")
+    assert str(err) == "Bla"
+
+    # Case 2: Response is an incomplete XML, e.g. due to very unlucky timeout
+    err = HTTPServerError("<incomplete>XML</inc")
+    assert str(err) == "<incomplete>XML</inc"
+
+    # Case 3: Response is complete XML but doesn't have response and or error information
+    err = HTTPServerError("<complete>XML</complete>")
+    assert str(err) == "<complete>XML</complete>"
+
+    # Case 4: Response is an XML response but the error is lacking a description
+    err = HTTPServerError("<Response><Error>complete error</Error></Response>")
+    assert str(err) == "complete error"
+
+    # Case 5: Healthy error Response
+    err = HTTPServerError("<Response><Error description='Error'>complete error</Error></Response>")
+    assert str(err) == "Error\n\ncomplete error"
diff --git a/unittests/test_issues.py b/unittests/test_issues.py
index e24afbe8b7be8d9a87d85819eccd3a4bf0d453e8..3b0117b28c1300ea1eb0919fce02e3881c2ab025 100644
--- a/unittests/test_issues.py
+++ b/unittests/test_issues.py
@@ -26,6 +26,7 @@ import linkahead as db
 
 from datetime import date, datetime
 from pytest import raises
+from linkahead.common.utils import xml2str
 
 
 def test_issue_100():
@@ -90,3 +91,40 @@ def test_issue_128():
     assert prop_list.value == [today, today]
     prop_list.value = [now, now]
     assert prop_list.value == [now, now]
+
+
+def test_issue_73():
+    """
+    Test to_xml infinite recursion handling with cross- and self-references.
+    https://gitlab.com/linkahead/linkahead-pylib/-/issues/73
+    """
+    # Cross-reference in the property values
+    rt = db.RecordType(name="RT")
+    recA = db.Record().add_parent(rt)
+    recB = db.Record().add_parent(rt)
+    recA.add_property(name="RT", value=recB)
+    recB.add_property(name="RT", value=recA)
+    xml_str = xml2str(recB.to_xml())
+    assert "<Parent name=\"RT" in xml_str
+    assert "<Property name=\"RT" in xml_str
+    assert "Recursive reference" in xml_str
+    assert len(xml_str) < 500
+
+    # Cross-reference in the properties themselves
+    prop1 = db.Property(name="Prop1")
+    prop2 = db.Property(name="Prop2")
+    prop1.add_property(prop2)
+    prop2.add_property(prop1)
+    xml_str = xml2str(prop2.to_xml())
+    assert "<Property name=\"Prop1" in xml_str
+    assert "<Property name=\"Prop2" in xml_str
+    assert "Recursive reference" in xml_str
+    assert len(xml_str) < 500
+
+    # Self-reference in the datatype
+    prop = db.Property()
+    prop.datatype = prop
+    xml_str = xml2str(prop.to_xml())
+    assert "datatype=" in xml_str
+    assert "Recursive reference" in xml_str
+    assert len(xml_str) < 500