diff --git a/src/main/python/covata/delta/apiclient.py b/src/main/python/covata/delta/apiclient.py index 06bf2ae..163bb8c 100644 --- a/src/main/python/covata/delta/apiclient.py +++ b/src/main/python/covata/delta/apiclient.py @@ -17,8 +17,7 @@ import requests -from . import signer -from . import utils +from . import signer, utils class ApiClient(utils.LogMixin): @@ -95,6 +94,38 @@ def get_identity(self, requestor_id, identity_id): identity = response.json() return identity + @utils.check_arguments( + "page, page_size", + lambda x: True if x is None else int(x) > 0, + "must be a non-zero positive integer") + def get_identities_by_metadata(self, requestor_id, metadata, + page=None, page_size=None): + """ + Gets a list of identities matching the given metadata key and value + pairs, bound by the pagination parameters. + + :param str requestor_id: the authenticating identity id + :param metadata: the metadata key and value pairs to filter + :type metadata: dict[str, str] + :param page: the page number + :type page: int | None + :param page_size: the page size + :type page_size: int | None + :return: a list of identities satisfying the request + :rtype: list[dict[str, any]] + """ + metadata_ = dict(("metadata." + k, v) for k, v in metadata.items()) + response = requests.get( + url="{base_url}{resource}".format( + base_url=self.DELTA_URL, + resource=self.RESOURCE_IDENTITIES), + params=dict(metadata_, + page=int(page) if page else None, + pageSize=int(page_size) if page_size else None), + auth=self.signer(requestor_id)) + response.raise_for_status() + return response.json() + def create_secret(self, requestor_id, content, encryption_details): """ Creates a new secret in Delta. The key used for encryption should diff --git a/src/main/python/covata/delta/utils.py b/src/main/python/covata/delta/utils.py index 341541b..b0c6682 100644 --- a/src/main/python/covata/delta/utils.py +++ b/src/main/python/covata/delta/utils.py @@ -14,31 +14,48 @@ import logging import inspect +import functools -__all__ = ["LogMixin"] - -class LogMixin(object): +class LogMixin: @property def logger(self): - return logging.getLogger(self.__caller()) - - def __caller(self): - """ - Gets the name of the caller in {package}.{module}.{class} format - - :return: the name of the caller - """ - # type: () -> str - stack = inspect.stack() - if len(stack) < 3: - return '' - - caller_frame = stack[2][0] - module = inspect.getmodule(caller_frame) - name = filter(lambda x: x is not None, [ - module.__name__ if module else None, - self.__class__.__name__]) - - del caller_frame - return ".".join(name) + return logging.getLogger(caller()) + + +def caller(): + """ + Gets the name of the caller in {package}.{module}.{class} format + + :return: the caller name + :rtype: str + """ + stack = inspect.stack() + if len(stack) < 3: + return '' + + caller_frame = stack[2][0] + module = inspect.getmodule(caller_frame) + + name = filter(lambda x: x is not None, [ + module.__name__ if module else None, + caller_frame.f_locals['self'].__class__.__name__ + if 'self' in caller_frame.f_locals else None]) + del caller_frame + return ".".join(name) + + +def check_arguments(arguments, validation_function, fail_message): + def decorator(function): + @functools.wraps(function) + def _f(*args, **kwargs): + keys, _, _, _ = inspect.getargspec(function) + ins = dict(zip(keys, args)) + ins.update(kwargs) + generator = ((x, y) for x, y in ins.items() if x in arguments) + for arg, value in generator: + if not validation_function(value): + raise ValueError("{} {}".format(arg, fail_message)) + return function(*args, **kwargs) + return _f + return decorator diff --git a/src/test/python/test_apiclient.py b/src/test/python/test_apiclient.py index 32466dc..8c7c572 100644 --- a/src/test/python/test_apiclient.py +++ b/src/test/python/test_apiclient.py @@ -19,6 +19,7 @@ import pytest import requests import responses +from six.moves import urllib from covata.delta import ApiClient from covata.delta import crypto @@ -324,6 +325,64 @@ def test_get_secret_content(api_client, mock_signer): assert retrieved_content == expected_content +@responses.activate +@pytest.mark.parametrize("page", [1, 3.0, "5", None]) +@pytest.mark.parametrize("page_size", [1, "3", 5.0, None]) +def test_get_identities_by_metadata_with_valid_page_parameters( + api_client, mock_signer, page, page_size): + requestor_id = "requestor_id" + expected_json = [dict(cryptoPublicKey="cryptoPublicKey", + id="1", + metadata=dict(name="test123"), + version=2)] + responses.add( + responses.GET, + "{base_path}{resource}".format( + base_path=ApiClient.DELTA_URL, + resource=ApiClient.RESOURCE_IDENTITIES), + json=expected_json) + + response = api_client.get_identities_by_metadata( + requestor_id=requestor_id, + metadata=dict(name="test123"), + page=page, + page_size=page_size) + + mock_signer.assert_called_once_with(requestor_id) + + assert len(responses.calls) == 1 + assert response == expected_json + url = urllib.parse.urlparse(responses.calls[0].request.url) + query_params = dict(urllib.parse.parse_qsl(url.query)) + expected_query_params = { + "metadata.name": "test123" + } + + if page is not None: + expected_query_params["page"] = str(int(page)) + + if page_size is not None: + expected_query_params["pageSize"] = str(int(page_size)) + + assert query_params == expected_query_params + + +@responses.activate +@pytest.mark.parametrize("page", [0, -3.0, "-5"]) +@pytest.mark.parametrize("page_size", [0, "-3", 5.0]) +def test_get_identities_by_metadata_with_invalid_page_parameters( + api_client, mock_signer, page, page_size): + requestor_id = "requestor_id" + with pytest.raises(ValueError) as excinfo: + api_client.get_identities_by_metadata( + requestor_id=requestor_id, + metadata=dict(name="test123"), + page=page, + page_size=page_size) + mock_signer.assert_not_called() + assert "must be a non-zero positive integer" in str(excinfo.value) + + def test_construct_signer(mocker, api_client, key_store, private_key): get_private_signing_key = mocker.patch.object( key_store, 'get_private_signing_key', return_value=private_key)