diff --git a/CHANGELOG.md b/CHANGELOG.md index de37f0463ea1154eb2fc5fe98f49d7d242b9fce6..4d5cf2d4a53c605bd0305c87c0732b52207727d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added ### * New setup extra `test` which installs the dependencies for testing. +* The Container class has a new member function `filter` which is based o `_filter_entity_list`. * The `Entity` properties `_cuid` and `_flags` are now available for read-only access as `cuid` and `flags`, respectively. @@ -29,7 +30,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `authentication/interface/on_response()` does not overwrite `auth_token` if new value is `None` * [#119](https://gitlab.com/linkahead/linkahead-pylib/-/issues/119) - The diff returned by compare_entities now uses id instead of name as key if either property does not have a name + The diff returned by compare_entities now uses id instead of name as + key if either property does not have a name +* [#87](https://gitlab.com/linkahead/linkahead-pylib/-/issues/87) + `XMLSyntaxError` messages when parsing (incomplete) responses in + case of certain connection timeouts. ### Security ### diff --git a/src/doc/tutorials/complex_data_models.rst b/src/doc/tutorials/complex_data_models.rst index 168cf3b9f0d6839ed8f78beb01ae24fb9d489e88..6c9520c94105e4106343ed2ab1a4c807d669d977 100644 --- a/src/doc/tutorials/complex_data_models.rst +++ b/src/doc/tutorials/complex_data_models.rst @@ -78,7 +78,7 @@ Examples Finding parents and properties --------- +------------------------------ To find a specific parent or property of an Entity, its ParentList or PropertyList can be filtered using names, ids, or entities. A short example: @@ -126,3 +126,19 @@ entities. A short example: # Result: [p2_1] The filter function of ParentList works analogously. + +Finding entities in a Container +------------------------------- +In the same way as described above, Container can be filtered. +A short example: + +.. code-block:: python3 + + import linkahead as db + + # Setup a record with six properties + p1 = db.Property(id=101, name="Property 1") + p2 = db.Property(name="Property 2") + c = db.Container().extend([p1,p2]) + c.filter(name="Property 1") + # Result: [p1] diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py index 9093ebcc080f507d485d3db745fa32eb103f2613..c556629a71c68579d30607cfbfa0b04ddf081d74 100644 --- a/src/linkahead/common/models.py +++ b/src/linkahead/common/models.py @@ -2584,8 +2584,6 @@ class PropertyList(list): Params ------ - listobject : Iterable(Property) - List to be filtered prop : Property Property to match name and ID with. Cannot be set simultaneously with ID or name. @@ -3102,12 +3100,12 @@ def _basic_sync(e_local, e_remote): if e_local.role is None: e_local.role = e_remote.role elif e_remote.role is not None and not e_local.role.lower() == e_remote.role.lower(): - raise ValueError("The resulting entity had a different role ({0}) " - "than the local one ({1}). This probably means, that " + raise ValueError(f"The resulting entity had a different role ({e_remote.role}) " + f"than the local one ({e_local.role}). This probably means, that " "the entity was intialized with a wrong class " "by this client or it has changed in the past and " - "this client did't know about it yet.".format( - e_remote.role, e_local.role)) + "this client did't know about it yet.\nThis is the local version of the" + f" Entity:\n{e_local}\nThis is the remote one:\n{e_remote}") e_local.id = e_remote.id e_local.name = e_remote.name @@ -3739,6 +3737,36 @@ class Container(list): return sync_dict + def filter(self, entity: Optional[Entity] = None, + pid: Union[None, str, int] = None, + name: Optional[str] = None, + conjunction: bool = False) -> list: + """ + Return all Entities from this Container that match the selection criteria. + + Please refer to the documentation of _filter_entity_list for a detailed + description of behaviour. + + Params + ------ + entity : Entity + Entity to match name and ID with + pid : str, int + Parent ID to match + name : str + Parent name to match + simultaneously with ID or name. + conjunction : bool, defaults to False + Set to return only entities that match both id and name + if both are given. + + Returns + ------- + matches : list + List containing all matching Entities + """ + return _filter_entity_list(self, pid=pid, name=name, entity=entity, + conjunction=conjunction) @staticmethod def _find_dependencies_in_container(container: Container): """Find elements in a container that are a dependency of another element of the same. diff --git a/src/linkahead/exceptions.py b/src/linkahead/exceptions.py index 609d3654ac670a993185ba1faa33db921c44409c..7d4dc0850b811c0d696cc66252aa62541c6d3029 100644 --- a/src/linkahead/exceptions.py +++ b/src/linkahead/exceptions.py @@ -94,12 +94,26 @@ class HTTPServerError(LinkAheadException): """HTTPServerError represents 5xx HTTP server errors.""" def __init__(self, body): - xml = etree.fromstring(body) - error = xml.xpath('/Response/Error')[0] - msg = error.get("description") - - if error.text is not None: - msg = msg + "\n\n" + error.text + try: + # This only works if the server sends a valid XML + # response. Then it can be parsed for more information. + xml = etree.fromstring(body) + if xml.xpath('/Response/Error'): + error = xml.xpath('/Response/Error')[0] + msg = error.get("description") if error.get("description") is not None else "" + + if error.text is not None: + if msg: + msg = msg + "\n\n" + error.text + else: + msg = error.text + else: + # Valid XML, but no error information + msg = body + except etree.XMLSyntaxError: + # Handling of incomplete responses, e.g., due to timeouts, + # c.f. https://gitlab.com/linkahead/linkahead-pylib/-/issues/87. + msg = body LinkAheadException.__init__(self, msg) diff --git a/unittests/test_container.py b/unittests/test_container.py index c3a60140d43383c81f03c38c9dd5cc7779bc77ba..4ef85910fcd2f5328b5208122a8683d4ce3b1ed6 100644 --- a/unittests/test_container.py +++ b/unittests/test_container.py @@ -199,3 +199,12 @@ def test_container_slicing(): with pytest.raises(TypeError): cont[[0, 2, 3]] + +def test_container_filter(): + # this is a very rudimentary test since filter is based on _filter_entity_list which is tested + # separately + cont = db.Container() + cont.extend([db.Record(name=f"TestRec{ii+1}") for ii in range(5)]) + recs = cont.filter(name="TestRec2") + assert len(recs)==1 + recs[0].name =="TestRec2" diff --git a/unittests/test_error_handling.py b/unittests/test_error_handling.py index 3f5241466e9a8f810b581cbb587e17ccf8f123ee..64f743c85e9df554e7428cf7d8477e8c823a9758 100644 --- a/unittests/test_error_handling.py +++ b/unittests/test_error_handling.py @@ -30,7 +30,7 @@ import linkahead as db from linkahead.common.models import raise_errors from linkahead.exceptions import (AuthorizationError, EntityDoesNotExistError, EntityError, - EntityHasNoDatatypeError, + EntityHasNoDatatypeError, HTTPServerError, TransactionError, UniqueNamesError, UnqualifiedParentsError, UnqualifiedPropertiesError) @@ -315,3 +315,26 @@ def test_container_with_faulty_elements(): # record raises both of them assert (isinstance(err, UnqualifiedParentsError) or isinstance(err, UnqualifiedPropertiesError)) + + +def test_incomplete_server_error_response(): + """The reason behind https://gitlab.com/linkahead/linkahead-pylib/-/issues/87.""" + # Case 1: Response is no XML at all + err = HTTPServerError("Bla") + assert str(err) == "Bla" + + # Case 2: Response is an incomplete XML, e.g. due to very unlucky timeout + err = HTTPServerError("<incomplete>XML</inc") + assert str(err) == "<incomplete>XML</inc" + + # Case 3: Response is complete XML but doesn't have response and or error information + err = HTTPServerError("<complete>XML</complete>") + assert str(err) == "<complete>XML</complete>" + + # Case 4: Response is an XML response but the error is lacking a description + err = HTTPServerError("<Response><Error>complete error</Error></Response>") + assert str(err) == "complete error" + + # Case 5: Healthy error Response + err = HTTPServerError("<Response><Error description='Error'>complete error</Error></Response>") + assert str(err) == "Error\n\ncomplete error"