diff --git a/CHANGELOG.md b/CHANGELOG.md
index 765b3c66ff2d0f1e47254b8dbd89b1e7330d2fcd..cd4f2ab2863999774af6a751f378da00b81a83e7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,51 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [Unreleased]
+
+### Added ###
+
+### Changed ###
+
+### Deprecated ###
+
+### Removed ###
+
+### Fixed ###
+
+### Security ###
+
+### Documentation ###
+
+## [0.7.3] - 2022-05-03
+(Henrik tom Wörden)
+
+### Added ###
+
+- New function in apiutils that copies an Entity.
+- New EXPERIMENTAL module `high_level_api` which is a completely refactored version of
+  the old `high_level_api` from apiutils. Please see the included documentation for details.
+- `to_graphics` now has  `no_shadow` option.
+
+### Changed ###
+
+- Added additional customization options to the plantuml module.
+- The to_graphics function in the plantuml module uses a temporary directory now for creating the output files.
+
+### Deprecated ###
+
+### Removed ###
+
+### Fixed ###
+
+* [#75](https://gitlab.indiscale.com/caosdb/src/caosdb-pylib/-/issues/75), [#103](https://gitlab.indiscale.com/caosdb/src/caosdb-pylib/-/issues/103) Fixed JSON schema to allow more sections, and correct requirements for
+  password method.
+
+### Security ###
+
+### Documentation ###
+
+
 ## [0.7.2] - 2022-03-25 ##
 (Timm Fitschen)
 
diff --git a/FEATURES.md b/FEATURES.md
new file mode 100644
index 0000000000000000000000000000000000000000..977d7a482279af00bc5e2b02a13d5d23564f1d04
--- /dev/null
+++ b/FEATURES.md
@@ -0,0 +1,8 @@
+# Experimental Features
+
+- High Level API in the module `high_level_api` is experimental and might be removed in future. It is for playing around with a possible future implementation of the Python client. See `src/doc/future_caosdb.md`
+
+
+# Features
+TODO: This is currently an incomplete list.
+- `to_graphics` defined in `caosdb.utils.plantuml` can be used to create an UML diagram of a data model
diff --git a/README_SETUP.md b/README_SETUP.md
index e58f934ceba176e4b5ba42239565f8e3bd48171a..dc667da8aa5877132c1212d2ddd2827e85992118 100644
--- a/README_SETUP.md
+++ b/README_SETUP.md
@@ -82,60 +82,8 @@ pip3 install --user .[jsonschema]
 
 ## Configuration ##
 
-The  configuration is done using `ini` configuration files.
-PyCaosDB tries to read from the inifile specified in the environment variable `PYCAOSDBINI` or
-alternatively in `~/.pycaosdb.ini` upon import.  After that, the ini file `pycaosdb.ini` in the
-current working directory will be read additionally, if it exists.
-
-Here, we will look at the most common configuration options. For a full and 
-comprehensive description please check out 
-[pycaosdb.ini file](https://gitlab.com/caosdb/caosdb-pylib/-/blob/main/examples/pycaosdb.ini) 
-You can download this file and use it as a starting point.
-
-
-Typically, you need to change at least the `url` and `username` fields as required. 
-(Ask your CaosDB administrator or IT crowd if
-you do not know what to put there, but for the demo instances https://demo.indiscale.com, `username=admin`
-and `password=caosdb` should work).
-
-### Authentication ###
-
-The default configuration (that your are asked for your password when ever a connection is created
-can be changed by setting `password_method`:
-
-* with `password_method=input` password (and possibly user) will be queried on demand (**default**)
-* use the password manager [pass](https://www.passwordstore.org) by using `pass` as value, see also the [ArchWiki
-  entry](https://wiki.archlinux.org/index.php/Pass#Basic_usage). This also requires `password_identifier` which refers to the identifier within pass
-  for the desired password.
-* install the python package [keyring](https://pypi.org/project/keyring), to use the system keyring/wallet (macOS, GNOME, KDE,
-  Windows). The password will be queried on first usage.
-* with `password_method=plain` (**strongly discouraged**)
-
-The following illustrates the recommended options:
-
-```ini
-[Connection]
-# using "pass" password manager
-#password_method=pass
-#password_identifier=...
-
-# using the system keyring/wallet (macOS, GNOME, KDE, Windows)
-#password_method=keyring
-```
-
-### SSL Certificate ###
-In some cases (especially if you are testing CaosDB) you might need to supply 
-an SSL certificate to allow SSL encryption.
-
-```ini
-[Connection]
-cacert=/path/to/caosdb.ca.pem
-```
-
-### Further Settings ###
-As mentioned above, a complete list of options can be found in the 
-[pycaosdb.ini file](https://gitlab.com/caosdb/caosdb-pylib/-/blob/main/examples/pycaosdb.ini) in 
-the examples folder of the source code.
+The configuration is done using `ini` configuration files.  The content of these configuration files
+is described in detail in the [configuration section of the documentation](https://docs.indiscale.com/caosdb-pylib/configuration.html).
 
 ## Try it out ##
 
@@ -155,7 +103,10 @@ like this, check out the "Authentication" section in the [configuration document
 Now would be a good time to continue with the [tutorials](tutorials/index).
 
 ## Run Unit Tests
-tox
+
+- 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`
 
 ## Documentation ##
 
diff --git a/RELEASE_GUIDELINES.md b/RELEASE_GUIDELINES.md
index e71234b8e2bc95f954ffbebdc26acf6edd8e0b2d..b4e38d643756798f0ba8b07d6eceec529cbb3054 100644
--- a/RELEASE_GUIDELINES.md
+++ b/RELEASE_GUIDELINES.md
@@ -36,8 +36,10 @@ guidelines of the CaosDB Project
 9. Publish the release by executing `./release.sh` with uploads the caosdb
    module to the Python Package Index [pypi.org](https://pypi.org).
 
-10. Merge the main branch back into the dev branch.
+10. Create a gitlab release on gitlab.indiscale.com and gitlab.com
 
-11. After the merge of main to dev, start a new development version by
+11. Merge the main branch back into the dev branch.
+
+12. After the merge of main to dev, start a new development version by
     setting `ISRELEASED` to `False` and by increasing at least the `MICRO`
     version in [setup.py](./setup.py) and preparing CHANGELOG.md.
diff --git a/setup.py b/setup.py
index 2207a8d8ff0a56c7b92ddc481946f5c43f27420f..8d75a0c646d1c78448af7af62530376dfc0f443b 100755
--- a/setup.py
+++ b/setup.py
@@ -48,8 +48,12 @@ from setuptools import find_packages, setup
 ISRELEASED = True
 MAJOR = 0
 MINOR = 7
-MICRO = 2
-PRE = None  # e.g. rc0, alpha.1, 0.beta-23
+MICRO = 3
+# Do not tag as pre-release until this commit
+# https://github.com/pypa/packaging/pull/515
+# has made it into a release. Probably we should wait for pypa/packaging>=21.4
+# https://github.com/pypa/packaging/releases
+PRE = "" # "dev"  # e.g. rc0, alpha.1, 0.beta-23
 
 if PRE:
     VERSION = "{}.{}.{}-{}".format(MAJOR, MINOR, MICRO, PRE)
diff --git a/src/caosdb/apiutils.py b/src/caosdb/apiutils.py
index dc9209b58c8163da552f29e7a4435a0c640b1ecf..a376068c372c1b6f460c7927467b8da8df328545 100644
--- a/src/caosdb/apiutils.py
+++ b/src/caosdb/apiutils.py
@@ -33,11 +33,15 @@ import warnings
 from collections.abc import Iterable
 from subprocess import call
 
+from typing import Optional, Any
+
 from caosdb.common.datatype import (BOOLEAN, DATETIME, DOUBLE, FILE, INTEGER,
                                     REFERENCE, TEXT, is_reference)
 from caosdb.common.models import (Container, Entity, File, Property, Query,
                                   Record, RecordType, execute_query,
-                                  get_config)
+                                  get_config, SPECIAL_ATTRIBUTES)
+
+import logging
 
 
 def new_record(record_type, name=None, description=None,
@@ -97,22 +101,6 @@ def create_id_query(ids):
         ["ID={}".format(id) for id in ids])
 
 
-def retrieve_entity_with_id(eid):
-    return execute_query("FIND ENTITY WITH ID={}".format(eid), unique=True)
-
-
-def retrieve_entities_with_ids(entities):
-    collection = Container()
-    step = 20
-
-    for i in range(len(entities)//step+1):
-        collection.extend(
-            execute_query(
-                create_id_query(entities[i*step:(i+1)*step])))
-
-    return collection
-
-
 def get_type_of_entity_with(id_):
     objs = retrieve_entities_with_ids([id_])
 
@@ -136,385 +124,20 @@ def get_type_of_entity_with(id_):
         return Entity
 
 
-class CaosDBPythonEntity(object):
-
-    _last_id = 0
-
-    def __init__(self):
-        warnings.warn("The CaosDBPythonEntity class is deprecated, replacements will be provided by"
-                      " the high_level_api module.",
-                      DeprecationWarning, stacklevel=2)
-        # Save a copy of the dry state
-        # of this object in order to be
-        # able to detect conflicts.
-        self.do_not_expand = False
-        self._parents = []
-        self._id = CaosDBPythonEntity._get_id()
-        self._path = None
-        self._file = None
-        self.pickup = None
-        # TODO:
-        # 3.) resolve references up to a specific depth (including infinity)
-        # 4.) resolve parents function -> partially implemented by
-        # get_parent_names
-        self._references = {}
-        self._properties = set()
-        self._datatypes = {}
-        self._forbidden = dir(self)
-
-    @staticmethod
-    def _get_id():
-        CaosDBPythonEntity._last_id -= 1
-
-        return CaosDBPythonEntity._last_id
-
-    def _set_property_from_entity(self, ent):
-        name = ent.name
-        val = ent.value
-        pr = ent.datatype
-        val, reference = self._type_converted_value(val, pr)
-        self.set_property(name, val, reference, datatype=pr)
-
-    def set_property(self, name, value, is_reference=False,
-                     overwrite=False, datatype=None):
-        """
-        overwrite: Use this if you definitely only want one property with that name (set to True).
-        """
-        self._datatypes[name] = datatype
-
-        if isinstance(name, Entity):
-            name = name.name
-
-        if name in self._forbidden:
-            raise RuntimeError("Entity cannot be converted to a corresponding "
-                               "Python representation. Name of property " +
-                               name + " is forbidden!")
-        already_exists = (name in dir(self))
-
-        if already_exists and not overwrite:
-            # each call to _set_property checks first if it already exists
-            #        if yes: Turn the attribute into a list and
-            #                place all the elements into that list.
-            att = self.__getattribute__(name)
-
-            if isinstance(att, list):
-                pass
-            else:
-                old_att = self.__getattribute__(name)
-                self.__setattr__(name, [old_att])
-
-                if is_reference:
-                    self._references[name] = [
-                        self._references[name]]
-            att = self.__getattribute__(name)
-            att.append(value)
-
-            if is_reference:
-                self._references[name].append(int(value))
-        else:
-            if is_reference:
-                self._references[name] = value
-            self.__setattr__(name, value)
-
-        if not (already_exists and overwrite):
-            self._properties.add(name)
-
-    add_property = set_property
-
-    def set_id(self, idx):
-        self._id = idx
-
-    def _type_converted_list(self, val, pr):
-        """Convert a list to a python list of the correct type."""
-        prrealpre = pr.replace("&lt;", "<").replace("&gt;", ">")
-        prreal = prrealpre[prrealpre.index("<") + 1:prrealpre.rindex(">")]
-        lst = [self._type_converted_value(i, prreal) for i in val]
-
-        return ([i[0] for i in lst], lst[0][1])
-
-    def _type_converted_value(self, val, pr):
-        """Convert val to the correct type which is indicated by the database
-        type string in pr.
-
-        Returns a tuple with two entries:
-        - the converted value
-        - True if the value has to be interpreted as an id acting as a reference
-        """
-
-        if val is None:
-            return (None, False)
-        elif pr == DOUBLE:
-            return (float(val), False)
-        elif pr == BOOLEAN:
-            return (bool(val), False)
-        elif pr == INTEGER:
-            return (int(val), False)
-        elif pr == TEXT:
-            return (val, False)
-        elif pr == FILE:
-            return (int(val), False)
-        elif pr == REFERENCE:
-            return (int(val), True)
-        elif pr == DATETIME:
-            return (val, False)
-        elif pr[0:4] == "LIST":
-            return self._type_converted_list(val, pr)
-        elif isinstance(val, Entity):
-            return (convert_to_python_object(val), False)
-        else:
-            return (int(val), True)
-
-    def attribute_as_list(self, name):
-        """This is a workaround for the problem that lists containing only one
-        element are indistinguishable from simple types in this
-        representation."""
-        att = self.__getattribute__(name)
-
-        if isinstance(att, list):
-            return att
-        else:
-            return [att]
-
-    def _add_parent(self, parent):
-        self._parents.append(parent.id)
-
-    def add_parent(self, parent_id=None, parent_name=None):
-        if parent_id is not None:
-            self._parents.append(parent_id)
-        elif parent_name is not None:
-            self._parents.append(parent_name)
-        else:
-            raise ValueError("no parent identifier supplied")
-
-    def get_parent_names(self):
-        new_plist = []
-
-        for p in self._parents:
-            obj_type = get_type_of_entity_with(p)
-            ent = obj_type(id=p).retrieve()
-            new_plist.append(ent.name)
-
-        return new_plist
-
-    def resolve_references(self, deep=False, visited=dict()):
-        for i in self._references:
-            if isinstance(self._references[i], list):
-                for j in range(len(self._references[i])):
-                    new_id = self._references[i][j]
-                    obj_type = get_type_of_entity_with(new_id)
-
-                    if new_id in visited:
-                        new_object = visited[new_id]
-                    else:
-                        ent = obj_type(id=new_id).retrieve()
-                        new_object = convert_to_python_object(ent)
-                        visited[new_id] = new_object
-
-                        if deep:
-                            new_object.resolve_references(deep, visited)
-                    self.__getattribute__(i)[j] = new_object
-            else:
-                new_id = self._references[i]
-                obj_type = get_type_of_entity_with(new_id)
-
-                if new_id in visited:
-                    new_object = visited[new_id]
-                else:
-                    ent = obj_type(id=new_id).retrieve()
-                    new_object = convert_to_python_object(ent)
-                    visited[new_id] = new_object
-
-                    if deep:
-                        new_object.resolve_references(deep, visited)
-                self.__setattr__(i, new_object)
-
-    def __str__(self, indent=1, name=None):
-        if name is None:
-            result = str(self.__class__.__name__) + "\n"
-        else:
-            result = name + "\n"
-
-        for p in self._properties:
-            value = self.__getattribute__(p)
-
-            if isinstance(value, CaosDBPythonEntity):
-                result += indent * "\t" + \
-                    value.__str__(indent=indent + 1, name=p)
-            else:
-                result += indent * "\t" + p + "\n"
-
-        return result
-
-
-class CaosDBPythonRecord(CaosDBPythonEntity):
-    pass
-
-
-class CaosDBPythonRecordType(CaosDBPythonEntity):
-    pass
-
-
-class CaosDBPythonProperty(CaosDBPythonEntity):
-    pass
-
-
-class CaosDBPythonFile(CaosDBPythonEntity):
-    def get_File(self, target=None):
-        f = File(id=self._id).retrieve()
-        self._file = f.download(target)
-
-
-def _single_convert_to_python_object(robj, entity):
-    robj._id = entity.id
-
-    for i in entity.properties:
-        robj._set_property_from_entity(i)
-
-    for i in entity.parents:
-        robj._add_parent(i)
-
-    if entity.path is not None:
-        robj._path = entity.path
-
-    if entity.file is not None:
-        robj._file = entity.file
-    # if entity.pickup is not None:
-    #     robj.pickup = entity.pickup
-
-    return robj
-
-
-def _single_convert_to_entity(entity, robj, **kwargs):
-    if robj._id is not None:
-        entity.id = robj._id
-
-    if robj._path is not None:
-        entity.path = robj._path
-
-    if robj._file is not None:
-        entity.file = robj._file
-
-    if robj.pickup is not None:
-        entity.pickup = robj.pickup
-    children = []
+def retrieve_entity_with_id(eid):
+    return execute_query("FIND ENTITY WITH ID={}".format(eid), unique=True)
 
-    for parent in robj._parents:
-        if sys.version_info[0] < 3:
-            if hasattr(parent, "encode"):
-                entity.add_parent(name=parent)
-            else:
-                entity.add_parent(id=parent)
-        else:
-            if hasattr(parent, "encode"):
-                entity.add_parent(name=parent)
-            else:
-                entity.add_parent(id=parent)
 
-    def add_property(entity, prop, name, recursive=False, datatype=None):
-        if datatype is None:
-            raise ArgumentError("datatype must not be None")
+def retrieve_entities_with_ids(entities):
+    collection = Container()
+    step = 20
 
-        if isinstance(prop, CaosDBPythonEntity):
-            entity.add_property(name=name, value=str(
-                prop._id), datatype=datatype)
+    for i in range(len(entities)//step+1):
+        collection.extend(
+            execute_query(
+                create_id_query(entities[i*step:(i+1)*step])))
 
-            if recursive and not prop.do_not_expand:
-                return convert_to_entity(prop, recursive=recursive)
-            else:
-                return []
-        else:
-            if isinstance(prop, float) or isinstance(prop, int):
-                prop = str(prop)
-            entity.add_property(name=name, value=prop, datatype=datatype)
-
-            return []
-
-    for prop in robj._properties:
-        value = robj.__getattribute__(prop)
-
-        if isinstance(value, list):
-            if robj._datatypes[prop][0:4] == "LIST":
-                lst = []
-
-                for v in value:
-                    if isinstance(v, CaosDBPythonEntity):
-                        lst.append(v._id)
-
-                        if recursive and not v.do_not_expand:
-                            children.append(convert_to_entity(
-                                v, recursive=recursive))
-                    else:
-                        if isinstance(v, float) or isinstance(v, int):
-                            lst.append(str(v))
-                        else:
-                            lst.append(v)
-                entity.add_property(name=prop, value=lst,
-                                    datatype=robj._datatypes[prop])
-            else:
-                for v in value:
-                    children.extend(
-                        add_property(
-                            entity,
-                            v,
-                            prop,
-                            datatype=robj._datatypes[prop],
-                            **kwargs))
-        else:
-            children.extend(
-                add_property(
-                    entity,
-                    value,
-                    prop,
-                    datatype=robj._datatypes[prop],
-                    **kwargs))
-
-    return [entity] + children
-
-
-def convert_to_entity(python_object, **kwargs):
-    warnings.warn("The convert_to_entity function is deprecated, replacement will be provided by "
-                  "the high_level_api module.", DeprecationWarning, stacklevel=2)
-
-    if isinstance(python_object, Container):
-        # Create a list of objects:
-
-        return [convert_to_python_object(i, **kwargs) for i in python_object]
-    elif isinstance(python_object, CaosDBPythonRecord):
-        return _single_convert_to_entity(Record(), python_object, **kwargs)
-    elif isinstance(python_object, CaosDBPythonFile):
-        return _single_convert_to_entity(File(), python_object, **kwargs)
-    elif isinstance(python_object, CaosDBPythonRecordType):
-        return _single_convert_to_entity(RecordType(), python_object, **kwargs)
-    elif isinstance(python_object, CaosDBPythonProperty):
-        return _single_convert_to_entity(Property(), python_object, **kwargs)
-    elif isinstance(python_object, CaosDBPythonEntity):
-        return _single_convert_to_entity(Entity(), python_object, **kwargs)
-    else:
-        raise ValueError("Cannot convert an object of this type.")
-
-
-def convert_to_python_object(entity):
-    """"""
-
-    warnings.warn("The convert_to_python_object function is deprecated, replacement will be "
-                  "provided by the high_level_api module.", DeprecationWarning, stacklevel=2)
-    if isinstance(entity, Container):
-        # Create a list of objects:
-
-        return [convert_to_python_object(i) for i in entity]
-    elif isinstance(entity, Record):
-        return _single_convert_to_python_object(CaosDBPythonRecord(), entity)
-    elif isinstance(entity, RecordType):
-        return _single_convert_to_python_object(
-            CaosDBPythonRecordType(), entity)
-    elif isinstance(entity, File):
-        return _single_convert_to_python_object(CaosDBPythonFile(), entity)
-    elif isinstance(entity, Property):
-        return _single_convert_to_python_object(CaosDBPythonProperty(), entity)
-    elif isinstance(entity, Entity):
-        return _single_convert_to_python_object(CaosDBPythonEntity(), entity)
-    else:
-        raise ValueError("Cannot convert an object of this type.")
+    return collection
 
 
 def getOriginUrlIn(folder):
@@ -565,10 +188,6 @@ def getCommitIn(folder):
         return t.readline().strip()
 
 
-COMPARED = ["name", "role", "datatype", "description", "importance",
-            "id", "path", "checksum", "size"]
-
-
 def compare_entities(old_entity: Entity, new_entity: Entity):
     """
     Compare two entites.
@@ -586,13 +205,13 @@ def compare_entities(old_entity: Entity, new_entity: Entity):
         In case of changed information the value listed under the respective key shows the
         value that is stored in the respective entity.
     """
-    olddiff = {"properties": {}, "parents": []}
-    newdiff = {"properties": {}, "parents": []}
+    olddiff: dict[str, Any] = {"properties": {}, "parents": []}
+    newdiff: dict[str, Any] = {"properties": {}, "parents": []}
 
     if old_entity is new_entity:
         return (olddiff, newdiff)
 
-    for attr in COMPARED:
+    for attr in SPECIAL_ATTRIBUTES:
         try:
             oldattr = old_entity.__getattribute__(attr)
             old_entity_attr_exists = True
@@ -681,10 +300,77 @@ def compare_entities(old_entity: Entity, new_entity: Entity):
     return (olddiff, newdiff)
 
 
+def merge_entities(entity_a: Entity, entity_b: Entity):
+    """
+    Merge entity_b into entity_a such that they have the same parents and properties.
+
+    datatype, unit, value, name and description will only be changed in entity_a if they
+    are None for entity_a and set for entity_b. If there is a corresponding value
+    for entity_a different from None a RuntimeError will be raised informing of an
+    unresolvable merge conflict.
+
+    The merge operation is done in place.
+
+    Returns entity_a.
+
+    WARNING: This function is currently experimental and insufficiently tested. Use with care.
+    """
+
+    logging.warning(
+        "This function is currently experimental and insufficiently tested. Use with care.")
+
+    # Compare both entities:
+    diff_r1, diff_r2 = compare_entities(entity_a, entity_b)
+
+    # Go through the comparison and try to apply changes to entity_a:
+    for key in diff_r2["parents"]:
+        entity_a.add_parent(entity_b.get_parent(key))
+
+    for key in diff_r2["properties"]:
+        if key in diff_r1["properties"]:
+            if ("importance" in diff_r1["properties"][key] and
+                    "importance" in diff_r2["properties"][key]):
+                if (diff_r1["properties"][key]["importance"] !=
+                        diff_r2["properties"][key]["importance"]):
+                    raise NotImplementedError()
+            elif ("importance" in diff_r1["properties"][key] or
+                  "importance" in diff_r2["properties"][key]):
+                raise NotImplementedError()
+
+            for attribute in ("datatype", "unit", "value"):
+                if diff_r1["properties"][key][attribute] is None:
+                    setattr(entity_a.get_property(key), attribute,
+                            diff_r2["properties"][key][attribute])
+                else:
+                    raise RuntimeError("Merge conflict.")
+        else:
+            # TODO: This is a temporary FIX for
+            #       https://gitlab.indiscale.com/caosdb/src/caosdb-pylib/-/issues/105
+            entity_a.add_property(id=entity_b.get_property(key).id,
+                                  name=entity_b.get_property(key).name,
+                                  datatype=entity_b.get_property(key).datatype,
+                                  value=entity_b.get_property(key).value,
+                                  unit=entity_b.get_property(key).unit,
+                                  importance=entity_b.get_importance(key))
+            # entity_a.add_property(
+            #     entity_b.get_property(key),
+            #     importance=entity_b.get_importance(key))
+
+    for special_attribute in ("name", "description"):
+        sa_a = getattr(entity_a, special_attribute)
+        sa_b = getattr(entity_b, special_attribute)
+        if sa_a != sa_b:
+            if sa_a is None:
+                setattr(entity_a, special_attribute, sa_b)
+            else:
+                raise RuntimeError("Merge conflict.")
+    return entity_a
+
+
 def describe_diff(olddiff, newdiff, name=None, as_update=True):
     description = ""
 
-    for attr in list(set(list(olddiff.keys())+list(newdiff.keys()))):
+    for attr in list(set(list(olddiff.keys()) + list(newdiff.keys()))):
         if attr == "parents" or attr == "properties":
             continue
         description += "{} differs:\n".format(attr)
@@ -779,3 +465,26 @@ def resolve_reference(prop: Property):
     else:
         if isinstance(prop.value, int):
             prop.value = retrieve_entity_with_id(prop.value)
+
+
+def create_flat_list(ent_list: list[Entity], flat: list[Entity]):
+    """
+    Recursively adds all properties contained in entities from ent_list to
+    the output list flat. Each element will only be added once to the list.
+
+    TODO: Currently this function is also contained in newcrawler module crawl.
+          We are planning to permanently move it to here.
+    """
+    for ent in ent_list:
+        for p in ent.properties:
+            # For lists append each element that is of type Entity to flat:
+            if isinstance(p.value, list):
+                for el in p.value:
+                    if isinstance(el, Entity):
+                        if el not in flat:
+                            flat.append(el)
+                        create_flat_list([el], flat)  # TODO: move inside if block?
+            elif isinstance(p.value, Entity):
+                if p.value not in flat:
+                    flat.append(p.value)
+                create_flat_list([p.value], flat)  # TODO: move inside if block?
diff --git a/src/caosdb/common/models.py b/src/caosdb/common/models.py
index 181750aae6fd3e1aeab2c61b59f53d8b8111d5bd..3421f9ce39fc848f774b5d5d38280434354da8de 100644
--- a/src/caosdb/common/models.py
+++ b/src/caosdb/common/models.py
@@ -79,6 +79,10 @@ ALL = "ALL"
 NONE = "NONE"
 
 
+SPECIAL_ATTRIBUTES = ["name", "role", "datatype", "description",
+                      "id", "path", "checksum", "size"]
+
+
 class Entity(object):
 
     """Entity is a generic CaosDB object.
@@ -121,6 +125,47 @@ class Entity(object):
         self.id = id
         self.state = None
 
+    def copy(self):
+        """
+        Return a copy of entity.
+
+        If deep == True return a deep copy, recursively copying all sub entities.
+
+        Standard properties are copied using add_property.
+        Special attributes, as defined by the global variable SPECIAL_ATTRIBUTES and additionaly
+        the "value" are copied using setattr.
+        """
+        if self.role == "File":
+            new = File()
+        elif self.role == "Property":
+            new = Property()
+        elif self.role == "RecordType":
+            new = RecordType()
+        elif self.role == "Record":
+            new = Record()
+        elif self.role == "Entity":
+            new = Entity()
+        else:
+            raise RuntimeError("Unkonwn role.")
+
+        # Copy special attributes:
+        # TODO: this might rise an exception when copying
+        #       special file attributes like checksum and size.
+        for attribute in SPECIAL_ATTRIBUTES + ["value"]:
+            val = getattr(self, attribute)
+            if val is not None:
+                setattr(new, attribute, val)
+
+        # Copy parents:
+        for p in self.parents:
+            new.add_parent(p)
+
+        # Copy properties:
+        for p in self.properties:
+            new.add_property(p, importance=self.get_importance(p))
+
+        return new
+
     @property
     def version(self):
         if self._version is not None or self._wrapped_entity is None:
diff --git a/src/caosdb/configuration.py b/src/caosdb/configuration.py
index 51e3749aaca3045afec9334ef987a174d5d19f26..75827df0d00d6c82251c2c04fa47413ac2801928 100644
--- a/src/caosdb/configuration.py
+++ b/src/caosdb/configuration.py
@@ -84,23 +84,28 @@ def config_to_yaml(config):
 
 
 def validate_yaml_schema(valobj):
-    # TODO: Re-enable warning once the schema has been extended to also cover
-    # SSS pycaosdb.inis and integration tests.
     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"])
-    # else:
-    #     warnings.warn("""
-    #                     Warning: The validation could not be performed because `jsonschema` is not installed.
-    #                 """)
+    else:
+        warnings.warn("""
+            Warning: The validation could not be performed because `jsonschema` is not installed.
+        """)
 
 
 def _read_config_files():
-    """Function to read config files from different paths. Checks for path in $PYCAOSDBINI or home directory (.pycaosdb.ini) and in the current working directory (pycaosdb.ini).
+    """Function to read config files from different paths.
+
+    Checks for path either in ``$PYCAOSDBINI`` or home directory (``.pycaosdb.ini``), and
+    additionally in the current working directory (``pycaosdb.ini``).
+
+    Returns
+    -------
+
+    ini files: list
+      The successfully parsed ini-files. Order: env_var or home directory, cwd. Used for testing the function.
 
-    Returns:
-        [list]: list with successfully parsed ini-files. Order: env_var or home directory, cwd. Used for testing the function.
     """
     return_var = []
     if "PYCAOSDBINI" in environ:
diff --git a/src/caosdb/high_level_api.py b/src/caosdb/high_level_api.py
new file mode 100644
index 0000000000000000000000000000000000000000..0c936112993ccdbb5afdd91f3286880a16bdf431
--- /dev/null
+++ b/src/caosdb/high_level_api.py
@@ -0,0 +1,1025 @@
+# -*- coding: utf-8 -*-
+#
+# This file is a part of the CaosDB Project.
+#
+# Copyright (C) 2018 Research Group Biomedical Physics,
+# Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+# Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+# Copyright (C) 2020-2022 IndiScale GmbH <info@indiscale.com>
+# Copyright (C) 2022 Alexander Schlemmer <alexander.schlemmer@ds.mpg.de>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+# ** end header
+#
+
+"""
+A high level API for accessing CaosDB entities from within python.
+
+This is refactored from apiutils.
+"""
+
+from caosdb.common.datatype import (BOOLEAN, DATETIME, DOUBLE, FILE, INTEGER,
+                                    REFERENCE, TEXT,
+                                    is_list_datatype,
+                                    get_list_datatype,
+                                    is_reference)
+import caosdb as db
+
+from .apiutils import get_type_of_entity_with, create_flat_list
+import warnings
+
+from typing import Any, Optional, List, Union, Dict
+
+import yaml
+
+from dataclasses import dataclass, fields
+from datetime import datetime
+from dateutil import parser
+
+warnings.warn("""EXPERIMENTAL! The high_level_api module is experimental and may be changed or
+removed in future. Its purpose is to give an impression on how the Python client user interface
+might be changed.""")
+
+
+def standard_type_for_high_level_type(high_level_record: "CaosDBPythonEntity",
+                                      return_string: bool = False):
+    """
+    For a given CaosDBPythonEntity either return the corresponding
+    class in the standard CaosDB API or - if return_string is True - return
+    the role as a string.
+    """
+    if type(high_level_record) == CaosDBPythonRecord:
+        if not return_string:
+            return db.Record
+        return "Record"
+    elif type(high_level_record) == CaosDBPythonFile:
+        if not return_string:
+            return db.File
+        return "File"
+    elif type(high_level_record) == CaosDBPythonProperty:
+        if not return_string:
+            return db.Property
+        return "Property"
+    elif type(high_level_record) == CaosDBPythonRecordType:
+        if not return_string:
+            return db.RecordType
+        return "RecordType"
+    elif type(high_level_record) == CaosDBPythonEntity:
+        if not return_string:
+            return db.Entity
+        return "Entity"
+    raise RuntimeError("Incompatible type.")
+
+
+def high_level_type_for_role(role: str):
+    if role == "Record":
+        return CaosDBPythonRecord
+    if role == "File":
+        return CaosDBPythonFile
+    if role == "Property":
+        return CaosDBPythonProperty
+    if role == "RecordType":
+        return CaosDBPythonRecordType
+    if role == "Entity":
+        return CaosDBPythonEntity
+    raise RuntimeError("Unknown role.")
+
+
+def high_level_type_for_standard_type(standard_record: db.Entity):
+    if not isinstance(standard_record, db.Entity):
+        raise ValueError()
+    role = standard_record.role
+    if role == "Record" or type(standard_record) == db.Record:
+        return CaosDBPythonRecord
+    elif role == "File" or type(standard_record) == db.File:
+        return CaosDBPythonFile
+    elif role == "Property" or type(standard_record) == db.Property:
+        return CaosDBPythonProperty
+    elif role == "RecordType" or type(standard_record) == db.RecordType:
+        return CaosDBPythonRecordType
+    elif role == "Entity" or type(standard_record) == db.Entity:
+        return CaosDBPythonEntity
+    raise RuntimeError("Incompatible type.")
+
+
+@dataclass
+class CaosDBPropertyMetaData:
+    # name is already the name of the attribute
+    unit: Optional[str] = None
+    datatype: Optional[str] = None
+    description: Optional[str] = None
+    id: Optional[int] = None
+    importance: Optional[str] = None
+
+
+class CaosDBPythonUnresolved:
+    pass
+
+
+@dataclass
+class CaosDBPythonUnresolvedParent(CaosDBPythonUnresolved):
+    """
+    Parents can be either given by name or by ID.
+
+    When resolved, both fields should be set.
+    """
+
+    id: Optional[int] = None
+    name: Optional[str] = None
+
+
+@dataclass
+class CaosDBPythonUnresolvedReference(CaosDBPythonUnresolved):
+
+    def __init__(self, id=None):
+        self.id = id
+
+
+class CaosDBPythonEntity(object):
+
+    def __init__(self):
+        """
+        Initialize a new CaosDBPythonEntity for the high level python api.
+
+        Parents are either unresolved references or CaosDB RecordTypes.
+
+        Properties are stored directly as attributes for the object.
+        Property metadata is maintained in a dctionary _properties_metadata that should
+        never be accessed directly, but only using the get_property_metadata function.
+        If property values are references to other objects, they will be stored as
+        CaosDBPythonUnresolvedReference objects that can be resolved later into
+        CaosDBPythonRecords.
+        """
+
+        # Parents are either unresolved references or CaosDB RecordTypes
+        self._parents: List[Union[
+            CaosDBPythonUnresolvedParent, CaosDBPythonRecordType]] = []
+        # self._id: int = CaosDBPythonEntity._get_new_id()
+        self._id: Optional[int] = None
+        self._name: Optional[str] = None
+        self._description: Optional[str] = None
+        self._version: Optional[str] = None
+
+        self._file: Optional[str] = None
+        self._path: Optional[str] = None
+
+        # name: name of property, value: property metadata
+        self._properties_metadata: Dict[CaosDBPropertyMetaData] = dict()
+
+        # Store all current attributes as forbidden attributes
+        # which must not be changed by the set_property function.
+        self._forbidden = dir(self) + ["_forbidden"]
+
+    def use_parameter(self, name, value):
+        self.__setattr__(name, value)
+        return value
+
+    @property
+    def id(self):
+        """
+        Getter for the id.
+        """
+        return self._id
+
+    @id.setter
+    def id(self, val: int):
+        self._id = val
+
+    @property
+    def name(self):
+        """
+        Getter for the name.
+        """
+        return self._name
+
+    @name.setter
+    def name(self, val: str):
+        self._name = val
+
+    @property
+    def file(self):
+        """
+        Getter for the file.
+        """
+        if type(self) != CaosDBPythonFile:
+            raise RuntimeError("Please don't use the file attribute for entities"
+                               " that are no files.")
+        return self._file
+
+    @file.setter
+    def file(self, val: str):
+        if val is not None and type(self) != CaosDBPythonFile:
+            raise RuntimeError("Please don't use the file attribute for entities"
+                               " that are no files.")
+        self._file = val
+
+    @property
+    def path(self):
+        """
+        Getter for the path.
+        """
+        if type(self) != CaosDBPythonFile:
+            raise RuntimeError("Please don't use the path attribute for entities"
+                               " that are no files.")
+        return self._path
+
+    @path.setter
+    def path(self, val: str):
+        if val is not None and type(self) != CaosDBPythonFile:
+            raise RuntimeError("Please don't use the path attribute for entities"
+                               " that are no files.")
+        self._path = val
+
+    @property
+    def description(self):
+        """
+        Getter for the description.
+        """
+        return self._description
+
+    @description.setter
+    def description(self, val: str):
+        self._description = val
+
+    @property
+    def version(self):
+        """
+        Getter for the version.
+        """
+        return self._version
+
+    @version.setter
+    def version(self, val: str):
+        self._version = val
+
+    def _set_property_from_entity(self, ent: db.Entity, importance: str,
+                                  references: Optional[db.Container]):
+        """
+        Set a new property using an entity from the normal python API.
+
+        ent : db.Entity
+              The entity to be set.
+        """
+
+        if ent.name is None:
+            raise RuntimeError("Setting properties without name is impossible.")
+
+        if ent.name in self.get_properties():
+            raise RuntimeError("Multiproperty not implemented yet.")
+
+        val = self._type_converted_value(ent.value, ent.datatype,
+                                         references)
+        self.set_property(
+            ent.name,
+            val,
+            datatype=ent.datatype)
+        metadata = self.get_property_metadata(ent.name)
+
+        for prop_name in fields(metadata):
+            k = prop_name.name
+            if k == "importance":
+                metadata.importance = importance
+            else:
+                metadata.__setattr__(k, ent.__getattribute__(k))
+
+    def get_property_metadata(self, prop_name: str) -> CaosDBPropertyMetaData:
+        """
+        Retrieve the property metadata for the property with name prop_name.
+
+        If the property with the given name does not exist or is forbidden, raise an exception.
+        Else return the metadata associated with this property.
+
+        If no metadata does exist yet for the given property, a new object will be created
+        and returned.
+
+        prop_name: str
+                   Name of the property to retrieve metadata for.
+        """
+
+        if not self.property_exists(prop_name):
+            raise RuntimeError("The property with name {} does not exist.".format(prop_name))
+
+        if prop_name not in self._properties_metadata:
+            self._properties_metadata[prop_name] = CaosDBPropertyMetaData()
+
+        return self._properties_metadata[prop_name]
+
+    def property_exists(self, prop_name: str):
+        """
+        Check whether a property exists already.
+        """
+        return prop_name not in self._forbidden and prop_name in self.__dict__
+
+    def set_property(self,
+                     name: str,
+                     value: Any,
+                     overwrite: bool = False,
+                     datatype: Optional[str] = None):
+        """
+        Set a property for this entity with a name and a value.
+
+        If this property is already set convert the value into a list and append the value.
+        This behavior can be overwritten using the overwrite flag, which will just overwrite
+        the existing value.
+
+        name: str
+              Name of the property.
+
+        value: Any
+               Value of the property.
+
+        overwrite: bool
+                   Use this if you definitely only want one property with
+                   that name (set to True).
+        """
+
+        if name in self._forbidden:
+            raise RuntimeError("Entity cannot be converted to a corresponding "
+                               "Python representation. Name of property " +
+                               name + " is forbidden!")
+
+        already_exists = self.property_exists(name)
+
+        if already_exists and not overwrite:
+            # each call to set_property checks first if it already exists
+            #        if yes: Turn the attribute into a list and
+            #                place all the elements into that list.
+            att = self.__getattribute__(name)
+
+            if isinstance(att, list):
+                # just append, see below
+                pass
+            else:
+                old_att = self.__getattribute__(name)
+                self.__setattr__(name, [old_att])
+            att = self.__getattribute__(name)
+            att.append(value)
+        else:
+            self.__setattr__(name, value)
+
+    def __setattr__(self, name: str, val: Any):
+        """
+        Allow setting generic properties.
+        """
+
+        # TODO: implement checking the value to correspond to one of the datatypes
+        #       known for conversion.
+
+        super().__setattr__(name, val)
+
+    def _type_converted_list(self,
+                             val: List,
+                             pr: str,
+                             references: Optional[db.Container]):
+        """
+        Convert a list to a python list of the correct type.
+
+        val: List
+             The value of a property containing the list.
+
+        pr: str
+            The datatype according to the database entry.
+        """
+        if not is_list_datatype(pr) and not isinstance(val, list):
+            raise RuntimeError("Not a list.")
+
+        return [
+            self._type_converted_value(i, get_list_datatype(pr), references
+                                       ) for i in val]
+
+    def _type_converted_value(self,
+                              val: Any,
+                              pr: str,
+                              references: Optional[db.Container]):
+        """
+        Convert val to the correct type which is indicated by the database
+        type string in pr.
+
+        References with ids will be turned into CaosDBPythonUnresolvedReference.
+        """
+
+        if val is None:
+            return None
+        elif isinstance(val, db.Entity):
+            # this needs to be checked as second case as it is the ONLY
+            # case which does not depend on pr
+            # TODO: we might need to pass through the reference container
+            return convert_to_python_object(val, references)
+        elif isinstance(val, list):
+            return self._type_converted_list(val, pr, references)
+        elif pr is None:
+            return val
+        elif pr == DOUBLE:
+            return float(val)
+        elif pr == BOOLEAN:
+            return bool(val)
+        elif pr == INTEGER:
+            return int(val)
+        elif pr == TEXT:
+            return str(val)
+        elif pr == FILE:
+            return CaosDBPythonUnresolvedReference(val)
+        elif pr == REFERENCE:
+            return CaosDBPythonUnresolvedReference(val)
+        elif pr == DATETIME:
+            return self._parse_datetime(val)
+        elif is_list_datatype(pr):
+            return self._type_converted_list(val, pr, references)
+        else:
+            # Generic references to entities:
+            return CaosDBPythonUnresolvedReference(val)
+
+    def _parse_datetime(self, val: Union[str, datetime]):
+        """
+        Convert val into a datetime object.
+        """
+        if isinstance(val, datetime):
+            return val
+        return parser.parse(val)
+
+    def get_property(self, name: str):
+        """
+        Return the value of the property with name name.
+
+        Raise an exception if the property does not exist.
+        """
+        if not self.property_exists(name):
+            raise RuntimeError("Property {} does not exist.".format(name))
+        att = self.__getattribute__(name)
+        return att
+
+    def attribute_as_list(self, name: str):
+        """
+        This is a workaround for the problem that lists containing only one
+        element are indistinguishable from simple types in this
+        representation.
+
+        TODO: still relevant? seems to be only a problem if LIST types are not used.
+        """
+        att = self.get_property(name)
+
+        if isinstance(att, list):
+            return att
+        else:
+            return [att]
+
+    def add_parent(self, parent: Union[
+            CaosDBPythonUnresolvedParent, "CaosDBPythonRecordType", str]):
+        """
+        Add a parent to this entity. Either using an unresolved parent or
+        using a real record type.
+
+        Strings as argument for parent will automatically be converted to an
+        unresolved parent. Likewise, integers as argument will be automatically converted
+        to unresolved parents with just an id.
+        """
+
+        if isinstance(parent, str):
+            parent = CaosDBPythonUnresolvedParent(name=parent)
+
+        if isinstance(parent, int):
+            parent = CaosDBPythonUnresolvedParent(id=parent)
+
+        if self.has_parent(parent):
+            raise RuntimeError("Duplicate parent.")
+        self._parents.append(parent)
+
+    def get_parents(self):
+        """
+        Returns all parents of this entity.
+
+        Use has_parent for checking for existence of parents
+        and add_parent for adding parents to this entity.
+        """
+        return self._parents
+
+    def has_parent(self, parent: Union[
+            CaosDBPythonUnresolvedParent, "CaosDBPythonRecordType"]):
+        """
+        Check whether this parent already exists for this entity.
+
+        Strings as argument for parent will automatically be converted to an
+        unresolved parent. Likewise, integers as argument will be automatically converted
+        to unresolved parents with just an id.
+        """
+
+        if isinstance(parent, str):
+            parent = CaosDBPythonUnresolvedParent(name=parent)
+
+        if isinstance(parent, int):
+            parent = CaosDBPythonUnresolvedParent(id=parent)
+
+        for p in self._parents:
+            if p.id is not None and p.id == parent.id:
+                return True
+            elif p.name is not None and p.name == parent.name:
+                return True
+        return False
+
+    def _resolve_caosdb_python_unresolved_reference(self, propval, deep,
+                                                    references, visited):
+        # This does not make sense for unset ids:
+        if propval.id is None:
+            raise RuntimeError("Unresolved property reference without an ID.")
+        # have we encountered this id before:
+        if propval.id in visited:
+            # self.__setattr__(prop, visited[propval.id])
+            # don't do the lookup in the references container
+            return visited[propval.id]
+
+        if references is None:
+            ent = db.Entity(id=propval.id).retrieve()
+            obj = convert_to_python_object(ent, references)
+            visited[propval.id] = obj
+            if deep:
+                obj.resolve_references(deep, references, visited)
+            return obj
+
+        # lookup in container:
+        for ent in references:
+            # Entities in container without an ID will be skipped:
+            if ent.id is not None and ent.id == propval.id:
+                # resolve this entity:
+                obj = convert_to_python_object(ent, references)
+                visited[propval.id] = obj
+                # self.__setattr__(prop, visited[propval.id])
+                if deep:
+                    obj.resolve_references(deep, references, visited)
+                return obj
+        return propval
+
+    def resolve_references(self, deep: bool, references: db.Container,
+                           visited: dict[Union[str, int],
+                                         "CaosDBPythonEntity"] = None):
+        """
+        Resolve this entity's references. This affects unresolved properties as well
+        as unresolved parents.
+
+        deep: bool
+              If True recursively resolve references also for all resolved references.
+
+        references: Optional[db.Container]
+                    A container with references that might be resolved.
+                    If None is passed as the container, this function tries to resolve entities from a running
+                    CaosDB instance directly.
+        """
+
+        # This parameter is used in the recursion to keep track of already visited
+        # entites (in order to detect cycles).
+        if visited is None:
+            visited = dict()
+
+        for parent in self.get_parents():
+            # TODO
+            if isinstance(parent, CaosDBPythonUnresolvedParent):
+                pass
+
+        for prop in self.get_properties():
+            propval = self.__getattribute__(prop)
+            # Resolve all previously unresolved attributes that are entities:
+            if deep and isinstance(propval, CaosDBPythonEntity):
+                propval.resolve_references(deep, references)
+            elif isinstance(propval, list):
+                resolvedelements = []
+                for element in propval:
+                    if deep and isinstance(element, CaosDBPythonEntity):
+                        element.resolve_references(deep, references)
+                        resolvedelements.append(element)
+                    if isinstance(element, CaosDBPythonUnresolvedReference):
+                        resolvedelements.append(
+                            self._resolve_caosdb_python_unresolved_reference(element, deep,
+                                                                             references, visited))
+                    else:
+                        resolvedelements.append(element)
+                self.__setattr__(prop, resolvedelements)
+
+            elif isinstance(propval, CaosDBPythonUnresolvedReference):
+                val = self._resolve_caosdb_python_unresolved_reference(propval, deep,
+                                                                       references, visited)
+                self.__setattr__(prop, val)
+
+    def get_properties(self):
+        """
+        Return the names of all properties.
+        """
+
+        return [p for p in self.__dict__
+                if p not in self._forbidden]
+
+    @staticmethod
+    def deserialize(serialization: dict):
+        """
+        Deserialize a yaml representation of an entity in high level API form.
+        """
+
+        if "role" in serialization:
+            entity = high_level_type_for_role(serialization["role"])()
+        else:
+            entity = CaosDBPythonRecord()
+
+        for parent in serialization["parents"]:
+            if "unresolved" in parent:
+                id = None
+                name = None
+                if "id" in parent:
+                    id = parent["id"]
+                if "name" in parent:
+                    name = parent["name"]
+                entity.add_parent(CaosDBPythonUnresolvedParent(
+                    id=id, name=name))
+            else:
+                raise NotImplementedError()
+
+        for baseprop in ("name", "id", "description", "version"):
+            if baseprop in serialization:
+                entity.__setattr__(baseprop, serialization[baseprop])
+
+        if type(entity) == CaosDBPythonFile:
+            entity.file = serialization["file"]
+            entity.path = serialization["path"]
+
+        for p in serialization["properties"]:
+            # The property needs to be set first:
+
+            prop = serialization["properties"][p]
+            if isinstance(prop, dict):
+                if "unresolved" in prop:
+                    entity.__setattr__(p, CaosDBPythonUnresolvedReference(
+                        id=prop["id"]))
+                else:
+                    entity.__setattr__(p,
+                                       entity.deserialize(prop))
+            else:
+                entity.__setattr__(p, prop)
+
+            # if there is no metadata in the yaml file just initialize an empty metadata object
+            if "metadata" in serialization and p in serialization["metadata"]:
+                metadata = serialization["metadata"][p]
+                propmeta = entity.get_property_metadata(p)
+
+                for f in fields(propmeta):
+                    if f.name in metadata:
+                        propmeta.__setattr__(f.name, metadata[f.name])
+            else:
+                raise NotImplementedError()
+
+        return entity
+
+    def serialize(self, without_metadata: bool = False, visited: dict = None):
+        """
+        Serialize necessary information into a dict.
+
+        without_metadata: bool
+                          If True don't set the metadata field in order to increase
+                          readability. Not recommended if deserialization is needed.
+        """
+
+        if visited is None:
+            visited = dict()
+
+        if self in visited:
+            return visited[self]
+
+        metadata: dict[str, Any] = dict()
+        properties = dict()
+        parents = list()
+
+        # The full information to be returned:
+        fulldict = dict()
+        visited[self] = fulldict
+
+        # Add CaosDB role:
+        fulldict["role"] = standard_type_for_high_level_type(self, True)
+
+        for parent in self._parents:
+            if isinstance(parent, CaosDBPythonEntity):
+                parents.append(parent.serialize(without_metadata, visited))
+            elif isinstance(parent, CaosDBPythonUnresolvedParent):
+                parents.append({"name": parent.name, "id": parent.id,
+                                "unresolved": True})
+            else:
+                raise RuntimeError("Incompatible class used as parent.")
+
+        for baseprop in ("name", "id", "description", "version"):
+            val = self.__getattribute__(baseprop)
+            if val is not None:
+                fulldict[baseprop] = val
+
+        if type(self) == CaosDBPythonFile:
+            fulldict["file"] = self.file
+            fulldict["path"] = self.path
+
+        for p in self.get_properties():
+            m = self.get_property_metadata(p)
+            metadata[p] = dict()
+            for f in fields(m):
+                val = m.__getattribute__(f.name)
+                if val is not None:
+                    metadata[p][f.name] = val
+
+            val = self.get_property(p)
+            if isinstance(val, CaosDBPythonUnresolvedReference):
+                properties[p] = {"id": val.id, "unresolved": True}
+            elif isinstance(val, CaosDBPythonEntity):
+                properties[p] = val.serialize(without_metadata, visited)
+            elif isinstance(val, list):
+                serializedelements = []
+                for element in val:
+                    if isinstance(element, CaosDBPythonUnresolvedReference):
+                        elm = dict()
+                        elm["id"] = element.id
+                        elm["unresolved"] = True
+                        serializedelements.append(elm)
+                    elif isinstance(element, CaosDBPythonEntity):
+                        serializedelements.append(
+                            element.serialize(without_metadata,
+                                              visited))
+                    else:
+                        serializedelements.append(element)
+                properties[p] = serializedelements
+            else:
+                properties[p] = val
+
+        fulldict["properties"] = properties
+        fulldict["parents"] = parents
+
+        if not without_metadata:
+            fulldict["metadata"] = metadata
+        return fulldict
+
+    def __str__(self):
+        return yaml.dump(self.serialize(False))
+
+    # This seemed like a good solution, but makes it difficult to
+    # compare python objects directly:
+    #
+    # def __repr__(self):
+    #     return yaml.dump(self.serialize(True))
+
+
+class CaosDBPythonRecord(CaosDBPythonEntity):
+    pass
+
+
+class CaosDBPythonRecordType(CaosDBPythonEntity):
+    pass
+
+
+class CaosDBPythonProperty(CaosDBPythonEntity):
+    pass
+
+
+class CaosDBMultiProperty:
+    """
+    This implements a multi property using a python list.
+    """
+
+    def __init__(self):
+        raise NotImplementedError()
+
+
+class CaosDBPythonFile(CaosDBPythonEntity):
+    def download(self, target=None):
+        if self.id is None:
+            raise RuntimeError("Cannot download file when id is missing.")
+        f = db.File(id=self.id).retrieve()
+        return f.download(target)
+
+
+BASE_ATTRIBUTES = (
+    "id", "name", "description", "version", "path", "file")
+
+
+def _single_convert_to_python_object(robj: CaosDBPythonEntity,
+                                     entity: db.Entity,
+                                     references: Optional[db.Container] = None):
+    """
+    Convert a db.Entity from the standard API to a (previously created)
+    CaosDBPythonEntity from the high level API.
+
+    This method will not resolve any unresolved references, so reference properties
+    as well as parents will become unresolved references in the first place.
+
+    The optional third parameter can be used
+    to resolve references that occur in the converted entities and resolve them
+    to their correct representations. (Entities that are not found remain as
+    CaosDBPythonUnresolvedReferences.)
+
+    Returns the input object robj.
+    """
+    for base_attribute in BASE_ATTRIBUTES:
+        val = entity.__getattribute__(base_attribute)
+        if val is not None:
+            if isinstance(val, db.common.models.Version):
+                val = val.id
+            robj.__setattr__(base_attribute, val)
+
+    for prop in entity.properties:
+        robj._set_property_from_entity(prop, entity.get_importance(prop), references)
+
+    for parent in entity.parents:
+        robj.add_parent(CaosDBPythonUnresolvedParent(id=parent.id,
+                                                     name=parent.name))
+
+    return robj
+
+
+def _convert_property_value(propval):
+    if isinstance(propval, CaosDBPythonUnresolvedReference):
+        propval = propval.id
+    elif isinstance(propval, CaosDBPythonEntity):
+        propval = _single_convert_to_entity(
+            standard_type_for_high_level_type(propval)(), propval)
+    elif isinstance(propval, list):
+        propval = [_convert_property_value(element) for element in propval]
+
+    # TODO: test case for list missing
+
+    return propval
+
+
+def _single_convert_to_entity(entity: db.Entity,
+                              robj: CaosDBPythonEntity):
+    """
+    Convert a CaosDBPythonEntity to an entity in standard pylib format.
+
+    entity: db.Entity
+            An empty entity.
+
+    robj: CaosDBPythonEntity
+          The CaosDBPythonEntity that is supposed to be converted to the entity.
+    """
+
+    for base_attribute in BASE_ATTRIBUTES:
+        if base_attribute in ("file", "path") and not isinstance(robj, CaosDBPythonFile):
+            continue
+
+        # Skip version:
+        if base_attribute == "version":
+            continue
+
+        val = robj.__getattribute__(base_attribute)
+
+        if val is not None:
+            entity.__setattr__(base_attribute, val)
+
+    for parent in robj.get_parents():
+        if isinstance(parent, CaosDBPythonUnresolvedParent):
+            entity.add_parent(name=parent.name, id=parent.id)
+        elif isinstance(parent, CaosDBPythonRecordType):
+            raise NotImplementedError()
+        else:
+            raise RuntimeError("Incompatible class used as parent.")
+
+    for prop in robj.get_properties():
+        propval = robj.__getattribute__(prop)
+        metadata = robj.get_property_metadata(prop)
+
+        propval = _convert_property_value(propval)
+
+        entity.add_property(
+            name=prop,
+            value=propval,
+            unit=metadata.unit,
+            importance=metadata.importance,
+            datatype=metadata.datatype,
+            description=metadata.description,
+            id=metadata.id)
+
+    return entity
+
+
+def convert_to_entity(python_object):
+    if isinstance(python_object, db.Container):
+        # Create a list of objects:
+
+        return [convert_to_entity(i) for i in python_object]
+    elif isinstance(python_object, CaosDBPythonRecord):
+        return _single_convert_to_entity(db.Record(), python_object)
+    elif isinstance(python_object, CaosDBPythonFile):
+        return _single_convert_to_entity(db.File(), python_object)
+    elif isinstance(python_object, CaosDBPythonRecordType):
+        return _single_convert_to_entity(db.RecordType(), python_object)
+    elif isinstance(python_object, CaosDBPythonProperty):
+        return _single_convert_to_entity(db.Property(), python_object)
+    elif isinstance(python_object, CaosDBPythonEntity):
+        return _single_convert_to_entity(db.Entity(), python_object)
+    else:
+        raise ValueError("Cannot convert an object of this type.")
+
+
+def convert_to_python_object(entity: Union[db.Container, db.Entity],
+                             references: Optional[db.Container] = None):
+    """
+    Convert either a container of CaosDB entities or a single CaosDB entity
+    into the high level representation.
+
+    The optional second parameter can be used
+    to resolve references that occur in the converted entities and resolve them
+    to their correct representations. (Entities that are not found remain as
+    CaosDBPythonUnresolvedReferences.)
+    """
+    if isinstance(entity, db.Container):
+        # Create a list of objects:
+        return [convert_to_python_object(i, references) for i in entity]
+
+    return _single_convert_to_python_object(
+        high_level_type_for_standard_type(entity)(), entity, references)
+
+
+def new_high_level_entity(entity: db.RecordType,
+                          importance_level: str,
+                          name: str = None):
+    """
+    Create an new record in high level format based on a record type in standard format.
+
+    entity: db.RecordType
+            The record type to initialize the new record from.
+
+    importance_level: str
+                      None, obligatory, recommended or suggested
+                      Initialize new properties up to this level.
+                      Properties in the record type with no importance will be added
+                      regardless of the importance_level.
+
+    name: str
+          Name of the new record.
+    """
+
+    r = db.Record(name=name)
+    r.add_parent(entity)
+
+    impmap = {
+        None: 0, "SUGGESTED": 3, "RECOMMENDED": 2, "OBLIGATORY": 1}
+
+    for prop in entity.properties:
+        imp = entity.get_importance(prop)
+        if imp is not None and impmap[importance_level] < impmap[imp]:
+            continue
+
+        r.add_property(prop)
+
+    return convert_to_python_object(r)
+
+
+def create_record(rtname: str, name: str = None, **kwargs):
+    """
+    Create a new record based on the name of a record type. The new record is returned.
+
+    rtname: str
+            The name of the record type.
+
+    name: str
+          This is optional. A name for the new record.
+
+    kwargs:
+            Additional arguments are used to set attributes of the
+            new record.
+    """
+    obj = new_high_level_entity(
+        db.RecordType(name=rtname).retrieve(), "SUGGESTED", name)
+    for key, value in kwargs.items():
+        obj.__setattr__(key, value)
+    return obj
+
+
+def load_external_record(record_name: str):
+    """
+    Retrieve a record by name and convert it to the high level API format.
+    """
+    return convert_to_python_object(db.Record(name=record_name).retrieve())
+
+
+def create_entity_container(record: CaosDBPythonEntity):
+    """
+    Convert this record into an entity container in standard format that can be used
+    to insert or update entities in a running CaosDB instance.
+    """
+    ent = convert_to_entity(record)
+    lse: List[db.Entity] = [ent]
+    create_flat_list([ent], lse)
+    return db.Container().extend(lse)
+
+
+def query(query: str, resolve_references: bool = True, references: db.Container = None):
+    """
+
+    """
+    res = db.execute_query(query)
+    objects = convert_to_python_object(res)
+    if resolve_references:
+        for obj in objects:
+            obj.resolve_references(True, references)
+    return objects
diff --git a/src/caosdb/schema-pycaosdb-ini.yml b/src/caosdb/schema-pycaosdb-ini.yml
index bfe8fe7c63679507bba795bb45d7afa2b097f07b..5dabdd89795e19a757209e03cc843776be705777 100644
--- a/src/caosdb/schema-pycaosdb-ini.yml
+++ b/src/caosdb/schema-pycaosdb-ini.yml
@@ -65,26 +65,39 @@ schema-pycaosdb-ini:
             properties:
               password_method:
                 const: input
+            required: [password_method]
           then:
             required: [url]
         - if:
             properties:
               password_method:
                 const: plain
+            required: [password_method]
           then:
             required: [url, username, password]
         - if:
             properties:
               password_method:
                 const: pass
+            required: [password_method]
           then:
             required: [url, username, password_identifier]
         - if:
             properties:
               password_method:
                 const: keyring
+            required: [password_method]
           then:
             required: [url, username]
     IntegrationTests:
       description: "Used by the integration test suite from the caosdb-pyinttest repo."
       additionalProperties: true
+    Misc:
+      description: "Some additional configuration settings."
+      additionalProperties: true
+    advancedtools:
+      description: "Configuration settings for the caosadvancedtools."
+      additionalProperties: true
+    sss_helper:
+      description: "Configuration settings for server-side scripting."
+      additionalProperties: true
diff --git a/src/caosdb/utils/plantuml.py b/src/caosdb/utils/plantuml.py
index be34b2604f3682bb71b48bbd73e00fe854b3af51..16bd81d9507b9ecd11870e18b1020d9f47b8f047 100644
--- a/src/caosdb/utils/plantuml.py
+++ b/src/caosdb/utils/plantuml.py
@@ -34,10 +34,15 @@ plantuml FILENAME.pu -> FILENAME.png
 """
 
 import os
+import shutil
 
 import caosdb as db
 from caosdb.common.datatype import is_reference, get_referenced_recordtype
 
+from typing import Optional
+
+import tempfile
+
 REFERENCE = "REFERENCE"
 
 
@@ -79,13 +84,24 @@ class Grouped(object):
         return self.parents
 
 
-def recordtypes_to_plantuml_string(iterable):
+def recordtypes_to_plantuml_string(iterable,
+                                   add_properties: bool = True,
+                                   add_recordtypes: bool = True,
+                                   add_legend: bool = True,
+                                   no_shadow: bool = False,
+                                   style: str = "default"):
     """Converts RecordTypes into a string for PlantUML.
 
     This function obtains an iterable and returns a string which can
     be input into PlantUML for a representation of all RecordTypes in
     the iterable.
 
+    Current options for style
+    -------------------------
+
+    "default" - Standard rectangles with uml class circle and methods section
+    "salexan" - Round rectangles, hide circle and methods section
+
     Current limitations
     -------------------
 
@@ -94,8 +110,24 @@ def recordtypes_to_plantuml_string(iterable):
       either the "type" attribute is None or
       type(element) == RecordType.
     - Inheritance of Properties is not rendered nicely at the moment.
+
+    Parameters
+    ----------
+    iterable: iterable of caosdb.Entity
+      The objects to be rendered with plantuml.
+
+    no_shadow : bool, optional
+      If true, tell plantuml to use a skin without blurred shadows.
+
+
+    Returns
+    -------
+    out : str
+      The plantuml string for the given container.
     """
 
+    # TODO: This function needs a review of python type hints.
+
     classes = [el for el in iterable
                if isinstance(el, db.RecordType)]
     dependencies = {}
@@ -140,74 +172,90 @@ def recordtypes_to_plantuml_string(iterable):
         return result
 
     result = "@startuml\n\n"
-    result += "skinparam classAttributeIconSize 0\n"
 
-    result += "package Properties #DDDDDD {\n"
+    if no_shadow:
+        result += "skinparam shadowing false\n"
+
+    if style == "default":
+        result += "skinparam classAttributeIconSize 0\n"
+    elif style == "salexan":
+        result += """skinparam roundcorner 20\n
+skinparam boxpadding 20\n
+\n
+hide methods\n
+hide circle\n
+"""
+    else:
+        raise ValueError("Unknown style.")
 
-    for p in properties:
-        inheritances[p] = p.get_parents()
-        dependencies[p] = []
+    if add_properties:
+        result += "package Properties #DDDDDD {\n"
+        for p in properties:
+            inheritances[p] = p.get_parents()
+            dependencies[p] = []
 
-        result += "class \"{klass}\" << (P,#008800) >> {{\n".format(klass=p.name)
+            result += "class \"{klass}\" << (P,#008800) >> {{\n".format(klass=p.name)
 
-        if p.description is not None:
-            result += get_description(p.description)
-        result += "\n..\n"
+            if p.description is not None:
+                result += get_description(p.description)
+            result += "\n..\n"
 
-        if isinstance(p.datatype, str):
-            result += "datatype: " + p.datatype + "\n"
-        elif isinstance(p.datatype, db.Entity):
-            result += "datatype: " + p.datatype.name + "\n"
-        else:
-            result += "datatype: " + str(p.datatype) + "\n"
+            if isinstance(p.datatype, str):
+                result += "datatype: " + p.datatype + "\n"
+            elif isinstance(p.datatype, db.Entity):
+                result += "datatype: " + p.datatype.name + "\n"
+            else:
+                result += "datatype: " + str(p.datatype) + "\n"
+            result += "}\n\n"
         result += "}\n\n"
-    result += "}\n\n"
 
-    result += "package RecordTypes #DDDDDD {\n"
+    if add_recordtypes:
+        result += "package RecordTypes #DDDDDD {\n"
 
-    for c in classes:
-        inheritances[c] = c.get_parents()
-        dependencies[c] = []
-        result += "class \"{klass}\" << (C,#FF1111) >> {{\n".format(klass=c.name)
+        for c in classes:
+            inheritances[c] = c.get_parents()
+            dependencies[c] = []
+            result += "class \"{klass}\" << (C,#FF1111) >> {{\n".format(klass=c.name)
 
-        if c.description is not None:
-            result += get_description(c.description)
+            if c.description is not None:
+                result += get_description(c.description)
 
-        props = ""
-        props += _add_properties(c, importance=db.FIX)
-        props += _add_properties(c, importance=db.OBLIGATORY)
-        props += _add_properties(c, importance=db.RECOMMENDED)
-        props += _add_properties(c, importance=db.SUGGESTED)
+            props = ""
+            props += _add_properties(c, importance=db.FIX)
+            props += _add_properties(c, importance=db.OBLIGATORY)
+            props += _add_properties(c, importance=db.RECOMMENDED)
+            props += _add_properties(c, importance=db.SUGGESTED)
 
-        if len(props) > 0:
-            result += "__Properties__\n" + props
-        else:
-            result += "\n..\n"
-        result += "}\n\n"
+            if len(props) > 0:
+                result += "__Properties__\n" + props
+            else:
+                result += "\n..\n"
+            result += "}\n\n"
 
-    for g in grouped:
-        inheritances[g] = g.get_parents()
-        result += "class \"{klass}\" << (G,#0000FF) >> {{\n".format(klass=g.name)
-    result += "}\n\n"
+        for g in grouped:
+            inheritances[g] = g.get_parents()
+            result += "class \"{klass}\" << (G,#0000FF) >> {{\n".format(klass=g.name)
+        result += "}\n\n"
 
-    for c, parents in inheritances.items():
-        for par in parents:
-            result += "\"{par}\" <|-- \"{klass}\"\n".format(
-                klass=c.name, par=par.name)
+        for c, parents in inheritances.items():
+            for par in parents:
+                result += "\"{par}\" <|-- \"{klass}\"\n".format(
+                    klass=c.name, par=par.name)
 
-    for c, deps in dependencies.items():
-        for dep in deps:
-            result += "\"{klass}\" *-- \"{dep}\"\n".format(
-                klass=c.name, dep=dep)
+        for c, deps in dependencies.items():
+            for dep in deps:
+                result += "\"{klass}\" *-- \"{dep}\"\n".format(
+                    klass=c.name, dep=dep)
 
-    result += """
+    if add_legend:
+        result += """
 
 package \"B is a subtype of A\" <<Rectangle>> {
  A <|-right- B
  note  "This determines what you find when you query for the RecordType.\\n'FIND RECORD A' will provide Records which have a parent\\nA or B, while 'FIND RECORD B' will provide only Records which have a parent B." as N1
 }
 """
-    result += """
+        result += """
 
 package \"The property P references an instance of D\" <<Rectangle>> {
  class C {
@@ -246,7 +294,8 @@ def retrieve_substructure(start_record_types, depth, result_id_set=None, result_
     Returns
     -------
     db.Container
-                A container containing all the retrieved entites or None if cleanup is False.
+                A container containing all the retrieved entites
+                or None if cleanup is False.
     """
     # Initialize the id set and result container for level zero recursion depth:
     if result_id_set is None:
@@ -260,9 +309,19 @@ def retrieve_substructure(start_record_types, depth, result_id_set=None, result_
             result_container.append(entity)
         result_id_set.add(entity.id)
         for prop in entity.properties:
-            if is_reference(prop.datatype) and prop.datatype != db.FILE and depth > 0:
-                rt = db.RecordType(name=get_referenced_recordtype(prop.datatype)).retrieve()
-                retrieve_substructure([rt], depth-1, result_id_set, result_container, False)
+            if (is_reference(prop.datatype) and prop.datatype != db.FILE and depth > 0):
+                rt = db.RecordType(
+                    name=get_referenced_recordtype(prop.datatype)).retrieve()
+                retrieve_substructure([rt], depth-1, result_id_set,
+                                      result_container, False)
+            # TODO: clean up this hack
+            # TODO: make it also work for files
+            if is_reference(prop.datatype) and prop.value is not None:
+                r = db.Record(id=prop.value).retrieve()
+                retrieve_substructure([r], depth-1, result_id_set, result_container, False)
+                if r.id not in result_id_set:
+                    result_container.append(r)
+                    result_id_set.add(r.id)
 
             if prop.id not in result_id_set:
                 result_container.append(prop)
@@ -274,14 +333,23 @@ def retrieve_substructure(start_record_types, depth, result_id_set=None, result_
                 result_container.append(rt)
             result_id_set.add(parent.id)
             if depth > 0:
-                retrieve_substructure([rt], depth-1, result_id_set, result_container, False)
+                retrieve_substructure([rt], depth-1, result_id_set,
+                                      result_container, False)
 
     if cleanup:
         return result_container
     return None
 
 
-def to_graphics(recordtypes, filename):
+def to_graphics(recordtypes: list[db.Entity], filename: str,
+                output_dirname: Optional[str] = None,
+                formats: list[str] = ["tsvg"],
+                silent: bool = True,
+                add_properties: bool = True,
+                add_recordtypes: bool = True,
+                add_legend: bool = True,
+                no_shadow: bool = False,
+                style: str = "default"):
     """Calls recordtypes_to_plantuml_string(), saves result to file and
     creates an svg image
 
@@ -293,17 +361,55 @@ def to_graphics(recordtypes, filename):
                   Iterable with the entities to be displayed.
     filename : str
                filename of the image without the extension(e.g. data_structure;
+               also without the preceeding path.
                data_structure.pu and data_structure.svg will be created.)
+    output_dirname : str
+                     the destination directory for the resulting images as defined by the "-o"
+                     option by plantuml
+                     default is to use current working dir
+    formats : list[str]
+              list of target formats as defined by the -t"..." options by plantuml, e.g. "tsvg"
+    silent : bool
+             Don't output messages.
+    no_shadow : bool, optional
+      If true, tell plantuml to use a skin without blurred shadows.
     """
-    pu = recordtypes_to_plantuml_string(recordtypes)
-
-    pu_filename = filename+".pu"
-    with open(pu_filename, "w") as pu_file:
-        pu_file.write(pu)
-
-    cmd = "plantuml -tsvg %s" % pu_filename
-    print("Executing:", cmd)
-
-    if os.system(cmd) != 0:
-        raise Exception("An error occured during the execution of plantuml. "
-                        "Is plantuml installed?")
+    pu = recordtypes_to_plantuml_string(iterable=recordtypes,
+                                        add_properties=add_properties,
+                                        add_recordtypes=add_recordtypes,
+                                        add_legend=add_legend,
+                                        no_shadow=no_shadow,
+                                        style=style)
+
+    if output_dirname is None:
+        output_dirname = os.getcwd()
+
+    allowed_formats = [
+        "tpng", "tsvg", "teps", "tpdf", "tvdx", "txmi",
+        "tscxml", "thtml", "ttxt", "tutxt", "tlatex", "tlatex:nopreamble"]
+
+    with tempfile.TemporaryDirectory() as td:
+
+        pu_filename = os.path.join(td, filename + ".pu")
+        with open(pu_filename, "w") as pu_file:
+            pu_file.write(pu)
+
+        for format in formats:
+            extension = format[1:]
+            if ":" in extension:
+                extension = extension[:extension.index(":")]
+
+            if format not in allowed_formats:
+                raise RuntimeError("Format not allowed.")
+            cmd = "plantuml -{} {}".format(format, pu_filename)
+            if not silent:
+                print("Executing:", cmd)
+
+            if os.system(cmd) != 0:  # TODO: replace with subprocess.run
+                raise Exception("An error occured during the execution of "
+                                "plantuml when using the format {}. "
+                                "Is plantuml installed? "
+                                "You might want to dry a different format.".format(format))
+            # copy only the final product into the target directory
+            shutil.copy(os.path.join(td, filename + "." + extension),
+                        output_dirname)
diff --git a/src/doc/conf.py b/src/doc/conf.py
index bb1e8e4ffaf50ac685ca99cb2361c435f44e60bd..9a0483597ea89680121576339f2d5b74f96797ee 100644
--- a/src/doc/conf.py
+++ b/src/doc/conf.py
@@ -29,10 +29,10 @@ copyright = '2022, IndiScale GmbH'
 author = 'Daniel Hornung'
 
 # The short X.Y version
-version = '0.7.2'
+version = '0.7.3'
 # The full version, including alpha/beta/rc tags
 # release = '0.5.2-rc2'
-release = '0.7.2'
+release = '0.7.3'
 
 
 # -- General configuration ---------------------------------------------------
diff --git a/src/doc/configuration.md b/src/doc/configuration.md
index 802da4e91818ba65bd0184a9a5ac49f5c2ba02d2..02cbbd7b13d916a676ad26c277e370ae76bf3725 100644
--- a/src/doc/configuration.md
+++ b/src/doc/configuration.md
@@ -4,6 +4,15 @@ PyCaosDB tries to read from the inifile specified in the environment variable `P
 alternatively in `~/.pycaosdb.ini` upon import.  After that, the ini file `pycaosdb.ini` in the
 current working directory will be read additionally, if it exists.
 
+Here, we will look at the most common configuration options. For a full and comprehensive
+description please check out the [example pycaosdb.ini file](https://gitlab.com/caosdb/caosdb-pylib/-/blob/main/examples/pycaosdb.ini).  You can download this file and use
+it as a starting point.
+
+
+Typically, you need to change at least the `url` and `username` fields as required.  (Ask your
+CaosDB administrator or IT crowd if you do not know what to put there, but for the demo instance at
+https://demo.indiscale.com, `username=admin` and `password=caosdb` should work).
+
 ## Authentication ##
 
 The default configuration (that your are asked for your password when ever a connection is created
@@ -17,6 +26,8 @@ can be changed by setting `password_method`:
   Windows). The password will be queried on first usage.
 * with `password_method=plain` (**strongly discouraged**)
 
+The following illustrates the recommended options:
+
 ```ini
 [Connection]
 username=YOUR_USERNAME
@@ -35,7 +46,10 @@ username=YOUR_USERNAME
 
 ## SSL Certificate ##
 
-You can set the pass to the ssl certificate to be used:
+In some cases (especially if you are testing CaosDB) you might need to supply an SSL certificate to
+allow SSL encryption.
+
+The `cacert` option sets the path to the ssl certificate for the connection:
 
 ```ini
 [Connection]
diff --git a/src/doc/future_caosdb.md b/src/doc/future_caosdb.md
new file mode 100644
index 0000000000000000000000000000000000000000..de6170fa42674ed4e3161fb791a397a149dba659
--- /dev/null
+++ b/src/doc/future_caosdb.md
@@ -0,0 +1,193 @@
+# The future of the CaosDB Python Client
+
+The current Python client has done us great services but its structure and the 
+way it is used sometimes feels outdated and clumsy. In this document we sketch
+how it might look different in future and invite everyone to comment or
+contribute to this development.
+
+At several locations in this document there will be links to discussion issues.
+If you want to discuss something new, you can create a new issue
+[here](https://gitlab.com/caosdb/caosdb-pylib/-/issues/new).
+
+## Overview
+Let's get a general impression before discussing single aspects.
+
+``` python
+import caosdb as db
+experiments = db.query("FIND Experiment")
+# print name and date for each `Experiment`
+for exp in experiments:
+   print(exp.name, exp.date)
+
+# suppose `Experiments` reference `Projects` which have a `Funding` Property
+one_exp = experiments[0]
+print(one_exp.Project.Funding)
+
+new_one = db.create_record("Experiment")
+new_one.date = "2022-01-01"
+new_one.name = "Needle Measurement"
+new_one.insert()
+```
+Related discussions:
+- [recursive retrieve in query](https://gitlab.com/caosdb/caosdb-pylib/-/issues/57)
+- [create_record function](https://gitlab.com/caosdb/caosdb-pylib/-/issues/58)
+- [data model utility](https://gitlab.com/caosdb/caosdb-pylib/-/issues/59)
+
+## Quickstart
+Note that you can try out one possible implementation using the 
+`caosdb.high_level_api` module. It is experimental and might be removed in 
+future!
+
+A `resolve_references` function allows to retrieve the referenced entities of 
+an entity, container or a query result set (which is a container).
+It has the following parameters which can also be supplied to the `query` 
+function:
+
+-   `deep`: Whether to use recursive retrieval
+-   `depth`: Maximum recursion depth
+-   `references`: Whether to use the supplied db.Container to resolve
+    references. This allows offline usage. Set it to None if you want to
+    automatically retrieve entities from the current CaosDB connection.
+
+In order to allow a quick look at the object structures an easily readable 
+serialization is provided by the `to_dict` function. It has the following 
+argument:
+-   `without_metadata`: Set this to True if you don\'t want to see
+    property metadata like \"unit\" or \"importance\".
+
+This function creates a simple dictionary containing a representation of
+the entity, which can be stored to disk and completely deserialized
+using the function `from_dict`.
+
+Furthermore, the `__str__` function uses this to display objects in yaml 
+format by default statement
+
+## Design Decisions
+
+### Dot Notation
+Analogue, to what Pandas does. Provide bracket notation 
+`rec.properties["test"]` for Properties with names that are in conflict with 
+default attributes or contain spaces (or other forbidden characters).
+
+Entities can be initialized with a set of Propertynames. Those Propertynames will be used as 
+attributes such that tab completion is possible in interactive use. The value however will be a special
+value (e.g. UnsetPropertyValue) and accessing it results in an Exception. Thus, tab completion can be used 
+but no Properties are inserted unexpectedly with NULL values. 
+
+- Raise Exception if attribute does not exist but is accessed?
+
+[Discussion](https://gitlab.com/caosdb/caosdb-pylib/-/issues/60)
+
+We aim for a distinction between "concrete" Properties of Records/RecordTypes and "abstract" Properties as part of the definition of a data model. Concrete properties are always "contained" in a record or record type while abstract properties stand for themselves.
+
+Draft:
+```
+class ConcreteProperty:
+  def __init__(self, v, u):
+    self.value = v
+    self.unit = u
+    
+class Entity:
+  def __init__(self):
+    pass
+    
+  def __setattr__(self, name, val):
+    if name not in dir(self):
+        # setattr(self, name, ConcreteProperty(val, None))
+        self.properties[name] = ConcreteProperty(val, None)
+    else:
+        # getattribute(self, name).value = val
+        self.properties[name].value = val
+```
+
+The old "get_property" functions serves the same purpose as the new "[]" notation.
+
+Instead of `get_property` / `add_property` etc. functions belonging to class Entity, we should refactor the list of properties (of an entity) to be a special kind of list, e.g. PropertyList.
+This list should enherit from a standard list, have all the known functions like "append", "extend", "__in__" and allow for all property-related functionality as part of its member functions (instead of access via Entity directly).
+Same story for the parents.
+
+**GET RID OF MULTI PROPERTIES!!!**
+
+#### how to deal with "property metadata"
+
+Current suggestion: stored in a special field "property_metadata" belonging to the object.
+`property_metadata` is a dict:
+- importance
+- unit
+- description
+- ...
+
+### Serialization
+What information needs to be contained in (meta)data? How compatible is it with 
+GRPC json serialization?
+
+
+### Recursive Retrieval
+
+
+
+I can resolve later and end up with the same result:
+`recs =db.query("FIND Experiment", depth=2)`  equals `recs = db.query("FIND Experiment"); recs = resolve_references(recs, depth=2)`
+
+[Discussion](https://gitlab.com/caosdb/caosdb-pylib/-/issues/57)
+
+
+#### Alternative
+
+`FIND Experiment` with `depth=2` will retrieve all referenced entities from any experiment found. A typical use case could also be:
+
+```python
+recs = db.query("FIND Experiment")
+recs[0].resolve_references(depth=2)
+```
+
+#### Idea
+
+Recursive retrievel as functionality of the server.
+
+retrieve and query commands should support the `depth` argument.
+
+### In-Place operations
+Default behavior is to return new objects instead of modifying them in-place.
+This can be changed with the argument `inplace=True`.
+Especially the following functions operate by default NOT in-place:
+- update
+- insert
+- retrieve
+- resolve_references
+[Discussion](https://gitlab.com/caosdb/caosdb-pylib/-/issues/61)
+
+## Extended Example
+``` python
+import caosdb as db
+
+dm = db.get_data_model()
+
+new_one = db.create_record(dm.Experiment)
+new_one.date = "2022-01-01"
+new_one.name = "Needle Measurement"
+new_one.dataset = db.create_record(dm.Dataset)
+new_one.dataset.voltage = (5, "V")
+new_one.dataset.pulses = [5, 5.3]
+inserted = new_one.insert()
+print("The new record has the ID:", inserted.id)
+```
+
+### Factory method
+While creating an Entity will not talk to a CaosDB server and can thus be done offline, the factory method
+`create_record` allows to 
+1. Retrieve the parent and set attributes according to inheritance
+2. Use a container to resolve the parent and set attributes
+
+In general, more complex "magic" will be placed in the factory and only the straight forward version 
+in the constructor.
+
+### References and sub entities
+
+Several possibilities exist for references:
+
+- value is the id of a referenced entity
+- value is a "sub object"
+- value is a reference to another (entity-)list element (similar to second variant, but with "sub object" always contained in container/entity-list)
+
+To be discussed: Which should be the obligatory/preferred variant?
diff --git a/src/doc/high_level_api.org b/src/doc/high_level_api.org
new file mode 100644
index 0000000000000000000000000000000000000000..516df1b41d500fab000a72517fd2d12ba61753b7
--- /dev/null
+++ b/src/doc/high_level_api.org
@@ -0,0 +1,171 @@
+* High Level API
+
+In addition to the old standard pylib API, new versions of pylib ship with a high level API
+that facilitates usage of CaosDB entities within data analysis scripts. In a nutshell that
+API exposes all properties of CaosDB Records as standard python attributes making their
+access easier.
+
+Or to spell it out directly in Python:
+#+BEGIN_SRC python
+
+  import caosdb as db
+  # Old API:
+  r = db.Record()
+  r.add_parent("Experiment")
+  r.add_property(name="alpha", value=5)
+  r.get_property("alpha").value = 25 # setting properties (old api)
+  print(r.get_property("alpha").value + 25) # getting properties (old api)
+
+  from caosdb.high_level_api import convert_to_python_entity
+  obj = convert_to_python_object(r) # create a high level entity
+  obj.r = 25 # setting properties (new api)
+  print(obj.r + 25) # getting properties (new api)
+
+#+END_SRC
+
+
+** Quickstart
+
+The module, needed for the high level API is called:
+~caosdb.high_level_api~
+
+There are two functions converting entities between the two representation (old API and new API):
+- ~convert_to_python_object~: Convert entities from **old** into **new** representation.
+- ~convert_to_entity~: Convert entities from **new** into **old** representation.
+
+Furthermore there are a few utility functions which expose very practical shorthands:
+- ~new_high_level_entity~: Retrieve a record type and create a new high level entity which contains properties of a certain importance level preset.
+- ~create_record~: Create a new high level entity using the name of a record type and a list of key value pairs as properties.
+- ~load_external_record~: Retrieve a record with a specific name and return it as high level entity.
+- ~create_entity_container~: Convert a high level entity into a standard entity including all sub entities.
+- ~query~: Do a CaosDB query and return the result as a container of high level objects.
+
+So as a first example, you could retrieve any record from CaosDB and use it using its high level representation:
+#+BEGIN_SRC python
+  from caosdb.high_level_api import query
+
+  res = query("FIND Record Experiment")
+  experiment = res[0]
+  # Use a property:
+  print(experiment.date)
+
+  # Use sub properties:
+  print(experiment.output[0].path)
+#+END_SRC
+
+The latter example demonstrates, that the function query is very powerful. For its default parameter
+values it automatically resolves and retrieves references recursively, so that sub properties,
+like the list of output files "output", become immediately available.
+
+**Note** that for the old API you were supposed to run the following series of commands
+to achieve the same result:
+#+BEGIN_SRC python
+  import caosdb as db
+
+  res = db.execute_query("FIND Record Experiment")
+  output = res.get_property("output")
+  output_file = db.File(id=output.value[0].id).retrieve()
+  print(output_file.path)
+#+END_SRC
+
+Resolving subproperties makes use of the "resolve_reference" function provided by the high level
+entity class (~CaosDBPythonEntity~), with the following parameters:
+- ~deep~: Whether to use recursive retrieval
+- ~references~: Whether to use the supplied db.Container to resolve references. This allows offline usage. Set it to None if you want to automatically retrieve entities from the current CaosDB connection.
+- ~visited~: Needed for recursion, set this to None.
+
+Objects in the high level representation can be serialized to a simple yaml form using the function
+~serialize~ with the following parameters:
+- ~without_metadata~: Set this to True if you don't want to see property metadata like "unit" or "importance".
+- ~visited~: Needed for recursion, set this to None.
+
+This function creates a simple dictionary containing a representation of the entity, which can be
+stored to disk and completely deserialized using the function ~deserialize~.
+
+Furthermore the "__str__" function is overloaded, so that you can use print to directly inspect
+high level objects using the following statement:
+#+BEGIN_SRC python
+print(str(obj))
+#+END_SRC
+
+
+** Concepts
+
+As described in the section [[Quickstart]] the two functions ~convert_to_python_object~ and ~convert_to_entity~ convert
+entities beetween the high level and the standard representation.
+
+The high level entities are represented using the following classes from the module ~caosdb.high_level_api~:
+- ~CaosDBPythonEntity~: Base class of the following entity classes.
+- ~CaosDBPythonRecord~
+- ~CaosDBPythonRecordType~
+- ~CaosDBPythonProperty~
+- ~CaosDBPythonMultiProperty~: **WARNING** Not implemented yet.
+- ~CaosDBPythonFile~: Used for file entities and provides an additional ~download~ function for being able to directly retrieve files from CaosDB.
+
+In addition, there are the following helper structures which are realized as Python data classes:
+- ~CaosDBPropertyMetaData~: For storing meta data about properties.
+- ~CaosDBPythonUnresolved~: The base class of unresolved "things".
+- ~CaosDBPythonUnresolvedParent~: Parents of entities are stored as unresolved parents by default, storing an id or a name of a parent (or both).
+- ~CaosDBPythonUnresolvedReference~: An unresolved reference is a reference property with an id which has not (yet) been resolved to an Entity.
+
+The function "resolve_references" can be used to recursively replace ~CaosDBPythonUnresolvedReferences~ into members of type ~CaosDBPythonRecords~
+or ~CaosDBPythonFile~.
+
+Each property stored in a CaosDB record corresponds to:
+- a member attribute of ~CaosDBPythonRecord~ **and**
+- an entry in a dict called "metadata" storing a CaosDBPropertyMetadata object with the following information about proeprties:
+  - ~unit~
+  - ~datatype~
+  - ~description~
+  - ~id~
+  - ~importance~
+
+
+* Example
+
+The following shows a more complex example taken from a real world use case:
+A numerical experiment is created to simulate cardiac electric dynamics. The physical problem 
+is modelled using the monodomain equation with the local current term given by the Mitchell 
+Schaeffer Model.
+
+The data model for the numerical experiment consists of multiple record types which stores assosciated paremeters:
+- `MonodomainTissueSimulation`
+- `MitchellSchaefferModel`
+- `SpatialExtent2d`
+- `SpatialDimension`
+- `ConstantTimestep`
+- `ConstantDiffusion`
+
+First, the data model will be filled with the parameter values for this specific simulation run. It will be stored in the python variable `MonodomainRecord`. Passing the `MonodomainRecord` through the python functions, the simulation parameters can be easily accessed everywhere in the code when needed.
+
+Records are created by the `create_record` function. Parameter values can be set at record creation and also after creation as python properties of the corresponding record instance. The following example shows how to create a record, how to set the parameter at creation and how to set them as python properties
+
+#+BEGIN_SRC python
+  from caosdb.high_level_api import create_record
+
+  MonodomainRecord = create_record("MonodomainTissueSimulation")
+  MonodomainRecord.LocalModel = create_record("MitchellSchaefferModel")
+  MonodomainRecord.SpatialExtent = create_record(
+       "SpatialExtent2d", spatial_extent_x=100, spatial_extent_y=100)
+  MonodomainRecord.SpatialExtent.cell_sizes = [0.1, 0.1] # parameters can be set as properties
+  MonodomainRecord.SpatialDimension = create_record("SpatialDimension", 
+  num_dim=2)
+
+  MonodomainRecord.TimestepInformation = create_record("ConstantTimestep")
+  MonodomainRecord.TimestepInformation.DeltaT = 0.1
+
+  D = create_record("ConstantDiffusion", diffusion_constant=0.1)
+  MonodomainRecord.DiffusionConstantType = D
+  model = MonodomainRecord.LocalModel
+  model.t_close = 150
+  model.t_open = 120
+  model.t_out = 6
+  model.t_in = 0.3
+  model.v_gate = 0.13
+  model.nvars = 2
+#+END_SRC
+
+At any position in the algorithm you are free to:
+- Convert this model to the standard python API and insert or update the records in a running instance of CaosDB.
+- Serialize this model in the high level API yaml format. This enables the CaosDB crawler to pickup the file and synchronize it with a running instance 
+of CaosDB.
diff --git a/src/doc/high_level_api.rst b/src/doc/high_level_api.rst
new file mode 100644
index 0000000000000000000000000000000000000000..603052b135ad2289caea7e3bed59ae9d3301f811
--- /dev/null
+++ b/src/doc/high_level_api.rst
@@ -0,0 +1,163 @@
+High Level API
+==============
+
+In addition to the old standard pylib API, new versions of pylib ship
+with a high level API that facilitates usage of CaosDB entities within
+data analysis scripts. In a nutshell that API exposes all properties of
+CaosDB Records as standard python attributes making their access easier.
+
+Or to speak it out directly in Python:
+
+.. code:: python
+
+
+   import caosdb as db
+   # Old API:
+   r = db.Record()
+   r.add_parent("Experiment")
+   r.add_property(name="alpha", value=5)
+   r.get_property("alpha").value = 25 # setting properties (old api)
+   print(r.get_property("alpha").value + 25) # getting properties (old api)
+
+   from caosdb.high_level_api import convert_to_python_entity
+   obj = convert_to_python_object(r) # create a high level entity
+   obj.r = 25 # setting properties (new api)
+   print(obj.r + 25) # getting properties (new api)
+
+Quickstart
+----------
+
+The module, needed for the high level API is called:
+``caosdb.high_level_api``
+
+There are two functions converting entities between the two
+representation (old API and new API):
+
+-  ``convert_to_python_object``: Convert entities from **old** into
+   **new** representation.
+-  ``convert_to_entity``: Convert entities from **new** into **old**
+   representation.
+
+Furthermore there are a few utility functions which expose very
+practical shorthands:
+
+-  ``new_high_level_entity``: Retrieve a record type and create a new
+   high level entity which contains properties of a certain importance
+   level preset.
+-  ``create_record``: Create a new high level entity using the name of a
+   record type and a list of key value pairs as properties.
+-  ``load_external_record``: Retrieve a record with a specific name and
+   return it as high level entity.
+-  ``create_entity_container``: Convert a high level entity into a
+   standard entity including all sub entities.
+-  ``query``: Do a CaosDB query and return the result as a container of
+   high level objects.
+
+So as a first example, you could retrieve any record from CaosDB and use
+it using its high level representation:
+
+.. code:: python
+
+   from caosdb.high_level_api import query
+
+   res = query("FIND Record Experiment")
+   experiment = res[0]
+   # Use a property:
+   print(experiment.date)
+
+   # Use sub properties:
+   print(experiment.output[0].path)
+
+The latter example demonstrates, that the function query is very
+powerful. For its default parameter values it automatically resolves and
+retrieves references recursively, so that sub properties, like the list
+of output files "output", become immediately available.
+
+**Note** that for the old API you were supposed to run the following
+series of commands to achieve the same result:
+
+.. code:: python
+
+   import caosdb as db
+
+   res = db.execute_query("FIND Record Experiment")
+   output = res.get_property("output")
+   output_file = db.File(id=output.value[0].id).retrieve()
+   print(output_file.path)
+
+Resolving subproperties makes use of the "resolve\ :sub:`reference`"
+function provided by the high level entity class
+(``CaosDBPythonEntity``), with the following parameters:
+
+-  ``deep``: Whether to use recursive retrieval
+-  ``references``: Whether to use the supplied db.Container to resolve
+   references. This allows offline usage. Set it to None if you want to
+   automatically retrieve entities from the current CaosDB connection.
+-  ``visited``: Needed for recursion, set this to None.
+
+Objects in the high level representation can be serialized to a simple
+yaml form using the function ``serialize`` with the following
+parameters:
+
+-  ``without_metadata``: Set this to True if you don't want to see
+   property metadata like "unit" or "importance".
+-  ``visited``: Needed for recursion, set this to None.
+
+This function creates a simple dictionary containing a representation of
+the entity, which can be stored to disk and completely deserialized
+using the function ``deserialize``.
+
+Furthermore the "*str*" function is overloaded, so that you can use
+print to directly inspect high level objects using the following
+statement:
+
+.. code:: python
+
+   print(str(obj))
+
+Concepts
+--------
+
+As described in the section Quickstart the two functions
+``convert_to_python_object`` and ``convert_to_entity`` convert entities
+beetween the high level and the standard representation.
+
+The high level entities are represented using the following classes from
+the module ``caosdb.high_level_api``:
+
+-  ``CaosDBPythonEntity``: Base class of the following entity classes.
+-  ``CaosDBPythonRecord``
+-  ``CaosDBPythonRecordType``
+-  ``CaosDBPythonProperty``
+-  ``CaosDBPythonMultiProperty``: **WARNING** Not implemented yet.
+-  ``CaosDBPythonFile``: Used for file entities and provides an
+   additional ``download`` function for being able to directly retrieve
+   files from CaosDB.
+
+In addition, there are the following helper structures which are
+realized as Python data classes:
+
+-  ``CaosDBPropertyMetaData``: For storing meta data about properties.
+-  ``CaosDBPythonUnresolved``: The base class of unresolved "things".
+-  ``CaosDBPythonUnresolvedParent``: Parents of entities are stored as
+   unresolved parents by default, storing an id or a name of a parent
+   (or both).
+-  ``CaosDBPythonUnresolvedReference``: An unresolved reference is a
+   reference property with an id which has not (yet) been resolved to an
+   Entity.
+
+The function "resolve\ :sub:`references`" can be used to recursively
+replace ``CaosDBPythonUnresolvedReferences`` into members of type
+``CaosDBPythonRecords`` or ``CaosDBPythonFile``.
+
+Each property stored in a CaosDB record corresponds to:
+
+-  a member attribute of ``CaosDBPythonRecord`` **and**
+-  an entry in a dict called "metadata" storing a CaosDBPropertyMetadata
+   object with the following information about proeprties:
+
+   -  ``unit``
+   -  ``datatype``
+   -  ``description``
+   -  ``id``
+   -  ``importance``
diff --git a/src/doc/index.rst b/src/doc/index.rst
index 004ae3a9926ed7a9a27720db1f3c28e72f1f3f28..7344b6aacdd55fd75f4940d834104faa00c33069 100644
--- a/src/doc/index.rst
+++ b/src/doc/index.rst
@@ -12,6 +12,7 @@ Welcome to PyCaosDB's documentation!
    Concepts <concepts>
    Configuration <configuration>
    Administration <administration>
+   High Level API <high_level_api>
    Code gallery <gallery/index>
    API documentation<_apidoc/caosdb>
 
diff --git a/tox.ini b/tox.ini
index 0d245e03ef9c8fe2151e173cd1a10964d47ef82b..62658ae501234a276db8d570328bbe80f1348a4c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -7,6 +7,7 @@ deps = .
     nose
     pytest
     pytest-cov
+    python-dateutil
     jsonschema==4.0.1
 commands=py.test --cov=caosdb -vv {posargs}
 
diff --git a/unittests/test_apiutils.py b/unittests/test_apiutils.py
index 0294646f6c526230a8e9fb722d56aa23a8f9285c..43ab8107183f16bf8df1d0ea8e447b378bcf8123 100644
--- a/unittests/test_apiutils.py
+++ b/unittests/test_apiutils.py
@@ -25,29 +25,14 @@
 # Test apiutils
 # A. Schlemmer, 02/2018
 
-import pickle
-import tempfile
 
+import pytest
 import caosdb as db
 import caosdb.apiutils
 from caosdb.apiutils import (apply_to_ids, compare_entities, create_id_query,
-                             resolve_reference)
+                             resolve_reference, merge_entities)
 
-from .test_property import testrecord
-
-
-def test_convert_object():
-    r2 = db.apiutils.convert_to_python_object(testrecord)
-    assert r2.species == "Rabbit"
-
-
-def test_pickle_object():
-    r2 = db.apiutils.convert_to_python_object(testrecord)
-    with tempfile.TemporaryFile() as f:
-        pickle.dump(r2, f)
-        f.seek(0)
-        rn2 = pickle.load(f)
-    assert r2.date == rn2.date
+from caosdb.common.models import SPECIAL_ATTRIBUTES
 
 
 def test_apply_to_ids():
@@ -201,8 +186,6 @@ def test_compare_special_properties():
             setattr(r2, set_key, 1)
 
         diff_r1, diff_r2 = compare_entities(r1, r2)
-        print(diff_r1)
-        print(diff_r2)
         assert key not in diff_r1
         assert key not in diff_r2
         assert len(diff_r1["parents"]) == 0
@@ -216,10 +199,6 @@ def test_compare_special_properties():
             setattr(r2, set_key, 2)
 
         diff_r1, diff_r2 = compare_entities(r1, r2)
-        print(r1)
-        print(r2)
-        print(diff_r1)
-        print(diff_r2)
         assert key in diff_r1
         assert key in diff_r2
         if key not in INTS:
@@ -230,3 +209,134 @@ def test_compare_special_properties():
             assert diff_r2[key] == 2
         assert len(diff_r1["properties"]) == 0
         assert len(diff_r2["properties"]) == 0
+
+
+@pytest.mark.xfail
+def test_compare_properties():
+    p1 = db.Property()
+    p2 = db.Property()
+
+    diff_r1, diff_r2 = compare_entities(p1, p2)
+    assert len(diff_r1["parents"]) == 0
+    assert len(diff_r2["parents"]) == 0
+    assert len(diff_r1["properties"]) == 0
+    assert len(diff_r2["properties"]) == 0
+
+    p1.importance = "SUGGESTED"
+    diff_r1, diff_r2 = compare_entities(p1, p2)
+    assert len(diff_r1["parents"]) == 0
+    assert len(diff_r2["parents"]) == 0
+    assert len(diff_r1["properties"]) == 0
+    assert len(diff_r2["properties"]) == 0
+    assert "importance" in diff_r1
+    assert diff_r1["importance"] == "SUGGESTED"
+
+    # TODO: I'm not sure why it is not like this:
+    # assert diff_r2["importance"] is None
+    # ... but:
+    assert "importance" not in diff_r2
+
+    p2.importance = "SUGGESTED"
+    p1.value = 42
+    p2.value = 4
+
+    diff_r1, diff_r2 = compare_entities(p1, p2)
+    assert len(diff_r1["parents"]) == 0
+    assert len(diff_r2["parents"]) == 0
+    assert len(diff_r1["properties"]) == 0
+    assert len(diff_r2["properties"]) == 0
+
+    # Comparing values currently does not seem to be implemented:
+    assert "value" in diff_r1
+    assert diff_r1["value"] == 42
+    assert "value" in diff_r2
+    assert diff_r2["value"] == 4
+
+
+def test_copy_entities():
+    r = db.Record(name="A")
+    r.add_parent(name="B")
+    r.add_property(name="C", value=4, importance="OBLIGATORY")
+    r.add_property(name="D", value=[3, 4, 7], importance="OBLIGATORY")
+    r.description = "A fancy test record"
+
+    c = r.copy()
+
+    assert c is not r
+    assert c.name == "A"
+    assert c.role == r.role
+    assert c.parents[0].name == "B"
+    # parent and property objects are not shared among copy and original:
+    assert c.parents[0] is not r.parents[0]
+
+    for i in [0, 1]:
+        assert c.properties[i] is not r.properties[i]
+        for special in SPECIAL_ATTRIBUTES:
+            assert getattr(c.properties[i], special) == getattr(r.properties[i], special)
+        assert c.get_importance(c.properties[i]) == r.get_importance(r.properties[i])
+
+
+def test_merge_entities():
+    r = db.Record(name="A")
+    r.add_parent(name="B")
+    r.add_property(name="C", value=4, importance="OBLIGATORY")
+    r.add_property(name="D", value=[3, 4, 7], importance="OBLIGATORY")
+    r.description = "A fancy test record"
+
+    r2 = db.Record()
+    r2.add_property(name="F", value="text")
+    merge_entities(r2, r)
+    assert r2.get_parents()[0].name == "B"
+    assert r2.get_property("C").name == "C"
+    assert r2.get_property("C").value == 4
+    assert r2.get_property("D").name == "D"
+    assert r2.get_property("D").value == [3, 4, 7]
+
+    assert r2.get_property("F").name == "F"
+    assert r2.get_property("F").value == "text"
+
+
+def test_merge_bug_109():
+    rt = db.RecordType(name="TestBug")
+    p = db.Property(name="test_bug_property", datatype=db.LIST(db.INTEGER))
+
+    r_b = db.Record(name="TestRecord")
+    r_b.add_parent(rt)
+    r_b.add_property(p, value=[18, 19])
+
+    r_a = db.Record(name="TestRecord")
+    r_a.add_parent(rt)
+
+    merge_entities(r_a, r_b)
+
+    assert r_b.get_property("test_bug_property").value == [18, 19]
+    assert r_a.get_property("test_bug_property").value == [18, 19]
+
+    assert "<Value>18</Value>\n    <Value>19</Value>" in str(r_b)
+    assert "<Value>18</Value>\n    <Value>19</Value>\n    <Value>18</Value>\n    <Value>19</Value>" not in str(r_b)
+
+    assert "<Value>18</Value>\n    <Value>19</Value>" in str(r_a)
+    assert "<Value>18</Value>\n    <Value>19</Value>\n    <Value>18</Value>\n    <Value>19</Value>" not in str(r_a)
+
+
+@pytest.mark.xfail
+def test_bug_109():
+    rt = db.RecordType(name="TestBug")
+    p = db.Property(name="test_bug_property", datatype=db.LIST(db.INTEGER))
+
+    r_b = db.Record(name="TestRecord")
+    r_b.add_parent(rt)
+    r_b.add_property(p, value=[18, 19])
+
+    r_a = db.Record(name="TestRecord")
+    r_a.add_parent(rt)
+    r_a.add_property(r_b.get_property("test_bug_property"))
+
+    assert r_b.get_property("test_bug_property").value == [18, 19]
+    assert r_a.get_property("test_bug_property").value == [18, 19]
+
+    assert "<Value>18</Value>\n    <Value>19</Value>" in str(r_b)
+    assert "<Value>18</Value>\n    <Value>19</Value>\n    <Value>18</Value>\n    <Value>19</Value>" not in str(r_b)
+
+    assert "<Value>18</Value>\n    <Value>19</Value>" in str(r_a)
+    assert "<Value>18</Value>\n    <Value>19</Value>\n    <Value>18</Value>\n    <Value>19</Value>" not in str(r_a)
diff --git a/unittests/test_configs/pycaosdb-IntegrationTests.ini b/unittests/test_configs/pycaosdb-IntegrationTests.ini
new file mode 100644
index 0000000000000000000000000000000000000000..cb9871708f7f23c489de0cbc8f4fbda15dfa6ad0
--- /dev/null
+++ b/unittests/test_configs/pycaosdb-IntegrationTests.ini
@@ -0,0 +1,37 @@
+# -*- mode:conf; -*-
+## This sections needs to exist in addition to the usual section
+[IntegrationTests]
+# test_server_side_scripting.bin_dir.local=/path/to/scripting/bin
+test_server_side_scripting.bin_dir.local=/home/myself/test/caosdb-server/scripting/bin
+# test_server_side_scripting.bin_dir.server=/opt/caosdb/git/caosdb-server/scripting/bin
+
+# # location of the files from the pyinttest perspective
+# test_files.test_insert_files_in_dir.local=/extroot/test_insert_files_in_dir/
+test_files.test_insert_files_in_dir.local=/home/myself/test/debug_advanced/paths/extroot/test_insert_files_in_dir
+# # location of the files from the caosdb_servers perspective
+test_files.test_insert_files_in_dir.server=/opt/caosdb/mnt/extroot/test_insert_files_in_dir/
+
+########## Files ##################
+## Used by tests of file handling. Specify the path to an existing
+## directory in which file tests are performed, once as seen by the
+## host and once as seen by the server.
+
+# location of the files from the pyinttest (i.e. host) perspective
+#test_files.test_insert_files_in_dir.local=/extroot/test_insert_files_in_dir/
+
+# location of the files from the caosdb server's perspective
+#test_files.test_insert_files_in_dir.server=/opt/caosdb/mnt/extroot/test_insert_files_in_dir/
+
+# # location of the one-time tokens from the pyinttest's perspective
+# test_authentication.admin_token_crud = /authtoken/admin_token_crud.txt
+# test_authentication.admin_token_expired = /authtoken/admin_token_expired.txt
+# test_authentication.admin_token_3_attempts = /authtoken/admin_token_3_attempts.txt
+
+
+## Insert your usual settings here
+[Connection]
+url=https://localhost:10443/
+username=admin
+password_method=plain
+password=caosdb
+
diff --git a/unittests/test_configs/pycaosdb-empty.ini b/unittests/test_configs/pycaosdb-empty.ini
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/unittests/test_configs/pycaosdb-real-world-1.ini b/unittests/test_configs/pycaosdb-real-world-1.ini
new file mode 100644
index 0000000000000000000000000000000000000000..e524f1d3465c61d89ae4a4dda54536a722f99837
--- /dev/null
+++ b/unittests/test_configs/pycaosdb-real-world-1.ini
@@ -0,0 +1,17 @@
+[Connection]
+url = https://localhost:10443
+cacert = /opt/caosdb/cert/caosdb.cert.pem
+debug = 0
+timeout = 5000
+
+[Misc]
+sendmail = /usr/local/bin/sendmail_to_file
+entity_loan.curator_mail_from=crawler-test@example.com
+entity_loan.curator_mail_to=crawler-test@example.com
+
+[sss_helper]
+external_uri = https://caosdb.example.com:443
+
+[advancedtools]
+crawler.from_mail=admin@example.com
+crawler.to_mail=admin@example.com
diff --git a/unittests/test_configs/pycaosdb-real-world-2.ini b/unittests/test_configs/pycaosdb-real-world-2.ini
new file mode 100644
index 0000000000000000000000000000000000000000..5ebd115a4a4de189d22180130acca2a4b78b6daf
--- /dev/null
+++ b/unittests/test_configs/pycaosdb-real-world-2.ini
@@ -0,0 +1,15 @@
+[Connection]
+url = https://samplemanager.example.com:443
+cacert = /opt/caosdb/cert/caosdb.cert.pem
+debug = 0
+timeout = 5000
+[Misc]
+sendmail = /usr/local/bin/sendmail_to_file
+entity_loan.curator_mail_from=crawler-test@example.com
+entity_loan.curator_mail_to=crawler-test@example.com
+[sss_helper]
+external_uri = https://localhost:10443
+[advancedtools]
+crawler.from_mail=crawler-test@example.com
+crawler.to_mail=crawler-test@example.com           
+
diff --git a/unittests/test_configs/pycaosdb-server-side-scripting.ini b/unittests/test_configs/pycaosdb-server-side-scripting.ini
new file mode 100644
index 0000000000000000000000000000000000000000..de2867f8dc66b3e81f10f35e40c36f9cb8591604
--- /dev/null
+++ b/unittests/test_configs/pycaosdb-server-side-scripting.ini
@@ -0,0 +1,9 @@
+; this is the pycaosdb.ini for the server-side-scripting home.
+[Connection]
+url = https://caosdb-server:10443
+cacert = /opt/caosdb/cert/caosdb.cert.pem
+debug = 0
+timeout = 5000
+
+[Misc]
+sendmail = /usr/local/bin/sendmail_to_file
diff --git a/unittests/test_configs/pycaosdb4.ini b/unittests/test_configs/pycaosdb4.ini
new file mode 100644
index 0000000000000000000000000000000000000000..ddbc7ca6f969e55ea6131d96f091177a13687ece
--- /dev/null
+++ b/unittests/test_configs/pycaosdb4.ini
@@ -0,0 +1,4 @@
+[Connection]
+url=https://localhost:10443/
+username=admin
+password_method=input
diff --git a/unittests/test_configs/pycaosdb5.ini b/unittests/test_configs/pycaosdb5.ini
new file mode 100644
index 0000000000000000000000000000000000000000..3f365efdd92641a39b742e22f825033a69e12dc5
--- /dev/null
+++ b/unittests/test_configs/pycaosdb5.ini
@@ -0,0 +1,4 @@
+[Connection]
+url=https://localhost:10443/
+username=admin
+# No password method: should be "input" by default
diff --git a/unittests/test_entity.py b/unittests/test_entity.py
index 1e88702ac016d7dcfdf00919dd0f93b5d3345e00..f2891fda266e1d62139b4cb2667c31b090ca6498 100644
--- a/unittests/test_entity.py
+++ b/unittests/test_entity.py
@@ -26,10 +26,13 @@
 import unittest
 from lxml import etree
 
+import os
 from caosdb import (INTEGER, Entity, Property, Record, RecordType,
                     configure_connection)
 from caosdb.connection.mockup import MockUpServerConnection
 
+UNITTESTDIR = os.path.dirname(os.path.abspath(__file__))
+
 
 class TestEntity(unittest.TestCase):
 
@@ -87,7 +90,7 @@ class TestEntity(unittest.TestCase):
         """
         parser = etree.XMLParser(remove_comments=True)
         entity = Entity._from_xml(Entity(),
-                                  etree.parse("unittests/test_record.xml",
+                                  etree.parse(os.path.join(UNITTESTDIR, "test_record.xml"),
                                               parser).getroot())
 
         self.assertEqual(entity.role, "Record")
diff --git a/unittests/test_high_level_api.py b/unittests/test_high_level_api.py
new file mode 100644
index 0000000000000000000000000000000000000000..a9e55c9c2a79f7ead8bbb3fb652c1b81427e69e9
--- /dev/null
+++ b/unittests/test_high_level_api.py
@@ -0,0 +1,643 @@
+# This file is a part of the CaosDB Project.
+#
+# Copyright (C) 2018 Research Group Biomedical Physics,
+# Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+# Copyright (C) 2022 Alexander Schlemmer <alexander.schlemmer@ds.mpg.de>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+# ** end header
+#
+# Test high level api module
+# A. Schlemmer, 02/2022
+
+
+import caosdb as db
+from caosdb.high_level_api import (convert_to_entity, convert_to_python_object,
+                                   new_high_level_entity)
+from caosdb.high_level_api import (CaosDBPythonUnresolvedParent,
+                                   CaosDBPythonUnresolvedReference,
+                                   CaosDBPythonRecord, CaosDBPythonFile,
+                                   high_level_type_for_standard_type,
+                                   standard_type_for_high_level_type,
+                                   high_level_type_for_role,
+                                   CaosDBPythonEntity)
+from caosdb.apiutils import compare_entities
+
+from caosdb.common.datatype import (is_list_datatype,
+                                    get_list_datatype,
+                                    is_reference)
+
+import pytest
+from lxml import etree
+import os
+import tempfile
+import pickle
+
+import sys
+import traceback
+import pdb
+
+
+@pytest.fixture
+def testrecord():
+    parser = etree.XMLParser(remove_comments=True)
+    testrecord = db.Record._from_xml(
+        db.Record(),
+        etree.parse(os.path.join(os.path.dirname(__file__), "test_record.xml"),
+                    parser).getroot())
+    return testrecord
+
+
+def test_convert_object(testrecord):
+    r2 = convert_to_python_object(testrecord)
+    assert r2.species == "Rabbit"
+
+
+def test_pickle_object(testrecord):
+    r2 = convert_to_python_object(testrecord)
+    with tempfile.TemporaryFile() as f:
+        pickle.dump(r2, f)
+        f.seek(0)
+        rn2 = pickle.load(f)
+    assert r2.date == rn2.date
+
+
+def test_convert_record():
+    """
+    Test the high level python API.
+    """
+    r = db.Record()
+    r.add_parent("bla")
+    r.add_property(name="a", value=42)
+    r.add_property(name="b", value="test")
+
+    obj = convert_to_python_object(r)
+    assert obj.a == 42
+    assert obj.b == "test"
+
+    # There is no such property
+    with pytest.raises(AttributeError):
+        assert obj.c == 18
+
+    assert obj.has_parent("bla") is True
+    assert obj.has_parent(CaosDBPythonUnresolvedParent(name="bla")) is True
+
+    # Check the has_parent function:
+    assert obj.has_parent("test") is False
+    assert obj.has_parent(CaosDBPythonUnresolvedParent(name="test")) is False
+
+    # duplicate parent
+    with pytest.raises(RuntimeError):
+        obj.add_parent("bla")
+
+    # add parent with just an id:
+    obj.add_parent(CaosDBPythonUnresolvedParent(id=225))
+    assert obj.has_parent(225) is True
+    assert obj.has_parent(CaosDBPythonUnresolvedParent(id=225)) is True
+    assert obj.has_parent(226) is False
+    assert obj.has_parent(CaosDBPythonUnresolvedParent(id=228)) is False
+
+    # same with just a name:
+    obj.add_parent(CaosDBPythonUnresolvedParent(name="another"))
+    assert obj.has_parent("another") is True
+
+
+def test_convert_with_references():
+    r_ref = db.Record()
+    r_ref.add_property(name="a", value=42)
+
+    r = db.Record()
+    r.add_property(name="ref", value=r_ref)
+
+    # try:
+    obj = convert_to_python_object(r)
+    # except:
+    #     extype, value, tb = sys.exc_info()
+    #     traceback.print_exc()
+    #     pdb.post_mortem(tb)
+    assert obj.ref.a == 42
+
+    # With datatype:
+    r_ref = db.Record()
+    r_ref.add_parent("bla")
+    r_ref.add_property(name="a", value=42)
+
+    r = db.Record()
+    r.add_property(name="ref", value=r_ref)
+
+    obj = convert_to_python_object(r)
+    assert obj.ref.a == 42
+    # Parent does not automatically lead to a datatype:
+    assert obj.get_property_metadata("ref").datatype is None
+    assert obj.ref.has_parent("bla") is True
+
+    # Add datatype explicitely:
+    r_ref = db.Record()
+    r_ref.add_parent("bla")
+    r_ref.add_property(name="a", value=42)
+
+    r = db.Record()
+    r.add_property(name="ref", value=r_ref, datatype="bla")
+
+    obj = convert_to_python_object(r)
+    assert obj.ref.a == 42
+    # Parent does not automatically lead to a datatype:
+    assert obj.get_property_metadata("ref").datatype is "bla"
+    assert obj.ref.has_parent("bla") is True
+
+    # Unresolved Reference:
+    r = db.Record()
+    r.add_property(name="ref", value=27, datatype="bla")
+
+    obj = convert_to_python_object(r)
+    # Parent does not automatically lead to a datatype:
+    assert obj.get_property_metadata("ref").datatype is "bla"
+    assert isinstance(obj.ref, CaosDBPythonUnresolvedReference)
+    assert obj.ref.id == 27
+
+
+def test_resolve_references():
+    r = db.Record()
+    r.add_property(name="ref", value=27, datatype="bla")
+    r.add_property(name="ref_false", value=27)  # this should be interpreted as integer property
+    obj = convert_to_python_object(r)
+
+    ref = db.Record(id=27)
+    ref.add_property(name="a", value=57)
+
+    unused_ref1 = db.Record(id=28)
+    unused_ref2 = db.Record(id=29)
+    unused_ref3 = db.Record(name="bla")
+
+    references = db.Container().extend([
+        unused_ref1, ref, unused_ref2, unused_ref3])
+
+    # Nothing is going to be resolved:
+    obj.resolve_references(False, db.Container())
+    assert isinstance(obj.ref, CaosDBPythonUnresolvedReference)
+    assert obj.ref.id == 27
+    assert obj.ref_false == 27
+
+    # deep == True does not help:
+    obj.resolve_references(True, db.Container())
+    assert isinstance(obj.ref, CaosDBPythonUnresolvedReference)
+    assert obj.ref.id == 27
+
+    # But adding the reference container will do:
+    obj.resolve_references(False, references)
+    assert not isinstance(obj.ref, CaosDBPythonUnresolvedReference)
+    assert isinstance(obj.ref, CaosDBPythonRecord)
+    assert obj.ref.id == 27
+    assert obj.ref.a == 57
+    # Datatypes will not automatically be set:
+    assert obj.ref.get_property_metadata("a").datatype is None
+
+    # Test deep resolve:
+    ref2 = db.Record(id=225)
+    ref2.add_property(name="c", value="test")
+    ref.add_property(name="ref", value=225, datatype="bla")
+
+    obj = convert_to_python_object(r)
+    assert isinstance(obj.ref, CaosDBPythonUnresolvedReference)
+    obj.resolve_references(False, references)
+    assert not isinstance(obj.ref, CaosDBPythonUnresolvedReference)
+    assert isinstance(obj.ref.ref, CaosDBPythonUnresolvedReference)
+    assert obj.ref.ref.id == 225
+
+    # Will not help, because ref2 is missing in container:
+    obj.resolve_references(True, references)
+    assert not isinstance(obj.ref, CaosDBPythonUnresolvedReference)
+    assert isinstance(obj.ref.ref, CaosDBPythonUnresolvedReference)
+    assert obj.ref.ref.id == 225
+
+    references.append(ref2)
+    obj.resolve_references(False, references)
+    assert not isinstance(obj.ref, CaosDBPythonUnresolvedReference)
+    assert isinstance(obj.ref.ref, CaosDBPythonUnresolvedReference)
+    assert obj.ref.ref.id == 225
+
+    obj.resolve_references(True, references)
+    assert not isinstance(obj.ref, CaosDBPythonUnresolvedReference)
+    assert not isinstance(obj.ref.ref, CaosDBPythonUnresolvedReference)
+    assert obj.ref.ref.c == "test"
+
+    # Test circular dependencies:
+    ref2.add_property(name="ref", value=27, datatype="bla")
+    obj = convert_to_python_object(r)
+    obj.resolve_references(True, references)
+    assert obj.ref.ref.ref == obj.ref
+
+
+def equal_entities(r1, r2):
+    res = compare_entities(r1, r2)
+    if len(res) != 2:
+        return False
+    for i in range(2):
+        if len(res[i]["parents"]) != 0 or len(res[i]["properties"]) != 0:
+            return False
+    return True
+
+
+def test_conversion_to_entity():
+    r = db.Record()
+    r.add_parent("bla")
+    r.add_property(name="a", value=42)
+    r.add_property(name="b", value="test")
+    obj = convert_to_python_object(r)
+    rconv = convert_to_entity(obj)
+    assert equal_entities(r, rconv)
+
+    # With a reference:
+    r_ref = db.Record()
+    r_ref.add_parent("bla")
+    r_ref.add_property(name="a", value=42)
+
+    r = db.Record()
+    r.add_property(name="ref", value=r_ref)
+    obj = convert_to_python_object(r)
+    rconv = convert_to_entity(obj)
+    assert (rconv.get_property("ref").value.get_property("a").value
+            == r.get_property("ref").value.get_property("a").value)
+    # TODO: add more tests here
+
+
+def test_base_properties():
+    r = db.Record(id=5, name="test", description="ok")
+    r.add_property(name="v", value=15, datatype=db.INTEGER, unit="kpx",
+                   importance="RECOMMENDED", description="description")
+    obj = convert_to_python_object(r)
+    assert obj.name == "test"
+    assert obj.id == 5
+    assert obj.description == "ok"
+    metadata = obj.get_property_metadata("v")
+    assert metadata.id is None
+    assert metadata.datatype == db.INTEGER
+    assert metadata.unit == "kpx"
+    assert metadata.importance == "RECOMMENDED"
+    assert metadata.description == "description"
+
+    rconv = convert_to_entity(obj)
+    assert rconv.name == "test"
+    assert rconv.id == 5
+    assert rconv.description == "ok"
+    prop = rconv.get_property("v")
+    assert prop.value == 15
+    assert prop.datatype == db.INTEGER
+    assert prop.unit == "kpx"
+    assert prop.description == "description"
+    assert rconv.get_importance("v") == "RECOMMENDED"
+
+
+def test_empty():
+    r = db.Record()
+    obj = convert_to_python_object(r)
+    assert isinstance(obj, CaosDBPythonRecord)
+    assert len(obj.get_properties()) == 0
+    assert len(obj.get_parents()) == 0
+
+    rconv = convert_to_entity(obj)
+    assert len(rconv.properties) == 0
+
+
+def test_wrong_entity_for_file():
+    r = db.Record()
+    r.path = "test.dat"
+    r.file = "/local/path/test.dat"
+    assert r.path == "test.dat"
+    assert r.file == "/local/path/test.dat"
+    with pytest.raises(RuntimeError):
+        obj = convert_to_python_object(r)
+
+
+def test_serialization():
+    r = db.Record(id=5, name="test", description="ok")
+    r.add_property(name="v", value=15, datatype=db.INTEGER, unit="kpx",
+                   importance="RECOMMENDED")
+
+    obj = convert_to_python_object(r)
+    text = str(obj)
+    teststrs = ["description: ok", "id: 5", "datatype: INTEGER",
+                "importance: RECOMMENDED", "unit: kpx", "name: test", "v: 15"]
+    for teststr in teststrs:
+        assert teststr in text
+
+    r = db.Record(description="ok")
+    r.add_property(name="v", value=15, datatype=db.INTEGER, unit="kpx",
+                   importance="RECOMMENDED")
+    obj = convert_to_python_object(r)
+    text = str(obj)
+    assert "name" not in text
+    assert "id" not in text
+
+
+def test_files():
+    # empty file:
+    r = db.File()
+    obj = convert_to_python_object(r)
+    print(type(obj))
+    assert isinstance(obj, CaosDBPythonFile)
+    assert len(obj.get_properties()) == 0
+    assert len(obj.get_parents()) == 0
+
+    rconv = convert_to_entity(obj)
+    assert len(rconv.properties) == 0
+
+    r.path = "test.dat"
+    r.file = "/local/path/test.dat"
+    obj = convert_to_python_object(r)
+    assert r.path == "test.dat"
+    assert r.file == "/local/path/test.dat"
+    assert isinstance(obj, CaosDBPythonFile)
+
+    assert obj.path == "test.dat"
+    assert obj.file == "/local/path/test.dat"
+
+    assert "path: test.dat" in str(obj)
+    assert "file: /local/path/test.dat" in str(obj)
+
+    # record with file property:
+    rec = db.Record()
+    rec.add_property(name="testfile", value=r)
+    assert rec.get_property("testfile").value.file == "/local/path/test.dat"
+    assert rec.get_property("testfile").value.path == "test.dat"
+
+    obj = convert_to_python_object(rec)
+    assert obj.testfile.file == "/local/path/test.dat"
+    assert obj.testfile.path == "test.dat"
+
+    rconv = convert_to_entity(obj)
+    assert rconv.get_property("testfile").value.file == "/local/path/test.dat"
+    assert rconv.get_property("testfile").value.path == "test.dat"
+
+    # record with file property as reference:
+    rec = db.Record()
+    rec.add_property(name="testfile", value=2, datatype=db.FILE)
+    obj = convert_to_python_object(rec)
+    assert type(obj.testfile) == CaosDBPythonUnresolvedReference
+    assert obj.testfile.id == 2
+    assert obj.get_property_metadata("testfile").datatype == db.FILE
+
+    # without resolving references:
+    rconv = convert_to_entity(obj)
+    p = rconv.get_property("testfile")
+    assert p.value == 2
+    assert p.datatype == db.FILE
+
+    # with previously resolved reference (should not work here, because id is missing):
+    obj.resolve_references(True, db.Container().extend(r))
+    rconv = convert_to_entity(obj)
+    p = rconv.get_property("testfile")
+    assert p.value == 2
+    assert p.datatype == db.FILE
+
+    # this time it must work:
+    r.id = 2
+    obj.resolve_references(True, db.Container().extend(r))
+    rconv = convert_to_entity(obj)
+    p = rconv.get_property("testfile")
+    assert type(p.value) == db.File
+    assert p.datatype == db.FILE
+    assert p.value.file == "/local/path/test.dat"
+    assert p.value.path == "test.dat"
+
+
+@pytest.mark.xfail
+def test_record_generator():
+    rt = db.RecordType(name="Simulation")
+    rt.add_property(name="a", datatype=db.INTEGER)
+    rt.add_property(name="b", datatype=db.DOUBLE)
+    rt.add_property(name="inputfile", datatype=db.FILE)
+
+    simrt = db.RecordType(name="SimOutput")
+    rt.add_property(name="outputfile", datatype="SimOutput")
+
+    obj = new_high_level_entity(
+        rt, "SUGGESTED", "", True)
+    print(obj)
+    assert False
+
+
+def test_list_types():
+    r = db.Record()
+    r.add_property(name="a", value=[1, 2, 4])
+
+    assert get_list_datatype(r.get_property("a").datatype) is None
+
+    obj = convert_to_python_object(r)
+    assert type(obj.a) == list
+    assert len(obj.a) == 3
+    assert 4 in obj.a
+    assert obj.get_property_metadata("a").datatype is None
+
+    conv = convert_to_entity(obj)
+    prop = r.get_property("a")
+    assert prop.value == [1, 2, 4]
+    assert prop.datatype is None
+
+    r.get_property("a").datatype = db.LIST(db.INTEGER)
+    assert r.get_property("a").datatype == "LIST<INTEGER>"
+    obj = convert_to_python_object(r)
+    assert type(obj.a) == list
+    assert len(obj.a) == 3
+    assert 4 in obj.a
+    assert obj.get_property_metadata("a").datatype == "LIST<INTEGER>"
+
+    conv = convert_to_entity(obj)
+    prop = r.get_property("a")
+    assert prop.value == [1, 2, 4]
+    assert obj.get_property_metadata("a").datatype == "LIST<INTEGER>"
+
+    # List of referenced objects:
+    r = db.Record()
+    r.add_property(name="a", value=[1, 2, 4], datatype="LIST<TestReference>")
+    obj = convert_to_python_object(r)
+    assert type(obj.a) == list
+    assert len(obj.a) == 3
+    assert obj.get_property_metadata("a").datatype == "LIST<TestReference>"
+    for i in range(3):
+        assert type(obj.a[i]) == CaosDBPythonUnresolvedReference
+    assert obj.a == [CaosDBPythonUnresolvedReference(id=i) for i in [1, 2, 4]]
+
+    # Try resolving:
+
+    # Should not work:
+    obj.resolve_references(False, db.Container())
+    assert type(obj.a) == list
+    assert len(obj.a) == 3
+    assert obj.get_property_metadata("a").datatype == "LIST<TestReference>"
+    for i in range(3):
+        assert type(obj.a[i]) == CaosDBPythonUnresolvedReference
+    assert obj.a == [CaosDBPythonUnresolvedReference(id=i) for i in [1, 2, 4]]
+
+    references = db.Container()
+    for i in [1, 2, 4]:
+        ref = db.Record(id=i)
+        ref.add_property(name="val", value=str(i) + " bla")
+        references.append(ref)
+
+    obj.resolve_references(False, references)
+    assert type(obj.a) == list
+    assert len(obj.a) == 3
+    assert obj.get_property_metadata("a").datatype == "LIST<TestReference>"
+    for i in range(3):
+        assert type(obj.a[i]) == CaosDBPythonRecord
+
+    assert obj.a[0].val == "1 bla"
+
+    # Conversion with embedded records:
+    r2 = db.Record()
+    r2.add_property(name="a", value=4)
+    r3 = db.Record()
+    r3.add_property(name="b", value=8)
+
+    r = db.Record()
+    r.add_property(name="a", value=[r2, r3])
+
+    obj = convert_to_python_object(r)
+    assert type(obj.a) == list
+    assert len(obj.a) == 2
+    assert obj.a[0].a == 4
+    assert obj.a[1].b == 8
+
+    # Serialization
+    text = str(obj)
+    text2 = str(convert_to_python_object(r2)).split("\n")
+    print(text)
+    # cut away first two characters in text
+    text = [line[4:] for line in text.split("\n")]
+    for line in text2:
+        assert line in text
+
+
+# Test utility functions:
+def test_type_conversion():
+    assert high_level_type_for_standard_type(db.Record()) == CaosDBPythonRecord
+    assert high_level_type_for_standard_type(db.Entity()) == CaosDBPythonEntity
+    assert standard_type_for_high_level_type(CaosDBPythonRecord()) == db.Record
+    assert standard_type_for_high_level_type(CaosDBPythonEntity()) == db.Entity
+    assert standard_type_for_high_level_type(CaosDBPythonFile(), True) == "File"
+    assert standard_type_for_high_level_type(CaosDBPythonRecord(), True) == "Record"
+    assert high_level_type_for_role("Record") == CaosDBPythonRecord
+    assert high_level_type_for_role("Entity") == CaosDBPythonEntity
+    assert high_level_type_for_role("File") == CaosDBPythonFile
+    with pytest.raises(RuntimeError, match="Unknown role."):
+        high_level_type_for_role("jkaldjfkaldsjf")
+
+    with pytest.raises(RuntimeError, match="Incompatible type."):
+        standard_type_for_high_level_type(42, True)
+
+    with pytest.raises(ValueError):
+        high_level_type_for_standard_type("ajsdkfjasfkj")
+
+    with pytest.raises(RuntimeError, match="Incompatible type."):
+        class IncompatibleType(db.Entity):
+            pass
+        high_level_type_for_standard_type(IncompatibleType())
+
+
+def test_deserialization():
+    r = db.Record(id=17, name="test")
+    r.add_parent("bla")
+    r.add_property(name="a", value=42)
+    r.add_property(name="b", value="test")
+
+    obj = convert_to_python_object(r)
+
+    serial = obj.serialize()
+    obj_des = CaosDBPythonEntity.deserialize(serial)
+
+    assert obj_des.name == "test"
+    assert obj_des.id == 17
+    assert obj_des.has_parent(CaosDBPythonUnresolvedParent(name="bla"))
+    print(obj)
+    print(obj_des)
+
+    # This test is very strict, and might fail if order in dictionary is not preserved:
+    assert obj.serialize() == obj_des.serialize()
+
+    f = db.File()
+    f.file = "bla.test"
+    f.path = "/test/n/bla.test"
+
+    obj = convert_to_python_object(f)
+
+    serial = obj.serialize()
+    obj_des = CaosDBPythonEntity.deserialize(serial)
+    assert obj_des.file == "bla.test"
+    assert obj_des.path == "/test/n/bla.test"
+
+    r = db.Record(id=17, name="test")
+    r.add_parent("bla")
+    r.add_property(name="a", value=42)
+    r.add_property(name="b", value="test")
+
+    ref = db.Record(id=28)
+    ref.add_parent("bla1")
+    ref.add_parent("bla2")
+    ref.add_property(name="c", value=5,
+                     unit="s", description="description missing")
+    r.add_property(name="ref", value=ref)
+
+    obj = convert_to_python_object(r)
+
+    serial = obj.serialize()
+    obj_des = CaosDBPythonEntity.deserialize(serial)
+    assert obj.serialize() == obj_des.serialize()
+
+
+@pytest.fixture
+def get_record_container():
+    record_xml = """
+<Entities>
+  <Record id="109">
+    <Version id="da669fce50554b2835c3826cf717d6a4532f02de" head="true">
+      <Predecessor id="68534369c5fd05e5bb1d37801a3dbc1532a8e094"/>
+    </Version>
+    <Parent id="103" name="Experiment" description="General type for all experiments in our lab"/>
+    <Property id="104" name="alpha" description="A fictitious measurement" datatype="DOUBLE" unit="km" importance="FIX" flag="inheritance:FIX">16.0</Property>
+    <Property id="107" name="date" datatype="DATETIME" importance="FIX" flag="inheritance:FIX">2022-03-16</Property>
+    <Property id="108" name="identifier" datatype="TEXT" importance="FIX" flag="inheritance:FIX">Demonstration</Property>
+    <Property id="111" name="sources" description="The elements of this lists are scientific activities that this scientific activity is based on." datatype="LIST&lt;ScientificActivity&gt;" importance="FIX" flag="inheritance:FIX">
+      <Value>109</Value>
+    </Property>
+  </Record>
+</Entities>"""
+
+    c = db.Container.from_xml(record_xml)
+    return c
+
+
+def test_recursion(get_record_container):
+    r = convert_to_python_object(get_record_container[0])
+    r.resolve_references(r, get_record_container)
+    assert r.id == 109
+    assert r.sources[0].id == 109
+    assert r.sources[0].sources[0].id == 109
+    assert "&id001" in str(r)
+    assert "*id001" in str(r)
+
+    d = r.serialize(True)
+    assert r.sources[0] == r.sources[0].sources[0]
+
+
+@pytest.mark.xfail
+def test_recursion_advanced(get_record_container):
+    # TODO:
+    # This currently fails, because resolve is done in a second step
+    # and therefore a new python object is created for the reference.
+    r = convert_to_python_object(get_record_container[0])
+    r.resolve_references(r, get_record_container)
+    d = r.serialize(True)
+    assert r == r.sources[0]
diff --git a/unittests/test_plantuml.py b/unittests/test_plantuml.py
new file mode 100644
index 0000000000000000000000000000000000000000..a507c36b2d3a4246205fc7507cb05119c575084c
--- /dev/null
+++ b/unittests/test_plantuml.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python3
+# encoding: utf-8
+#
+# ** header v3.0
+# This file is a part of the CaosDB Project.
+#
+# Copyright (C) 2022 Indiscale GmbH <info@indiscale.com>
+# Copyright (C) 2022 Henrik tom Wörden <h.tomwoerden@indiscale.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+# ** end header
+#
+
+"""
+test plantuml utility
+"""
+
+import tempfile
+import pytest
+import caosdb as db
+import shutil
+from caosdb.utils.plantuml import to_graphics
+
+
+@pytest.fixture
+def setup_n_teardown(autouse=True):
+
+    with tempfile.TemporaryDirectory() as td:
+        global output
+        output = td
+        yield
+
+
+@pytest.fixture
+def entities():
+    return [db.RecordType(name="TestRT1").add_property("testprop"),
+            db.RecordType(name="TestRT2").add_property("testprop2"),
+            db.Property("testprop")]
+
+
+@pytest.mark.skipif(shutil.which('plantuml') is None, reason="No plantuml found")
+def test_to_graphics1(entities, setup_n_teardown):
+    to_graphics(entities, "data_model", output_dirname=output)
+
+
+@pytest.mark.skipif(shutil.which('plantuml') is None, reason="No plantuml found")
+def test_to_graphics2(entities, setup_n_teardown):
+    to_graphics(entities, "data_model", output_dirname=output, formats=["tpng", "tsvg"],
+                add_properties=False, add_legend=False, style="salexan")
diff --git a/unittests/test_schema.py b/unittests/test_schema.py
index 1552179a3e43dacb3ecca705466bb7ff84d330cf..fc3f63a4cbaeadcac3c1cb9be2d861a0688fe4b0 100644
--- a/unittests/test_schema.py
+++ b/unittests/test_schema.py
@@ -2,7 +2,9 @@
 #
 # This file is a part of the CaosDB Project.
 #
+# Copyright (C) 2022 Indiscale GmbH <info@indiscale.com>
 # Copyright (C) 2021 Alexander Schlemmer
+# Copyright (C) 2022 Daniel Hornung <d.hornung@indiscale.com>
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as
@@ -31,15 +33,18 @@ from configparser import ConfigParser
 
 def test_config_files():
     for fn in glob(os.path.join(os.path.dirname(__file__), "test_configs", "*.ini")):
+        print(f"Testing {fn}.")
         c = ConfigParser()
         c.read(fn)
+        print(config_to_yaml(c))
         validate_yaml_schema(config_to_yaml(c))
 
 
 def test_broken_config_files():
     for fn in glob(os.path.join(os.path.dirname(__file__), "broken_configs", "*.ini")):
-        print(fn)
+        print(f"Testing {fn}.")
         with raises(ValidationError):
             c = ConfigParser()
             c.read(fn)
+            print(config_to_yaml(c))
             validate_yaml_schema(config_to_yaml(c))