Skip to content
35 changes: 33 additions & 2 deletions src/main/python/covata/delta/apiclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@

import requests

from . import signer
from . import utils
from . import signer, utils


class ApiClient(utils.LogMixin):
Expand Down Expand Up @@ -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
Expand Down
65 changes: 41 additions & 24 deletions src/main/python/covata/delta/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
59 changes: 59 additions & 0 deletions src/test/python/test_apiclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down