Skip to content
Snippets Groups Projects
Verified Commit ab5db0ad authored by Daniel Hornung's avatar Daniel Hornung
Browse files

Merge branch 'dev' into f-fix-linter-errors

parents fab901d3 75531f1c
Branches
Tags
2 merge requests!175BUG: Request responses without the "Set-Cookie" header no longer overwrite the...,!170Add flag and cuid getters to Entity
Pipeline #59725 canceled
......@@ -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 ###
......
......@@ -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]
......@@ -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.
......
......@@ -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)
......
......@@ -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"
......@@ -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"
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment