From 76df197c0a7718d33aaa2dfe032ec738f7d54038 Mon Sep 17 00:00:00 2001 From: Andrew Whitehead Date: Wed, 30 Oct 2019 13:41:51 -0700 Subject: [PATCH] use issue-credential protocol in performance demo; use 'credential_preview' consistently as input parameter on admin routes Signed-off-by: Andrew Whitehead --- .../issue_credential/v1_0/manager.py | 8 +- .../messaging/issue_credential/v1_0/routes.py | 88 ++++++++----------- .../v1_0/tests/test_manager.py | 8 +- .../v1_0/tests/test_routes.py | 39 ++++---- demo/runners/performance.py | 48 +++++++--- demo/runners/support/agent.py | 36 ++++++-- 6 files changed, 124 insertions(+), 103 deletions(-) diff --git a/aries_cloudagent/messaging/issue_credential/v1_0/manager.py b/aries_cloudagent/messaging/issue_credential/v1_0/manager.py index 95a96ede72..6fccc30124 100644 --- a/aries_cloudagent/messaging/issue_credential/v1_0/manager.py +++ b/aries_cloudagent/messaging/issue_credential/v1_0/manager.py @@ -49,7 +49,7 @@ def context(self) -> InjectionContext: async def prepare_send( self, connection_id: str, credential_proposal: CredentialProposal - ) -> V10CredentialExchange: + ) -> Tuple[V10CredentialExchange, CredentialOffer]: """ Set up a new credential exchange for an automated send. @@ -59,7 +59,7 @@ async def prepare_send( attribute values to use if auto_issue is enabled Returns: - A new credential exchange record + A tuple of the new credential exchange record and credential offer message """ @@ -77,11 +77,11 @@ async def prepare_send( credential_definition_id=credential_definition_id, credential_proposal_dict=credential_proposal.serialize(), ) - (credential_exchange, _) = await self.create_offer( + (credential_exchange, credential_offer) = await self.create_offer( credential_exchange_record=credential_exchange, comment="create automated credential exchange", ) - return credential_exchange + return credential_exchange, credential_offer async def create_proposal( self, diff --git a/aries_cloudagent/messaging/issue_credential/v1_0/routes.py b/aries_cloudagent/messaging/issue_credential/v1_0/routes.py index 6c3d2abc07..0bcd93486b 100644 --- a/aries_cloudagent/messaging/issue_credential/v1_0/routes.py +++ b/aries_cloudagent/messaging/issue_credential/v1_0/routes.py @@ -14,7 +14,6 @@ from .manager import CredentialManager from .messages.credential_proposal import CredentialProposal from .messages.inner.credential_preview import ( - CredAttrSpec, CredentialPreview, CredentialPreviewSchema, ) @@ -51,7 +50,7 @@ class V10CredentialProposalRequestSchema(Schema): **INDY_CRED_DEF_ID, ) comment = fields.Str(description="Human-readable comment", required=False) - credential_proposal = fields.Nested(CredentialPreviewSchema, required=True) + credential_preview = fields.Nested(CredentialPreviewSchema, required=True) class V10CredentialOfferRequestSchema(Schema): @@ -190,27 +189,12 @@ async def credential_exchange_send(request: web.BaseRequest): connection_id = body.get("connection_id") credential_definition_id = body.get("credential_definition_id") comment = body.get("comment") - credential_proposal = CredentialProposal( - comment=comment, - credential_proposal=CredentialPreview( - attributes=[ - CredAttrSpec( - name=attr_preview["name"], - mime_type=attr_preview.get("mime-type", None), - value=attr_preview["value"], - ) - for attr_preview in body.get("credential_proposal")["attributes"] - ] - ), - cred_def_id=credential_definition_id, - ) + preview_spec = body.get("credential_preview") - if not credential_proposal: - raise web.HTTPBadRequest( - reason="credential_proposal must be provided with attribute values." - ) - - credential_manager = CredentialManager(context) + if not credential_definition_id: + raise web.HTTPBadRequest(reason="credential_definition_id must be provided.") + if not preview_spec: + raise web.HTTPBadRequest(reason="credential_preview must be provided.") try: connection_record = await ConnectionRecord.retrieve_by_id( @@ -222,18 +206,19 @@ async def credential_exchange_send(request: web.BaseRequest): if not connection_record.is_ready: raise web.HTTPForbidden() - credential_exchange_record = await credential_manager.prepare_send( - connection_id, credential_proposal=credential_proposal + credential_proposal = CredentialProposal( + comment=comment, + credential_proposal=CredentialPreview.deserialize(preview_spec), + cred_def_id=credential_definition_id, ) + credential_manager = CredentialManager(context) + ( credential_exchange_record, credential_offer_message, - ) = await credential_manager.create_offer( - credential_exchange_record, - comment="Automated offer creation on cred def id " - f"{credential_exchange_record.credential_definition_id}, " - f"parent thread {credential_exchange_record.parent_thread_id}", + ) = await credential_manager.prepare_send( + connection_id, credential_proposal=credential_proposal ) await outbound_handler( credential_offer_message, connection_id=credential_exchange_record.connection_id @@ -264,14 +249,10 @@ async def credential_exchange_send_proposal(request: web.BaseRequest): connection_id = body.get("connection_id") credential_definition_id = body.get("credential_definition_id") comment = body.get("comment") - proposal_spec = body.get("credential_proposal") - - if not proposal_spec: - raise web.HTTPBadRequest(reason="credential_proposal must be provided.") - - credential_preview = CredentialPreview.deserialize(proposal_spec) + preview_spec = body.get("credential_preview") - credential_manager = CredentialManager(context) + if not preview_spec: + raise web.HTTPBadRequest(reason="credential_preview must be provided.") try: connection_record = await ConnectionRecord.retrieve_by_id( @@ -283,6 +264,10 @@ async def credential_exchange_send_proposal(request: web.BaseRequest): if not connection_record.is_ready: raise web.HTTPForbidden() + credential_preview = CredentialPreview.deserialize(preview_spec) + + credential_manager = CredentialManager(context) + credential_exchange_record = await credential_manager.create_proposal( connection_id, comment=comment, @@ -332,19 +317,17 @@ async def credential_exchange_send_free_offer(request: web.BaseRequest): "auto_issue", context.settings.get("debug.auto_respond_credential_request") ) comment = body.get("comment") - proposal_spec = body.get("credential_proposal") + preview_spec = body.get("credential_preview") if not credential_definition_id: raise web.HTTPBadRequest(reason="credential_definition_id is required") - if auto_issue and not proposal_spec: + if auto_issue and not preview_spec: raise web.HTTPBadRequest( reason="If auto_issue is set to" + " true then credential_preview must also be provided." ) - credential_manager = CredentialManager(context) - try: connection_record = await ConnectionRecord.retrieve_by_id( context, connection_id @@ -355,8 +338,8 @@ async def credential_exchange_send_free_offer(request: web.BaseRequest): if not connection_record.is_ready: raise web.HTTPForbidden() - if proposal_spec: - credential_preview = CredentialPreview.deserialize(proposal_spec) + if preview_spec: + credential_preview = CredentialPreview.deserialize(preview_spec) credential_proposal = CredentialProposal( comment=comment, credential_proposal=credential_preview, @@ -374,6 +357,8 @@ async def credential_exchange_send_free_offer(request: web.BaseRequest): auto_issue=auto_issue, ) + credential_manager = CredentialManager(context) + ( credential_exchange_record, credential_offer_message, @@ -466,8 +451,6 @@ async def credential_exchange_send_request(request: web.BaseRequest): V10CredentialExchange.STATE_OFFER_RECEIVED ) - credential_manager = CredentialManager(context) - try: connection_record = await ConnectionRecord.retrieve_by_id( context, connection_id @@ -478,6 +461,8 @@ async def credential_exchange_send_request(request: web.BaseRequest): if not connection_record.is_ready: raise web.HTTPForbidden() + credential_manager = CredentialManager(context) + ( credential_exchange_record, credential_request_message, @@ -508,7 +493,10 @@ async def credential_exchange_issue(request: web.BaseRequest): body = await request.json() comment = body.get("comment") - credential_preview = CredentialPreview.deserialize(body["credential_preview"]) + preview_spec = body.get("credential_preview") + + if not preview_spec: + raise web.HTTPBadRequest(reason="credential_preview must be provided.") credential_exchange_id = request.match_info["cred_ex_id"] cred_exch_record = await V10CredentialExchange.retrieve_by_id( @@ -518,8 +506,6 @@ async def credential_exchange_issue(request: web.BaseRequest): assert cred_exch_record.state == V10CredentialExchange.STATE_REQUEST_RECEIVED - credential_manager = CredentialManager(context) - try: connection_record = await ConnectionRecord.retrieve_by_id( context, connection_id @@ -530,6 +516,10 @@ async def credential_exchange_issue(request: web.BaseRequest): if not connection_record.is_ready: raise web.HTTPForbidden() + credential_preview = CredentialPreview.deserialize(preview_spec) + + credential_manager = CredentialManager(context) + ( cred_exch_record, credential_issue_message, @@ -569,8 +559,6 @@ async def credential_exchange_store(request: web.BaseRequest): V10CredentialExchange.STATE_CREDENTIAL_RECEIVED ) - credential_manager = CredentialManager(context) - try: connection_record = await ConnectionRecord.retrieve_by_id( context, connection_id @@ -581,6 +569,8 @@ async def credential_exchange_store(request: web.BaseRequest): if not connection_record.is_ready: raise web.HTTPForbidden() + credential_manager = CredentialManager(context) + ( credential_exchange_record, credential_stored_message, diff --git a/aries_cloudagent/messaging/issue_credential/v1_0/tests/test_manager.py b/aries_cloudagent/messaging/issue_credential/v1_0/tests/test_manager.py index 5c90552116..9c6a3de904 100644 --- a/aries_cloudagent/messaging/issue_credential/v1_0/tests/test_manager.py +++ b/aries_cloudagent/messaging/issue_credential/v1_0/tests/test_manager.py @@ -41,13 +41,11 @@ async def test_prepare_send(self): with async_mock.patch.object( self.manager, "create_offer", autospec=True ) as create_offer: - create_offer.return_value = (object(), None) - ret_exchange = await self.manager.prepare_send(connection_id, proposal) + create_offer.return_value = (async_mock.MagicMock(), async_mock.MagicMock()) + ret_exchange, ret_cred_offer = await self.manager.prepare_send(connection_id, proposal) create_offer.assert_called_once() assert ret_exchange is create_offer.return_value[0] - exchange: V10CredentialExchange = create_offer.call_args[1][ - "credential_exchange_record" - ] + exchange = create_offer.call_args[1]["credential_exchange_record"] assert exchange.auto_issue assert exchange.connection_id == connection_id assert exchange.credential_definition_id == cred_def_id diff --git a/aries_cloudagent/messaging/issue_credential/v1_0/tests/test_routes.py b/aries_cloudagent/messaging/issue_credential/v1_0/tests/test_routes.py index cf3dc5c9f5..6eb2c62917 100644 --- a/aries_cloudagent/messaging/issue_credential/v1_0/tests/test_routes.py +++ b/aries_cloudagent/messaging/issue_credential/v1_0/tests/test_routes.py @@ -21,7 +21,9 @@ async def test_credential_exchange_send(self): test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( test_module, "CredentialManager", autospec=True - ) as mock_credential_manager: + ) as mock_credential_manager, async_mock.patch.object( + test_module.CredentialPreview, "deserialize", autospec=True + ): test_module.web.json_response = async_mock.CoroutineMock() mock_credential_manager.return_value.create_offer = ( @@ -34,17 +36,16 @@ async def test_credential_exchange_send(self): ) mock_cred_ex_record = async_mock.MagicMock() + mock_cred_offer = async_mock.MagicMock() mock_credential_manager.return_value.prepare_send.return_value = ( - mock_cred_ex_record + (mock_cred_ex_record, mock_cred_offer) ) await test_module.credential_exchange_send(mock) test_module.web.json_response.assert_called_once_with( - mock_credential_manager.return_value.create_offer.return_value[ - 0 - ].serialize.return_value + mock_cred_ex_record.serialize.return_value ) async def test_credential_exchange_send_no_conn_record(self): @@ -65,13 +66,8 @@ async def test_credential_exchange_send_no_conn_record(self): test_module.web.json_response = async_mock.CoroutineMock() # Emulate storage not found (bad connection id) - mock_connection_record.retrieve_by_id = async_mock.CoroutineMock( - side_effect=StorageNotFoundError - ) + mock_connection_record.retrieve_by_id.side_effect = StorageNotFoundError - mock_credential_manager.return_value.create_offer = ( - async_mock.CoroutineMock() - ) mock_credential_manager.return_value.create_offer.return_value = ( async_mock.MagicMock(), async_mock.MagicMock(), @@ -98,12 +94,8 @@ async def test_credential_exchange_send_not_ready(self): test_module.web.json_response = async_mock.CoroutineMock() # Emulate connection not ready - mock_connection_record.retrieve_by_id = async_mock.CoroutineMock() mock_connection_record.retrieve_by_id.return_value.is_ready = False - mock_credential_manager.return_value.create_offer = ( - async_mock.CoroutineMock() - ) mock_credential_manager.return_value.create_offer.return_value = ( async_mock.MagicMock(), async_mock.MagicMock(), @@ -114,14 +106,11 @@ async def test_credential_exchange_send_not_ready(self): async def test_credential_exchange_send_proposal(self): conn_id = "connection-id" - proposal_spec = {"attributes": [{"name": "attr", "value": "value"}]} + preview_spec = {"attributes": [{"name": "attr", "value": "value"}]} mock = async_mock.MagicMock() mock.json = async_mock.CoroutineMock( - return_value={ - "connection_id": conn_id, - "credential_proposal": proposal_spec, - } + return_value={"connection_id": conn_id, "credential_preview": preview_spec} ) mock.app = { "outbound_message_router": async_mock.CoroutineMock(), @@ -218,7 +207,10 @@ async def test_credential_exchange_send_proposal_not_ready(self): async def test_credential_exchange_send_free_offer(self): mock = async_mock.MagicMock() mock.json = async_mock.CoroutineMock( - return_value={"auto_issue": False, "credential_definition_id": "cred-def-id"} + return_value={ + "auto_issue": False, + "credential_definition_id": "cred-def-id", + } ) mock.app = { @@ -257,7 +249,10 @@ async def test_credential_exchange_send_free_offer(self): async def test_credential_exchange_send_free_offer_no_conn_record(self): mock = async_mock.MagicMock() mock.json = async_mock.CoroutineMock( - return_value={"auto_issue": False, "credential_definition_id": "cred-def-id"} + return_value={ + "auto_issue": False, + "credential_definition_id": "cred-def-id", + } ) mock.app = { diff --git a/demo/runners/performance.py b/demo/runners/performance.py index e992c09689..f3779dd5a8 100644 --- a/demo/runners/performance.py +++ b/demo/runners/performance.py @@ -71,7 +71,7 @@ async def handle_connections(self, payload): self.log("Connected") self._connection_ready.set_result(True) - async def handle_credentials(self, payload): + async def handle_issue_credential(self, payload): cred_id = payload["credential_exchange_id"] self.credential_state[cred_id] = payload["state"] self.credential_event.set() @@ -91,6 +91,15 @@ async def check_received_creds(self) -> (int, int): async def update_creds(self): await self.credential_event.wait() + def check_task_exception(self, fut: asyncio.Task): + if fut.done(): + try: + exc = fut.exception() + except asyncio.CancelledError as e: + exc = e + if exc: + self.log(f"Task raised exception: {str(exc)}") + class AliceAgent(BaseAgent): def __init__(self, port: int, **kwargs): @@ -135,19 +144,17 @@ async def publish_defs(self): ] self.log(f"Credential Definition ID: {self.credential_definition_id}") - async def send_credential(self): - cred_attrs = { - "name": "Alice Smith", - "date": "2018-05-28", - "degree": "Maths", - "age": "24", + async def send_credential(self, cred_attrs: dict, comment: str = None): + cred_preview = { + "attributes": [{"name": n, "value": v} for (n, v) in cred_attrs.items()] } await self.admin_POST( - "/credential_exchange/send", + "/issue-credential/send", { - "credential_values": cred_attrs, "connection_id": self.connection_id, "credential_definition_id": self.credential_definition_id, + "credential_preview": cred_preview, + "comment": comment, }, ) @@ -228,11 +235,22 @@ async def main( semaphore = asyncio.Semaphore(10) - async def send(): + def done_send(fut: asyncio.Task): + semaphore.release() + faber.check_task_exception(fut) + + async def send(index: int): await semaphore.acquire() - asyncio.ensure_future(faber.send_credential()).add_done_callback( - lambda fut: semaphore.release() - ) + comment = f"issue test credential {index}" + attributes = { + "name": "Alice Smith", + "date": "2018-05-28", + "degree": "Maths", + "age": "24", + } + asyncio.ensure_future( + faber.send_credential(attributes, comment) + ).add_done_callback(done_send) recv_timer = alice.log_timer(f"Received {issue_count} credentials in ") recv_timer.start() @@ -268,14 +286,16 @@ async def check_received(agent, issue_count, pb): issue_task = asyncio.ensure_future( check_received(faber, issue_count, issue_pg) ) + issue_task.add_done_callback(faber.check_task_exception) receive_task = asyncio.ensure_future( check_received(alice, issue_count, receive_pg) ) + receive_task.add_done_callback(alice.check_task_exception) with faber.log_timer( f"Done starting {issue_count} credential exchanges in " ): for idx in range(0, issue_count): - await send() + await send(idx + 1) if not (idx + 1) % batch_size and idx < issue_count - 1: batch_timer.reset() diff --git a/demo/runners/support/agent.py b/demo/runners/support/agent.py index a82f959a89..f8952da959 100644 --- a/demo/runners/support/agent.py +++ b/demo/runners/support/agent.py @@ -8,7 +8,14 @@ import subprocess from timeit import default_timer -from aiohttp import web, ClientSession, ClientRequest, ClientError, ClientTimeout +from aiohttp import ( + web, + ClientSession, + ClientRequest, + ClientResponse, + ClientError, + ClientTimeout, +) from .utils import flatten, log_json, log_msg, log_timer, output_reader @@ -323,13 +330,14 @@ async def handle_webhook(self, topic: str, payload): f"to handle webhook on topic {topic}" ) - async def admin_request(self, method, path, data=None, text=False, params=None): + async def admin_request( + self, method, path, data=None, text=False, params=None + ) -> ClientResponse: params = {k: v for (k, v) in (params or {}).items() if v is not None} async with self.client_session.request( method, self.admin_url + path, json=data, params=params ) as resp: - if resp.status < 200 or resp.status > 299: - raise Exception(f"Unexpected HTTP response: {resp.status}") + resp.raise_for_status() resp_text = await resp.text() if not resp_text and not text: return None @@ -340,11 +348,21 @@ async def admin_request(self, method, path, data=None, text=False, params=None): raise Exception(f"Error decoding JSON: {resp_text}") from e return resp_text - async def admin_GET(self, path, text=False, params=None): - return await self.admin_request("GET", path, None, text, params) - - async def admin_POST(self, path, data=None, text=False, params=None): - return await self.admin_request("POST", path, data, text, params) + async def admin_GET(self, path, text=False, params=None) -> ClientResponse: + try: + return await self.admin_request("GET", path, None, text, params) + except ClientError as e: + self.log(f"Error during GET {path}: {str(e)}") + raise + + async def admin_POST( + self, path, data=None, text=False, params=None + ) -> ClientResponse: + try: + return await self.admin_request("POST", path, data, text, params) + except ClientError as e: + self.log(f"Error during POST {path}: {str(e)}") + raise async def detect_process(self): text = None