diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b45dde5de1e4cafdbc02a15c4b775b763d5447bd..9f8c131968c050fb18001b5dc7c5468d0ed26dae 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -22,7 +22,7 @@
 
 variables:
   DEPLOY_REF: dev
-  CI_REGISTRY_IMAGE: $CI_REGISTRY/caosdb/caosdb-pylib/testenv:latest
+  CI_REGISTRY_IMAGE: $CI_REGISTRY/caosdb/src/caosdb-pylib/testenv:latest
   # When using dind, it's wise to use the overlayfs driver for
   # improved performance.
 
@@ -65,7 +65,7 @@ trigger_build:
   stage: deploy
   script:
     - /usr/bin/curl -X POST
-      -F token=$DEPLOY_TRIGGER_TOKEN
+      -F token=$CI_JOB_TOKEN
       -F "variables[F_BRANCH]=$CI_COMMIT_REF_NAME"
       -F "variables[PYLIB]=$CI_COMMIT_REF_NAME"
       -F "variables[TriggerdBy]=PYLIB"
@@ -92,15 +92,13 @@ build-testenv:
     - docker push $CI_REGISTRY_IMAGE
 
 # Build the sphinx documentation and make it ready for deployment by Gitlab Pages
-# documentation:
-#   stage: deploy
-
 # Special job for serving a static website. See https://docs.gitlab.com/ee/ci/yaml/README.html#pages
-pages:
+pages_prepare: &pages_prepare
+  tags: [ cached-dind ]
   stage: deploy
   only:
-      # TODO this should be for master only, once releases are more regularly
-    - dev
+    refs:
+      - /^release-.*$/i
   script:
     - echo "Deploying"
     - make doc
@@ -108,3 +106,8 @@ pages:
   artifacts:
     paths:
       - public
+pages:
+  <<: *pages_prepare
+  only:
+    refs:
+      - main
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fd2bbfd27503488cbb7b2e7aef30ff959f3f323d..38b470db9d36675ffcef0b7e1434a08c3be7f407 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,11 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added ###
 
-* Versioning support (experimental). The version db.Version class can
-  represents particular entity versions and also the complete history of an
-  entity.
-* Automated documentation builds: `make doc`
-
 ### Changed ###
 
 ### Deprecated ###
@@ -22,10 +17,91 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Fixed ###
 
-* deepcopy of `_Messages` objects
+* #53 Documentation of inheritance
+
+### Security ###
+
+## [0.5.2] - 2021-06-03 ##
+
+### Added ###
+
+* Entity State support (experimental, no StateModel support yet). See the
+  `caosdb.State` class for more information.
+* `etag` property for the `caosdb.Query` class. The etag allows to debug the
+  caching and to decide whether the server has changed between queries.
+* function `_read_config_files` to read `pycaosdb.ini` files from different paths.
+
+### Changed ###
+
+* Updated error-handling tutorial in documentation to reflect the new
+  error classes
+
+### Deprecated ###
+
+### Removed ###
+
+### Fixed ###
+* #45 - test_config_ini_via_envvar
 
 ### Security ###
 
+## [0.5.1] - 2021-02-12 ##
+
+### Fixed ###
+
+* #43 - Error with `execute_query` when server doesn't support query caching.
+
+## [0.5.0] - 2021-02-11 ##
+
+### Added ###
+
+* New exceptions `HTTPForbiddenException` and
+  `HTTPResourceNotFoundException` for HTTP 403 and 404 errors,
+  respectively.
+* `BadQueryError`, `EmptyUniqueQueryError`, and `QueryNotUniqueError`
+  for bad (unique) queries.
+* Added `cache` paramter to `execute_query` and `Query.execute` which indicates
+  whether server may use the cache for the query execution.
+* Added `cached` property to the `Query` class which indicates whether the
+  server used the cache for the execution of the last query.
+* Documentation moved from wiki to this repository and enhanced.
+
+### Changed ###
+
+* Renaming of `URITooLongException` to `HTTPURITooLongError`.
+* Raising of entity exceptions and transaction errors. Whenever any
+  transaction fails, a `TransactionError` is raised. If one ore more
+  entities caused that failure, corresponding entity errors are
+  attached as direct and indirect children of the
+  `TransactionError`. They can be accessed via the `get_errors`
+  (direct children) and `get_all_errors` (direct and indirect
+  children) methods; the causing entities are accessed by
+  `get_entities` and `get_all_entities`. The `has_error` method can be
+  used to check whether a `TransactionError` was caused by a specific
+  `EntityError`.
+* Unique queries will now result in `EmptyUniqueQueryError` or
+  `QueryNotUniqueError` if no or more than one possible candidate is
+  found, respectively.
+
+### Removed ###
+
+* Dynamic exception type `EntityMultiError`. 
+* `get_something` functions from all error object in `exceptions.py`
+* `AmbiguityException`
+
+## [0.4.1] - 2021-02-10 ##
+
+### Added ###
+
+* Versioning support (experimental). The version db.Version class can
+  represents particular entity versions and also the complete history of an
+  entity.
+* Automated documentation builds: `make doc`
+
+### Fixed ###
+
+* deepcopy of `_Messages` objects
+
 ## [0.4.0] - 2020-07-17##
 
 ### Added ###
@@ -79,7 +155,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 * import bugs in apiutils
 
-## [0.2.4] -- 2020-04-23
+## [0.2.4] - 2020-04-23
 
 ### Added
 
diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md
new file mode 100644
index 0000000000000000000000000000000000000000..04e783a7fa31b1d5c3a600a1009c8f040db1620d
--- /dev/null
+++ b/DEPENDENCIES.md
@@ -0,0 +1,6 @@
+* caosdb-server == 0.3
+* Python >= 3.5
+* pip >= 20.0.2
+
+
+Any other dependencies are being installed via pip
diff --git a/README.md b/README.md
index ef26a604905d5140ae9a775065002af35ffe2121..04b34cbc07c98e73740b13200ed83fe067af99d2 100644
--- a/README.md
+++ b/README.md
@@ -1,25 +1,53 @@
-# Welcome
+
+# README
+
+## Welcome
 
 This is the **CaosDB Python Client Library** repository and a part of the
 CaosDB project.
 
-# Setup
+## Setup
 
 Please read the [README_SETUP.md](README_SETUP.md) for instructions on how to
 setup this code.
 
 
