diff --git a/CHANGELOG b/CHANGELOG index ce9f046..d67a4d9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -38,6 +38,9 @@ (only up to `limit` users are returned, by default 20) - enh: wait between retries of failed S3 upload requests (5 s, doubling with each retry) instead of retrying immediately + - enh: use the API action `dcor_group_list_for_user` (ckanext-dcor_schemas + 1.2.0) for listing the collections of a user, with a fallback for + older servers (#108) - ref: convert `dcoraid.cli` module into a package; the upload task CLI now lives in `dcoraid.cli.task_upload_task` - ref: use `super()`, PEP 604 optional type hints, and builtin/ diff --git a/dcoraid/dbmodel/db_api.py b/dcoraid/dbmodel/db_api.py index bf098f6..277470e 100644 --- a/dcoraid/dbmodel/db_api.py +++ b/dcoraid/dbmodel/db_api.py @@ -1,16 +1,22 @@ from __future__ import annotations from itertools import islice +import logging import sys import time import urllib.parse import numpy as np +from ..api.errors import APIBadRequest + from .db_core import DBInterrogator from .extract import DBExtract +logger = logging.getLogger(__name__) + + if sys.version_info >= (3, 12): from itertools import batched else: @@ -50,13 +56,22 @@ def get_collections_user(self): This includes all collections in which the user has any role (also "member"). """ + user_id = self.api.user_id + if user_id is None: + return [] + try: + # DCOR servers with ckanext-dcor_schemas >= 1.2.0 + return self.api.get("dcor_group_list_for_user") + except APIBadRequest as e: + if "Action name not known" not in str(e): + raise + logger.info("The server does not support the API action " + "`dcor_group_list_for_user`, falling back to " + "checking the members of all collections.") # CKAN's `group_list_authz` only returns the collections that the # user may edit, and there is no API call that returns the # collections in which the user is a "member". Therefore, we have # to check the members of every collection. - user_id = self.api.user_id - if user_id is None: - return [] collections = [] for col in self.get_collections(): members = self.api.get("member_list", diff --git a/tests/test_dbmodel_api_lists.py b/tests/test_dbmodel_api_lists.py index bea10e8..41d96b9 100644 --- a/tests/test_dbmodel_api_lists.py +++ b/tests/test_dbmodel_api_lists.py @@ -3,11 +3,12 @@ import pytest +from dcoraid.api.errors import APIBadRequest from dcoraid.dbmodel import db_api def make_interrogator(num_circles=60, num_collections=40, user_id="user-1", - member_of=(), max_items=25): + member_of=(), max_items=25, group_list_action=False): """Return an APIInterrogator with a mocked CKAN API Parameters @@ -20,6 +21,9 @@ def make_interrogator(num_circles=60, num_collections=40, user_id="user-1", Collection names in which the user is a member max_items: int Maximum number of items CKAN returns with `all_fields=True` + group_list_action: bool + Whether the server supports the `dcor_group_list_for_user` action + (ckanext-dcor_schemas >= 1.2.0) """ circles = [{"id": f"oid-{ii:03d}", "name": f"circle-{ii:03d}"} for ii in range(num_circles)] @@ -44,6 +48,12 @@ def get(api_call, **kwargs): return [["other-user", "user", "admin"]] elif api_call == "organization_list_for_user": return circles[:2] + elif api_call == "dcor_group_list_for_user": + if group_list_action: + return [c for c in collections if c["name"] in member_of] + else: + raise APIBadRequest("Bad request - Action name not known: " + "dcor_group_list_for_user") raise ValueError(f"Unexpected API call {api_call}") api = mock.MagicMock() @@ -99,10 +109,36 @@ def test_get_circles_user(): def test_get_collections_user(): """Collections in which the user has any role (also "member")""" - ai, _ = make_interrogator(num_collections=40, - member_of=["collection-003", "collection-030"]) + ai, calls = make_interrogator(num_collections=40, + member_of=["collection-003", + "collection-030"], + group_list_action=True) + assert [c["name"] for c in ai.get_collections_user()] == \ + ["collection-003", "collection-030"] + # the server-side action is used (one request) + assert [c[0] for c in calls] == ["dcor_group_list_for_user"] + + +def test_get_collections_user_fallback(): + """Older servers do not support `dcor_group_list_for_user`""" + ai, calls = make_interrogator(num_collections=40, + member_of=["collection-003", + "collection-030"], + group_list_action=False) assert [c["name"] for c in ai.get_collections_user()] == \ ["collection-003", "collection-030"] + # fall back to checking the members of all collections + api_calls = [c[0] for c in calls] + assert api_calls[0] == "dcor_group_list_for_user" + assert api_calls.count("member_list") == 40 + + +def test_get_collections_user_other_bad_request(): + """Only fall back if the action is not known to the server""" + ai, _ = make_interrogator() + ai.api.get.side_effect = APIBadRequest("Bad request - something else") + with pytest.raises(APIBadRequest, match="something else"): + ai.get_collections_user() def test_get_lists_user_anonymous():