diff --git a/CHANGELOG.md b/CHANGELOG.md index 51bc4a3f4de7c0b193406747631642b3e2b7b5e6..e2d7d3f738a4ce5a4ce1fe04784edb409f95c257 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,8 +52,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * a `role` argument for `get_entity_by_name` and `get_entity_by_id` ### Changed ### -* `in` operator now test whether a parent with the appropriate ID, name or both is in `ParentList` -* `in` operator now test whether a parent with the appropriate ID, name or both is in `PropertyList` * Using environment variable PYLINKAHEADINI instead of PYCAOSDBINI. diff --git a/src/linkahead/__init__.py b/src/linkahead/__init__.py index cd54f8f4e05326579521fbbf226f027d32fa616e..567748e3b3a58fb73b91f652d82ed10f818d6014 100644 --- a/src/linkahead/__init__.py +++ b/src/linkahead/__init__.py @@ -42,7 +42,7 @@ from .common.datatype import (BOOLEAN, DATETIME, DOUBLE, FILE, INTEGER, LIST, REFERENCE, TEXT) # Import of the basic API classes: from .common.models import (ACL, ALL, FIX, NONE, OBLIGATORY, RECOMMENDED, - SUGGESTED, Container, DropOffBox, Entity, File, + SUGGESTED, Container, DropOffBox, Entity, File, Parent, Info, Message, Permissions, Property, Query, QueryTemplate, Record, RecordType, delete, execute_query, get_global_acl, diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py index f537a34bfe902fc8fa4991a0bb26a6d2d7e7cb9f..78988bd1bf14c557aad5020b1b7a22962d106a68 100644 --- a/src/linkahead/common/models.py +++ b/src/linkahead/common/models.py @@ -2522,15 +2522,23 @@ class PropertyList(list): return xml2str(xml) - def __contains__(self, prop): - missing = False - if prop.name is not None: - if prop.name not in self._element_by_name: - missing = True - if prop.id is not None: - if str(prop.id) not in self._element_by_id: - missing = True - return not missing + def filter(self, pid:Union[str, int]=None, name:str =None, prop:Property=None): + """ + Filters all Properties from this PropertyList that match either name or ID. + + You can provide name and or ID via the corresponding arguments or you + pass a Property object to this function. + + Parameters + ---------- + pid: Union[str,int], ID of the Properties to be returned + name: str, name of the Properties to be returned + prop: Property, name of the Properties to be returned + Returns + ------- + list, a list with all matching Properties + """ + return _filter_entity_list(self, "Property", pid=pid, name=name, element=prop) def _get_entity_by_cuid(self, cuid: str): ''' @@ -2610,11 +2618,6 @@ class ParentList(list): super().__init__(*args, **kwargs) self._element_by_name = dict() self._element_by_id = dict() - for el in self: - if el.name is not None: - self._element_by_name[el.name] = el - if el.id is not None: - self._element_by_name[str(el.id)] = el def extend(self, parents): self.append(parents) @@ -2625,15 +2628,9 @@ class ParentList(list): if isinstance(parent, list): for p in parent: self.append(p) - return if isinstance(parent, Entity): - if parent.id: - self._element_by_id[str(parent.id)] = parent - - if parent.name: - self._element_by_name[parent.name] = parent list.append(self, parent) else: raise TypeError("Argument was not an Entity") @@ -2675,17 +2672,34 @@ class ParentList(list): return xml2str(xml) - def __contains__(self, parent): - missing = False - if parent.name is not None: - if parent.name not in self._element_by_name: - missing = True - if parent.id is not None: - if str(parent.id) not in self._element_by_id: - missing = True - return not missing + def filter(self, pid:Union[str, int]=None, name:str =None, parent:Parent=None): + """ + Filters all Parents from this ParentList that match either name or ID. + + You can provide name and or ID via the corresponding arguments or you + pass a Parent object to this function. + + Returns + ------- + a list with all matching Parents + """ + return _filter_entity_list(self, "Parent", pid=pid, name=name, element=parent) def remove(self, parent: Union[Entity, int, str]): + """ + Remove first occurrence of parent. + + Parameters + ---------- + parent: Union[Entity, int, str], the parent to be removed identified via ID or name. If a + Parent object is provided the ID and then the name is used to identify the parent to be + removed. + + Returns + ------- + None + """ + if isinstance(parent, Entity): if parent in self: list.remove(self, parent) @@ -2703,11 +2717,11 @@ class ParentList(list): # by name for e in self: - if e.name is not None and e.name == parent.name: + if e.name is not None and e.name.lower() == parent.name.lower(): list.remove(self, e) return - elif hasattr(parent, "encode"): + elif isinstance(parent, str): # by name for e in self: @@ -5433,3 +5447,30 @@ def delete(ids: Union[list[int], range], raise_exception_on_error: bool = True): c.append(Entity(id=ids)) return c.delete(raise_exception_on_error=raise_exception_on_error) + +def _filter_entity_list(listobject, element_type, pid:Union[str, int]=None, name:str =None, element:Any=None): + """ + Filterss all elements from the given list that match either name or ID. + + You can provide name and or ID via the corresponding arguments or you + pass an object to this function that has id and/or name. + + Returns + ------- + a list with all matching elements + """ + if element is not None: + if pid is not None or name is not None: + raise ValueError(f"Please provide either a {element_type} or one of " + "pid or name") + pid = element.id + name = element.name + + candidates = [] + if name is not None: + candidates.extend( + [p for p in listobject if p.name is not None and p.name.lower() == name.lower()]) + if pid is not None: + candidates.extend( + [p for p in listobject if p.id == pid]) + return candidates diff --git a/unittests/test_entity.py b/unittests/test_entity.py index 87ae606ce10db7ce4411e929d66a54bc19c2dffd..9c7816a1beefa56a86dd973e63551133e0f22e22 100644 --- a/unittests/test_entity.py +++ b/unittests/test_entity.py @@ -28,7 +28,7 @@ import unittest from pytest import raises import linkahead -from linkahead import (INTEGER, Entity, Property, Record, RecordType, +from linkahead import (INTEGER, Entity, Property, Record, RecordType, Parent, configure_connection) from linkahead.connection.mockup import MockUpServerConnection from lxml import etree @@ -105,50 +105,43 @@ def test_parent_list(): p1 = RecordType(name="A") pl = linkahead.common.models.ParentList([p1]) assert p1 in pl - assert RecordType(name="A") in pl - assert pl.index(RecordType(name="A")) == 0 + assert pl.index(p1) == 0 + assert RecordType(name="A") not in pl assert RecordType(id=101) not in pl - pl.append(RecordType(id=101)) - assert RecordType(name="A") in pl - assert RecordType(id=101) in pl + p2 = RecordType(id=101) + pl.append(p2) + assert p2 in pl assert len(pl) == 2 - assert pl.index(RecordType(id=101)) - assert RecordType(id=102) not in pl - pl.append(RecordType(id=103, name='B')) - assert RecordType(name="A") in pl - assert RecordType(name="B") in pl - assert RecordType(id=101) in pl - assert RecordType(id=103) in pl + assert p1 in pl.filter(name="A") + assert p2 in pl.filter(pid=101) + assert p2 in pl.filter(pid=101, name="A") + assert p1 in pl.filter(pid=101, name="A") + assert p1 in pl.filter(parent=Parent(id=101, name="A")) + assert p2 in pl.filter(parent=Parent(id=101, name="A")) + p3 = RecordType(id=103, name='B') + pl.append(p3) assert len(pl) == 3 - assert pl.index(RecordType(id=103, name='B')) == 2 - assert pl.index(RecordType(id=103)) == 2 - assert pl.index(RecordType(name='B')) == 2 - assert RecordType(id=105, name="B") not in pl + assert p3 in pl.filter(name="B") + assert p3 in pl.filter(pid=103) # test removal # remove by id only, even though element in parent list has name and id pl.remove(RecordType(id=103)) assert len(pl) == 2 - assert RecordType(name='B') not in pl - assert RecordType(id=103) not in pl + assert p3 not in pl + assert p2 in pl + assert p1 in pl # Same for removal by name - pl.append(RecordType(id=103, name='B')) + pl.append(p3) assert len(pl) == 3 pl.remove(RecordType(name='B')) assert len(pl) == 2 - assert RecordType(name='B') not in pl - assert RecordType(id=103) not in pl - # And an error if the element is wrongly specified - pl.append(RecordType(id=103, name='B')) - assert len(pl) == 3 - with raises(ValueError) as ve: + assert p3 not in pl + # And an error if no suitable element can be found + with raises(KeyError) as ve: pl.remove(RecordType(id=105, name='B')) - assert "not in list" in str(ve.value) - assert len(pl) == 3 - with raises(ValueError) as ve: - pl.remove(RecordType(id=103, name='C')) - assert "not in list" in str(ve.value) - assert len(pl) == 3 + assert "not found" in str(ve.value) + assert len(pl) == 2 # TODO also check other built-in list functions: insert, pop, clear, count # TODO also check pl1 == pl2 @@ -159,22 +152,28 @@ def test_parent_list(): # TODO what is with the ambiguous RecordType(name='A', id=101) in pl? + + def test_property_list(): - # TODO: Resolve parent-list TODOs, the transfer to here. + # TODO: Resolve parent-list TODOs, then transfer to here. # TODO: What other considerations have to be done with properties? p1 = Property(name="A") pl = linkahead.common.models.PropertyList() pl.append(p1) assert p1 in pl - assert Property(name="A") in pl - assert not Property(id=101) in pl - pl.append(Property(id=101)) - assert Property(name="A") in pl - assert Property(id=101) in pl - assert not Property(id=102) in pl - pl.append(Property(id=103, name='B')) - assert Property(name="A") in pl - assert Property(name="B") in pl - assert Property(id=101) in pl - assert Property(id=103) in pl - assert not Property(id=105, name="B") in pl + assert Property(id=101) not in pl + p2 = Property(id=101) + pl.append(p2) + assert p1 in pl + assert p2 in pl + p3 = Property(id=103, name='B') + pl.append(p3) + + assert p1 in pl.filter(name="A") + assert p2 in pl.filter(pid=101) + assert p2 in pl.filter(pid=101, name="A") + assert p1 in pl.filter(pid=101, name="A") + assert p1 in pl.filter(prop=Property(id=101, name="A")) + assert p2 in pl.filter(prop=Property(id=101, name="A")) + assert p3 in pl.filter(name="B") + assert p3 in pl.filter(pid=103)