diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7b24c1ef360433601c298f4066ed8db99b7d8ead..9f8c131968c050fb18001b5dc7c5468d0ed26dae 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -93,15 +93,12 @@ build-testenv: # Build the sphinx documentation and make it ready for deployment by Gitlab Pages # 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: refs: - /^release-.*$/i - - master - variables: - # run pages only on gitlab.com - - $CI_SERVER_HOST == "gitlab.com" script: - echo "Deploying" - make doc @@ -109,3 +106,8 @@ pages: artifacts: paths: - public +pages: + <<: *pages_prepare + only: + refs: + - main diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c8a144dc939f3a58d1f9346929ad5e32d62d213..38b470db9d36675ffcef0b7e1434a08c3be7f407 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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 ### ### Deprecated ### @@ -22,26 +16,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed ### ### Fixed ### -* #45 - test_config_ini_via_envvar + +* #53 Documentation of inheritance ### Security ### -## [0.5.1] - 2021-02-12 ## +## [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 ### - -* #43 - Error with `execute_query` when server doesn't support query caching. +* #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 ### @@ -74,18 +83,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `QueryNotUniqueError` if no or more than one possible candidate is found, respectively. -### Deprecated ### - ### Removed ### * Dynamic exception type `EntityMultiError`. * `get_something` functions from all error object in `exceptions.py` * `AmbiguityException` -### Fixed ### - -### Security ### - ## [0.4.1] - 2021-02-10 ## ### Added ### @@ -95,18 +98,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 entity. * Automated documentation builds: `make doc` -### Changed ### - -### Deprecated ### - -### Removed ### - ### Fixed ### * deepcopy of `_Messages` objects -### Security ### - ## [0.4.0] - 2020-07-17## ### Added ### diff --git a/README.md b/README.md index 55645700c1762213fa35a14cb8a37c36eb43066f..04b34cbc07c98e73740b13200ed83fe067af99d2 100644 --- a/README.md +++ b/README.md @@ -28,14 +28,20 @@ By participating, you are expected to uphold our [Code of Conduct](https://gitla * 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? 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. -- If you have a suggestion for the [documentation](https://docs.indiscale.com/caosdb-pylib/), +* 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**. +* 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 diff --git a/README_SETUP.md b/README_SETUP.md index 4980b850ecd9fdc10c9fd02c6437aeb5826578b3..9da548395073643c16539cef180c4d6412dd8d46 100644 --- a/README_SETUP.md +++ b/README_SETUP.md @@ -75,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. @@ -84,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`: @@ -109,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. @@ -118,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 ## @@ -138,24 +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. \ No newline at end of file +### 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 dd6d5af4c43e1eea17c70f29da837c905694fed4..e015b598117abdcd575cf17e2f095fec459a4c4c 100644 --- a/RELEASE_GUIDELINES.md +++ b/RELEASE_GUIDELINES.md @@ -24,9 +24,9 @@ guidelines of the CaosDB Project variables and set `ISRELEASED` to `True`. Use the possibility to issue pre-release versions for testing. -5. Merge the release branch into the master branch. +5. Merge the release branch into the main branch. -6. Tag the latest commit of the master branch with `v<VERSION>`. +6. Tag the latest commit of the main branch with `v<VERSION>`. 7. Delete the release branch. @@ -35,8 +35,8 @@ guidelines of the CaosDB Project 9. Publish the release by executing `./release.sh` with uploads the caosdb module to the Python Package Index [pypi.org](https://pypi.org). -10. Merge the master branch back into the dev branch. +10. Merge the main branch back into the dev branch. -11. After the merge of master to dev, start a new development version by +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 3d9012959fb036705ef9b086af35489238360c14..8162b11bfefb41b1bcdbc74b8e314f99a61d1a4e 100755 --- a/examples/set_permissions.py +++ b/examples/set_permissions.py @@ -25,8 +25,8 @@ As a result, only a specific user or group may access it. -This script assumes that the user specified in the -pycaosdb.ini configuration can create new entities. +This script assumes that the user specified in the pycaosdb.ini +configuration can create new entities. """ @@ -206,7 +206,8 @@ def create_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() + rt = db.RecordType( + name="Human Food", description="Food that can be eaten only by humans").insert() food = ("Bread", "Tomatoes", "Twinkies") cont = db.Container() diff --git a/setup.py b/setup.py index d491a1d340796b9b4701b307003c3e62e75d6001..e1d39458ea8d1b0b17ea12a82ebd7133b27b045a 100755 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ from setuptools import find_packages, setup MAJOR = 0 MINOR = 5 -MICRO = 2 +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 4043bbed3283c07d5a1ff7e3dbd77593a7f82fb1..7e06885fe495c1e8c4ccc99b7d0c0f8ff8c34b5b 100644 --- a/src/caosdb/__init__.py +++ b/src/caosdb/__init__.py @@ -49,6 +49,10 @@ from caosdb.common.models import (ACL, ALL, FIX, NONE, OBLIGATORY, RECOMMENDED, 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_config_files() diff --git a/src/caosdb/common/models.py b/src/caosdb/common/models.py index e38ca3399e4e03a8ac491d8aa9f5e1fd39008e2e..16001ed8847ec853c21851e101aea54f7377d632 100644 --- a/src/caosdb/common/models.py +++ b/src/caosdb/common/models.py @@ -40,26 +40,23 @@ from sys import hexversion from tempfile import NamedTemporaryFile from warnings import warn -from caosdb.common.datatype import BOOLEAN, DATETIME, DOUBLE, INTEGER, TEXT, is_reference, is_list_datatype -from caosdb.common.versioning import Version +from caosdb.common.datatype import (BOOLEAN, DATETIME, DOUBLE, INTEGER, TEXT, + is_list_datatype, is_reference) from caosdb.common.state import State from caosdb.common.utils import uuid, xml2str +from caosdb.common.versioning import Version 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 (AmbiguousEntityError, - AuthorizationError, - CaosDBException, CaosDBConnectionError, - ConsistencyError, - EmptyUniqueQueryError, +from caosdb.exceptions import (AmbiguousEntityError, AuthorizationError, + CaosDBConnectionError, CaosDBException, + ConsistencyError, EmptyUniqueQueryError, EntityDoesNotExistError, EntityError, - EntityHasNoDatatypeError, - MismatchingEntitiesError, - QueryNotUniqueError, TransactionError, - UniqueNamesError, + EntityHasNoDatatypeError, HTTPURITooLongError, + MismatchingEntitiesError, QueryNotUniqueError, + TransactionError, UniqueNamesError, UnqualifiedParentsError, - UnqualifiedPropertiesError, - HTTPURITooLongError) + UnqualifiedPropertiesError) from lxml import etree _ENTITY_URI_SEGMENT = "Entity" @@ -431,14 +428,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) @@ -555,6 +577,7 @@ class Entity(object): entity, or name. """ + if isinstance(key, int): for p in self.parents: if p.id is not None and int(p.id) == int(key): @@ -563,14 +586,17 @@ class Entity(object): if key.id is not None: # first try by id found = self.get_parent(int(key.id)) + if found is not None: return found # otherwise by name + return self.get_parent(key.name) else: for p in self.parents: if (p.name is not None and str(p.name).lower() == str(key).lower()): + return p return None @@ -655,6 +681,7 @@ class Entity(object): special_selector = None # iterating through the entity tree according to the selector + for subselector in selector: # selector does not match the structure, we cannot get a # property of non-entity @@ -666,11 +693,13 @@ class Entity(object): # selector does not match the structure, we did not get a # property + if prop is None: return None # if the property is a reference, we are interested in the # corresponding entities attributes + if isinstance(prop.value, Entity): ref = prop.value @@ -679,6 +708,7 @@ class Entity(object): ref = prop # if we saved a special selector before, apply it + if special_selector is None: return prop.value else: @@ -810,6 +840,7 @@ class Entity(object): assert isinstance(xml, etree._Element) # unwrap wrapped entity + if self._wrapped_entity is not None: xml = self._wrapped_entity.to_xml(xml, add_properties) @@ -856,6 +887,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) @@ -1047,6 +1080,7 @@ class Entity(object): if len(c) == 1: c[0].messages.extend(c.messages) + return c[0] raise QueryNotUniqueError("This retrieval was not unique!!!") @@ -1433,6 +1467,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: @@ -1511,6 +1562,24 @@ class RecordType(Entity): **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: @@ -2083,6 +2152,7 @@ class _Messages(dict): else: raise TypeError( "('description', 'body'), ('body'), or 'body' expected.") + if isinstance(value, Message): body = value.body description = value.description @@ -2252,6 +2322,7 @@ def _deletion_sync(e_local, e_remote): except KeyError: # deletion info wasn't there e_local.messages = e_remote.messages + return _basic_sync(e_local, e_remote) @@ -2444,6 +2515,7 @@ class Container(list): tmpid = 0 # users might already have specified some tmpids. -> look for smallest. + for e in self: tmpid = min(tmpid, Container._get_smallest_tmpid(e)) tmpid -= 1 @@ -2663,6 +2735,7 @@ class Container(list): used_remote_entities = [] # match by cuid + for local_entity in self: sync_dict[local_entity] = None @@ -2691,6 +2764,7 @@ class Container(list): 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 = [] @@ -2715,6 +2789,7 @@ class Container(list): 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): @@ -2744,6 +2819,7 @@ class Container(list): 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): @@ -2811,11 +2887,14 @@ class Container(list): for container_item in container: item_id.add(container_item.id) + for parents in container_item.get_parents(): is_parent.add(parents.id) + for references in container_item.get_properties(): if is_reference(references.datatype): # add only if it is a reference, not a property + if isinstance(references.value, int): has_references.add(references.value) elif is_list_datatype(references.datatype): @@ -2850,24 +2929,28 @@ class Container(list): 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: dependencies = self._test_dependencies_in_container(self) ''' - If there are as many dependencies as entities in the container and it is larger than chunk_size it cannot be split and deleted. + If there are as many dependencies as entities in the container and it is larger than chunk_size it cannot be split and deleted. This case cannot be handled at the moment. ''' + if len(dependencies) == item_count: if raise_exception_on_error: te = TransactionError( msg="The container is too large and with too many dependencies within to be deleted.", container=self) raise te + return self dependencies_delete = Container() # items which have to be deleted later because of dependencies. 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)): if len(dependencies): if self[j].id in dependencies: @@ -2880,6 +2963,7 @@ class Container(list): if len(chunk): chunk.delete() dependencies_delete.delete() + return self if len(self) == 0: @@ -3517,6 +3601,7 @@ class ACL(): 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): @@ -3807,6 +3892,7 @@ class Query(): connection = get_connection() flags = self.flags + if cache is False: flags["cache"] = "false" query_dict = dict(flags) @@ -3833,9 +3919,11 @@ class Query(): if len(cresp) > 1 and raise_exception_on_error: 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) @@ -3942,6 +4030,7 @@ class Info(): for e in xml: m = _parse_single_xml_element(e) + if isinstance(m, UserInfo): self.user_info = m else: @@ -4101,13 +4190,16 @@ def _evaluate_and_add_error(parent_error, ent): 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, @@ -4124,6 +4216,7 @@ def _evaluate_and_add_error(parent_error, ent): found114 = True new_exc = UnqualifiedPropertiesError(entity=ent, error=err) + for prop in ent.get_properties(): new_exc = _evaluate_and_add_error(new_exc, prop) @@ -4131,6 +4224,7 @@ def _evaluate_and_add_error(parent_error, ent): found116 = True new_exc = UnqualifiedParentsError(entity=ent, error=err) + for par in ent.get_parents(): new_exc = _evaluate_and_add_error(new_exc, par) @@ -4141,21 +4235,28 @@ def _evaluate_and_add_error(parent_error, ent): 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: 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 @@ -4163,6 +4264,7 @@ def _evaluate_and_add_error(parent_error, ent): 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) @@ -4188,10 +4290,12 @@ def raise_errors(arg0): 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 diff --git a/src/caosdb/connection/connection.py b/src/caosdb/connection/connection.py index 2cb31f425b14bf382fae32f62af79a4bd6ef0301..9e273a56778737033fda9f342f967f56946b501b 100644 --- a/src/caosdb/connection/connection.py +++ b/src/caosdb/connection/connection.py @@ -41,7 +41,11 @@ from caosdb.exceptions import (CaosDBException, HTTPClientError, HTTPResourceNotFoundError, HTTPServerError, HTTPURITooLongError) -from caosdb.version import version +try: + from caosdb.version import version +except ModuleNotFoundError: + version = "uninstalled" + from pkg_resources import resource_filename from .interface import CaosDBHTTPResponse, CaosDBServerConnection diff --git a/src/caosdb/exceptions.py b/src/caosdb/exceptions.py index f02a4630356726f99d8439fd821b6dd327ab22c7..fdd2e11f1dfb8857f86942df2534d732bad9a793 100644 --- a/src/caosdb/exceptions.py +++ b/src/caosdb/exceptions.py @@ -189,8 +189,8 @@ class TransactionError(CaosDBException): error_t. If direct_children_only is True, only direct children are checked. - Parameters: - ----------- + Parameters + ---------- error_t : EntityError error type to be checked direct_children_only: bool, optional @@ -199,8 +199,8 @@ class TransactionError(CaosDBException): children, i.e., all errors in self.all_errors are used. Default is false. - Returns: - -------- + Returns + ------- has_error : bool True if at least one of the children is of type error_t, False otherwise. 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/conf.py b/src/doc/conf.py index f276f325273b71d4b697bc57990259e842b2dbc3..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,14 +186,22 @@ epub_exclude_files = ['search.html'] # -- Extension configuration ------------------------------------------------- +# 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://caosdb.gitlab.io/caosdb-mysqlbackend/", + "caosdb-mysqlbackend": ("https://docs.indiscale.com/caosdb-mysqlbackend/", None), - "caosdb-server": ("https://caosdb.gitlab.io/caosdb-server/", None), + "caosdb-server": ("https://docs.indiscale.com/caosdb-server/", None), } 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 76a2f88f6d31dd9b5f17995b6d54ccc63eb33631..bd29c6c56acf5c173e94ae6471a6aeba56ea4b93 100644 --- a/src/doc/index.rst +++ b/src/doc/index.rst @@ -12,12 +12,12 @@ Welcome to PyCaosDB's documentation! Concepts <concepts> Configuration <configuration> Administration <administration> - API documentation<_apidoc/modules> + 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 index 2ead1cc259c807ee12f2459f7e452558eb1b63a2..22fb9461d6916003b2dad496ff3487df335c8dcc 100644 --- a/src/doc/tutorials/Data-Insertion.rst +++ b/src/doc/tutorials/Data-Insertion.rst @@ -4,9 +4,9 @@ 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 :any:`caosdb-server:Data Model` . +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 @@ -35,6 +35,42 @@ two more Properties and a RecordType: 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 ~~~~~~~~~~~~~~~~~~ @@ -108,32 +144,6 @@ Useful is also, that you can insert directly tabular data. With this example file `test.csv <uploads/4f2c8756a26a3984c0af09d206d583e5/test.csv>`__. -Inheritance of Properties -------------------------- - -Given, you want to insert a new RecordType “Fridge temperatur -experiment” as a child of the existing RecordType “Experiment”. The -latter may have an obligatory Property “date” (since every experiment is -conducted at some time). It is a natural way of thinking, that every sub -type of “Experiment” also has this obligatory Property—in terms of -object oriented programing the “Fridge temperatur experiment” *inherits* -that Property. - -:: - - rt = h.RecordType(name="Fridge temperatur experiment", - description="RecordType which inherits all obligatory properties from Experiment" - ).add_parent(name="Experiment", inheritance="obligatory").insert() - - print(rt.get_property(name="date").importance) ### rt now has a "date"-property -> this line prints "obligatory" - -The parameter *``inheritance=(obligatory|recommended|fix|all|none)``* of -``add_parent`` tells the server to assign obligatory:: properties of the -parent to the child automatically, recommended:: properties of the -parent to the child automatically, fix:: properties of the parent to the -child automatically, all:: properties of the parent to the child -automatically, none:: of the properties of the parent to child -automatically, File Update ----------- diff --git a/src/doc/tutorials/errors.rst b/src/doc/tutorials/errors.rst index ba386dc31baad1021f01d53b2b12a2623d8278ad..37c53c9b527a0435f9f24ae6c6e71687e73eb963 100644 --- a/src/doc/tutorials/errors.rst +++ b/src/doc/tutorials/errors.rst @@ -1,53 +1,176 @@ Error Handling --------------- +============== -HeartDBException -~~~~~~~~~~~~~~~~ +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 can -help you to find the crucial problems with your transaction by two -important methods: \* ``get_errors()`` which returns a list of instances -of ``EntityError``. \* ``get_entities()`` which returns a list of -entities in the transaction container which are erroneous. - -Additionally, ``print(transaction_error`` prints a tree-like +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`` represents a single error that has been returned by -the server. You might call \* ``get_entity()`` which returns the entity -which caused the error. \* ``get_description()`` which returns a -description of the error. \* ``get_code()`` which returns the error code -(if specified) or 0 (if not). +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. -In fact, the ``EntityError`` class is a subclass of -``TransactionError``. So, it inherits the ``get_entities()``. Unless -overridden by subclasses of ``EntityError``, it return a list with only -one item—the entity which caused this error. Similarly, unless -overridden by subclasses, the ``get_errors()`` method returns a list -with only one item—``[self]``. +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`` - overrides -``get_entities()``: returns all parent entities with errors. - overrides -``get_errors()``: returns a list of EntityErrors which have been caused -by parent entities. \* ``UnqualifiedPropertiesError`` - overrides -``get_entities()``: returns all properties with errors. - overrides -``get_errors()``: returns a list of EntityErrors which have been caused -by properties. +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 3eb804776960ba93acb9baf78824f0cf8a06e1f7..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. @@ -124,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