diff --git a/arkindex_worker/worker/element.py b/arkindex_worker/worker/element.py index 2d54563f04de1973a07dafa9ee17844fd8ac3e8c..985481ad0b7a6d1a7dddd630c9bcdf8802bf257a 100644 --- a/arkindex_worker/worker/element.py +++ b/arkindex_worker/worker/element.py @@ -497,3 +497,117 @@ class ElementMixin(object): ) return children + + def list_element_parents( + self, + element: Union[Element, CachedElement], + folder: Optional[bool] = None, + name: Optional[str] = None, + recursive: Optional[bool] = None, + transcription_worker_version: Optional[Union[str, bool]] = None, + transcription_worker_run: Optional[Union[str, bool]] = None, + type: Optional[str] = None, + with_classes: Optional[bool] = None, + with_corpus: Optional[bool] = None, + with_metadata: Optional[bool] = None, + with_has_children: Optional[bool] = None, + with_zone: Optional[bool] = None, + worker_version: Optional[Union[str, bool]] = None, + worker_run: Optional[Union[str, bool]] = None, + ) -> Iterable[dict]: + """ + List parents of an element through the API. + + :param element: Child element to find parents of. + :param folder: Restrict to or exclude elements with folder types. + :param name: Restrict to elements whose name contain a substring (case-insensitive). + :param recursive: Look for elements recursively (grand-parents, etc.) + :param transcription_worker_version: Restrict to elements that have a transcription created by a worker version with this UUID. + :param transcription_worker_run: Restrict to elements that have a transcription created by a worker run with this UUID. + :param type: Restrict to elements with a specific type slug + :param with_classes: Include each element's classifications in the response. + :param with_corpus: Include each element's corpus in the response. + :param with_has_children: Include the ``has_children`` attribute in the response, + indicating if this element has child elements of its own. + :param with_metadata: Include each element's metadata in the response. + :param with_zone: Include the ``zone`` attribute in the response, + holding the element's image and polygon. + :param worker_version: Restrict to elements created by a worker version with this UUID. + :param worker_run: Restrict to elements created by a worker run with this UUID. + :return: An iterable of dicts from the ``ListElementChildren`` API endpoint. + """ + assert element and isinstance( + element, (Element, CachedElement) + ), "element shouldn't be null and should be an Element or CachedElement" + query_params = {} + if folder is not None: + assert isinstance(folder, bool), "folder should be of type bool" + query_params["folder"] = folder + if name: + assert isinstance(name, str), "name should be of type str" + query_params["name"] = name + if recursive is not None: + assert isinstance(recursive, bool), "recursive should be of type bool" + query_params["recursive"] = recursive + if transcription_worker_version is not None: + assert isinstance( + transcription_worker_version, (str, bool) + ), "transcription_worker_version should be of type str or bool" + if isinstance(transcription_worker_version, bool): + assert ( + transcription_worker_version is False + ), "if of type bool, transcription_worker_version can only be set to False" + query_params["transcription_worker_version"] = transcription_worker_version + if transcription_worker_run is not None: + assert isinstance( + transcription_worker_run, (str, bool) + ), "transcription_worker_run should be of type str or bool" + if isinstance(transcription_worker_run, bool): + assert ( + transcription_worker_run is False + ), "if of type bool, transcription_worker_run can only be set to False" + query_params["transcription_worker_run"] = transcription_worker_run + if type: + assert isinstance(type, str), "type should be of type str" + query_params["type"] = type + if with_classes is not None: + assert isinstance(with_classes, bool), "with_classes should be of type bool" + query_params["with_classes"] = with_classes + if with_corpus is not None: + assert isinstance(with_corpus, bool), "with_corpus should be of type bool" + query_params["with_corpus"] = with_corpus + if with_has_children is not None: + assert isinstance( + with_has_children, bool + ), "with_has_children should be of type bool" + query_params["with_has_children"] = with_has_children + if with_metadata is not None: + assert isinstance( + with_metadata, bool + ), "with_metadata should be of type bool" + query_params["with_metadata"] = with_metadata + if with_zone is not None: + assert isinstance(with_zone, bool), "with_zone should be of type bool" + query_params["with_zone"] = with_zone + if worker_version is not None: + assert isinstance( + worker_version, (str, bool) + ), "worker_version should be of type str or bool" + if isinstance(worker_version, bool): + assert ( + worker_version is False + ), "if of type bool, worker_version can only be set to False" + query_params["worker_version"] = worker_version + if worker_run is not None: + assert isinstance( + worker_run, (str, bool) + ), "worker_run should be of type str or bool" + if isinstance(worker_run, bool): + assert ( + worker_run is False + ), "if of type bool, worker_run can only be set to False" + query_params["worker_run"] = worker_run + + return self.api_client.paginate( + "ListElementParents", id=element.id, **query_params + ) diff --git a/tests/test_elements_worker/test_elements.py b/tests/test_elements_worker/test_elements.py index 12b438722866ab7932aa1fae855cf6cac7722d79..1b7f96d7aba6eae0b62b18070982bd1e8c82597e 100644 --- a/tests/test_elements_worker/test_elements.py +++ b/tests/test_elements_worker/test_elements.py @@ -1862,3 +1862,352 @@ def test_list_element_children_with_cache( assert [ (call.request.method, call.request.url) for call in responses.calls ] == BASE_API_CALLS + + +def test_list_element_parents_wrong_element(mock_elements_worker): + with pytest.raises(AssertionError) as e: + mock_elements_worker.list_element_parents(element=None) + assert ( + str(e.value) + == "element shouldn't be null and should be an Element or CachedElement" + ) + + with pytest.raises(AssertionError) as e: + mock_elements_worker.list_element_parents(element="not element type") + assert ( + str(e.value) + == "element shouldn't be null and should be an Element or CachedElement" + ) + + +def test_list_element_parents_wrong_folder(mock_elements_worker): + elt = Element({"id": "12341234-1234-1234-1234-123412341234"}) + + with pytest.raises(AssertionError) as e: + mock_elements_worker.list_element_parents( + element=elt, + folder="not bool", + ) + assert str(e.value) == "folder should be of type bool" + + +def test_list_element_parents_wrong_name(mock_elements_worker): + elt = Element({"id": "12341234-1234-1234-1234-123412341234"}) + + with pytest.raises(AssertionError) as e: + mock_elements_worker.list_element_parents( + element=elt, + name=1234, + ) + assert str(e.value) == "name should be of type str" + + +def test_list_element_parents_wrong_recursive(mock_elements_worker): + elt = Element({"id": "12341234-1234-1234-1234-123412341234"}) + + with pytest.raises(AssertionError) as e: + mock_elements_worker.list_element_parents( + element=elt, + recursive="not bool", + ) + assert str(e.value) == "recursive should be of type bool" + + +def test_list_element_parents_wrong_type(mock_elements_worker): + elt = Element({"id": "12341234-1234-1234-1234-123412341234"}) + + with pytest.raises(AssertionError) as e: + mock_elements_worker.list_element_parents( + element=elt, + type=1234, + ) + assert str(e.value) == "type should be of type str" + + +def test_list_element_parents_wrong_with_classes(mock_elements_worker): + elt = Element({"id": "12341234-1234-1234-1234-123412341234"}) + + with pytest.raises(AssertionError) as e: + mock_elements_worker.list_element_parents( + element=elt, + with_classes="not bool", + ) + assert str(e.value) == "with_classes should be of type bool" + + +def test_list_element_parents_wrong_with_corpus(mock_elements_worker): + elt = Element({"id": "12341234-1234-1234-1234-123412341234"}) + + with pytest.raises(AssertionError) as e: + mock_elements_worker.list_element_parents( + element=elt, + with_corpus="not bool", + ) + assert str(e.value) == "with_corpus should be of type bool" + + +def test_list_element_parents_wrong_with_has_children(mock_elements_worker): + elt = Element({"id": "12341234-1234-1234-1234-123412341234"}) + + with pytest.raises(AssertionError) as e: + mock_elements_worker.list_element_parents( + element=elt, + with_has_children="not bool", + ) + assert str(e.value) == "with_has_children should be of type bool" + + +def test_list_element_parents_wrong_with_zone(mock_elements_worker): + elt = Element({"id": "12341234-1234-1234-1234-123412341234"}) + + with pytest.raises(AssertionError) as e: + mock_elements_worker.list_element_parents( + element=elt, + with_zone="not bool", + ) + assert str(e.value) == "with_zone should be of type bool" + + +def test_list_element_parents_wrong_with_metadata(mock_elements_worker): + elt = Element({"id": "12341234-1234-1234-1234-123412341234"}) + + with pytest.raises(AssertionError) as e: + mock_elements_worker.list_element_parents( + element=elt, + with_metadata="not bool", + ) + assert str(e.value) == "with_metadata should be of type bool" + + +@pytest.mark.parametrize( + "param, value", + ( + ("worker_version", 1234), + ("worker_run", 1234), + ("transcription_worker_version", 1234), + ("transcription_worker_run", 1234), + ), +) +def test_list_element_parents_wrong_worker_version(mock_elements_worker, param, value): + elt = Element({"id": "12341234-1234-1234-1234-123412341234"}) + + with pytest.raises(AssertionError) as e: + mock_elements_worker.list_element_parents( + element=elt, + **{param: value}, + ) + assert str(e.value) == f"{param} should be of type str or bool" + + +@pytest.mark.parametrize( + "param", + ( + ("worker_version"), + ("worker_run"), + ("transcription_worker_version"), + ("transcription_worker_run"), + ), +) +def test_list_element_parents_wrong_bool_worker_version(mock_elements_worker, param): + elt = Element({"id": "12341234-1234-1234-1234-123412341234"}) + + with pytest.raises(AssertionError) as e: + mock_elements_worker.list_element_parents( + element=elt, + **{param: True}, + ) + assert str(e.value) == f"if of type bool, {param} can only be set to False" + + +def test_list_element_parents_api_error(responses, mock_elements_worker): + elt = Element({"id": "12341234-1234-1234-1234-123412341234"}) + responses.add( + responses.GET, + "http://testserver/api/v1/elements/12341234-1234-1234-1234-123412341234/parents/", + status=500, + ) + + with pytest.raises( + Exception, match="Stopping pagination as data will be incomplete" + ): + next(mock_elements_worker.list_element_parents(element=elt)) + + assert len(responses.calls) == len(BASE_API_CALLS) + 5 + assert [ + (call.request.method, call.request.url) for call in responses.calls + ] == BASE_API_CALLS + [ + # We do 5 retries + ( + "GET", + "http://testserver/api/v1/elements/12341234-1234-1234-1234-123412341234/parents/", + ), + ( + "GET", + "http://testserver/api/v1/elements/12341234-1234-1234-1234-123412341234/parents/", + ), + ( + "GET", + "http://testserver/api/v1/elements/12341234-1234-1234-1234-123412341234/parents/", + ), + ( + "GET", + "http://testserver/api/v1/elements/12341234-1234-1234-1234-123412341234/parents/", + ), + ( + "GET", + "http://testserver/api/v1/elements/12341234-1234-1234-1234-123412341234/parents/", + ), + ] + + +def test_list_element_parents(responses, mock_elements_worker): + elt = Element({"id": "12341234-1234-1234-1234-123412341234"}) + expected_parents = [ + { + "id": "0000", + "type": "page", + "name": "Test", + "corpus": {}, + "thumbnail_url": None, + "zone": {}, + "best_classes": None, + "has_children": None, + "worker_version_id": None, + "worker_run_id": None, + }, + { + "id": "1111", + "type": "page", + "name": "Test 2", + "corpus": {}, + "thumbnail_url": None, + "zone": {}, + "best_classes": None, + "has_children": None, + "worker_version_id": None, + "worker_run_id": None, + }, + { + "id": "2222", + "type": "page", + "name": "Test 3", + "corpus": {}, + "thumbnail_url": None, + "zone": {}, + "best_classes": None, + "has_children": None, + "worker_version_id": None, + "worker_run_id": None, + }, + ] + responses.add( + responses.GET, + "http://testserver/api/v1/elements/12341234-1234-1234-1234-123412341234/parents/", + status=200, + json={ + "count": 3, + "next": None, + "results": expected_parents, + }, + ) + + for idx, parent in enumerate( + mock_elements_worker.list_element_parents(element=elt) + ): + assert parent == expected_parents[idx] + + assert len(responses.calls) == len(BASE_API_CALLS) + 1 + assert [ + (call.request.method, call.request.url) for call in responses.calls + ] == BASE_API_CALLS + [ + ( + "GET", + "http://testserver/api/v1/elements/12341234-1234-1234-1234-123412341234/parents/", + ), + ] + + +def test_list_element_parents_manual_worker_version(responses, mock_elements_worker): + elt = Element({"id": "12341234-1234-1234-1234-123412341234"}) + expected_parents = [ + { + "id": "0000", + "type": "page", + "name": "Test", + "corpus": {}, + "thumbnail_url": None, + "zone": {}, + "best_classes": None, + "has_children": None, + "worker_version_id": None, + "worker_run_id": None, + } + ] + responses.add( + responses.GET, + "http://testserver/api/v1/elements/12341234-1234-1234-1234-123412341234/parents/?worker_version=False", + status=200, + json={ + "count": 1, + "next": None, + "results": expected_parents, + }, + ) + + for idx, parent in enumerate( + mock_elements_worker.list_element_parents(element=elt, worker_version=False) + ): + assert parent == expected_parents[idx] + + assert len(responses.calls) == len(BASE_API_CALLS) + 1 + assert [ + (call.request.method, call.request.url) for call in responses.calls + ] == BASE_API_CALLS + [ + ( + "GET", + "http://testserver/api/v1/elements/12341234-1234-1234-1234-123412341234/parents/?worker_version=False", + ), + ] + + +def test_list_element_parents_manual_worker_run(responses, mock_elements_worker): + elt = Element({"id": "12341234-1234-1234-1234-123412341234"}) + expected_parents = [ + { + "id": "0000", + "type": "page", + "name": "Test", + "corpus": {}, + "thumbnail_url": None, + "zone": {}, + "best_classes": None, + "has_children": None, + "worker_version_id": None, + "worker_run_id": None, + } + ] + responses.add( + responses.GET, + "http://testserver/api/v1/elements/12341234-1234-1234-1234-123412341234/parents/?worker_run=False", + status=200, + json={ + "count": 1, + "next": None, + "results": expected_parents, + }, + ) + + for idx, parent in enumerate( + mock_elements_worker.list_element_parents(element=elt, worker_run=False) + ): + assert parent == expected_parents[idx] + + assert len(responses.calls) == len(BASE_API_CALLS) + 1 + assert [ + (call.request.method, call.request.url) for call in responses.calls + ] == BASE_API_CALLS + [ + ( + "GET", + "http://testserver/api/v1/elements/12341234-1234-1234-1234-123412341234/parents/?worker_run=False", + ), + ]