-# Further Reading
+## Further Reading
+
+Please refer to the [official documentation](https://docs.indiscale.com/caosdb-pylib/) for more information.
+
+## Contributing
+
+Thank you very much to all contributers—[past, present](https://gitlab.com/caosdb/caosdb/-/blob/dev/HUMANS.md), and prospective ones.
 
-Please refer to the [official gitlab repository of the CaosDB
-project](https://gitlab.com/caosdb/caosdb) for more information.
+### Code of Conduct
 
-# License
+By participating, you are expected to uphold our [Code of Conduct](https://gitlab.com/caosdb/caosdb/-/blob/dev/CODE_OF_CONDUCT.md).
 
-Copyright (C) 2018 Research Group Biomedical Physics, Max Planck Institute for
-Dynamics and Self-Organization Göttingen.
-Copyright (C) 2020 Indiscale GmbH <info@indiscale.com>
+### How to Contribute
+
+* You found a bug, have a question, or want to request a feature? Please 
+[create an issue](https://gitlab.com/caosdb/caosdb-pylib/-/issues).
+* You want to contribute code?
+    * **Forking:** Please fork the repository and create a merge request in GitLab and choose this repository as
+      target. Make sure to select "Allow commits from members who can merge the target branch" under
+      Contribution when creating the merge request. This allows our team to work with you on your
+      request.
+    * **Code style:** This project adhers to the PEP8 recommendations, you can test your code style
+      using the `autopep8` tool (`autopep8 -i -r ./`).  Please write your doc strings following the
+      [NumpyDoc](https://numpydoc.readthedocs.io/en/latest/format.html) conventions.
+* If you have a suggestion for the [documentation](https://docs.indiscale.com/caosdb-pylib/),
+the preferred way is also a merge request as describe above (the documentation resides in `src/doc`).
+However, you can also create an issue for it. 
+* You can also contact us at **info (AT) caosdb.de** and join the
+  CaosDB community on
+  [#caosdb:matrix.org](https://matrix.to/#/!unwwlTfOznjEnMMXxf:matrix.org).
+
+## License
+
+* Copyright (C) 2018 Research Group Biomedical Physics, Max Planck Institute
+  for Dynamics and Self-Organization Göttingen.
+* Copyright (C) 2020-2021 Indiscale GmbH <info@indiscale.com>
 
 All files in this repository are licensed under a [GNU Affero General Public
 License](LICENCE.md) (version 3 or later).
-
diff --git a/README_SETUP.md b/README_SETUP.md
index 481182d02d4c25acc5c5ed9607a282b22e861448..9da548395073643c16539cef180c4d6412dd8d46 100644
--- a/README_SETUP.md
+++ b/README_SETUP.md
@@ -30,6 +30,23 @@ packages you will ever need out of the box.  If you prefer, you may also install
 After installation, open an Anaconda prompt from the Windows menu and continue in the [Generic
 installation](#generic-installation) section.
 
+#### iOS ####
+
+If there is no Python 3 installed yet, there are two main ways to obtain it: Either get the binary
+package from [python.org](https://www.python.org/downloads/) or, for advanced users, install via [Homebrew](https://brew.sh/).  After installation
+from python.org, it is recommended to also update the TLS certificates for Python (this requires
+administrator rights for your user):
+
+```sh
+# Replace this with your Python version number:
+cd /Applications/Python\ 3.9/
+
+# This needs administrator rights:
+sudo ./Install\ Certificates.command
+```
+
+After these steps, you may continue with the [Generic installation](#generic-installation).
+
 #### Generic installation ####
 
 To install PyCaosDB locally, use `pip3` (also called `pip` on some systems):
@@ -58,7 +75,7 @@ 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/master/examples/pycaosdb.ini) 
+[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.
 
 
@@ -67,7 +84,7 @@ Typically, you need to change at least the `url` and `username` fields as requir
 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 ##
+### Authentication ###
 
 The default configuration (that your are asked for your password when ever a connection is created
 can be changed by setting `password_method`:
@@ -92,7 +109,7 @@ The following illustrates the recommended options:
 #password_method=keyring
 ```
 
-### SSL Certificate ##
+### SSL Certificate ###
 In some cases (especially if you are testing CaosDB) you might need to supply 
 an SSL certificate to allow SSL encryption.
 
@@ -101,9 +118,9 @@ an SSL certificate to allow SSL encryption.
 cacert=/path/to/caosdb.ca.pem
 ```
 
-### Further Settings ##
+### 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/master/examples/pycaosdb.ini) in 
+[pycaosdb.ini file](https://gitlab.com/caosdb/caosdb-pylib/-/blob/main/examples/pycaosdb.ini) in 
 the examples folder of the source code.
 
 ## Try it out ##
@@ -121,21 +138,20 @@ Out[2]: Connection to CaosDB with 501 Records.
 Note: This setup will ask you for your password whenever a new connection is created. If you do not
 like this, check out the "Authentication" section in the [configuration documentation](configuration.md).
 
-Now would be a good time to continue with the [tutorials](tutorials.html).
+Now would be a good time to continue with the [tutorials](tutorials/index).
 
 ## Run Unit Tests
 tox
 
-## Code Formatting
-
-autopep8 -i -r ./
-
-## Documentation #
+## Documentation ##
 
 Build documentation in `build/` with `make doc`.
 
-### Requirements ##
+### Requirements ###
 
 - `sphinx`
 - `sphinx-autoapi`
 - `recommonmark`
+
+### Troubleshooting ###
+If the client is to be executed directly from the `/src` folder, an initial `.\setup.py install --user` must be called.
diff --git a/RELEASE_GUIDELINES.md b/RELEASE_GUIDELINES.md
index 02be5c1ad19f6a3a405fb08d62e23dab350ad445..e015b598117abdcd575cf17e2f095fec459a4c4c 100644
--- a/RELEASE_GUIDELINES.md
+++ b/RELEASE_GUIDELINES.md
@@ -16,21 +16,27 @@ guidelines of the CaosDB Project
 1. Create a release branch from the dev branch. This prevents further changes
    to the code base and a never ending release process. Naming: `release-<VERSION>`
 
-2. Check all general prerequisites.
+2. Update CHANGELOG.md
 
-3. Prepare [setup.py](./setup.py): Update the `MAJOR`, `MINOR`, `MICRO`, `PRE`
+3. Check all general prerequisites.
+
+4. Prepare [setup.py](./setup.py): Check the `MAJOR`, `MINOR`, `MICRO`, `PRE`
    variables and set `ISRELEASED` to `True`. Use the possibility to issue
    pre-release versions for testing.
 
-4. Merge the release branch into the master branch.
+5. Merge the release branch into the main branch.
 
-5. Tag the latest commit of the master branch with `v<VERSION>`.
+6. Tag the latest commit of the main branch with `v<VERSION>`.
 
-6. Delete the release branch.
+7. Delete the release branch.
 
-7. Remove possibly existing `./dist` directory with old release.
+8. Remove possibly existing `./dist` directory with old release.
 
-8. Publish the release by executing `./release.sh` with uploads the caosdb
+9. Publish the release by executing `./release.sh` with uploads the caosdb
    module to the Python Package Index [pypi.org](https://pypi.org).
 
-9. Merge the master branch back into the dev branch.
+10. Merge the main branch back into the dev branch.
+
+11. After the merge of main to dev, start a new development version by
+    setting `ISRELEASED` to `False` and by increasing at least the `MIRCO`
+    version in [setup.py](./setup.py)
diff --git a/examples/set_permissions.py b/examples/set_permissions.py
index 8b2b59f10ac033110af846eef0ed90356a09553d..8162b11bfefb41b1bcdbc74b8e314f99a61d1a4e 100755
--- a/examples/set_permissions.py
+++ b/examples/set_permissions.py
@@ -25,15 +25,13 @@
 
 As a result, only a specific user or group may access it.
 
-This script assumes that data similar to the demo server of IndiScale (at
-demo.indiscale.com) exists on the server specified in the pycaosdb.ini
-configuration.
+This script assumes that the user specified in the pycaosdb.ini
+configuration can create new entities.
 
 """
 
 import caosdb as db
 from caosdb import administration as admin
-import lxml
 
 
 def assert_user_and_role():
@@ -50,27 +48,27 @@ out : tuple
     """
     try:
         human_user = admin._retrieve_user("jane")
-        _activate_user("jane")
-    except db.EntityDoesNotExistError:
+        admin._update_user(name="jane", status="ACTIVE")
+    except db.HTTPResourceNotFoundError:
         human_user = admin._insert_user(
             "jane", password="Human_Rememberable_Password_1234", status="ACTIVE")
 
     try:
         alien_user = admin._retrieve_user("xaxys")
-        _activate_user("xaxys")
-    except db.EntityDoesNotExistError:
+        admin._update_user(name="xaxys", status="ACTIVE")
+    except db.HTTPResourceNotFoundError:
         alien_user = admin._insert_user("xaxys", password="4321_Syxax",
                                         status="ACTIVE")
 
     # At the moment, the return value is only "ok" for successful insertions.
     try:
         human_role = admin._retrieve_role("human")
-    except db.EntityDoesNotExistError:
+    except db.HTTPResourceNotFoundError:
         human_role = admin._insert_role("human", "An Earthling.")
 
     try:
         alien_role = admin._retrieve_role("alien")
-    except db.EntityDoesNotExistError:
+    except db.HTTPResourceNotFoundError:
         alien_role = admin._insert_role("alien", "An Extra-terrestrial.")
 
     admin._set_roles("jane", ["human"])
@@ -80,24 +78,6 @@ out : tuple
             ("xaxys", list(admin._get_roles("xaxys"))))
 
 
-def _activate_user(user):
-    """Set the user state to "ACTIVE" if necessary.
-
-Parameters
-----------
-user : str
-    The user to activate.
-
-Returns
--------
-None
-
-    """
-    user_xml = lxml.etree.fromstring(admin._retrieve_user(user))
-    if user_xml.xpath("User")[0].attrib["status"] != "ACTIVE":
-        admin._update_user(user, status="ACTIVE")
-
-
 def get_entities(count=1):
     """Retrieve one or more entities.
 
@@ -111,9 +91,11 @@ Returns
 out : Container
     A container of retrieved entities, the length is given by the parameter count.
     """
-    cont = db.execute_query("FIND RECORD Guitar", flags={"P": "0L{n}".format(n=count)})
+    cont = db.execute_query("FIND RECORD 'Human Food'", flags={
+                            "P": "0L{n}".format(n=count)})
     if len(cont) != count:
-        raise db.CaosDBException(msg="Incorrect number of entitities returned.")
+        raise db.CaosDBException(
+            msg="Incorrect number of entitities returned.")
     return cont
 
 
@@ -138,7 +120,8 @@ general : bool, optional
 
     # Set general permissions
     if general:
-        grant = admin.PermissionRule(action="grant", permission="RETRIEVE:OWNER")
+        grant = admin.PermissionRule(
+            action="grant", permission="RETRIEVE:OWNER")
         deny = admin.PermissionRule(action="deny", permission="RETRIEVE:FILE")
 
         admin._set_permissions(role=role_grant, permission_rules=[grant])
@@ -189,9 +172,12 @@ None
         for ent in cont:
             ent.retrieve()
         print("Successfully retrieved all entities.")
-    except db.AuthorizationException:
-        print(ent)
-        print("Could not retrieve this entity although it should have been possible!")
+    except db.TransactionError as te:
+        if te.has_error(db.AuthorizationError):
+            print(ent)
+            print("Could not retrieve this entity although it should have been possible!")
+        else:
+            raise te
 
     # Switch to user without permissions
     db.configure_connection(username=denied_user[0], password=denied_user[1],
@@ -206,23 +192,45 @@ None
             denied_all = False
             print(ent)
             print("Could retrieve this entity although it should not have been possible!")
-        except db.AuthorizationException:
-            pass
+        except db.TransactionError as te:
+            # Only do something if an error wasn't caused by an
+            # AuthorizationError
+            if not te.has_error(db.AuthorizationError):
+                raise te
     if denied_all:
         print("Retrieval of all entities was successfully denied.")
 
 
+def create_test_entities():
+    """Create some test entities.
+    After calling this function, there will be a RecordType "Human Food" with the corresponding Records
+    "Bread", "Tomatoes", and "Twinkies" inserted in the database.
+    """
+    rt = db.RecordType(
+        name="Human Food", description="Food that can be eaten only by humans").insert()
+    food = ("Bread", "Tomatoes", "Twinkies")
+
+    cont = db.Container()
+    for i in range(len(food)):
+        rec = db.Record(food[i])
+        rec.add_parent(name="Human Food")
+        cont.append(rec)
+
+    cont.insert()
+
+
 def main():
     """The main function of this script."""
 
-    db.connection.connection.get_connection()._login()
-
+    """Create some test entities"""
+    create_test_entities()
+    """Create new users"""
     human, alien = assert_user_and_role()
-
-    # public, private, undefined entities
+    """Load the newly created entities."""
     entities = get_entities(count=3)
-
+    """Set permission for the entities (only humans are allowed to eat human food)"""
     set_permission(human[1][0], alien[1][0], entities)
+    """Test the permissions"""
     test_permission((human[0], "Human_Rememberable_Password_1234"),
                     (alien[0], "4321_Syxax"), entities)
 
diff --git a/release.sh b/release.sh
index a5f3654291743e5ac22fd3b938719e294b3a1e90..1af097f014de6cd9eb3d3e8ba5da34aea0fe1671 100755
--- a/release.sh
+++ b/release.sh
@@ -1,4 +1,4 @@
 #!/bin/bash
-rm -r dist/ build/ .eggs/
+rm -rf dist/ build/ .eggs/
 python setup.py sdist bdist_wheel
 python -m twine upload -s dist/*
diff --git a/setup.py b/setup.py
index 4ab2d7669cc4f6d3f4873c0265da6c8cf06932ba..e1d39458ea8d1b0b17ea12a82ebd7133b27b045a 100755
--- a/setup.py
+++ b/setup.py
@@ -46,8 +46,8 @@ from setuptools import find_packages, setup
 ########################################################################
 
 MAJOR = 0
-MINOR = 4
-MICRO = 1
+MINOR = 5
+MICRO = 3
 PRE = ""  # e.g. rc0, alpha.1, 0.beta-23
 ISRELEASED = False
 
diff --git a/src/caosdb/__init__.py b/src/caosdb/__init__.py
index b83442f1bdf0f2b111a66d63e4549f68b6bedb69..7e06885fe495c1e8c4ccc99b7d0c0f8ff8c34b5b 100644
--- a/src/caosdb/__init__.py
+++ b/src/caosdb/__init__.py
@@ -37,7 +37,8 @@ from os.path import expanduser, join
 import caosdb.apiutils
 from caosdb.common import administration
 from caosdb.common.datatype import (BOOLEAN, DATETIME, DOUBLE, FILE, INTEGER,
-                                    REFERENCE, TEXT, LIST)
+                                    LIST, REFERENCE, TEXT)
+from caosdb.common.state import State, Transition
 # Import of the basic  API classes:
 from caosdb.common.models import (ACL, ALL, FIX, NONE, OBLIGATORY, RECOMMENDED,
                                   SUGGESTED, Container, DropOffBox, Entity,
@@ -45,15 +46,13 @@ from caosdb.common.models import (ACL, ALL, FIX, NONE, OBLIGATORY, RECOMMENDED,
                                   Query, QueryTemplate, Record, RecordType,
                                   delete, execute_query, get_global_acl,
                                   get_known_permissions, raise_errors)
-from caosdb.configuration import configure, get_config
+from caosdb.configuration import _read_config_files, configure, get_config
 from caosdb.connection.connection import configure_connection, get_connection
 from caosdb.exceptions import *
-from caosdb.version import version as __version__
+try:
+    from caosdb.version import version as __version__
+except ModuleNotFoundError:
+    version = "uninstalled"
+    __version__ = version
 
-# read configuration these files
-
-if "PYCAOSDBINI" in environ:
-    configure(expanduser(environ["PYCAOSDBINI"]))
-else:
-    configure(expanduser('~/.pycaosdb.ini'))
-configure(join(getcwd(), "pycaosdb.ini"))
+_read_config_files()
diff --git a/src/caosdb/apiutils.py b/src/caosdb/apiutils.py
index bd279fcfe5c394b9b5cff787169cff5b9f2d3031..73074efc3057e0548c5abfd56ef3cf1ac9e9bf47 100644
--- a/src/caosdb/apiutils.py
+++ b/src/caosdb/apiutils.py
@@ -34,10 +34,10 @@ from collections.abc import Iterable
 from subprocess import call
 
 from caosdb.common.datatype import (BOOLEAN, DATETIME, DOUBLE, FILE, INTEGER,
-                                    REFERENCE, TEXT)
+                                    REFERENCE, TEXT, is_reference)
 from caosdb.common.models import (Container, Entity, File, Property, Query,
                                   Record, RecordType, get_config,
-                                  execute_query, is_reference)
+                                  execute_query)
 
 
 def new_record(record_type, name=None, description=None,
@@ -244,6 +244,8 @@ class CaosDBPythonEntity(object):
             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)
 
diff --git a/src/caosdb/common/administration.py b/src/caosdb/common/administration.py
index 7997088a102e86cbf2daeb42a996a1b6ae57f446..e7ba94182d7a4d8b60c6400cd1d804f62f7bf03c 100644
--- a/src/caosdb/common/administration.py
+++ b/src/caosdb/common/administration.py
@@ -30,8 +30,12 @@ from lxml import etree
 
 from caosdb.common.utils import xml2str
 from caosdb.connection.connection import get_connection
-from caosdb.exceptions import (AuthorizationException, ClientErrorException,
-                               EntityDoesNotExistError)
+from caosdb.exceptions import (HTTPClientError,
+                               HTTPForbiddenError,
+                               HTTPResourceNotFoundError,
+                               EntityDoesNotExistError,
+                               ServerConfigurationException,
+                               )
 
 
 def set_server_property(key, value):
@@ -52,9 +56,11 @@ def set_server_property(key, value):
     None
     """
     con = get_connection()
-
-    con._form_data_request(method="POST", path="_server_properties",
-                           params={key: value}).read()
+    try:
+        con._form_data_request(method="POST", path="_server_properties",
+                               params={key: value}).read()
+    except EntityDoesNotExistError:
+        raise ServerConfigurationException("Debug mode in server is probably disabled.") from None
 
 
 def get_server_properties():
@@ -68,7 +74,11 @@ def get_server_properties():
         The server properties.
     """
     con = get_connection()
-    body = con._http_request(method="GET", path="_server_properties").response
+    try:
+        body = con._http_request(method="GET", path="_server_properties").response
+    except EntityDoesNotExistError:
+        raise ServerConfigurationException("Debug mode in server is probably disabled.") from None
+
     xml = etree.parse(body)
     props = dict()
 
@@ -106,10 +116,10 @@ def _retrieve_user(name, realm=None, **kwargs):
     con = get_connection()
     try:
         return con._http_request(method="GET", path="User/" + (realm + "/" + name if realm is not None else name), **kwargs).read()
-    except AuthorizationException as e:
+    except HTTPForbiddenError as e:
         e.msg = "You are not permitted to retrieve this user."
         raise
-    except EntityDoesNotExistError as e:
+    except HTTPResourceNotFoundError as e:
         e.msg = "User does not exist."
         raise
 
@@ -118,10 +128,10 @@ def _delete_user(name, **kwargs):
     con = get_connection()
     try:
         return con._http_request(method="DELETE", path="User/" + name, **kwargs).read()
-    except AuthorizationException as e:
+    except HTTPForbiddenError as e:
         e.msg = "You are not permitted to delete this user."
         raise
-    except EntityDoesNotExistError as e:
+    except HTTPResourceNotFoundError as e:
         e.msg = "User does not exist."
         raise
 
@@ -144,13 +154,13 @@ def _update_user(name, realm=None, password=None, status=None,
         params["entity"] = str(entity)
     try:
         return con.put_form_data(entity_uri_segment="User/" + (realm + "/" + name if realm is not None else name), params=params, **kwargs).read()
-    except EntityDoesNotExistError as e:
+    except HTTPResourceNotFoundError as e:
         e.msg = "User does not exist."
         raise
-    except AuthorizationException as e:
+    except HTTPForbiddenError as e:
         e.msg = "You are not permitted to update this user."
         raise
-    except ClientErrorException as e:
+    except HTTPClientError as e:
         if e.status == 409:
             e.msg = "Entity does not exist."
         raise
@@ -173,10 +183,10 @@ def _insert_user(name, password=None, status=None, email=None, entity=None, **kw
         params["entity"] = entity
     try:
         return con.post_form_data(entity_uri_segment="User", params=params, **kwargs).read()
-    except AuthorizationException as e:
+    except HTTPForbiddenError as e:
         e.msg = "You are not permitted to insert a new user."
         raise e
-    except ClientErrorException as e:
+    except HTTPClientError as e:
         if e.status == 409:
             e.msg = "User name is already in use."
 
@@ -189,10 +199,10 @@ def _insert_role(name, description, **kwargs):
     con = get_connection()
     try:
         return con.post_form_data(entity_uri_segment="Role", params={"role_name": name, "role_description": description}, **kwargs).read()
-    except AuthorizationException as e:
+    except HTTPForbiddenError as e:
         e.msg = "You are not permitted to insert a new role."
         raise
-    except ClientErrorException as e:
+    except HTTPClientError as e:
         if e.status == 409:
             e.msg = "Role name is already in use. Choose a different name."
         raise
@@ -202,10 +212,10 @@ def _update_role(name, description, **kwargs):
     con = get_connection()
     try:
         return con.put_form_data(entity_uri_segment="Role/" + name, params={"role_description": description}, **kwargs).read()
-    except AuthorizationException as e:
+    except HTTPForbiddenError as e:
         e.msg = "You are not permitted to update this role."
         raise
-    except EntityDoesNotExistError as e:
+    except HTTPResourceNotFoundError as e:
         e.msg = "Role does not exist."
         raise
 
@@ -214,10 +224,10 @@ def _retrieve_role(name, **kwargs):
     con = get_connection()
     try:
         return con._http_request(method="GET", path="Role/" + name, **kwargs).read()
-    except AuthorizationException as e:
+    except HTTPForbiddenError as e:
         e.msg = "You are not permitted to retrieve this role."
         raise
-    except EntityDoesNotExistError as e:
+    except HTTPResourceNotFoundError as e:
         e.msg = "Role does not exist."
         raise
 
@@ -226,10 +236,10 @@ def _delete_role(name, **kwargs):
     con = get_connection()
     try:
         return con._http_request(method="DELETE", path="Role/" + name, **kwargs).read()
-    except AuthorizationException as e:
+    except HTTPForbiddenError as e:
         e.msg = "You are not permitted to delete this role."
         raise
-    except EntityDoesNotExistError as e:
+    except HTTPResourceNotFoundError as e:
         e.msg = "Role does not exist."
         raise
 
@@ -243,14 +253,15 @@ def _set_roles(username, roles, realm=None, **kwargs):
     body = xml2str(xml)
     con = get_connection()
     try:
-        body = con._http_request(method="PUT", path="UserRoles/" + (realm + "/" + username if realm is not None else username), body=body, **kwargs).read()
-    except AuthorizationException as e:
+        body = con._http_request(method="PUT", path="UserRoles/" + (realm + "/" +
+                                                                    username if realm is not None else username), body=body, **kwargs).read()
+    except HTTPForbiddenError as e:
         e.msg = "You are not permitted to set this user's roles."
         raise
-    except EntityDoesNotExistError as e:
+    except HTTPResourceNotFoundError as e:
         e.msg = "User does not exist."
         raise
-    except ClientErrorException as e:
+    except HTTPClientError as e:
         if e.status == 409:
             e.msg = "Role does not exist."
         raise
@@ -266,11 +277,12 @@ def _set_roles(username, roles, realm=None, **kwargs):
 def _get_roles(username, realm=None, **kwargs):
     con = get_connection()
     try:
-        body = con._http_request(method="GET", path="UserRoles/" + (realm + "/" + username if realm is not None else username), **kwargs).read()
-    except AuthorizationException as e:
+        body = con._http_request(method="GET", path="UserRoles/" + (
+            realm + "/" + username if realm is not None else username), **kwargs).read()
+    except HTTPForbiddenError as e:
         e.msg = "You are not permitted to retrieve this user's roles."
         raise
-    except EntityDoesNotExistError as e:
+    except HTTPResourceNotFoundError as e:
         e.msg = "User does not exist."
         raise
     ret = set()
@@ -310,10 +322,10 @@ Returns
     con = get_connection()
     try:
         return con._http_request(method="PUT", path="PermissionRules/" + role, body=body, **kwargs).read()
-    except AuthorizationException as e:
+    except HTTPForbiddenError as e:
         e.msg = "You are not permitted to set this role's permissions."
         raise
-    except EntityDoesNotExistError as e:
+    except HTTPResourceNotFoundError as e:
         e.msg = "Role does not exist."
         raise
 
@@ -322,10 +334,10 @@ def _get_permissions(role, **kwargs):
     con = get_connection()
     try:
         return PermissionRule._parse_body(con._http_request(method="GET", path="PermissionRules/" + role, **kwargs).read())
-    except AuthorizationException as e:
+    except HTTPForbiddenError as e:
         e.msg = "You are not permitted to retrieve this role's permissions."
         raise
-    except EntityDoesNotExistError as e:
+    except HTTPResourceNotFoundError as e:
         e.msg = "Role does not exist."
         raise
 
diff --git a/src/caosdb/common/datatype.py b/src/caosdb/common/datatype.py
index 246485c3957462fc98ac83f9d904413ad7518302..eb8c1e4e0088f1924940a104ec3916b9d5d40f99 100644
--- a/src/caosdb/common/datatype.py
+++ b/src/caosdb/common/datatype.py
@@ -25,7 +25,7 @@
 
 import re
 
-from ..exceptions import AmbiguityException, EntityDoesNotExistError
+from ..exceptions import EmptyUniqueQueryError, QueryNotUniqueError
 
 DOUBLE = "DOUBLE"
 REFERENCE = "REFERENCE"
@@ -91,9 +91,9 @@ def get_id_of_datatype(datatype):
 
     Raises
     ------
-    AmbiguityException
+    QueryNotUniqueError
         If there are more than one entities with the same name as the datatype.
-    EntityDoesNotExistError
+    EmptyUniqueQueryError
         If there is no entity with the name of the datatype.
     """
     from caosdb import execute_query
@@ -107,11 +107,11 @@ def get_id_of_datatype(datatype):
     res = [el for el in res if el.name.lower() == datatype.lower()]
 
     if len(res) > 1:
-        raise AmbiguityException(
+        raise QueryNotUniqueError(
             "Name {} did not lead to unique result; Missing "
             "implementation".format(datatype))
     elif len(res) != 1:
-        raise EntityDoesNotExistError(
+        raise EmptyUniqueQueryError(
             "No RecordType named {}".format(datatype))
 
     return res[0].id
diff --git a/src/caosdb/common/models.py b/src/caosdb/common/models.py
index a5adf6af68b72663c0fe8cd6ab58c39cdcea5d27..6ec49df2722170805fb6230753f36503870a8821 100644
--- a/src/caosdb/common/models.py
+++ b/src/caosdb/common/models.py
@@ -5,8 +5,9 @@
 #
 # Copyright (C) 2018 Research Group Biomedical Physics,
 # Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+# Copyright (C) 2020 Indiscale GmbH <info@indiscale.com>
+# Copyright (C) 2020 Florian Spreckelsen <f.spreckelsen@indiscale.com>
 # Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
-# Copyright (C) 2020 IndiScale GmbH <info@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
@@ -29,7 +30,6 @@ from __future__ import print_function, unicode_literals
 
 import re
 import sys
-import traceback
 from builtins import str
 from functools import cmp_to_key
 from hashlib import sha512
@@ -40,24 +40,28 @@ from sys import hexversion
 from tempfile import NamedTemporaryFile
 from warnings import warn
 
-from caosdb.common.datatype import (BOOLEAN, DATETIME, DOUBLE, FILE, INTEGER,
-                                    LIST, REFERENCE, TEXT, is_reference)
+from caosdb.common.datatype import (BOOLEAN, DATETIME, DOUBLE, INTEGER, TEXT)
 from caosdb.common.versioning import Version
+from caosdb.common.state import State
 from caosdb.common.utils import uuid, xml2str
 from caosdb.configuration import get_config
 from caosdb.connection.connection import get_connection
 from caosdb.connection.encode import MultipartParam, multipart_encode
-from caosdb.exceptions import (AmbiguityException, AuthorizationException,
-                               CaosDBException, ConnectionException,
-                               ConsistencyError, EntityDoesNotExistError,
-                               EntityError, EntityHasNoDatatypeError,
-                               TransactionError, UniqueNamesError,
+from caosdb.exceptions import (AmbiguousEntityError,
+                               AuthorizationError,
+                               CaosDBException, CaosDBConnectionError,
+                               ConsistencyError,
+                               EmptyUniqueQueryError,
+                               EntityDoesNotExistError, EntityError,
+                               EntityHasNoDatatypeError,
+                               MismatchingEntitiesError,
+                               QueryNotUniqueError, TransactionError,
+                               UniqueNamesError,
                                UnqualifiedParentsError,
-                               UnqualifiedPropertiesError, URITooLongException)
+                               UnqualifiedPropertiesError,
+                               HTTPURITooLongError)
 from lxml import etree
 
-from .datatype import is_reference
-
 _ENTITY_URI_SEGMENT = "Entity"
 
 # importances/inheritance
@@ -109,6 +113,7 @@ class Entity(object):
         self.name = name
         self.description = description
         self.id = id
+        self.state = None
 
     @property
     def version(self):
@@ -426,14 +431,39 @@ class Entity(object):
     def add_parent(self, parent=None, **kwargs):  # @ReservedAssignment
         """Add a parent to this entity.
 
-        The first parameter is meant to identify the parent entity. So the method expects an instance of
-        Entity, an integer or a string here. Even though, by means of the **kwargs parameter you may pass
-        more parameters to this method. Accepted keywords are: id, name, inheritance. Any other keyword is
-        ignored right now but this may change in the future.
+        Parameters
+        ----------
+        parent : Entity or int or str or None
+            The parent entity, either specified by the Entity object
+            itself, or its id or its name. Default is None.
+        **kwargs : dict, optional
+            Additional keyword arguments for specifying the parent by
+            name or id, and for specifying the mode of inheritance.
+
+            id : int
+                Integer id of the parent entity. Ignored if `parent`
+                is not None.
+            name : str
+                Name of the parent entity. Ignored if `parent is not
+                none`.
+            inheritance : str
+                One of ``obligatory``, ``recommended``, ``suggested``, or ``fix``. Specifies the
+                minimum importance which parent properties need to have to be inherited by this
+                entity. If no `inheritance` is given, no properties will be inherited by the child.
+                This parameter is case-insensitive.
+
+                Note that the behaviour is currently not yet specified when assigning parents to
+                Records, it only works for inheritance of RecordTypes (and Properties).
+
+                For more information, it is recommended to look into the
+                :ref:`data insertion tutorial<tutorial-inheritance-properties>`.
+
+        Raises
+        ------
+        UserWarning
+            If neither a `parent` parameter, nor the `id`, nor `name`
+            parameter is passed to this method.
 
-        @param parent: An entity, an id or a name.
-        @param **kwargs: Accepted keywords: id, name, inheritance.
-        @raise UserWarning: If neither a 'parent' parameter, nor the 'id', nor 'name' parameter is passed to this method.
         """
         name = (kwargs['name'] if 'name' in kwargs else None)
         pid = (kwargs['id'] if 'id' in kwargs else None)
@@ -637,7 +667,6 @@ class Entity(object):
         if not isinstance(selector, (tuple, list)):
             selector = [selector]
 
-        val = None
         ref = self
 
         # there are some special selectors which can be applied to the
@@ -757,15 +786,14 @@ class Entity(object):
 
         return ret
 
-    def get_errors_deep(self, roots=[]):
+    def get_errors_deep(self, roots=None):
         """Get all error messages of this entity and all sub-entities /
-        parents.
-
-        / properties.
+        parents / properties.
 
         @return A list of tuples. Tuple index 0 contains the error message
                 and tuple index 1 contains the tree.
         """
+        roots = [] if roots is None else roots
         result_list = list()
         ret_self = self.get_errors()
         result_list.extend([
@@ -806,8 +834,7 @@ class Entity(object):
             xml = etree.Element("Entity")
         assert isinstance(xml, etree._Element)
 
-        ''' unwrap wrapped entity '''
-
+        # unwrap wrapped entity
         if self._wrapped_entity is not None:
             xml = self._wrapped_entity.to_xml(xml, add_properties)
 
@@ -854,6 +881,8 @@ class Entity(object):
                     xml.append(v_elem)
             elif self.value == "":
                 xml.append(etree.Element("EmptyString"))
+            elif str(self.value) == "nan":
+                xml.text = "NaN"
             else:
                 xml.text = str(self.value)
 
@@ -907,6 +936,9 @@ class Entity(object):
         if self.acl is not None:
             xml.append(self.acl.to_xml())
 
+        if self.state is not None:
+            xml.append(self.state.to_xml())
+
         return xml
 
     @staticmethod
@@ -951,6 +983,8 @@ class Entity(object):
                 entity.add_message(child)
             elif isinstance(child, Version):
                 entity.version = child
+            elif isinstance(child, State):
+                entity.state = child
             elif child is None or hasattr(child, "encode"):
                 vals.append(child)
             elif isinstance(child, Entity):
@@ -963,7 +997,7 @@ class Entity(object):
         # add VALUE
         value = None
 
-        if len(vals):
+        if vals:
             # The value[s] have been inside a <Value> tag.
             value = vals
         elif elem.text is not None and elem.text.strip() != "":
@@ -988,11 +1022,20 @@ class Entity(object):
         if self.id is None:
             c = Container().retrieve(query=self.name, sync=False)
 
-            if len(c == 1):
+            if len(c) == 1:
                 e = c[0]
+            elif len(c) == 0:
+                ee = EntityDoesNotExistError(
+                    "The entity to be updated does not exist on the server.",
+                    entity=self
+                )
+                raise TransactionError(ee)
             else:
-                raise AmbiguityException(
-                    "Could not determine the desired Entity which is to be updated by its name.")
+                ae = AmbiguousEntityError(
+                    "Could not determine the desired Entity which is to be updated by its name.",
+                    entity=self
+                )
+                raise TransactionError(ae)
         else:
             e = Container().retrieve(query=self.id, sync=False)[0]
         e.acl = ACL(self.acl.to_xml())
@@ -1031,13 +1074,12 @@ class Entity(object):
 
             if len(c) == 1:
                 c[0].messages.extend(c.messages)
-
                 return c[0]
-            else:
-                raise AmbiguityException("This retrieval was not unique!!!")
-        else:
-            return Container().append(self).retrieve(
-                unique=unique, raise_exception_on_error=raise_exception_on_error, flags=flags)
+
+            raise QueryNotUniqueError("This retrieval was not unique!!!")
+
+        return Container().append(self).retrieve(
+            unique=unique, raise_exception_on_error=raise_exception_on_error, flags=flags)
 
     def insert(self, raise_exception_on_error=True, unique=True,
                sync=True, strict=False, flags=None):
@@ -1070,7 +1112,7 @@ class Entity(object):
             flags=flags)[0]
 
     def update(self, strict=False, raise_exception_on_error=True,
-               unique=True, flags=None):
+               unique=True, flags=None, sync=True):
         """Update this entity.
 
         There are two possible work-flows to perform this update:
@@ -1104,6 +1146,7 @@ class Entity(object):
 
         return Container().append(self).update(
             strict=strict,
+            sync=sync,
             raise_exception_on_error=raise_exception_on_error,
             unique=unique,
             flags=flags)[0]
@@ -1239,9 +1282,10 @@ class QueryTemplate():
         self.is_valid = lambda: False
         self.is_deleted = lambda: False
         self.version = None
+        self.state = None
 
-    def retrieve(self, strict=True, raise_exception_on_error=True,
-                 unique=True, sync=True, flags=None):
+    def retrieve(self, raise_exception_on_error=True, unique=True, sync=True,
+                 flags=None):
 
         return Container().append(self).retrieve(
             raise_exception_on_error=raise_exception_on_error,
@@ -1386,8 +1430,7 @@ class Parent(Entity):
         if xml is None:
             xml = etree.Element("Parent")
 
-        return super(Parent, self).to_xml(
-            xml=xml, add_properties=add_properties)
+        return super().to_xml(xml=xml, add_properties=add_properties)
 
 
 class _EntityWrapper(object):
@@ -1417,6 +1460,23 @@ class Property(Entity):
             property=property, value=value, **copy_kwargs)
 
     def add_parent(self, parent=None, **kwargs):
+        """Add a parent Entity to this Property.
+
+        Parameters
+        ----------
+        parent : Entity or int or str or None, optional
+            The parent entity
+        **kwargs : dict, optional
+            Additional keyword arguments specifying the parent Entity
+            by id or name, and specifying the inheritance level. See
+            :py:meth:`Entity.add_parent` for more information. Note
+            that by default, `inheritance` is set to ``fix``.
+
+        See Also
+        --------
+        Entity.add_parent
+
+        """
         copy_kwargs = kwargs.copy()
 
         if 'inheritance' not in copy_kwargs:
@@ -1452,7 +1512,7 @@ class Message(object):
         if xml is None:
             xml = etree.Element(str(self.type))
 
-        if self.code:
+        if self.code is not None:
             xml.set("code", str(self.code))
 
         if self.description:
@@ -1468,7 +1528,7 @@ class Message(object):
 
     def __eq__(self, obj):
         if isinstance(obj, Message):
-            return self.type == obj.type and self.code == obj.codes
+            return self.type == obj.type and self.code == obj.code
 
         return False
 
@@ -1491,17 +1551,35 @@ class RecordType(Entity):
             # set default importance
             copy_kwargs['inheritance'] = FIX
 
-        return super(RecordType, self).add_property(
-            property=property, value=value, **copy_kwargs)
+        return super().add_property(property=property, value=value,
+                                    **copy_kwargs)
 
     def add_parent(self, parent=None, **kwargs):
+        """Add a parent to this RecordType
+
+        Parameters
+        ----------
+        parent : Entity or int or str or None, optional
+            The parent entity, either specified by the Entity object
+            itself, or its id or its name. Default is None.
+        **kwargs : dict, optional
+            Additional keyword arguments specifying the parent Entity by id or
+            name, and specifying the inheritance level. See
+            :py:meth:`Entity.add_parent` for more information. Note
+            that by default, `inheritance` is set to ``obligatory``.
+
+        See Also
+        --------
+        Entity.add_parent
+
+        """
         copy_kwargs = kwargs.copy()
 
         if 'inheritance' not in copy_kwargs:
             # set default importance
             copy_kwargs['inheritance'] = OBLIGATORY
 
-        return super(RecordType, self).add_parent(parent=parent, **copy_kwargs)
+        return super().add_parent(parent=parent, **copy_kwargs)
 
     def __init__(self, name=None, id=None, description=None, datatype=None):  # @ReservedAssignment
         Entity.__init__(self, name=name, id=id, description=description,
@@ -1529,7 +1607,7 @@ class Record(Entity):
             # set default importance
             copy_kwargs['inheritance'] = FIX
 
-        return super(Record, self).add_property(
+        return super().add_property(
             property=property, value=value, **copy_kwargs)
 
     def __init__(self, name=None, id=None, description=None):  # @ReservedAssignment
@@ -1693,7 +1771,7 @@ class File(Record):
             # set default importance
             copy_kwargs['inheritance'] = FIX
 
-        return super(File, self).add_property(
+        return super().add_property(
             property=property, value=value, **copy_kwargs)
 
 
@@ -2018,7 +2096,8 @@ class _Messages(dict):
     <<< del msgs["HelloWorld",2]
     <<< assert msgs.get("HelloWorld",2) == None
 
-    <<< # this Message has no code and no description (make easy things easy...)
+    # this Message has no code and no description (make easy things easy...)
+    <<<
     <<< msgs["HelloWorld"] = "Hello!"
     <<< assert msgs["HelloWorld"] == "Hello!"
 
@@ -2215,6 +2294,7 @@ def _basic_sync(e_local, e_remote):
     e_local.is_valid = e_remote.is_valid
     e_local.is_deleted = e_remote.is_deleted
     e_local.version = e_remote.version
+    e_local.state = e_remote.state
 
     if hasattr(e_remote, "query"):
         e_local.query = e_remote.query
@@ -2227,15 +2307,19 @@ def _basic_sync(e_local, e_remote):
 
 def _deletion_sync(e_local, e_remote):
     if e_local is None or e_remote is None:
-        return None
+        return
+
     try:
-        e_remote.get_messages()[('info', 10)]
-        _basic_sync(e_local, e_remote)
-        e_local.is_valid = lambda: False
-        e_local.is_deleted = lambda: True
-        e_local.id = None
+        e_remote.get_messages()["info", 10]  # try and get the deletion info
     except KeyError:
+        # deletion info wasn't there
         e_local.messages = e_remote.messages
+        return
+
+    _basic_sync(e_local, e_remote)
+    e_local.is_valid = lambda: False
+    e_local.is_deleted = lambda: True
+    e_local.id = None
 
 
 class Container(list):
@@ -2276,11 +2360,11 @@ class Container(list):
         """
 
         if entity in self:
-            super(Container, self).remove(entity)
+            super().remove(entity)
         else:
             for ee in self:
                 if entity == ee.id:
-                    super(Container, self).remove(ee)
+                    super().remove(ee)
 
                     return ee
             raise ValueError(
@@ -2398,13 +2482,13 @@ class Container(list):
         """
 
         if isinstance(entity, Entity):
-            super(Container, self).append(entity)
+            super().append(entity)
         elif isinstance(entity, int):
-            super(Container, self).append(Entity(id=entity))
+            super().append(Entity(id=entity))
         elif hasattr(entity, "encode"):
-            super(Container, self).append(Entity(name=entity))
+            super().append(Entity(name=entity))
         elif isinstance(entity, QueryTemplate):
-            super(Container, self).append(entity)
+            super().append(entity)
         else:
             raise TypeError(
                 "Entity was neither an id nor a name nor an entity." +
@@ -2420,8 +2504,8 @@ class Container(list):
         @return xml element
         """
         tmpid = 0
-        ''' users might already have specified some tmpids. -> look for smallest.'''
 
+        # users might already have specified some tmpids. -> look for smallest.
         for e in self:
             tmpid = min(tmpid, Container._get_smallest_tmpid(e))
         tmpid -= 1
@@ -2640,8 +2724,7 @@ class Container(list):
         # list of remote entities which already have a local equivalent
         used_remote_entities = []
 
-        ''' match by cuid '''
-
+        # match by cuid
         for local_entity in self:
 
             sync_dict[local_entity] = None
@@ -2667,10 +2750,9 @@ class Container(list):
                     local_entity.add_message(Message("Error", None, msg))
 
                     if raise_exception_on_error:
-                        raise AmbiguityException(msg)
-
-        ''' match by id '''
+                        raise MismatchingEntitiesError(msg)
 
+        # match by id
         for local_entity in self:
             if sync_dict[local_entity] is None and local_entity.id is not None:
                 sync_remote_entities = []
@@ -2692,10 +2774,9 @@ class Container(list):
                     local_entity.add_message(Message("Error", None, msg))
 
                     if raise_exception_on_error:
-                        raise AmbiguityException(msg)
-
-        ''' match by path '''
+                        raise MismatchingEntitiesError(msg)
 
+        # match by path
         for local_entity in self:
             if (sync_dict[local_entity] is None
                     and local_entity.path is not None):
@@ -2722,10 +2803,9 @@ class Container(list):
                     local_entity.add_message(Message("Error", None, msg))
 
                     if raise_exception_on_error:
-                        raise AmbiguityException(msg)
-
-        ''' match by name '''
+                        raise MismatchingEntitiesError(msg)
 
+        # match by name
         for local_entity in self:
             if (sync_dict[local_entity] is None
                     and local_entity.name is not None):
@@ -2752,7 +2832,7 @@ class Container(list):
                     local_entity.add_message(Message("Error", None, msg))
 
                     if raise_exception_on_error:
-                        raise AmbiguityException(msg)
+                        raise MismatchingEntitiesError(msg)
 
         # add remaining entities to this remote_container
         sync_remote_entities = []
@@ -2771,7 +2851,7 @@ class Container(list):
             remote_container.add_message(Message("Error", None, msg))
 
             if raise_exception_on_error:
-                raise AmbiguityException(msg)
+                raise MismatchingEntitiesError(msg)
 
         return sync_dict
 
@@ -2786,11 +2866,24 @@ class Container(list):
         this happens, none of them will be deleted. It occurs an error
         instead.
         """
+        chunk_size = 100
+        item_count = len(self)
+        # Split Container in 'chunk_size'-sized containers (if necessary) to avoid error 414 Request-URI Too Long
+        if item_count > chunk_size:
+            for i in range(0, int(item_count/chunk_size)+1):
+                chunk = Container()
+                for j in range(i*chunk_size, min(item_count, (i+1)*chunk_size)):
+                    chunk.append(self[j])
+                if len(chunk):
+                    chunk.delete()
+            return self
 
         if len(self) == 0:
             if raise_exception_on_error:
-                raise TransactionError(
-                    self, "There are no entities to be deleted. This container is empty.")
+                te = TransactionError(
+                    msg="There are no entities to be deleted. This container is empty.",
+                    container=self)
+                raise te
 
             return self
         self.clear_server_messages()
@@ -2814,21 +2907,23 @@ class Container(list):
                         description="This entity has no identifier. It cannot be deleted."))
 
                 if raise_exception_on_error:
-                    raise EntityError(
+                    ee = EntityError(
                         "This entity has no identifier. It cannot be deleted.", entity)
-                else:
-                    entity.is_valid = lambda: False
+                    raise TransactionError(ee)
+                entity.is_valid = lambda: False
 
         if len(id_str) == 0:
             if raise_exception_on_error:
-                raise TransactionError(
-                    self, "There are no entities to be deleted.")
+                te = TransactionError(
+                    msg="There are no entities to be deleted.",
+                    container=self)
+                raise te
 
             return self
         entity_url_segments = [_ENTITY_URI_SEGMENT, "&".join(id_str)]
 
         _log_request("DELETE: " + str(entity_url_segments) +
-                     ("?" + flags if flags is not None else ''))
+                     ("?" + str(flags) if flags is not None else ''))
 
         http_response = c.delete(entity_url_segments, query_dict=flags)
         cresp = Container._response_to_entities(http_response)
@@ -2882,10 +2977,11 @@ class Container(list):
                             description="This entity has no identifier. It cannot be retrieved."))
 
                     if raise_exception_on_error:
-                        raise EntityError(
-                            "This entity has no identifier. It cannot be retrieved.", entity)
-                    else:
-                        entity.is_valid = lambda: False
+                        ee = EntityError(
+                            "This entity has no identifier. It cannot be retrieved.",
+                            entity)
+                        raise TransactionError(ee)
+                    entity.is_valid = lambda: False
         else:
             entities_str.append(str(query))
 
@@ -2926,12 +3022,12 @@ class Container(list):
                         "&".join(entities))], query_dict=flags)
 
             return Container._response_to_entities(http_response)
-        except URITooLongException as uri_e:
+        except HTTPURITooLongError as uri_e:
             try:
                 # split up
                 uri1, uri2 = Container._split_uri_string(entities)
-            except ValueError:
-                raise uri_e
+            except ValueError as val_e:
+                raise uri_e from val_e
         c1 = self._retrieve(entities=uri1, flags=flags)
         c2 = self._retrieve(entities=uri2, flags=flags)
         c1.extend(c2)
@@ -2974,9 +3070,10 @@ class Container(list):
         """Update these entites."""
 
         if len(self) < 1:
-            raise TransactionError(
-                container=self,
-                msg="There are no entities to be updated. This container is empty.")
+            te = TransactionError(
+                msg="There are no entities to be updated. This container is empty.",
+                container=self)
+            raise te
 
         self.clear_server_messages()
         insert_xml = etree.Element("Update")
@@ -2993,8 +3090,10 @@ class Container(list):
 
         for entity in self:
             if (entity.id is None or entity.id < 0):
-                raise TransactionError(
-                    self, "You tried to update an entity without a valid id.")
+                ee = EntityError(
+                    "You tried to update an entity without a valid id.",
+                    entity)
+                raise TransactionError(ee)
 
         self._linearize()
 
@@ -3164,9 +3263,10 @@ class Container(list):
             insert_xml.append(entity_xml)
 
         if len(self) > 0 and len(insert_xml) < 1:
-            raise TransactionError(
-                container=self,
-                msg="There are no entities to be inserted. This container contains existent entities only.")
+            te = TransactionError(
+                msg="There are no entities to be inserted. This container contains existent entities only.",
+                container=self)
+            raise te
         _log_request("POST: " + _ENTITY_URI_SEGMENT +
                      ('' if flags is None else "?" + str(flags)), insert_xml)
 
@@ -3402,6 +3502,22 @@ class ACL():
                         self.deny(username=username, realm=realm, role=role,
                                   permission=permission, priority=priority)
 
+    def combine(self, other):
+        """ Combine and return new instance."""
+        result = ACL()
+        result._grants.update(other._grants)
+        result._grants.update(self._grants)
+        result._denials.update(other._denials)
+        result._denials.update(self._denials)
+        result._priority_grants.update(other._priority_grants)
+        result._priority_grants.update(self._priority_grants)
+        result._priority_denials.update(other._priority_denials)
+        result._priority_denials.update(self._priority_denials)
+        return result
+
+    def __eq__(self, other):
+        return isinstance(other, ACL) and other._grants == self._grants and self._denials == other._denials and self._priority_grants == other._priority_grants and self._priority_denials == other._priority_denials
+
     def is_empty(self):
         return len(self._grants) + len(self._priority_grants) + \
             len(self._priority_denials) + len(self._denials) == 0
@@ -3611,6 +3727,23 @@ class ACL():
 
 
 class Query():
+    """Query
+
+    Attributes
+    ----------
+    q : str
+        The query string.
+    flags : dict of str
+        A dictionary of flags to be send with the query request.
+    messages : _Messages()
+        A container of messages included in the last query response.
+    cached : bool
+        indicates whether the server used the query cache for the execution of
+        this query.
+    results : int or Container
+        The number of results (when this was a count query) or the container
+        with the resulting entities.
+    """
 
     def putFlag(self, key, value=None):
         self.flags[key] = value
@@ -3626,28 +3759,63 @@ class Query():
     def __init__(self, q):
         self.flags = dict()
         self.messages = _Messages()
+        self.cached = None
+        self.etag = None
 
         if isinstance(q, etree._Element):
             self.q = q.get("string")
             self.results = int(q.get("results"))
 
+            if q.get("cached") is None:
+                self.cached = False
+            else:
+                self.cached = q.get("cached").lower() == "true"
+            self.etag = q.get("etag")
+
             for m in q:
                 if m.tag.lower() == 'warning' or m.tag.lower() == 'error':
                     self.messages.append(_parse_single_xml_element(m))
         else:
             self.q = q
 
-    def execute(self, unique=False, raise_exception_on_error=True,
-                **kwargs):
+    def execute(self, unique=False, raise_exception_on_error=True, cache=True):
+        """Execute a query (via a server-requests) and return the results.
+
+        Parameters
+        ----------
+
+        unique : bool
+            Whether the query is expected to have only one entity as result.
+            Defaults to False.
+        raise_exception_on_error : bool
+            Whether an exception should be raises when there are errors in the
+            resulting entities. Defaults to True.
+        cache : bool
+            Whether to use the query cache (equivalent to adding a "cache"
+            flag) to the Query object. Defaults to True.
+
+        Returns
+        -------
+        results : Container or integer
+            Returns an integer when it was a `COUNT` query. Otherwise, returns a
+            Container with the resulting entities.
+        """
         connection = get_connection()
-        query_dict = dict(self.flags)
+
+        flags = self.flags
+        if cache is False:
+            flags["cache"] = "false"
+        query_dict = dict(flags)
         query_dict["query"] = str(self.q)
+
         _log_request("GET Entity?" + str(query_dict), None)
         http_response = connection.retrieve(
             entity_uri_segments=["Entity"],
-            query_dict=query_dict, **kwargs)
+            query_dict=query_dict)
         cresp = Container._response_to_entities(http_response)
         self.results = cresp.query.results
+        self.cached = cresp.query.cached
+        self.etag = cresp.query.etag
 
         if self.q.lower().startswith('count') and len(cresp) == 0:
             # this was a count query
@@ -3659,10 +3827,12 @@ class Query():
 
         if unique:
             if len(cresp) > 1 and raise_exception_on_error:
-                raise AmbiguityException("This query wasn't unique")
-            elif len(cresp) == 0 and raise_exception_on_error:
-                raise EntityDoesNotExistError("No such entity found.")
-            elif len(cresp) == 1:
+                raise QueryNotUniqueError(
+                    "Query '{}' wasn't unique.".format(self.q))
+            if len(cresp) == 0 and raise_exception_on_error:
+                raise EmptyUniqueQueryError(
+                    "Query '{}' found no results.".format(self.q))
+            if len(cresp) == 1:
                 r = cresp[0]
                 r.messages.extend(cresp.messages)
 
@@ -3672,8 +3842,32 @@ class Query():
         return cresp
 
 
-def execute_query(q, unique=False, raise_exception_on_error=True, flags=None,
-                  **kwargs):
+def execute_query(q, unique=False, raise_exception_on_error=True, cache=True, flags=None):
+    """Execute a query (via a server-requests) and return the results.
+
+    Parameters
+    ----------
+
+    q : str
+        The query string.
+    unique : bool
+        Whether the query is expected to have only one entity as result.
+        Defaults to False.
+    raise_exception_on_error : bool
+        Whether an exception should be raises when there are errors in the
+        resulting entities. Defaults to True.
+    cache : bool
+        Whether to use the query cache (equivalent to adding a "cache" flag).
+        Defaults to True.
+    flags : dict of str
+        Flags to be added to the request.
+
+    Returns
+    -------
+    results : Container or integer
+        Returns an integer when it was a `COUNT` query. Otherwise, returns a
+        Container with the resulting entities.
+    """
     query = Query(q)
 
     if flags is not None:
@@ -3681,7 +3875,7 @@ def execute_query(q, unique=False, raise_exception_on_error=True, flags=None,
 
     return query.execute(unique=unique,
                          raise_exception_on_error=raise_exception_on_error,
-                         **kwargs)
+                         cache=cache)
 
 
 class DropOffBox(list):
@@ -3735,7 +3929,7 @@ class Info():
         c = get_connection()
         try:
             http_response = c.retrieve(["Info"])
-        except ConnectionException as conn_e:
+        except CaosDBConnectionError as conn_e:
             print(conn_e)
 
             return
@@ -3850,11 +4044,15 @@ def _parse_single_xml_element(elem):
         return entity
     elif elem.tag.lower() == "version":
         return Version.from_xml(elem)
+    elif elem.tag.lower() == "state":
+        return State.from_xml(elem)
     elif elem.tag.lower() == "emptystring":
         return ""
     elif elem.tag.lower() == "value":
         if len(elem) == 1 and elem[0].tag.lower() == "emptystring":
             return ""
+        elif len(elem) == 1 and elem[0].tag.lower() in classmap:
+            return _parse_single_xml_element(elem[0])
         elif elem.text is None or elem.text.strip() == "":
             return None
 
@@ -3880,124 +4078,119 @@ def _parse_single_xml_element(elem):
             "code"), description=elem.get("description"), body=elem.text)
 
 
-def raise_errors(arg0):
-    if isinstance(arg0, (Entity, QueryTemplate)):
-        entity_error = EntityError(
-            entity=arg0, error=Message('Error', 0, 'EntityMultiError'))
-        found114 = False
-        found116 = False
+def _evaluate_and_add_error(parent_error, ent):
+    """Evaluate the error message(s) attached to entity and add a
+    corresponding exception to parent_error.
 
-        for e in arg0.get_errors():
-            try:
-                if e.code is not None:
-                    if int(e.code) == 101:  # arg0 does not exist
-                        raise EntityDoesNotExistError(error=e, entity=arg0)
-                    elif int(e.code) == 110:  # entity has no data type
-                        raise EntityHasNoDatatypeError(error=e, entity=arg0)
-                    elif int(e.code) == 403:  # Transaction not permitted
-                        raise AuthorizationException(error=e, entity=arg0)
-                    elif int(e.code) == 114:  # unqualified properties
-                        found114 = True
-                        unqualified_properties_error = UnqualifiedPropertiesError(
-                            error=e, entity=arg0)
-
-                        for p in arg0.get_properties():
-                            try:
-                                raise_errors(p)
-                            except EntityError as pe:
-                                unqualified_properties_error.add_error(pe)
-                        raise unqualified_properties_error
-                    elif int(e.code) == 116:  # unqualified parents
-                        found116 = True
-                        unqualified_parents_error = UnqualifiedParentsError(
-                            error=e, entity=arg0)
-
-                        for p in arg0.get_parents():
-                            try:
-                                raise_errors(p)
-                            except EntityError as pe:
-                                unqualified_parents_error.add_error(pe)
-                        raise unqualified_parents_error
-                    elif int(e.code) == 152:  # name was not unique
-                        raise UniqueNamesError(error=e, entity=arg0)
-                raise EntityError(error=e, entity=arg0)
-            except EntityError as ee:
-                entity_error.add_error(ee)
+    Parameters:
+    -----------
+    parent_error : TrancactionError
+        Parent error to which the new exception will be attached. This
+        exception will be a direct child.
+    ent : Entity
+        Entity that caused the TransactionError. An exception is
+        created depending on its error message(s).
 
-        if not found114:
-            for p in arg0.get_properties():
-                try:
-                    raise_errors(p)
-                except EntityError as pe:
-                    entity_error.add_error(pe)
+    Returns:
+    --------
+    TransactionError :
+        Parent error with new exception(s) attached to it.
 
+    """
+    if isinstance(ent, (Entity, QueryTemplate)):
+        # Check all error messages
+        found114 = False
+        found116 = False
+        for err in ent.get_errors():
+            # Evaluate specific EntityErrors depending on the error
+            # code
+            if err.code is not None:
+                if int(err.code) == 101:  # ent doesn't exist
+                    new_exc = EntityDoesNotExistError(entity=ent,
+                                                      error=err)
+                elif int(err.code) == 110:  # ent has no data type
+                    new_exc = EntityHasNoDatatypeError(entity=ent,
+                                                       error=err)
+                elif int(err.code) == 403:  # no permission
+                    new_exc = AuthorizationError(entity=ent,
+                                                 error=err)
+                elif int(err.code) == 152:  # name wasn't unique
+                    new_exc = UniqueNamesError(entity=ent, error=err)
+                elif int(err.code) == 114:  # unqualified properties
+                    found114 = True
+                    new_exc = UnqualifiedPropertiesError(entity=ent,
+                                                         error=err)
+                    for prop in ent.get_properties():
+                        new_exc = _evaluate_and_add_error(new_exc,
+                                                          prop)
+                elif int(err.code) == 116:  # unqualified parents
+                    found116 = True
+                    new_exc = UnqualifiedParentsError(entity=ent,
+                                                      error=err)
+                    for par in ent.get_parents():
+                        new_exc = _evaluate_and_add_error(new_exc,
+                                                          par)
+                else:  # General EntityError for other codes
+                    new_exc = EntityError(entity=ent, error=err)
+            else:  # No error code causes a general EntityError, too
+                new_exc = EntityError(entity=ent, error=err)
+            parent_error.add_error(new_exc)
+        # Check for possible errors in parents and properties that
+        # weren't detected up to here
+        if not found114:
+            dummy_err = EntityError(entity=ent)
+            for prop in ent.get_properties():
+                dummy_err = _evaluate_and_add_error(dummy_err, prop)
+            if dummy_err.errors:
+                parent_error.add_error(dummy_err)
         if not found116:
-            for p in arg0.get_parents():
-                try:
-                    raise_errors(p)
-                except EntityError as pe:
-                    entity_error.add_error(pe)
-
-        if len(entity_error.get_errors()) == 1:
-            r = entity_error.get_errors().pop()
-            raise r
-        elif len(entity_error.get_errors()) > 1:
-            r = entity_error._convert()
-            raise r
-    elif isinstance(arg0, Container):
-        transaction_error = TransactionError(
-            container=arg0, msg="This transaction terminated with Errors.")
-        doRaise = False
-        found12 = False
-
-        if arg0.get_errors() is not None:
-            for er in arg0.get_errors():
-                if er.code is not None:
-                    if int(er.code) == 12:  # atomicity violation
-                        found12 = True
-                        atomic_error = TransactionError(
-                            container=arg0, error=er, msg=er.description)
-
-                        for e in arg0:
-                            try:
-                                raise_errors(e)
-                            except EntityError as ee:
-                                atomic_error.add_error(ee)
-
-                        if len(atomic_error.get_errors()) > 0:
-                            transaction_error.add_error(
-                                atomic_error._convert())
-                            doRaise = True
-                        else:
-                            transaction_error.add_error(atomic_error)
-                            doRaise = True
-                    else:
-                        te = TransactionError(
-                            container=arg0, error=er, msg=er.description)
-                        transaction_error.add_error(te)
-                        doRaise = True
-
-            if len(transaction_error.get_errors()) == 1:
-                transaction_error = transaction_error.get_errors().pop()
-
-        if not found12:
-            for e in arg0:
-                try:
-                    raise_errors(e)
-                except EntityError as ee:
-                    transaction_error.add_error(ee)
-                    doRaise = True
-
-        if len(transaction_error.get_errors()) == 1:
-            t = transaction_error.get_errors().pop()
-            raise t
-        elif len(transaction_error.get_errors()) > 1:
-            t = transaction_error._convert()
-            raise t
-        elif doRaise:
-            raise transaction_error
+            dummy_err = EntityError(entity=ent)
+            for par in ent.get_parents():
+                dummy_err = _evaluate_and_add_error(dummy_err, par)
+            if dummy_err.errors:
+                parent_error.add_error(dummy_err)
+
+    elif isinstance(ent, Container):
+        parent_error.container = ent
+        if ent.get_errors() is not None:
+            parent_error.code = ent.get_errors()[0].code
+            # In the highly unusual case of more than one error
+            # message, attach all of them.
+            parent_error.msg = '\n'.join(
+                [x.description for x in ent.get_errors()])
+        # Go through all container elements and add them:
+        for elt in ent:
+            parent_error = _evaluate_and_add_error(parent_error, elt)
+
     else:
-        raise TypeError("Parameter arg0 is to be an Entity or a Container")
+        raise TypeError("Parameter ent is to be an Entity or a Container")
+
+    return parent_error
+
+
+def raise_errors(arg0):
+    """Raise a TransactionError depending on the error code(s) inside
+    Entity, QueryTemplate or Container arg0. More detailed errors may
+    be attached to the TransactionError depending on the contents of
+    arg0.
+
+    Parameters:
+    -----------
+    arg0 : Entity, QueryTemplate, or Container
+        CaosDB object whose messages are evaluated according to their
+        error codes
+
+    """
+    transaction_error = _evaluate_and_add_error(TransactionError(),
+                                                arg0)
+    # Raise if any error was found
+    if len(transaction_error.all_errors) > 0:
+        raise transaction_error
+    # Cover the special case of an empty container with error
+    # message(s) (e.g. query syntax error)
+    if (transaction_error.container is not None and
+            transaction_error.container.has_errors()):
+        raise transaction_error
 
 
 def delete(ids, raise_exception_on_error=True):
diff --git a/src/caosdb/common/state.py b/src/caosdb/common/state.py
new file mode 100644
index 0000000000000000000000000000000000000000..cb74022bef57a77c8270b2033c904eecabaadf83
--- /dev/null
+++ b/src/caosdb/common/state.py
@@ -0,0 +1,198 @@
+# ** header v3.0
+# This file is a part of the CaosDB Project.
+#
+# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+# Copyright (C) 2020 Timm Fitschen <t.fitschen@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
+
+import copy
+from lxml import etree
+
+
+def _translate_to_state_acis(acis):
+    result = set()
+    for aci in acis:
+        aci = copy.copy(aci)
+        if aci.role:
+            aci.role = "?STATE?" + aci.role + "?"
+        result.add(aci)
+    return result
+
+
+class Transition:
+    """Transition
+
+    Represents allowed transitions from one state to another.
+
+    Properties
+    ----------
+    name : str
+        The name of the transition
+    description: str
+        The description of the transition
+    from_state : str
+        A state name
+    to_state : str
+        A state name
+    """
+
+    def __init__(self, name, from_state, to_state, description=None):
+        self._name = name
+        self._from_state = from_state
+        self._to_state = to_state
+        self._description = description
+
+    @property
+    def name(self):
+        return self._name
+
+    @property
+    def description(self):
+        return self._description
+
+    @property
+    def from_state(self):
+        return self._from_state
+
+    @property
+    def to_state(self):
+        return self._to_state
+
+    def __repr__(self):
+        return f'Transition(name="{self.name}", from_state="{self.from_state}", to_state="{self.to_state}", description="{self.description}")'
+
+    def __eq__(self, other):
+        return (isinstance(other, Transition)
+                and other.name == self.name
+                and other.to_state == self.to_state
+                and other.from_state == self.from_state)
+
+    def __hash__(self):
+        return 23472 + hash(self.name) + hash(self.from_state) + hash(self.to_state)
+
+    @staticmethod
+    def from_xml(xml):
+        to_state = [to.get("name") for to in xml
+                    if to.tag.lower() == "tostate"]
+        from_state = [from_.get("name") for from_ in xml
+                      if from_.tag.lower() == "fromstate"]
+        result = Transition(name=xml.get("name"),
+                            description=xml.get("description"),
+                            from_state=from_state[0] if from_state else None,
+                            to_state=to_state[0] if to_state else None)
+        return result
+
+
+class State:
+    """State
+
+    Represents the state of an entity and take care of the serialization and
+    deserialization of xml for the entity state.
+
+    An entity state is always a State of a StateModel.
+
+    Properties
+    ----------
+    name : str
+        Name of the State
+    model : str
+        Name of the StateModel
+    description : str
+        Description of the State (read-only)
+    id : str
+        Id of the undelying State record (read-only)
+    transitions : set of Transition
+        All transitions which are available from this state (read-only)
+    """
+
+    def __init__(self, model, name):
+        self.name = name
+        self.model = model
+        self._id = None
+        self._description = None
+        self._transitions = None
+
+    @property
+    def id(self):
+        return self._id
+
+    @property
+    def description(self):
+        return self._description
+
+    @property
+    def transitions(self):
+        return self._transitions
+
+    def __eq__(self, other):
+        return (isinstance(other, State)
+                and self.name == other.name
+                and self.model == other.model)
+
+    def __hash__(self):
+        return hash(self.name) + hash(self.model)
+
+    def __repr__(self):
+        return f"State('{self.model}', '{self.name}')"
+
+    def to_xml(self):
+        """Serialize this State to xml.
+
+        Returns
+        -------
+        xml : etree.Element
+        """
+        xml = etree.Element("State")
+        if self.name is not None:
+            xml.set("name", self.name)
+        if self.model is not None:
+            xml.set("model", self.model)
+        return xml
+
+    @staticmethod
+    def from_xml(xml):
+        """Create a new State instance from an xml Element.
+
+        Parameters
+        ----------
+        xml : etree.Element
+
+        Returns
+        -------
+        state : State
+        """
+        name = xml.get("name")
+        model = xml.get("model")
+        result = State(name=name, model=model)
+        result._id = xml.get("id")
+        result._description = xml.get("description")
+        transitions = [Transition.from_xml(t) for t in xml if t.tag.lower() ==
+                       "transition"]
+        if transitions:
+            result._transitions = set(transitions)
+
+        return result
+
+    @staticmethod
+    def create_state_acl(acl):
+        from .models import ACL
+        state_acl = ACL()
+        state_acl._grants = _translate_to_state_acis(acl._grants)
+        state_acl._denials = _translate_to_state_acis(acl._denials)
+        state_acl._priority_grants = _translate_to_state_acis(acl._priority_grants)
+        state_acl._priority_denials = _translate_to_state_acis(acl._priority_denials)
+        return state_acl
diff --git a/src/caosdb/configuration.py b/src/caosdb/configuration.py
index 59f6c6a094d2df88384eb360dc4789ec99e8d787..842f1ee62a5b3178b7305e4c1e0c281a2dbd3b38 100644
--- a/src/caosdb/configuration.py
+++ b/src/caosdb/configuration.py
@@ -28,6 +28,9 @@ except ImportError:
     # python3
     from configparser import ConfigParser
 
+from os import environ, getcwd
+from os.path import expanduser, join, isfile
+
 
 def _reset_config():
     global _pycaosdbconf
@@ -50,3 +53,20 @@ def configure(inifile):
 def get_config():
     global _pycaosdbconf
     return _pycaosdbconf
+
+
+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).
+
+    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:
+        return_var.extend(configure(expanduser(environ["PYCAOSDBINI"])))
+    else:
+        return_var.extend(configure(expanduser('~/.pycaosdb.ini')))
+
+    if isfile(join(getcwd(), "pycaosdb.ini")):
+        return_var.extend(configure(join(getcwd(), "pycaosdb.ini")))
+    return return_var
diff --git a/src/caosdb/connection/authentication/auth_token.py b/src/caosdb/connection/authentication/auth_token.py
index fbce78fb86d78e06d97ef00dd162c1ed57f7560d..688123867f68153d3631bb8559baa235f6f02da5 100644
--- a/src/caosdb/connection/authentication/auth_token.py
+++ b/src/caosdb/connection/authentication/auth_token.py
@@ -30,7 +30,7 @@ An Authentictor which only uses only a pre-supplied authentication token.
 from __future__ import absolute_import, unicode_literals, print_function
 from .interface import AbstractAuthenticator, CaosDBServerConnection
 from caosdb.connection.utils import auth_token_to_cookie
-from caosdb.exceptions import LoginFailedException
+from caosdb.exceptions import LoginFailedError
 
 
 def get_authentication_provider():
@@ -68,11 +68,11 @@ class AuthTokenAuthenticator(AbstractAuthenticator):
         self._login()
 
     def _login(self):
-        raise LoginFailedException("The authentication token is expired or you "
-                                   "have been logged out otherwise. The "
-                                   "auth_token authenticator cannot log in "
-                                   "again. You must provide a new "
-                                   "authentication token.")
+        raise LoginFailedError("The authentication token is expired or you "
+                               "have been logged out otherwise. The "
+                               "auth_token authenticator cannot log in "
+                               "again. You must provide a new "
+                               "authentication token.")
 
     def logout(self):
         self._logout()
diff --git a/src/caosdb/connection/authentication/interface.py b/src/caosdb/connection/authentication/interface.py
index d9a9b4306b1dbdedbc67dfda81a3ca3d7b4aeb41..a364aeb564ee929d995b2f8098bd21e30e9733ab 100644
--- a/src/caosdb/connection/authentication/interface.py
+++ b/src/caosdb/connection/authentication/interface.py
@@ -31,7 +31,7 @@ import logging
 from caosdb.connection.utils import urlencode
 from caosdb.connection.interface import CaosDBServerConnection
 from caosdb.connection.utils import parse_auth_token, auth_token_to_cookie
-from caosdb.exceptions import LoginFailedException
+from caosdb.exceptions import LoginFailedError
 
 # meta class compatible with Python 2 *and* 3:
 ABC = ABCMeta('ABC', (object, ), {'__slots__': ()})
@@ -197,9 +197,9 @@ class CredentialsAuthenticator(AbstractAuthenticator):
 
         # we need a username for this:
         if username is None:
-            raise LoginFailedException("No username was given.")
+            raise LoginFailedError("No username was given.")
         if password is None:
-            raise LoginFailedException("No password was given")
+            raise LoginFailedError("No password was given")
 
         headers = {}
         headers["Content-Type"] = "application/x-www-form-urlencoded"
@@ -210,7 +210,7 @@ class CredentialsAuthenticator(AbstractAuthenticator):
 
         response.read()  # clear socket
         if response.status != 200:
-            raise LoginFailedException("LOGIN WAS NOT SUCCESSFUL")
+            raise LoginFailedError("LOGIN WAS NOT SUCCESSFUL")
         self.on_response(response)
         return response
 
diff --git a/src/caosdb/connection/authentication/keyring.py b/src/caosdb/connection/authentication/keyring.py
index 1dc986174acbe23191305632afda91cda0c718d2..d8be7ddf030577545230c9111fdad542b6d6e7e2 100644
--- a/src/caosdb/connection/authentication/keyring.py
+++ b/src/caosdb/connection/authentication/keyring.py
@@ -30,7 +30,7 @@ retrieve the password.
 import sys
 import imp
 from getpass import getpass
-from caosdb.exceptions import ConfigurationException
+from caosdb.exceptions import ConfigurationError
 from .external_credentials_provider import ExternalCredentialsProvider
 from .interface import CredentialsAuthenticator
 
@@ -67,10 +67,10 @@ def _get_external_keyring():
 
 def _call_keyring(**config):
     if "username" not in config:
-        raise ConfigurationException("Your configuration did not provide a "
-                                     "`username` which is needed by the "
-                                     "`KeyringCaller` to retrieve the "
-                                     "password in question.")
+        raise ConfigurationError("Your configuration did not provide a "
+                                 "`username` which is needed by the "
+                                 "`KeyringCaller` to retrieve the "
+                                 "password in question.")
     url = config.get("url")
     username = config.get("username")
     app = "caosdb — {}".format(url)
diff --git a/src/caosdb/connection/authentication/pass.py b/src/caosdb/connection/authentication/pass.py
index 9399fc4f4a76407ad94618785adcfbb945d4c788..853cdf0ed92039e7b5fc9beda8bb76cc0f3cc030 100644
--- a/src/caosdb/connection/authentication/pass.py
+++ b/src/caosdb/connection/authentication/pass.py
@@ -28,7 +28,7 @@ password.
 """
 
 from subprocess import check_output, CalledProcessError
-from caosdb.exceptions import ConfigurationException
+from caosdb.exceptions import ConfigurationError
 from .interface import CredentialsAuthenticator
 from .external_credentials_provider import ExternalCredentialsProvider
 
@@ -50,10 +50,10 @@ def get_authentication_provider():
 
 def _call_pass(**config):
     if "password_identifier" not in config:
-        raise ConfigurationException("Your configuration did not provide a "
-                                     "`password_identifier` which is needed "
-                                     "by the `PassCaller` to retrieve the "
-                                     "password in question.")
+        raise ConfigurationError("Your configuration did not provide a "
+                                 "`password_identifier` which is needed "
+                                 "by the `PassCaller` to retrieve the "
+                                 "password in question.")
 
     try:
         return check_output(
diff --git a/src/caosdb/connection/authentication/unauthenticated.py b/src/caosdb/connection/authentication/unauthenticated.py
index 53a2756eb59259a0be012e41f2ea213735568838..65febae8fd8f02f3ee0d339fafb36af512fc7be7 100644
--- a/src/caosdb/connection/authentication/unauthenticated.py
+++ b/src/caosdb/connection/authentication/unauthenticated.py
@@ -30,7 +30,7 @@ cookies.
 """
 from __future__ import absolute_import, unicode_literals, print_function
 from .interface import AbstractAuthenticator, CaosDBServerConnection
-from caosdb.exceptions import LoginFailedException
+from caosdb.exceptions import LoginFailedError
 
 
 def get_authentication_provider():
@@ -70,11 +70,11 @@ class Unauthenticated(AbstractAuthenticator):
         self._login()
 
     def _login(self):
-        raise LoginFailedException("This caosdb client is configured to stay "
-                                   "unauthenticated. Change your "
-                                   "`password_method` and provide an "
-                                   "`auth_token` or credentials if you want "
-                                   "to authenticate this client.")
+        raise LoginFailedError("This caosdb client is configured to stay "
+                               "unauthenticated. Change your "
+                               "`password_method` and provide an "
+                               "`auth_token` or credentials if you want "
+                               "to authenticate this client.")
 
     def logout(self):
         self._logout()
diff --git a/src/caosdb/connection/connection.py b/src/caosdb/connection/connection.py
index 703897d62a40a820a6ff578869a862f6c7c4c019..9e273a56778737033fda9f342f967f56946b501b 100644
--- a/src/caosdb/connection/connection.py
+++ b/src/caosdb/connection/connection.py
@@ -33,12 +33,19 @@ from errno import EPIPE as BrokenPipe
 from socket import error as SocketError
 
 from caosdb.configuration import get_config
-from caosdb.exceptions import (AuthorizationException, CaosDBException,
-                               ClientErrorException, ConfigurationException,
-                               ConnectionException, EntityDoesNotExistError,
-                               LoginFailedException, ServerErrorException,
-                               URITooLongException)
-from caosdb.version import version
+from caosdb.exceptions import (CaosDBException, HTTPClientError,
+                               ConfigurationError,
+                               CaosDBConnectionError,
+                               HTTPForbiddenError,
+                               LoginFailedError,
+                               HTTPResourceNotFoundError,
+                               HTTPServerError,
+                               HTTPURITooLongError)
+try:
+    from caosdb.version import version
+except ModuleNotFoundError:
+    version = "uninstalled"
+
 from pkg_resources import resource_filename
 
 from .interface import CaosDBHTTPResponse, CaosDBServerConnection
@@ -61,6 +68,10 @@ class _WrappedHTTPResponse(CaosDBHTTPResponse):
     def __init__(self, response):
         self.response = response
 
+    @property
+    def reason(self):
+        return self.response.reason
+
     @property
     def status(self):
         return self.response.status
@@ -88,7 +99,8 @@ class _DefaultCaosDBServerConnection(CaosDBServerConnection):
     """
 
     def __init__(self):
-        self._useragent = ("caosdb-pylib/{version} - {implementation}".format(version=version, implementation=type(self).__name__))
+        self._useragent = ("caosdb-pylib/{version} - {implementation}".format(
+            version=version, implementation=type(self).__name__))
         self._http_con = None
         self._base_path = None
 
@@ -132,7 +144,7 @@ class _DefaultCaosDBServerConnection(CaosDBServerConnection):
             self._http_con.request(method=method, url=self._base_path + path,
                                    headers=headers, body=body)
         except SocketError as socket_err:
-            raise ConnectionException(
+            raise CaosDBConnectionError(
                 "Connection failed. Network or server down? " + str(socket_err)
             )
 
@@ -156,7 +168,7 @@ class _DefaultCaosDBServerConnection(CaosDBServerConnection):
 
         Raises
         ------
-        ConnectionException
+        CaosDBConnectionError
             If no url has been specified, or if the CA certificate cannot be
             loaded.
         """
@@ -193,9 +205,9 @@ class _DefaultCaosDBServerConnection(CaosDBServerConnection):
             try:
                 context.load_verify_locations(config["cacert"])
             except Exception as exc:
-                raise ConnectionException("Could not load the cacert in"
-                                          "`{}`: {}".format(config["cacert"],
-                                                            exc))
+                raise CaosDBConnectionError("Could not load the cacert in"
+                                            "`{}`: {}".format(config["cacert"],
+                                                              exc))
 
         context.load_default_certs()
 
@@ -204,7 +216,7 @@ class _DefaultCaosDBServerConnection(CaosDBServerConnection):
             host = parsed_url.netloc
             self._base_path = parsed_url.path
         else:
-            raise ConnectionException(
+            raise CaosDBConnectionError(
                 "No connection url specified. Please "
                 "do so via caosdb.configure_connection(...) or in a config "
                 "file.")
@@ -276,7 +288,7 @@ def _get_authenticator(**config):
 
     Raises
     ------
-    ConnectionException
+    ConfigurationError
         If the password_method string cannot be resolved to a CaosAuthenticator
         class.
     """
@@ -292,10 +304,10 @@ def _get_authenticator(**config):
         return auth_provider
 
     except ImportError:
-        raise ConfigurationException("Password method \"{}\" not implemented. "
-                                     "Try `plain`, `pass`, `keyring`, or "
-                                     "`auth_token`."
-                                     .format(config["password_method"]))
+        raise ConfigurationError("Password method \"{}\" not implemented. "
+                                 "Try `plain`, `pass`, `keyring`, or "
+                                 "`auth_token`."
+                                 .format(config["password_method"]))
 
 
 def configure_connection(**kwargs):
@@ -398,29 +410,24 @@ def _handle_response_status(http_response):
     # emtpy response buffer
     body = http_response.read()
 
+    if status == 404:
+        raise HTTPResourceNotFoundError("This resource has not been found.")
+    elif status > 499:
+        raise HTTPServerError(body=body)
+
+    reason = http_response.reason
+    standard_message = ("Request failed. The response returned with status "
+                        "{} - {}.".format(status, reason))
     if status == 401:
-        raise LoginFailedException(
-            "Request failed. The response returned with status "
-            "{}.".format(status))
+        raise LoginFailedError(standard_message)
     elif status == 403:
-        raise AuthorizationException(
-            "Request failed. The response returned with status "
-            "{}.".format(status))
-    elif status == 404:
-        raise EntityDoesNotExistError("This entity does not exist.")
+        raise HTTPForbiddenError(standard_message)
     elif status in (413, 414):
-        raise URITooLongException(
-            "Request failed. The response returned with status "
-            "{}.".format(status))
+        raise HTTPURITooLongError(standard_message)
     elif 399 < status < 500:
-        raise ClientErrorException(msg=("Request failed. The response returned "
-                                        "with status {}.").format(status), status=status, body=body)
-    elif status > 499:
-        raise ServerErrorException(body=body)
+        raise HTTPClientError(msg=standard_message, status=status, body=body)
     else:
-        raise CaosDBException(
-            "Request failed. The response returned with status "
-            "{}.".format(status))
+        raise CaosDBException(standard_message)
 
 
 class _Connection(object):  # pylint: disable=useless-object-inheritance
@@ -454,7 +461,7 @@ class _Connection(object):  # pylint: disable=useless-object-inheritance
         self.is_configured = True
 
         if "implementation" not in config:
-            raise ConfigurationException(
+            raise ConfigurationError(
                 "Missing CaosDBServerConnection implementation. You did not "
                 "specify an `implementation` for the connection.")
         try:
@@ -465,20 +472,20 @@ class _Connection(object):  # pylint: disable=useless-object-inheritance
                 raise TypeError("The `implementation` callable did not return "
                                 "an instance of CaosDBServerConnection.")
         except TypeError as type_err:
-            raise ConfigurationException(
+            raise ConfigurationError(
                 "Bad CaosDBServerConnection implementation. The "
                 "implementation must be a callable object which returns an "
                 "instance of `CaosDBServerConnection` (e.g. a constructor "
-                "or a factory).", type_err)
+                "or a factory).\n{}".format(type_err.args[0]))
         self._delegate_connection.configure(**config)
 
         if "auth_token" in config:
             # deprecated, needed for older scripts
             config["password_method"] = "auth_token"
         if "password_method" not in config:
-            raise ConfigurationException("Missing password_method. You did "
-                                         "not specify a `password_method` for"
-                                         "the connection.")
+            raise ConfigurationError("Missing password_method. You did "
+                                     "not specify a `password_method` for"
+                                     "the connection.")
         self._authenticator = _get_authenticator(
             connection=self._delegate_connection, **config)
 
@@ -554,8 +561,8 @@ class _Connection(object):  # pylint: disable=useless-object-inheritance
             uri_segments.extend(path.split("/"))
 
             return self.retrieve(entity_uri_segments=uri_segments)
-        except EntityDoesNotExistError:
-            raise EntityDoesNotExistError("This file does not exist.")
+        except HTTPResourceNotFoundError:
+            raise HTTPResourceNotFoundError("This file does not exist.")
 
     def _login(self):
         self._authenticator.login()
@@ -576,7 +583,7 @@ class _Connection(object):  # pylint: disable=useless-object-inheritance
                                             headers=headers, body=body,
                                             reconnect=False,
                                             **kwargs)
-        except LoginFailedException:
+        except LoginFailedError:
             if kwargs.get("reconnect", True) is True:
                 self._login()
 
@@ -608,3 +615,11 @@ class _Connection(object):  # pylint: disable=useless-object-inheritance
         _handle_response_status(http_response)
 
         return http_response
+
+    def get_username(self):
+        """
+        Return the username of the current connection.
+
+        Shortcut for: get_connection()._authenticator._credentials_provider.username
+        """
+        return self._authenticator._credentials_provider.username
diff --git a/src/caosdb/exceptions.py b/src/caosdb/exceptions.py
index 5763bef4ac805a7dd1dfde8da2f6f3b7e5fd4bd8..fdd2e11f1dfb8857f86942df2534d732bad9a793 100644
--- a/src/caosdb/exceptions.py
+++ b/src/caosdb/exceptions.py
@@ -5,6 +5,8 @@
 #
 # Copyright (C) 2018 Research Group Biomedical Physics,
 # Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+# Copyright (C) 2020 Indiscale GmbH <info@indiscale.com>
+# Copyright (C) 2020 Florian Spreckelsen <f.spreckelsen@indiscale.com>
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as
@@ -21,6 +23,10 @@
 #
 # ** end header
 #
+"""The exceptions module defines exceptions for HTTP Errors (4xx and 5xx and
+HTTP response codes) and for transaction errors (i.e. missing permissions,
+dependencies, non-passing consistency checks etc.).
+"""
 
 from lxml import etree
 
@@ -28,13 +34,13 @@ from lxml import etree
 class CaosDBException(Exception):
     """Base class of all CaosDB exceptions."""
 
-    def __init__(self, msg=None, *args):
-        Exception.__init__(self, msg, *args)
+    def __init__(self, msg):
+        Exception.__init__(self, msg)
         self.msg = msg
 
 
-class ConfigurationException(CaosDBException):
-    """ConfigurationException.
+class ConfigurationError(CaosDBException):
+    """ConfigurationError.
 
     Indicates a misconfiguration.
 
@@ -43,7 +49,6 @@ class ConfigurationException(CaosDBException):
     msg : str
         A descriptin of the misconfiguration. The constructor adds
         a few lines with explainingg where to find the configuration.
-    *args
 
     Attributes
     ----------
@@ -51,23 +56,32 @@ class ConfigurationException(CaosDBException):
         A description of the misconfiguration.
     """
 
-    def __init__(self, msg, *args):
-        super(ConfigurationException, self).__init__(msg +
-                                                     ConfigurationException._INFO,
-                                                     *args)
+    def __init__(self, msg):
+        super().__init__(msg + ConfigurationError._INFO)
 
     _INFO = ("\n\nPlease check your ~/.pycaosdb.ini and your $PWD/"
-             ".pycaosdb.ini. Do at least one of them exist and are they correct?")
+             ".pycaosdb.ini. Does at least one of them exist and are they correct?")
 
 
-class ClientErrorException(CaosDBException):
+class ServerConfigurationException(CaosDBException):
+    """The server is configured in a different way than expected.
+
+    This can be for example unexpected flags or settings or missing extensions.
+    """
+
+
+class HTTPClientError(CaosDBException):
+    """HTTPClientError represents 4xx HTTP client errors."""
+
     def __init__(self, msg, status, body):
         self.status = status
         self.body = body
         CaosDBException.__init__(self, msg)
 
 
-class ServerErrorException(CaosDBException):
+class HTTPServerError(CaosDBException):
+    """HTTPServerError represents 5xx HTTP server errors."""
+
     def __init__(self, body):
         xml = etree.fromstring(body)
         error = xml.xpath('/Response/Error')[0]
@@ -78,107 +92,126 @@ class ServerErrorException(CaosDBException):
         CaosDBException.__init__(self, msg)
 
 
-class ConnectionException(CaosDBException):
+class CaosDBConnectionError(CaosDBException):
     """Connection is not configured or the network is down."""
 
     def __init__(self, msg=None):
         CaosDBException.__init__(self, msg)
 
 
-class URITooLongException(CaosDBException):
+class HTTPURITooLongError(HTTPClientError):
     """The URI of the last request was too long."""
 
     def __init__(self, msg=None):
-        CaosDBException.__init__(self, msg)
+        HTTPClientError.__init__(self, msg=msg, status=414, body=None)
+
 
+class LoginFailedError(CaosDBException):
+    """Login failed.
 
-class AmbiguityException(CaosDBException):
-    """A retrieval of an entity that was supposed to be uniquely identifiable
-    returned two or more entities."""
+    Probably, your username/password pair is wrong.
+    """
 
     def __init__(self, msg=None):
-        CaosDBException.__init__(self, msg)
+        CaosDBException.__init__(self, msg=msg)
 
 
-class LoginFailedException(CaosDBException):
-    """Login failed.
+class HTTPForbiddenError(HTTPClientError):
+    """You're lacking the required permissions. Corresponds to HTTP status
+    403.
 
-    Probably, your username/password pair is wrong.
     """
 
     def __init__(self, msg=None):
-        CaosDBException.__init__(self, msg=msg)
+        HTTPClientError.__init__(self, msg=msg, status=403, body=None)
 
 
-class TransactionError(CaosDBException):
+class HTTPResourceNotFoundError(HTTPClientError):
+    """The requested resource doesn't exist; corresponds to HTTP status
+    404.
 
-    def _calc_bases(self):
-        types = dict()
-        # collect each class once
+    """
 
-        for err in self.errors:
-            types[id(type(err))] = type(err)
-        # delete redundant super classes
+    def __init__(self, msg=None):
+        HTTPClientError.__init__(self, msg=msg, status=404, body=None)
 
-        if len(types.values()) > 1:
-            # remove TransactionError
-            try:
-                del types[id(TransactionError)]
-            except KeyError:
-                pass
 
-        if len(types.values()) > 1:
-            # remove EntityError
-            try:
-                del types[id(EntityError)]
-            except KeyError:
-                pass
+class MismatchingEntitiesError(CaosDBException):
+    """Mismatching entities were found during container sync."""
 
-        ret = ()
 
-        for t in types.values():
-            ret += (t,)
+# ######################### Bad query errors ###########################
 
-        if ret == ():
-            ret = (type(self),)
 
-        return ret
+class BadQueryError(CaosDBException):
+    """Base class for query errors that are not transaction errors."""
 
-    def __init__(self, container=None, error=None, msg=None):
-        self.container = container
-        self.errors = []
-        self.msg = msg if msg is not None else str(error)
-        self.error = error
 
-    def print_errs(self):
-        print(self)
+class QueryNotUniqueError(BadQueryError):
+    """A unique query or retrieve found more than one entity."""
 
-        for err in self.errors:
-            err.print_errs()
 
-    def _convert(self):
-        t = self._calc_bases()
-        try:
-            newtype = type('TransactionError', t, {})
-        except BaseException:
-            self.print_errs()
-            raise
-        newinstance = newtype(container=self.container, error=self.msg)
-        newinstance.errors = self.errors
-        newinstance.get_entities = self.get_entities
+class EmptyUniqueQueryError(BadQueryError):
+    """A unique query or retrieve dound no result."""
 
-        return newinstance
 
-    def get_container(self):
-        '''
-        @return: The container that raised this TransactionError during the last
-        transaction.
-        '''
+# ######################### Transaction errors #########################
 
-        return self.container
+
+class TransactionError(CaosDBException):
+    """An error of this type is raised whenever any transaction fails with
+    one or more entities between client and CaosDB server. More
+    detailed errors are collected as direct and indirect children in
+    the 'errors' list (direct children) and the 'all_errors' set (set
+    of all direct and indirect children).
+
+    """
+
+    def __init__(self, error=None,
+                 msg="An error occured during the transaction.",
+                 container=None):
+        CaosDBException.__init__(self, msg=msg)
+        self.errors = []
+        self.all_errors = set()
+        self.entities = []
+        self.all_entities = set()
+        self.container = container
+        # special case of faulty container
+        if container is not None and container.get_errors() is not None:
+            self.code = container.get_errors()[0].code
+        else:
+            self.code = None
+        if error is not None:
+            self.add_error(error)
+
+    def has_error(self, error_t, direct_children_only=False):
+        """Check whether this transaction error contains an error of type
+        error_t. If direct_children_only is True, only direct children
+        are checked.
+
+        Parameters
+        ----------
+        error_t : EntityError
+            error type to be checked
+        direct_children_only: bool, optional
+            If True, only direct children, i.e., all errors in
+            self.errors are checked. Else all direct and indirect
+            children, i.e., all errors in self.all_errors are
+            used. Default is false.
+
+        Returns
+        -------
+        has_error : bool
+            True if at least one of the children is of type error_t,
+            False otherwise.
+
+        """
+
+        test_set = self.errors if direct_children_only else self.all_errors
+        return any([isinstance(err, error_t) for err in test_set])
 
     def add_error(self, error):
-        """Add an error to this TransactionError.
+        """Add an error as a direct child to this TransactionError.
 
         @param error: An EntityError or a list of EntityErrors.
 
@@ -189,35 +222,31 @@ class TransactionError(CaosDBException):
         """
 
         if hasattr(error, "__iter__"):
-            for e in error:
-                self.add_error(e)
+            for err in error:
+                self.add_error(err)
 
             return self
-        elif isinstance(error, TransactionError):
+        elif isinstance(error, EntityError):
             self.errors.append(error)
+            self.entities.append(error.entity)
+
+            self.all_errors.add(error)
+            self.all_errors.update(error.all_errors)
+            self.all_entities.add(error.entity)
+            self.all_entities.update(error.all_entities)
 
             return self
         else:
             raise TypeError(
-                "Argument is to be an TransactionError or a list of TransactionErrors.")
-
-    def get_errors(self):
-        '''
-        @return: A list of all EntityError objects.
-        '''
-
-        if hasattr(self, 'errors'):
-            return self.errors
-
-        return None
+                "Argument is to be an EntityError or a list of EntityErrors.")
 
     def _repr_reasons(self, indent):
-        if self.get_errors() is not None and len(self.get_errors()) > 0:
+        if self.errors is not None and len(self.errors) > 0:
             ret = "\n" + indent + "    +--| REASONS |--"
 
-            for c in self.get_errors():
+            for err in self.errors:
                 ret += '\n' + indent + '    |  -> ' + \
-                    c.__str__(indent=indent + '    |')
+                    err.__str__(indent=indent + '    |')
             ret += "\n" + indent + "    +----------------"
 
             return ret
@@ -225,8 +254,11 @@ class TransactionError(CaosDBException):
             return ''
 
     def _repr_head(self, indent):
-        return str(type(self).__name__) + ((': ' + self.msg)
-                                           if hasattr(self, 'msg') and self.msg is not None else '')
+        return indent + str(type(self).__name__) + (
+            (': ' + self.msg)
+            if hasattr(self, 'msg') and self.msg is not None
+            else ''
+        )
 
     def __str__(self, indent=''):
         ret = self._repr_head(indent=indent)
@@ -237,58 +269,21 @@ class TransactionError(CaosDBException):
     def __repr__(self):
         return self.__str__()
 
-    def get_entities(self):
-        '''
-        @return: A list of all Entity objects with errors.
-        '''
-        ret = []
-
-        if hasattr(self, 'get_entity') and self.get_entity() is not None:
-            ret.append(self.get_entity())
-
-        for error in self.errors:
-            if hasattr(error, 'get_entity'):
-                if error.get_entity() not in ret:
-                    ret.append(error.get_entity())
-#             if hasattr(error, 'get_entities'):
-#                 for e in error.get_entities():
-#                     if e not in ret:
-#                         ret.append(e)
-        return ret
-
-    def get_error(self):
-        return self.error
-
 
 class EntityError(TransactionError):
+    """This is the most basic entity error. It is constructed using an
+    entity that caused the error and the error message attached by the
+    server.
 
-    @staticmethod
-    def _sort_t(t):
-        if len(t) > 1:
-            ret = ()
-            '''remove EntityError'''
-
-            for i in range(len(t)):
-                if t[i] != EntityError:
-                    ret += (t[i],)
-            t = ret
-
-        return t
-
-    def _convert(self):
-        t = self._calc_bases()
-        # TODO is it really a good idea to create dynamically types here?
-        newtype = type('EntityMultiError', t+(Exception,), {})
-        newinstance = newtype(error=self.error, entity=self.entity)
-        setattr(newinstance, 'msg', self.msg)
-        setattr(newinstance, 'errors', self.errors)
-        setattr(newinstance, 'container', self.container)
-
-        return newinstance
+    """
 
-    def __init__(self, error=None, container=None, entity=None):
-        TransactionError.__init__(self, container=container)
+    def __init__(self, error=None, entity=None):
+        TransactionError.__init__(self)
         self.error = error
+        if hasattr(error, "code"):
+            self.code = error.code
+        else:
+            self.code = None
         self.entity = entity
 
         if error is not None and hasattr(error, "encode"):
@@ -300,34 +295,17 @@ class EntityError(TransactionError):
         else:
             self.msg = str(error)
 
-    def get_entity(self):
-        '''
-        @return: The entity that caused this error.
-        '''
-
-        if hasattr(self, 'entity'):
-            return self.entity
-
-        return None
-
     @property
     def description(self):
+        """The description of the error."""
         return self.error.description if self.error is not None else None
 
-    def get_code(self):
-        return self.error.code if self.error is not None else None
-
-    def get_error(self):
-        '''
-        @return: Error Message object of this Error.
-        '''
-
-        return self.error
-
     def _repr_head(self, indent):
         if hasattr(self, 'entity') and self.entity is not None:
-            return str(type(self.entity).__name__).upper() + " (" + str(self.entity.id) + (("," + "'" + str(self.entity.name) + "'")
-                                                                                           if self.entity.name is not None else '') + ") CAUSED " + TransactionError._repr_head(self, indent)
+            return (str(type(self.entity).__name__).upper() + " (id: " +
+                    str(self.entity.id) + ((", name: " + "'" + str(self.entity.name) + "'") if
+                                           self.entity.name is not None else '') + ") CAUSED " +
+                    TransactionError._repr_head(self, indent))
         else:
             return TransactionError._repr_head(self, indent)
 
@@ -337,15 +315,19 @@ class UniqueNamesError(EntityError):
 
 
 class UnqualifiedParentsError(EntityError):
-    """This entity has unqualified parents (call 'get_errors()' for a list of
-    errors of the parent entities or 'get_entities()' for a list of parent
-    entities with errors)."""
+    """This entity has unqualified parents (see 'errors' attribute for a
+    list of errors of the parent entities or 'entities' attribute for
+    a list of parent entities with errors).
+
+    """
 
 
 class UnqualifiedPropertiesError(EntityError):
-    """This entity has unqualified properties (call 'get_errors()' for a list
-    of errors of the properties or 'get_entities()' for a list of properties
-    with errors)."""
+    """This entity has unqualified properties (see 'errors' attribute for
+    a list of errors of the properties or 'entities' attribute for a
+    list of properties with errors).
+
+    """
 
 
 class EntityDoesNotExistError(EntityError):
@@ -357,11 +339,17 @@ class EntityHasNoDatatypeError(EntityError):
 
 
 class ConsistencyError(EntityError):
-    pass
+    """The transaction violates database consistency."""
 
 
-class AuthorizationException(EntityError):
+class AuthorizationError(EntityError):
     """You are not allowed to do what ever you tried to do.
 
-    Maybe you need more privileges or a user account at all.
+    Maybe you need more privileges or a user account.
+    """
+
+
+class AmbiguousEntityError(EntityError):
+    """A retrieval of the entity was not possible because there is more
+    than one possible candidate.
     """
diff --git a/src/caosdb/utils/caosdb_admin.py b/src/caosdb/utils/caosdb_admin.py
index 250c2878d5d615b0815bdd7b0bb287d1567fe085..392d8bea2ce3d9a868c32854800ca6cb78f021ba 100755
--- a/src/caosdb/utils/caosdb_admin.py
+++ b/src/caosdb/utils/caosdb_admin.py
@@ -33,7 +33,7 @@ from argparse import ArgumentParser, RawDescriptionHelpFormatter
 
 import caosdb as db
 from caosdb import administration as admin
-from caosdb.exceptions import ClientErrorException
+from caosdb.exceptions import HTTPClientError
 
 __all__ = []
 __version__ = 0.3
@@ -73,7 +73,7 @@ def do_retrieve(args):
                 c.append(db.Entity(id=eid))
             except ValueError:
                 c.append(db.Entity(name=i))
-        c.retrieve()
+        c.retrieve(flags=eval(args.flags))
     print(c)
 
 
@@ -123,7 +123,7 @@ def do_create_user(args):
     try:
         admin._insert_user(name=args.user_name,
                            email=args.user_email, password=password)
-    except ClientErrorException as e:
+    except HTTPClientError as e:
         print(e.msg)
 
 
@@ -141,20 +141,20 @@ def do_set_user_password(args):
 
 
 def do_add_user_roles(args):
-    roles = admin._get_roles(user=args.user_name, realm=None)
+    roles = admin._get_roles(username=args.user_name, realm=None)
 
     for r in args.user_roles:
         roles.add(r)
-    admin._set_roles(user=args.user_name, roles=roles)
+    admin._set_roles(username=args.user_name, roles=roles)
 
 
 def do_remove_user_roles(args):
-    roles = admin._get_roles(user=args.user_name, realm=None)
+    roles = admin._get_roles(username=args.user_name, realm=None)
 
     for r in args.user_roles:
         if r in roles:
             roles.remove(r)
-    admin._set_roles(user=args.user_name, roles=roles)
+    admin._set_roles(username=args.user_name, roles=roles)
 
 
 def do_set_user_entity(args):
@@ -178,7 +178,7 @@ def do_delete_user(args):
 
 
 def do_retrieve_user_roles(args):
-    print(admin._get_roles(user=args.user_name))
+    print(admin._get_roles(username=args.user_name))
 
 
 def do_retrieve_role_permissions(args):
diff --git a/src/doc/Makefile b/src/doc/Makefile
index 5458c5300efc82e55686bc1cd6934182c5c8e39a..64219c5957ee963e84f9305685f2ec4e8ed3d761 100644
--- a/src/doc/Makefile
+++ b/src/doc/Makefile
@@ -32,6 +32,7 @@ PY_BASEDIR    = ../caosdb
 SOURCEDIR     = .
 BUILDDIR      = ../../build/doc
 
+
 .PHONY: doc-help Makefile
 
 # Put it first so that "make" without argument is like "make help".
@@ -44,4 +45,4 @@ doc-help:
 	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
 
 apidoc:
-	@$(SPHINXAPIDOC) -o _apidoc $(PY_BASEDIR)
+	@$(SPHINXAPIDOC) -o _apidoc --separate $(PY_BASEDIR)
diff --git a/src/doc/administration.rst b/src/doc/administration.rst
new file mode 100644
index 0000000000000000000000000000000000000000..061acc8364d2ef62f743a20d7b9e6562baac0fc5
--- /dev/null
+++ b/src/doc/administration.rst
@@ -0,0 +1,14 @@
+Administration
+==============
+
+The Python script ``caosdb_admin.py`` should be used for administrative tasks.
+Call ``caosdb_admin.py --help`` to see how to use it.
+
+The most common task is to create a new user (in the CaosDB realm) and set a 
+password for the user (note that a user typically needs to be activated)::
+
+     caosdb_admin.py create_user anna
+     caosdb_admin.py set_user_password anna
+     caosdb_admin.py add_user_roles anna administration
+     caosdb_admin.py activate_user anna
+
diff --git a/src/doc/conf.py b/src/doc/conf.py
index 9e2924ae726e13aacd2f955ae1904b39ad73cbc3..b05fa1c71c1dcd0b59916594818449d2ebc574bd 100644
--- a/src/doc/conf.py
+++ b/src/doc/conf.py
@@ -8,16 +8,18 @@
 
 # -- Path setup --------------------------------------------------------------
 
-# If extensions (or modules to document with autodoc) are in another directory,
-# add these directories to sys.path here. If the directory is relative to the
-# documentation root, use os.path.abspath to make it absolute, like shown here.
+# If extensions (or modules to document with autodoc) are in another directory, add these
+# directories to sys.path here. This is particularly necessary if this package is installed at a
+# different version, for example via `pip install`.
 #
-# import os
-# import sys
-# sys.path.insert(0, os.path.abspath('../caosdb'))
-
+# If the directory is relative to the documentation root, use os.path.abspath to make it absolute,
+# like shown here.
+#
+import os
+import sys
+sys.path.insert(0, os.path.abspath('..'))
 
-import sphinx_rtd_theme
+import sphinx_rtd_theme  # noqa: E402
 
 
 # -- Project information -----------------------------------------------------
@@ -27,9 +29,10 @@ copyright = '2020, IndiScale GmbH'
 author = 'Daniel Hornung'
 
 # The short X.Y version
-version = '0.4.0'
+version = '0.5.2'
 # The full version, including alpha/beta/rc tags
-release = '0.4.0-rc'
+# release = '0.5.2-rc2'
+release = '0.5.2'
 
 
 # -- General configuration ---------------------------------------------------
@@ -43,6 +46,7 @@ release = '0.4.0-rc'
 # ones.
 extensions = [
     'sphinx.ext.autodoc',
+    'sphinx.ext.autosectionlabel',
     'sphinx.ext.intersphinx',
     'sphinx.ext.napoleon',     # For Google style docstrings
     "recommonmark",            # For markdown files.
@@ -54,7 +58,6 @@ templates_path = ['_templates']
 
 # The suffix(es) of source filenames.
 # You can specify multiple suffix as a list of string:
-#
 source_suffix = ['.rst', '.md']
 
 # The master toctree document.
@@ -81,6 +84,7 @@ pygments_style = None
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
 #
+
 html_theme = "sphinx_rtd_theme"
 
 # Theme options are theme-specific and customize the look and feel of a theme
@@ -182,10 +186,24 @@ epub_exclude_files = ['search.html']
 
 # -- Extension configuration -------------------------------------------------
 
-# -- Options for intersphinx extension ---------------------------------------
+# True to prefix each section label with the name of the document it is in, followed by a colon. For
+# example, index:Introduction for a section called Introduction that appears in document
+# index.rst. Useful for avoiding ambiguity when the same section heading appears in different
+# documents.
+#
+# Note: This stops "normal" links from working, so it should be kept at False.
+# autosectionlabel_prefix_document = True
+
+# -- Options for intersphinx -------------------------------------------------
+
+# https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#confval-intersphinx_mapping
+intersphinx_mapping = {
+    "python": ("https://docs.python.org/", None),
+    "caosdb-mysqlbackend": ("https://docs.indiscale.com/caosdb-mysqlbackend/",
+                            None),
+    "caosdb-server": ("https://docs.indiscale.com/caosdb-server/", None),
+}
 
-# Example configuration for intersphinx: refer to the Python standard library.
-intersphinx_mapping = {'https://docs.python.org/': None}
 
 # TODO Which options do we want?
 autodoc_default_options = {
diff --git a/src/doc/configuration.md b/src/doc/configuration.md
index b2de2781d5adff4d59cb3648cd912e142327f676..6e53542f661dcae94622fef24a67cecf7491df9c 100644
--- a/src/doc/configuration.md
+++ b/src/doc/configuration.md
@@ -50,5 +50,5 @@ debugging (which I hope will not be necessary for this tutorial) or if you want
 the internals of the protocol. 
 
 A complete list of options can be found in the 
-[pycaosdb.ini file](https://gitlab.com/caosdb/caosdb-pylib/-/blob/master/examples/pycaosdb.ini) in 
+[pycaosdb.ini file](https://gitlab.com/caosdb/caosdb-pylib/-/blob/main/examples/pycaosdb.ini) in 
 the examples folder of the source code.
diff --git a/src/doc/index.rst b/src/doc/index.rst
index e8cc93aa398de36c30833bc97ca79ce20564daa6..bd29c6c56acf5c173e94ae6471a6aeba56ea4b93 100644
--- a/src/doc/index.rst
+++ b/src/doc/index.rst
@@ -10,13 +10,14 @@ Welcome to PyCaosDB's documentation!
    Getting started <README_SETUP>
    tutorials/index
    Concepts <concepts>
-      Configuration <configuration>
-   API documentation<_apidoc/modules>
+   Configuration <configuration>
+   Administration <administration>
+   API documentation<_apidoc/caosdb>
 
 This is the documentation for the Python client library for CaosDB, ``PyCaosDB``.
 
-This documentation helps you to :doc:`get started<getting_started>`, explains the most important
-:doc:`concepts<concepts>` and offers a range of :doc:`tutorials<tutorials>`.
+This documentation helps you to :doc:`get started<README_SETUP>`, explains the most important
+:doc:`concepts<concepts>` and offers a range of :doc:`tutorials<tutorials/index>`.
 
 
 Indices and tables
diff --git a/src/doc/tutorials/Data-Insertion.rst b/src/doc/tutorials/Data-Insertion.rst
new file mode 100644
index 0000000000000000000000000000000000000000..22fb9461d6916003b2dad496ff3487df335c8dcc
--- /dev/null
+++ b/src/doc/tutorials/Data-Insertion.rst
@@ -0,0 +1,173 @@
+Data Insertion
+==============
+
+Data Models
+~~~~~~~~~~~
+
+Data is stored and structured in CaosDB using a concept of RecordTypes, Properties, Records etc. If
+you do not know what these are, please look at the chapter :doc:`Data
+Model<caosdb-server:Data-Model>` in the CaosDB server documentation.
+
+In order to insert some actual data, we need to create a data model
+using RecordTypes and Properties (You may skip this if you use a CaosDB
+instance that already has the required types). So, let’s create a simple
+Property called “a” of datatype double. This is very easy in pylib:
+
+.. code:: python
+
+   a = db.Property(name="a", datatype=db.DOUBLE)
+
+There are a few basic datatypes: db.INTEGER, db.TEXT. See `data
+type <Specification/Datatype>`__ for a full list.
+
+We can create our own small data model for e.g. a simulation by adding
+two more Properties and a RecordType:
+
+.. code:: python
+
+   b = db.Property(name="b", datatype=db.DOUBLE)
+   epsilon = db.Property(name="epsilon", datatype=db.DOUBLE)
+   recordtype = db.RecordType(name="BarkleySimulation")
+   recordtype.add_property(a)
+   recordtype.add_property(b)
+   recordtype.add_property(epsilon)
+   container = db.Container()
+   container.extend([a, b, epsilon, recordtype])
+   container.insert()
+
+.. _tutorial-inheritance-properties:
+
+Inheritance of Properties
+-------------------------
+
+Suppose you want to create a new RecordType “2D_BarkleySimulation”
+that denotes spatially extended Barkley simulations. This is a subtype
+of the “BarkleySimulation” RecordType above and should have all its
+parameters, i.e., properties. It may be assigned more, e.g., spatial
+resolution, but we'll omit this for the sake of brevity for now.
+
+.. code:: python
+
+   rt = db.RecordType(name="2D_BarkleySimulation",
+	              description="Spatially extended Barkley simulation")
+   # inherit all properties from the BarkleySimulation RecordType
+   rt.add_parent(name="BarkleySimulation", inheritance="all")
+   rt.insert()
+       
+   print(rt.get_property(name="epsilon").importance) ### rt has a "epsilon" property with the same importance as "BarkleySimulation"
+
+The parameter ``inheritance=(obligatory|recommended|fix|all|none)`` of
+:py:meth:`Entity.add_parent()<caosdb.common.models.Entity.add_parent>` tells the server to assign
+all properties of the parent RecordType with the chosen importance (and properties with a higher
+importance) to the child RecordType
+automatically upon insertion. See the chapter on `importance
+<https://docs.indiscale.com/caosdb-server/specification/RecordType.html#importance>`_ in the
+documentation of the CaosDB server for more information on the importance and inheritance of
+properties.
+
+.. note::
+
+   The inherited properties will only be visible after the insertion since they are set by the
+   CaosDB server, not by the Python client.
+
+
+Insert Actual Data
+~~~~~~~~~~~~~~~~~~
+
+Suppose the RecordType “Experiment” and the Property “date” exist in the
+database. You can then create single data Records by using the
+corresponding python class:
+
+.. code:: python
+
+   rec = db.Record()
+   rec.add_parent(name="Experiment")
+   rec.add_property(name="date", value="2020-01-07")
+   rec.insert()
+
+Here, the record has a parent: The RecordType “Experiment”. And a
+Property: date.
+
+Note, that if you want to use a property that is not a primitive
+datatype like db.INTEGER and so on, you need to use the ID of the Entity
+that you are referencing.
+
+.. code:: python
+
+   rec = db.Record()
+   rec.add_parent(name="Experiment")
+   rec.add_property(name="report", value=235507)
+   rec.add_property(name="Analysis", value=230007)
+   rec.insert()
+
+Of course, the IDs 235507 and 230007 need to exist in CaosDB. The first
+example shows how to use a db.REFERENCE Property (report) and the second
+shows that you can use any RecordType as Property to reference a Record
+that has such a parent.
+
+Most Records do not have name however it can absolutely make sense. In
+that case use the name argument when creating it. Another useful feature
+is the fact that properties can have units:
+
+.. code:: python
+
+   rec = db.Record("DeviceNo-AB110")
+   rec.add_parent(name="SlicingMachine")
+   rec.add_property(name="weight", value="1749", unit="kg")
+   rec.insert()
+
+If you are in some kind of analysis you can do this in batch mode with a
+container. E.g. if you have a python list ``analysis_results``:
+
+.. code:: python
+
+   cont = db.Container()
+   for date, result in analysis_results:
+      rec = db.Record()
+      rec.add_parent(name="Experiment")
+      rec.add_property(name="date", value=date)
+      rec.add_property(name="result", value=result)
+      cont.append(rec)
+
+   cont.insert()
+
+Useful is also, that you can insert directly tabular data.
+
+.. code:: python
+
+   from caosadvancedtools.table_converter import from_tsv     
+          
+   recs = from_tsv("test.csv", "Experiment")     
+   print(recs)     
+   recs.insert()  
+
+With this example file
+`test.csv <uploads/4f2c8756a26a3984c0af09d206d583e5/test.csv>`__.
+
+
+File Update
+-----------
+
+Updating an existing file by uploading a new version.
+
+1. Retrieve the file record of interest, e.g. by ID:
+
+.. code:: python
+
+   import caosdb as db
+
+   file_upd = db.File(id=174).retrieve()
+
+2. Set the new local file path. The remote file path is stored in the
+   file object as ``file_upd.path`` while the local path can be found in
+   ``file_upd.file``.
+
+.. code:: python
+
+   file_upd.file = "./supplements.pdf"
+
+3. Update the file:
+
+.. code:: python
+
+   file_upd.update()
diff --git a/src/doc/tutorials/data-model-interface.md b/src/doc/tutorials/data-model-interface.md
new file mode 100644
index 0000000000000000000000000000000000000000..f6967c57a0a3de6e7c6fd3d2b64d3f59620526de
--- /dev/null
+++ b/src/doc/tutorials/data-model-interface.md
@@ -0,0 +1,36 @@
+# Data Models
+
+
+
+You also want to change the datamodel? Also call
+```bash
+pip3 install --user --no-deps .
+```
+in 
+```bash
+CaosDB/data_models
+```
+
+Change to the appropriate directory
+```bash
+cd CaosDB/data_models
+```
+There are "data models" defined in 
+```bash
+caosdb_models
+```
+having an ending like "_model.py"
+A set of data models is also considered to be a model
+You can create an UML representation of a model or a set of models by calling
+```bash
+./model_interface.py -u model_name [model_name2]
+```
+If you have troubles look at
+```bash
+./model_interface.py -h
+```
+You can change existing models (but be careful! I hope you know what you are doing) or add new ones by changing the appropriate files or adding a new XXXX_model.py
+Once you are done, you can sync your changes with the server
+```bash
+./model_interface.py -s model_name [model_name2]
+```
diff --git a/src/doc/tutorials/errors.rst b/src/doc/tutorials/errors.rst
new file mode 100644
index 0000000000000000000000000000000000000000..37c53c9b527a0435f9f24ae6c6e71687e73eb963
--- /dev/null
+++ b/src/doc/tutorials/errors.rst
@@ -0,0 +1,176 @@
+
+Error Handling
+==============
+
+In case of erroneous transactions, connection problems and a lot of
+other cases, PyCaosDB may raise specific errors in order to pinpoint
+the problem as precisely as possible. Some of these errors a
+representations of errors in the CaosDB server, others stem from
+problems that occurred on the client side.
+
+The errors and exceptions are ordered hierarchically form the most
+general exceptions to specific transaction or connection problems. The
+most important error types and the hierarchy will be explained in the
+following. For more information on specific error types, see also the
+:doc:`source code<../_apidoc/caosdb.exceptions>`.
+
+.. note::
+
+   Starting from PyCaosDB 0.5, the error handling has changed
+   significantly. New error classes have been introduced and the
+   behavior of ``TransactionError`` and ``EntityError`` has been
+   re-worked. In the following, only the "new" errors are
+   discussed. Please refer to the documentation of PyCaosDB 0.4.1 and
+   earlier for the old error handling.
+
+CaosDBException
+----------------
+
+``CaosDBException`` is the most generic exception and all other error classes inherit
+from this one. Because of its generality, it doesn't tell you much
+except that some component of PyCaosDB raised an exception. If you
+want to catch all possible CaosDB errors, this is the class to use.
+
+TransactionError
+----------------
+
+Every transaction (calling ``insert``, ``update``, ``retrieve``, or
+``delete`` on a container or an entity) may finish with errors. They
+indicate, for instance, that an entity does not exist or that you need
+to specify a data type for your property and much more. If and only if
+one or more errors occur during a transaction a ``TransactionError``
+will be raised by the transaction method. The ``TransactionError``
+class is a container for all errors which occur during a
+transaction. It usually contains one or more :ref:`entity
+errors<EntityError>` which you can inspect in order to learn why the
+transaction failed. For this inspection, there are some helpful
+attributes and methods provided by the ``TransactionError``:
+
+* ``entities``: a list of all entities that directly caused at least one error
+  in this transaction.
+
+* ``errors``: a list of all ``EntityError`` objects that directly caused the
+  transaction to fail.
+
+* ``all_entities``, ``all_errors``: sets of all entities and errors
+  that, directly or indirectly, caused either this ``TransactionError`` or any of the
+  ``EntityError`` objects it contains.
+
+* ``has_error(error_t)``: Check whether an error of type ``error_t``
+  occurred during the transaction.
+
+Additionally, ``print(transaction_error)`` prints a tree-like
+representation of all errors regarding the transaction in question.
+
+EntityError
+-----------
+
+An ``EntityError`` specifies the entity and the error proper that
+caused a transaction to fail. It is never raised on its own but is
+contained in a ``TransactionError`` (which may or may not contain
+other ``EntityError`` objects) which is then raised. ``EntityError``
+has several :ref:`subclasses<Special Errors>` that further specify the
+error that occurred.
+
+The ``EntityError`` class is in fact a subclass of
+``TransactionError``. Thus, it has the same methods and attributes as
+the ``TransactionError`` explained
+:ref:`above<TransactionError>`. This is important in case of an
+``EntityError`` that was caused by other faulty entities (e.g., broken
+parents or properties). In that case these problematic entities and
+errors can again be inspected by visiting the ``entities`` and
+``errors`` lists as above.
+
+Special Errors
+~~~~~~~~~~~~~~
+
+Subclasses of ``EntityError`` for special purposes:
+
+* ``EntityDoesNotExistError``
+
+* ``EntityHasNoDataTypeError``
+
+* ``UniqueNamesError``
+
+* ``UnqualifiedParentsError``
+
+* ``UnqualifiedPropertiesError``
+
+* ``ConsistencyError``
+
+* ``AuthorizationError``
+
+* ``AmbiguousEntityError``
+
+BadQueryError
+-------------
+
+A ``BadQueryError`` is raised when a query could not be processed by
+the server. In contrast to a ``TransactionError`` it is not
+necessarily caused by problematic entities or
+containers. ``BadQueryError`` has the two important subclasses
+``EmptyUniqueQueryError`` and ``QueryNotUniqueError`` for queries with
+``unique=True`` which found no or ambiguous entities, respectively.
+
+HTTP Errors
+-----------
+
+An ``HTTPClientError`` or an ``HTTPServerError`` is raised in case of
+http(s) connection problems caused by the Python client or the CaosDB
+server, respectively. There are the following subclasses of
+``HTTPClientError`` that are used to specify the connection problem:
+
+* ``HTTPURITooLongError``: The URI of the request was too long to be
+  processed by the server.
+
+* ``HTTPForbiddenError``: You're not allowed to access this resource.
+
+* ``HTTPResourceNotFoundError``: The requested resource doesn't exist.
+
+Other Errors
+------------
+
+There are further subclasses of ``CaosDBException`` that are raised in
+case of faulty configurations or other problems. They should be rather
+self-explanatory from their names; see the :doc:`source code<../_apidoc/caosdb.exceptions>`
+for further information.
+
+* ``ConfigurationError``
+
+* ``LoginFailedError``
+
+* ``MismatchingEntitiesError``
+
+* ``ServerConfigurationException``
+
+Examples
+--------
+
+.. code-block:: python3
+
+   import caosdb as db
+
+   def link_and_insert(entity, linked, link=True):
+     """Link the ENTITY to LINKED and insert it."""
+     if link:
+       entity.add_property(db.Property(name="link", value=linked))
+     try:
+       entity.insert()
+     except db.TransactionError as tre:
+       # Unique names problem may be worked around by using another name
+       if tre.has_error(db.UniqueNamesError):
+         for ent_error in tre.errors:
+           if (isinstance(ent_error, db.UniqueNamesError)
+               and entity in ent_error.entities):
+             entity.name = entity.name + "_new"  # Try again with new name.
+             link_and_insert(entity, linked, link=False)
+             break
+       # Unqualified properties will be handled by the caller
+       elif tre.has_error(db.UnqualifiedPropertiesError):
+         for ent_error in tre.errors:
+           if (isinstance(ent_error, db.UnqualifiedPropertiesError_
+               and entity in ent_error.entities):
+             raise RuntimeError("One of the properties was unqualified: " + str(ent_error))
+       # Other problems are not covered by this tutorial
+       else:
+         raise NotImplementedError("Unhandled TransactionError: " + str(tre))
diff --git a/src/doc/tutorials/first_steps.rst b/src/doc/tutorials/first_steps.rst
index 3da05b05c4d4025f6d81490550fd1482ae147fb4..34b96bbeca416107fb34feb4707b9ef46fc49fe7 100644
--- a/src/doc/tutorials/first_steps.rst
+++ b/src/doc/tutorials/first_steps.rst
@@ -2,14 +2,15 @@ First Steps
 ===========
 
 You should have a working connection to a CaosDB instance now. If not, please check out the 
-:doc:`Getting Started secton</getting_started>`.
+:doc:`Getting Started secton</README_SETUP>`.
 
 If you are not yet familiar with Records, RecordTypes and Properties used in CaosDB,
-please check out the respective part in the `Web Interface Tutorial`_. 
-You should also know the basics of the CaosDB Query Language (a tutorial is here_).
+please check out the respective part in the `Web Interface tutorial`_.
+You should also know the basics of the CaosDB Query Language (a tutorial is
+`here <https://docs.indiscale.com/caosdb-webui/tutorials/query.html>`_).
 
-We recommend that you connect to the demo instance in order to try out the following
-examples. You can do this with
+We recommend that you connect to the `demo instance`_ (hosted by `Indiscale`_) in order to try out
+the following examples. You can do this with
 
 >>> import caosdb as db
 >>> _ = db.configure_connection(
@@ -19,7 +20,7 @@ examples. You can do this with
 ...    password="caosdb")
 
 or by using corresponding settings in the configuration file
-(see :doc:`Getting Started secton</getting_started>`.). 
+(see :doc:`Getting Started secton</README_SETUP>`.).
 However, you can also translate the examples to the data model that you have at hand.
 
 Let's start with a simple query.
@@ -113,6 +114,8 @@ You can download files (if the LinkAhead server has access to them)
 The file will be saved under target_path.
 If the files are large data files, it is often a better idea to only retrieve the path of the file and access them via a local mount.
 
+
+
 Summary
 -------
 
@@ -122,9 +125,6 @@ the result Records and their properties.
 The next tutorial shows how to make some meaningful use of this.
 
 
-
-.. _here: https://gitlabio.something
 .. _`demo instance`: https://demo.indiscale.com
 .. _`IndiScale`: https://indiscale.com
-.. _`Web Interface Tutorial`: https://caosdb.gitlab.io/caosdb-webui/tutorials/model.html
-.. _here: https://caosdb.gitlab.io/caosdb-webui/tutorials/cql.html
+.. _`Web Interface tutorial`: https://docs.indiscale.com/caosdb-webui/tutorials/first_steps.html
diff --git a/src/doc/tutorials/index.rst b/src/doc/tutorials/index.rst
index 311f7080045e8925dd26eb03c2da3b12b1dba6e4..3889edb8f47e973cc7ae25c9134d75cfeab95f65 100644
--- a/src/doc/tutorials/index.rst
+++ b/src/doc/tutorials/index.rst
@@ -12,4 +12,7 @@ advanced usage of the Python client.
 
    first_steps
    basic_analysis
+   Data-Insertion
+   errors
+   data-model-interface
 
diff --git a/unittests/docker/Dockerfile b/unittests/docker/Dockerfile
index e41fb91b3e9ae54cff277519c03f481c21264e9e..7fa3f75bd198724628dee48ab328829fa071a639 100644
--- a/unittests/docker/Dockerfile
+++ b/unittests/docker/Dockerfile
@@ -5,6 +5,6 @@ RUN apt-get update && \
       curl pycodestyle \
       python3-sphinx
 ARG COMMIT="dev"
-RUN git clone -b dev https://gitlab.com/caosdb/caosdb-pylib.git && \
+RUN git clone -b dev https://gitlab.indiscale.com/caosdb/src/caosdb-pylib.git && \
     cd caosdb-pylib && git checkout $COMMIT && pip3 install .
 RUN pip3 install recommonmark sphinx-rtd-theme
diff --git a/unittests/test_authentication_auth_token.py b/unittests/test_authentication_auth_token.py
index 1eaf091863e23205c9ffca5373e51b654a5a42e4..15e54121fc0d7b5c2be645cdb88bc20804a10980 100644
--- a/unittests/test_authentication_auth_token.py
+++ b/unittests/test_authentication_auth_token.py
@@ -32,7 +32,7 @@ from unittest.mock import Mock
 from caosdb.connection.authentication import auth_token as at
 from caosdb.connection.mockup import MockUpServerConnection, MockUpResponse
 from caosdb.connection.utils import parse_auth_token
-from caosdb.exceptions import LoginFailedException
+from caosdb.exceptions import LoginFailedError
 from caosdb import configure_connection
 
 
@@ -73,7 +73,7 @@ def test_login_raises():
     c = configure_connection(url="https://example.com",
                              password_method="auth_token",
                              auth_token="[auth_token]")
-    with raises(LoginFailedException):
+    with raises(LoginFailedError):
         c._login()
 
 
diff --git a/unittests/test_authentication_unauthenticated.py b/unittests/test_authentication_unauthenticated.py
index 9ea864a9999c5e3a74fa22fd3f6942c4e5806256..52146b08ed4e1026660eebacedf348aeb2ff2721 100644
--- a/unittests/test_authentication_unauthenticated.py
+++ b/unittests/test_authentication_unauthenticated.py
@@ -32,7 +32,7 @@ from unittest.mock import Mock
 from caosdb.connection.authentication import unauthenticated
 from caosdb.connection.mockup import MockUpServerConnection, MockUpResponse
 from caosdb.connection.utils import parse_auth_token
-from caosdb.exceptions import LoginFailedException
+from caosdb.exceptions import LoginFailedError
 from caosdb import configure_connection
 from .test_authentication_auth_token import response_with_auth_token
 
@@ -67,5 +67,5 @@ def test_configure_connection():
 def test_login_raises():
     c = configure_connection(url="https://example.com",
                              password_method="unauthenticated")
-    with raises(LoginFailedException):
+    with raises(LoginFailedError):
         c._login()
diff --git a/unittests/test_configuration.py b/unittests/test_configuration.py
index 76445b6f262120d6a29c73527a9bf042f85f8a05..b135e7cd65b11be7cb6c4ef2237a41a6639ccbb7 100644
--- a/unittests/test_configuration.py
+++ b/unittests/test_configuration.py
@@ -22,19 +22,45 @@
 # ** end header
 #
 
+import pytest
 import caosdb as db
+from os import environ, getcwd, remove
+from os.path import expanduser, isfile, join
 from pytest import raises
 
 
-def test_config_ini_via_envvar():
-    from os import environ
-    from os.path import expanduser
+@pytest.fixture
+def temp_ini_files():
+    created_temp_ini_cwd = False
+    created_temp_ini_home = False
+    if not isfile(join(getcwd(), "pycaosdb.ini")):
+        open("pycaosdb.ini", 'a').close()  # create temporary ini file
+        created_temp_ini_cwd = True
+    if not isfile(expanduser("~/.pycaosdb.ini")):
+        open(expanduser("~/.pycaosdb.ini"), 'a').close()  # create temporary ini file in home directory
+        created_temp_ini_home = True
+    yield 0
+    if created_temp_ini_cwd:
+        remove("pycaosdb.ini")
+    if created_temp_ini_home:
+        remove(expanduser("~/.pycaosdb.ini"))
+    environ["PYCAOSDBINI"] = "~/.pycaosdb.ini"
+
+
+def test_config_ini_via_envvar(temp_ini_files):
 
     with raises(KeyError):
         environ["PYCAOSDBINI"]
 
     environ["PYCAOSDBINI"] = "bla bla"
     assert environ["PYCAOSDBINI"] == "bla bla"
-    assert db.configuration.configure(environ["PYCAOSDBINI"]) == []
+    # test wrong configuration file in envvar
+    assert not expanduser(environ["PYCAOSDBINI"]) in db.configuration._read_config_files()
+    # test good configuration file in envvar
     environ["PYCAOSDBINI"] = "~/.pycaosdb.ini"
-    assert db.configuration.configure(expanduser(environ["PYCAOSDBINI"])) == [expanduser("~/.pycaosdb.ini")]
+    assert expanduser("~/.pycaosdb.ini") in db.configuration._read_config_files()
+    # test without envvar
+    environ.pop("PYCAOSDBINI")
+    assert expanduser("~/.pycaosdb.ini") in db.configuration._read_config_files()
+    # test configuration file in cwd
+    assert join(getcwd(), "pycaosdb.ini") in db.configuration._read_config_files()
diff --git a/unittests/test_connection.py b/unittests/test_connection.py
index 1dfd1fce730d4d4ba627eb4c27830b0c5156fb29..16370f00b7d5e3389582befaac1762b1d2992fcf 100644
--- a/unittests/test_connection.py
+++ b/unittests/test_connection.py
@@ -37,7 +37,7 @@ from caosdb.connection.connection import (CaosDBServerConnection,
 from caosdb.connection.mockup import (MockUpResponse, MockUpServerConnection,
                                       _request_log_message)
 from caosdb.connection.utils import make_uri_path, quote, urlencode
-from caosdb.exceptions import ConfigurationException, LoginFailedException
+from caosdb.exceptions import ConfigurationError, LoginFailedError
 from nose.tools import assert_equal as eq
 from nose.tools import assert_false as falz
 from nose.tools import assert_is_not_none as there
@@ -234,7 +234,7 @@ def test_test_request_with_two_responses():
 
 def test_missing_implementation():
     connection = configure_connection()
-    with raises(ConfigurationException) as exc_info:
+    with raises(ConfigurationError) as exc_info:
         connection.configure()
     assert exc_info.value.args[0].startswith(
         "Missing CaosDBServerConnection implementation.")
@@ -242,27 +242,26 @@ def test_missing_implementation():
 
 def test_bad_implementation_not_callable():
     connection = configure_connection()
-    with raises(ConfigurationException) as exc_info:
+    with raises(ConfigurationError) as exc_info:
         connection.configure(implementation=None)
     assert exc_info.value.args[0].startswith(
         "Bad CaosDBServerConnection implementation.")
-    assert exc_info.value.args[1].args[0] == "'NoneType' object is not callable"
+    assert "'NoneType' object is not callable" in exc_info.value.args[0]
 
 
 def test_bad_implementation_wrong_class():
     connection = configure_connection()
-    with raises(ConfigurationException) as exc_info:
+    with raises(ConfigurationError) as exc_info:
         connection.configure(implementation=dict)
     assert exc_info.value.args[0].startswith(
         "Bad CaosDBServerConnection implementation.")
-    assert exc_info.value.args[1].args[0] == (
-        "The `implementation` callable did not return an instance of "
-        "CaosDBServerConnection.")
+    assert ("The `implementation` callable did not return an instance of "
+            "CaosDBServerConnection.") in exc_info.value.args[0]
 
 
 def test_missing_auth_method():
     connection = configure_connection()
-    with raises(ConfigurationException) as exc_info:
+    with raises(ConfigurationError) as exc_info:
         connection.configure(implementation=MockUpServerConnection)
     assert exc_info.value.args[0].startswith("Missing password_method.")
 
@@ -272,11 +271,12 @@ def test_missing_password():
     connection.configure(implementation=setup_two_resources,
                          password_method="plain")
     connection._authenticator.auth_token = "[test-auth-token]"
-    assert connection.retrieve(["some"]).headers["Cookie"] == "SessionToken=%5Btest-auth-token%5D;"
+    assert connection.retrieve(
+        ["some"]).headers["Cookie"] == "SessionToken=%5Btest-auth-token%5D;"
 
     connection.configure(implementation=setup_two_resources,
                          password_method="plain")
-    with raises(LoginFailedException):
+    with raises(LoginFailedError):
         connection.delete(["401"])
 
 
@@ -284,11 +284,13 @@ def test_auth_token_connection():
     connection = configure_connection(auth_token="blablabla",
                                       password_method="auth_token",
                                       implementation=setup_two_resources)
-    connection.retrieve(["some"]).headers["Cookie"] == "SessionToken=blablabla;"
+    connection.retrieve(
+        ["some"]).headers["Cookie"] == "SessionToken=blablabla;"
 
     connection._logout()
-    with raises(LoginFailedException) as cm:
-        connection.retrieve(["some"]).headers["Cookie"] == "SessionToken=blablabla;"
+    with raises(LoginFailedError) as cm:
+        connection.retrieve(
+            ["some"]).headers["Cookie"] == "SessionToken=blablabla;"
     assert cm.value.args[0] == ("The authentication token is expired or you "
                                 "have been logged out otherwise. The "
                                 "auth_token authenticator cannot log in "
diff --git a/unittests/test_connection_utils.py b/unittests/test_connection_utils.py
index c21b453e3a7588f30d86be8d2ed39bb4d7a1d31e..3890ae05cfe38b78a5ba0829753420246bdb560d 100644
--- a/unittests/test_connection_utils.py
+++ b/unittests/test_connection_utils.py
@@ -28,7 +28,7 @@ from pytest import raises
 from nose.tools import (assert_equal as eq, assert_raises as raiz, assert_true
                         as tru, assert_is_not_none as there, assert_false as
                         falz)
-from caosdb.exceptions import ConfigurationException, LoginFailedException
+from caosdb.exceptions import ConfigurationError, LoginFailedError
 from caosdb.connection.utils import parse_auth_token, auth_token_to_cookie
 from caosdb.connection.connection import (
     configure_connection, CaosDBServerConnection,
@@ -45,8 +45,10 @@ def setup_module():
 
 
 def test_parse_auth_token():
-    assert parse_auth_token("SessionToken=%5Bblablabla%5D; expires=bla; ...") == "[blablabla]"
+    assert parse_auth_token(
+        "SessionToken=%5Bblablabla%5D; expires=bla; ...") == "[blablabla]"
 
 
 def test_auth_token_to_cookie():
-    assert auth_token_to_cookie("[blablabla]") == "SessionToken=%5Bblablabla%5D;"
+    assert auth_token_to_cookie(
+        "[blablabla]") == "SessionToken=%5Bblablabla%5D;"
diff --git a/unittests/test_error_handling.py b/unittests/test_error_handling.py
new file mode 100644
index 0000000000000000000000000000000000000000..7f974e7db826d093e335b250953658b08db062cd
--- /dev/null
+++ b/unittests/test_error_handling.py
@@ -0,0 +1,317 @@
+# -*- encoding: utf-8 -*-
+#
+# ** header v3.0
+# This file is a part of the CaosDB Project.
+#
+# Copyright (C) 2020 Indiscale GmbH <info@indiscale.com>
+# Copyright (C) 2020 Florian Spreckelsen <f.spreckelsen@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 the new (as of June 2020) error handling. All errors should
+be TransactionErrors at first which may have one or more level of
+children.
+
+"""
+import caosdb as db
+from caosdb.common.models import raise_errors
+from caosdb.exceptions import (AuthorizationError,
+                               EntityDoesNotExistError, EntityError,
+                               EntityHasNoDatatypeError,
+                               TransactionError, UniqueNamesError,
+                               UnqualifiedParentsError,
+                               UnqualifiedPropertiesError)
+
+from pytest import raises
+
+
+# #################### Single Error Tests ####################
+
+
+def _add_error_message_to_entity(entity, code, description="Error message"):
+    """Attach error message with code and description to entity"""
+    message = db.Message(type="Error", code=code,
+                         description=description)
+    entity.add_message(message)
+
+    return entity
+
+
+def test_has_no_datatype_error():
+    """Code 110; property without datatype"""
+    code = 110
+    prop = _add_error_message_to_entity(db.Property(name="TestProp"),
+                                        code)
+    with raises(TransactionError) as e:
+        raise_errors(prop)
+    # There should be exactly one child
+    assert len(e.value.errors) == 1
+    err = e.value.errors[0]
+    # check type and entity of only child
+    assert isinstance(err, EntityHasNoDatatypeError)
+    assert err.entity.name == prop.name
+
+
+def test_entity_does_not_exist_error():
+    """Code 101; entity does not exist"""
+    code = 101
+    ent = _add_error_message_to_entity(db.Entity(name="TestEnt"),
+                                       code)
+    with raises(TransactionError) as e:
+        raise_errors(ent)
+    # There should be exactly one child
+    assert len(e.value.errors) == 1
+    err = e.value.errors[0]
+    # check type and entity of only child
+    assert isinstance(err, EntityDoesNotExistError)
+    assert err.entity.name == ent.name
+
+
+def test_entity_error():
+    """Code 0; most basic."""
+    code = 0
+    ent = _add_error_message_to_entity(db.Entity(name="TestEnt"),
+                                       code)
+    with raises(TransactionError) as e:
+        raise_errors(ent)
+    assert len(e.value.errors) == 1
+    err = e.value.errors[0]
+    assert isinstance(err, EntityError)
+    assert err.entity.name == ent.name
+
+
+def test_unique_names_error():
+    """Code 152; name is not unique"""
+    code = 152
+    ent = _add_error_message_to_entity(db.Entity(name="TestEnt"),
+                                       code)
+    with raises(TransactionError) as e:
+        raise_errors(ent)
+    assert len(e.value.errors) == 1
+    err = e.value.errors[0]
+    assert isinstance(err, UniqueNamesError)
+    assert err.entity.name == ent.name
+
+
+def test_authorization_exception():
+    """Code 403; transaction not allowed"""
+    code = 403
+    ent = _add_error_message_to_entity(db.Entity(name="TestEnt"),
+                                       code)
+    with raises(TransactionError) as e:
+        raise_errors(ent)
+    assert len(e.value.errors) == 1
+    err = e.value.errors[0]
+    assert isinstance(err, AuthorizationError)
+    assert err.entity.name == ent.name
+
+
+def test_empty_container_with_error():
+    """Has to raise an error, even though container is empty."""
+    code = 0
+    cont = _add_error_message_to_entity(db.Container(), code)
+    with raises(TransactionError) as e:
+        raise_errors(cont)
+    # No entity errors
+    assert len(e.value.errors) == 0
+    assert e.value.container == cont
+    assert int(e.value.code) == code
+
+
+def test_faulty_container_with_healthy_entities():
+    """Raises a TransactionError without any EntityErrors since only the
+    container, but none of its entities has an error.
+
+    """
+    code = 0
+    cont = _add_error_message_to_entity(db.Container(), code)
+    cont.append(db.Entity("TestHealthyEnt1"))
+    cont.append(db.Entity("TestHealthyEnt2"))
+    with raises(TransactionError) as e:
+        raise_errors(cont)
+    # No entity errors
+    assert len(e.value.errors) == 0
+    assert len(e.value.entities) == 0
+    assert e.value.container == cont
+    assert int(e.value.code) == code
+
+
+# #################### Children with children ####################
+
+
+def test_unqualified_parents_error():
+    """Code 116; parent does not exist"""
+    code = 116
+    entity_does_not_exist_code = 101
+    parent = _add_error_message_to_entity(
+        db.RecordType(name="TestParent"),
+        entity_does_not_exist_code)
+    rec = _add_error_message_to_entity(db.Record(name="TestRecord"),
+                                       code)
+    rec.add_parent(parent)
+    with raises(TransactionError) as e:
+        raise_errors(rec)
+    te = e.value
+    # One direct child, two errors in total
+    assert len(te.errors) == 1
+    assert len(te.all_errors) == 2
+    # UnqualifiedParentsError in Record ...
+    assert isinstance(te.errors[0], UnqualifiedParentsError)
+    assert te.errors[0].entity.name == rec.name
+    # ... caused by non-existing parent
+    assert isinstance(te.errors[0].errors[0], EntityDoesNotExistError)
+    assert te.errors[0].errors[0].entity.name == parent.name
+
+
+def test_unqualified_properties_error():
+    """Code 114; properties do not exist or have wrong data types or
+    values.
+
+    """
+    code = 114
+    entity_code = 0
+    no_entity_code = 101
+    prop1 = _add_error_message_to_entity(db.Property(
+        name="TestProp1"), entity_code)
+    prop2 = _add_error_message_to_entity(db.Property(
+        name="TestProp2"), no_entity_code)
+    rec = _add_error_message_to_entity(db.Record(name="TestRecord"),
+                                       code)
+    rec.add_property(prop1).add_property(prop2)
+    with raises(TransactionError) as e:
+        raise_errors(rec)
+    te = e.value
+    assert len(te.errors) == 1
+    upe = te.errors[0]
+    assert upe.entity.name == rec.name
+    assert len(upe.errors) == 2
+    for error_t in [UnqualifiedPropertiesError, EntityError,
+                    EntityDoesNotExistError]:
+        assert any([isinstance(x, error_t) for x in te.all_errors])
+    assert upe.code == code
+
+
+# #################### Multiple errors ####################
+
+def test_parent_and_properties_errors():
+    """Record with UnqualifiedParentsError and UnqualifiedPropertiesError,
+    and corresponding parent and properties with their errors as
+    above. Test whether all levels are in order.
+
+    """
+    prop_code = 114
+    parent_code = 116
+    entity_code = 0
+    no_entity_code = 101
+    parent = _add_error_message_to_entity(
+        db.RecordType(name="TestParent"), no_entity_code)
+    prop1 = _add_error_message_to_entity(db.Property(
+        name="TestProp1"), entity_code)
+    prop2 = _add_error_message_to_entity(db.Property(
+        name="TestProp2"), no_entity_code)
+    rec = _add_error_message_to_entity(db.Record(name="TestRecord"),
+                                       prop_code)
+    rec = _add_error_message_to_entity(rec, parent_code)
+    rec.add_parent(parent)
+    rec.add_property(prop1).add_property(prop2)
+    with raises(TransactionError) as e:
+        raise_errors(rec)
+    # Now there should be two direct children; both have to be
+    # displayed correctly.
+    te = e.value
+    # exactly two children:
+    assert len(te.errors) == 2
+    # both have to have the right codes and entities
+    found_parent = False
+    found_prop = False
+    for err in te.errors:
+        if err.code == parent_code:
+            found_parent = True
+            assert err.errors[0].entity.name == parent.name
+            assert prop1.name not in [x.name for x in
+                                      err.all_entities]
+            assert prop2.name not in [x.name for x in
+                                      err.all_entities]
+        elif err.code == prop_code:
+            found_prop = True
+            assert parent.name not in [x.name for x in
+                                       err.all_entities]
+            for sub_err in err.errors:
+                if sub_err.code == entity_code:
+                    assert sub_err.entity.name == prop1.name
+                elif sub_err.code == no_entity_code:
+                    assert sub_err.entity.name == prop2.name
+    assert found_parent
+    assert found_prop
+
+
+def test_container_with_faulty_elements():
+    """Code 12; container with valid and invalid entities. All faulty
+    entities have to be reflected correctly in the errors list of the
+    TransactionError raised by the container.
+
+    """
+    container_code = 12
+    prop_code = 114
+    parent_code = 116
+    name_code = 152
+    auth_code = 403
+    entity_code = 0
+    no_entity_code = 101
+    # Broken parents and properties
+    parent = _add_error_message_to_entity(
+        db.RecordType(name="TestParent"), no_entity_code)
+    prop1 = _add_error_message_to_entity(db.Property(
+        name="TestProp1"), entity_code)
+    prop2 = _add_error_message_to_entity(db.Property(
+        name="TestProp2"), no_entity_code)
+    cont = _add_error_message_to_entity(db.Container(),
+                                        container_code)
+    # healthy record and property
+    good_rec = db.Record(name="TestRecord1")
+    good_prop = db.Property(name="TestProp3")
+    cont.extend([good_rec, good_prop])
+    # broken records with single and multiole errors
+    rec_name = _add_error_message_to_entity(db.Record(name="TestRecord2"),
+                                            code=name_code)
+    rec_auth = _add_error_message_to_entity(db.Record(name="TestRecord3"),
+                                            code=auth_code)
+    rec_par_prop = _add_error_message_to_entity(
+        db.Record(name="TestRecord"), prop_code)
+    rec_par_prop = _add_error_message_to_entity(rec_par_prop, parent_code)
+    rec_par_prop.add_parent(parent)
+    rec_par_prop.add_property(prop1).add_property(prop2)
+    cont.extend([rec_name, rec_auth, rec_par_prop])
+    with raises(TransactionError) as e:
+        raise_errors(cont)
+    te = e.value
+    assert te.container == cont
+    assert te.code == container_code
+    # no healthy entity caused an error
+    for good in [good_rec, good_prop]:
+        assert good not in te.all_entities
+    # all records that caused problems
+    assert {rec_name, rec_auth, rec_par_prop}.issubset(te.all_entities)
+    # the container error contains the errors caused by the records
+    for err in te.errors:
+        if err.entity.name == rec_name.name:
+            assert isinstance(err, UniqueNamesError)
+        elif err.entity.name == rec_auth.name:
+            assert isinstance(err, AuthorizationError)
+        elif err.entity.name == rec_par_prop.name:
+            # record raises both of them
+            assert (isinstance(err, UnqualifiedParentsError) or
+                    isinstance(err, UnqualifiedPropertiesError))
diff --git a/unittests/test_property.py b/unittests/test_property.py
index 8b2deeb4b06cfbf3d42881201307a1e2122124e3..752ee01f0eafef14dbffd1e62c99d1c816c45d05 100644
--- a/unittests/test_property.py
+++ b/unittests/test_property.py
@@ -84,3 +84,8 @@ def test_get_property_with_entity():
     p = Property(id=1234)
     r.add_property(id=1234, value="bla")
     assert r.get_property(p).value == "bla"
+
+
+def test_selected_reference_list():
+    assert len(testrecord.get_property("Conductor").value) == 1
+    assert isinstance(testrecord.get_property("Conductor").value[0], Entity)
diff --git a/unittests/test_query.py b/unittests/test_query.py
new file mode 100644
index 0000000000000000000000000000000000000000..12622ea486dda717ca1fbc1255510575c5e0c8e6
--- /dev/null
+++ b/unittests/test_query.py
@@ -0,0 +1,48 @@
+# -*- encoding: utf-8 -*-
+#
+# ** header v3.0
+# This file is a part of the CaosDB Project.
+#
+# Copyright (C) 2021 Indiscale GmbH <info@indiscale.com>
+# Copyright (C) 2021 Timm Fitschen <f.fitschen@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
+#
+from lxml import etree
+import caosdb as db
+
+
+def test_query_parsing():
+    s = '<Query string="FIND bla" results="0" cached="true" etag="asdf"/>'
+    q = db.Query(etree.fromstring(s))
+    assert q.q == "FIND bla"
+    assert q.results == 0
+    assert q.cached is True
+    assert q.etag == "asdf"
+
+    s = '<Query string="COUNT bla" results="1" cached="false" etag="asdf"/>'
+    q = db.Query(etree.fromstring(s))
+    assert q.q == "COUNT bla"
+    assert q.results == 1
+    assert q.cached is False
+    assert q.etag == "asdf"
+
+    s = '<Query string="COUNT blub" results="4"/>'
+    q = db.Query(etree.fromstring(s))
+    assert q.q == "COUNT blub"
+    assert q.results == 4
+    assert q.cached is False
+    assert q.etag is None
diff --git a/unittests/test_record.xml b/unittests/test_record.xml
index e961bdc6b88eb8e62b92696b52d7ad6a2dbf8089..018c747c11027a2c3996c65d1deab1d18514e17b 100644
--- a/unittests/test_record.xml
+++ b/unittests/test_record.xml
@@ -328,4 +328,10 @@
     <Value>45531</Value>
     <Value>45532</Value>
   </Property>
+  <Property datatype="LIST&lt;Person&gt;" description="DESCRIBE ME!" id="1634561234" importance="FIX" name="Conductor">
+    <Value>
+      <Record id="23456543">
+      </Record>
+    </Value>
+  </Property>
 </Record>
diff --git a/unittests/test_state.py b/unittests/test_state.py
new file mode 100644
index 0000000000000000000000000000000000000000..202c7a02af3db28434406626e5164def46febed7
--- /dev/null
+++ b/unittests/test_state.py
@@ -0,0 +1,77 @@
+import pytest
+import caosdb as db
+from caosdb import State, Transition
+from caosdb.common.models import parse_xml, ACL
+from lxml import etree
+
+
+def test_state_xml():
+    state = State(model="model1", name="state1")
+    xml = etree.tostring(state.to_xml())
+
+    assert xml == b'<State name="state1" model="model1"/>'
+    state = State.from_xml(etree.fromstring(xml))
+    assert state.name == "state1"
+    assert state.model == "model1"
+
+    assert xml == etree.tostring(state.to_xml())
+
+
+def test_entity_xml():
+    r = db.Record()
+    assert r.state is None
+    r.state = State(model="model1", name="state1")
+
+    xml = etree.tostring(r.to_xml())
+    assert xml == b'<Record><State name="state1" model="model1"/></Record>'
+
+    r = parse_xml(xml)
+    assert r.state == State(model="model1", name="state1")
+
+
+def test_description():
+    xml = b'<State name="state1" model="model1"/>'
+    state = State.from_xml(etree.fromstring(xml))
+    assert state.description is None
+
+    with pytest.raises(AttributeError):
+        state.description = "test"
+
+    xml = b'<State name="state1" model="model1" description="test2"/>'
+    state = State.from_xml(etree.fromstring(xml))
+    assert state.description == "test2"
+
+
+def test_id():
+    xml = b'<State name="state1" model="model1"/>'
+    state = State.from_xml(etree.fromstring(xml))
+    assert state.id is None
+
+    with pytest.raises(AttributeError):
+        state.id = "2345"
+
+    xml = b'<State name="state1" model="model1" id="1234"/>'
+    state = State.from_xml(etree.fromstring(xml))
+    assert state.id == "1234"
+
+
+def test_create_state_acl():
+    acl = ACL()
+    acl.grant(role="role1", permission="DO:IT")
+    acl.grant(role="?OWNER?", permission="DO:THAT")
+    state_acl = State.create_state_acl(acl)
+    assert state_acl.get_permissions_for_role("?STATE?role1?") == {"DO:IT"}
+    assert state_acl.get_permissions_for_role("?STATE??OWNER??") == {"DO:THAT"}
+
+
+def test_transitions():
+    xml = b'<State name="state1" model="model1"/>'
+    state = State.from_xml(etree.fromstring(xml))
+    assert state.transitions is None
+
+    with pytest.raises(AttributeError):
+        state.transitions = []
+
+    xml = b'<State name="state1" model="model1" id="1234"><Transition name="t1"><FromState name="state1"/><ToState name="state2"/></Transition></State>'
+    state = State.from_xml(etree.fromstring(xml))
+    assert state.transitions == set([Transition(name="t1", from_state="state1", to_state="state2")])
diff --git a/unittests/test_utils.py b/unittests/test_utils.py
index f308445ed1a06331a54ebf67411dc154836557ca..42d18ba06eb7516bb318de54cb537f548cfe9081 100644
--- a/unittests/test_utils.py
+++ b/unittests/test_utils.py
@@ -24,7 +24,6 @@
 """Tests for caosdb.common.utils."""
 from __future__ import unicode_literals
 from lxml.etree import Element
-from nose.tools import assert_equals as eq
 from caosdb.common.utils import xml2str
 
 
@@ -32,4 +31,4 @@ def test_xml2str():
     name = 'Björn'
     element = Element(name)
     serialized = xml2str(element)
-    eq(serialized, "<Björn/>\n")
+    assert serialized == "<Björn/>\n"