From dcd298f9901516fc737a05529c5087a8370a5903 Mon Sep 17 00:00:00 2001 From: sklump Date: Mon, 30 Mar 2020 23:43:26 +0000 Subject: [PATCH 1/4] work in progress, modulo master merge Signed-off-by: sklump --- aries_cloudagent/issuer/tests/test_indy.py | 304 ++++++++++++++++-- .../issue_credential/v1_0/manager.py | 45 +-- .../v1_0/models/credential_exchange.py | 1 - .../protocols/issue_credential/v1_0/routes.py | 172 +++++----- .../v1_0/tests/test_manager.py | 38 +-- .../v1_0/tests/test_routes.py | 176 ++++------ aries_cloudagent/revocation/indy.py | 23 +- .../models/issuer_rev_reg_record.py | 11 +- aries_cloudagent/revocation/routes.py | 64 +++- .../revocation/tests/test_indy.py | 8 +- .../revocation/tests/test_routes.py | 56 +++- demo/runners/faber.py | 23 +- demo/runners/performance.py | 25 +- 13 files changed, 604 insertions(+), 342 deletions(-) diff --git a/aries_cloudagent/issuer/tests/test_indy.py b/aries_cloudagent/issuer/tests/test_indy.py index 42d29cc659..671fd737bd 100644 --- a/aries_cloudagent/issuer/tests/test_indy.py +++ b/aries_cloudagent/issuer/tests/test_indy.py @@ -5,18 +5,123 @@ from asynctest import TestCase as AsyncTestCase from asynctest import mock as async_mock -from indy.error import IndyError, ErrorCode +from indy.error import ( + AnoncredsRevocationRegistryFullError, + ErrorCode, + IndyError, + WalletItemNotFound +) -from aries_cloudagent.issuer.indy import IndyIssuer, IssuerError -from aries_cloudagent.wallet.indy import IndyWallet +from ...wallet.indy import IndyWallet + +from ..base import IssuerRevocationRegistryFullError +from ..indy import IndyIssuer, IssuerError + + +TEST_DID = "55GkHamhTU1ZbTbV2ab9DE" +SCHEMA_NAME = "resident" +SCHEMA_VERSION = "1.0" +SCHEMA_TXN=1234 +SCHEMA_ID = f"{TEST_DID}:2:{SCHEMA_NAME}:{SCHEMA_VERSION}" +CRED_DEF_ID = f"{TEST_DID}:3:CL:{SCHEMA_TXN}:default" +REV_REG_ID = f"{TEST_DID}:4:{CRED_DEF_ID}:CL_ACCUM:0" +TEST_RR_DELTA = { + "ver": "1.0", + "value": { + "prevAccum": "1 ...", + "accum": "21 ...", + "issued": [1, 2, 12, 42] + } +} @pytest.mark.indy class TestIndyIssuer(AsyncTestCase): - def test_init(self): - mock_wallet = async_mock.MagicMock() - issuer = IndyIssuer(mock_wallet) - assert issuer.wallet is mock_wallet + async def setUp(self): + self.wallet = IndyWallet( + { + "auto_create": True, + "auto_remove": True, + "key": await IndyWallet.generate_wallet_key(), + "key_derivation_method": "RAW", + "name": "test", + } + ) + self.issuer = IndyIssuer(self.wallet) + assert self.issuer.wallet is self.wallet + await self.wallet.open() + + async def tearDown(self): + await self.wallet.close() + + async def test_repr(self): + assert "IndyIssuer" in str(self.issuer) # cover __repr__ + + @async_mock.patch("indy.anoncreds.issuer_create_and_store_credential_def") + async def test_schema_cred_def(self, mock_indy_cred_def): + assert self.issuer.make_schema_id( + TEST_DID, + SCHEMA_NAME, + SCHEMA_VERSION + ) == SCHEMA_ID + + (s_id, schema_json) = await self.issuer.create_and_store_schema( + TEST_DID, + SCHEMA_NAME, + SCHEMA_VERSION, + ["name", "moniker", "genre", "effective"] + ) + assert s_id == SCHEMA_ID + schema = json.loads(schema_json) + schema['seqNo'] = SCHEMA_TXN + + assert self.issuer.make_credential_definition_id( + TEST_DID, + schema, + tag='default' + ) == CRED_DEF_ID + + (s_id, _) = await self.issuer.create_and_store_schema( + TEST_DID, + SCHEMA_NAME, + SCHEMA_VERSION, + ["name", "moniker", "genre", "effective"] + ) + assert s_id == SCHEMA_ID + + mock_indy_cred_def.return_value = ( + CRED_DEF_ID, + json.dumps({"dummy": "cred-def"}) + ) + assert (CRED_DEF_ID, json.dumps({"dummy": "cred-def"})) == ( + await self.issuer.create_and_store_credential_definition( + TEST_DID, + schema, + support_revocation=True + ) + ) + + @async_mock.patch("indy.anoncreds.issuer_create_credential_offer") + async def test_credential_definition_in_wallet(self, mock_indy_create_offer): + mock_indy_create_offer.return_value = { + "sample": "offer" + } + assert await self.issuer.credential_definition_in_wallet(CRED_DEF_ID) + + @async_mock.patch("indy.anoncreds.issuer_create_credential_offer") + async def test_credential_definition_in_wallet_no(self, mock_indy_create_offer): + mock_indy_create_offer.side_effect = WalletItemNotFound( + error_code=ErrorCode.WalletItemNotFound + ) + assert not await self.issuer.credential_definition_in_wallet(CRED_DEF_ID) + + @async_mock.patch("indy.anoncreds.issuer_create_credential_offer") + async def test_credential_definition_in_wallet_x(self, mock_indy_create_offer): + mock_indy_create_offer.side_effect = IndyError( + error_code=ErrorCode.WalletInvalidHandle + ) + with self.assertRaises(IssuerError): + await self.issuer.credential_definition_in_wallet(CRED_DEF_ID) @async_mock.patch("indy.anoncreds.issuer_create_credential_offer") async def test_create_credential_offer(self, mock_create_offer): @@ -30,28 +135,72 @@ async def test_create_credential_offer(self, mock_create_offer): mock_create_offer.assert_awaited_once_with(mock_wallet.handle, test_cred_def_id) @async_mock.patch("indy.anoncreds.issuer_create_credential") - async def test_create_credential(self, mock_create_credential): - mock_wallet = async_mock.MagicMock() - issuer = IndyIssuer(mock_wallet) - + @async_mock.patch("aries_cloudagent.issuer.indy.create_tails_reader") + @async_mock.patch("indy.anoncreds.issuer_revoke_credential") + async def test_create_revoke_credential( + self, + mock_indy_revoke_credential, + mock_tails_reader, + mock_indy_create_credential + ): test_schema = {"attrNames": ["attr1"]} - test_offer = {"test": "offer"} + test_offer = { + "schema_id": SCHEMA_ID, + "cred_def_id": CRED_DEF_ID, + "key_correctness_proof": { + "c": "...", + "xz_cap": "...", + "xr_cap": ["..."], + }, + "nonce": "..." + } test_request = {"test": "request"} test_values = {"attr1": "value1"} - test_credential = {"test": "credential"} - test_revoc_id = "revoc-id" - mock_create_credential.return_value = ( - json.dumps(test_credential), - test_revoc_id, - None, + test_cred = { + "schema_id": SCHEMA_ID, + "cred_def_id": CRED_DEF_ID, + "rev_reg_id": REV_REG_ID, + "values": { + "attr1": { + "raw": "value1", + "encoded": "123456123899216581404" + } + }, + "signature": { + "...": "..." + }, + "signature_correctness_proof": { + "...": "..." + }, + "rev_reg": { + "accum": "21 12E8..." + }, + "witness": { + "omega": "21 1369..." + } + } + test_cred_rev_id = "42" + test_rr_delta = TEST_RR_DELTA + mock_indy_create_credential.return_value = ( + json.dumps(test_cred), + test_cred_rev_id, + test_rr_delta, ) - cred_json, revoc_id = await issuer.create_credential( - test_schema, test_offer, test_request, test_values + with self.assertRaises(IssuerError): # missing attribute + cred_json, revoc_id = await self.issuer.create_credential( + test_schema, test_offer, test_request, {} + ) + + cred_json, cred_rev_id = await self.issuer.create_credential( # main line + test_schema, + test_offer, + test_request, + test_values, + REV_REG_ID, + "/tmp/tails/path/dummy" ) - assert json.loads(cred_json) == test_credential - assert revoc_id == test_revoc_id - mock_create_credential.assert_awaited_once() + mock_indy_create_credential.assert_awaited_once() ( call_wallet, call_offer, @@ -59,15 +208,114 @@ async def test_create_credential(self, mock_create_credential): call_values, call_etc1, call_etc2, - ) = mock_create_credential.call_args[0] - assert call_wallet is mock_wallet.handle + ) = mock_indy_create_credential.call_args[0] + assert call_wallet is self.wallet.handle assert json.loads(call_offer) == test_offer assert json.loads(call_request) == test_request values = json.loads(call_values) assert "attr1" in values + + mock_indy_revoke_credential.return_value = json.dumps(TEST_RR_DELTA) + result = await self.issuer.revoke_credential( + REV_REG_ID, + tails_file_path="dummy", + cred_revoc_id=cred_rev_id + ) + assert json.loads(result) == TEST_RR_DELTA + @async_mock.patch("indy.anoncreds.issuer_create_credential") + @async_mock.patch("aries_cloudagent.issuer.indy.create_tails_reader") + async def test_create_credential_rr_full( + self, + mock_tails_reader, + mock_indy_create_credential + ): + test_schema = {"attrNames": ["attr1"]} + test_offer = { + "schema_id": SCHEMA_ID, + "cred_def_id": CRED_DEF_ID, + "key_correctness_proof": { + "c": "...", + "xz_cap": "...", + "xr_cap": ["..."], + }, + "nonce": "..." + } + test_request = {"test": "request"} + test_values = {"attr1": "value1"} + test_credential = {"test": "credential"} + test_cred_rev_id = "42" + test_rr_delta = TEST_RR_DELTA + mock_indy_create_credential.side_effect = AnoncredsRevocationRegistryFullError( + error_code=ErrorCode.AnoncredsRevocationRegistryFullError + ) + with self.assertRaises(IssuerRevocationRegistryFullError): + await self.issuer.create_credential( + test_schema, + test_offer, + test_request, + test_values + ) + + @async_mock.patch("indy.anoncreds.issuer_create_credential") + @async_mock.patch("aries_cloudagent.issuer.indy.create_tails_reader") + async def test_create_credential_x_indy( + self, + mock_tails_reader, + mock_indy_create_credential + ): + test_schema = {"attrNames": ["attr1"]} + test_offer = { + "schema_id": SCHEMA_ID, + "cred_def_id": CRED_DEF_ID, + "key_correctness_proof": { + "c": "...", + "xz_cap": "...", + "xr_cap": ["..."], + }, + "nonce": "..." + } + test_request = {"test": "request"} + test_values = {"attr1": "value1"} + test_credential = {"test": "credential"} + test_cred_rev_id = "42" + test_rr_delta = TEST_RR_DELTA + + mock_indy_create_credential.side_effect = IndyError( + error_code=ErrorCode.WalletInvalidHandle + ) with self.assertRaises(IssuerError): - # missing attribute - cred_json, revoc_id = await issuer.create_credential( - test_schema, test_offer, test_request, {} + await self.issuer.create_credential( + test_schema, + test_offer, + test_request, + test_values ) + + @async_mock.patch("indy.anoncreds.issuer_create_and_store_revoc_reg") + @async_mock.patch("aries_cloudagent.issuer.indy.create_tails_writer") + async def test_create_and_store_revocation_registry( + self, + mock_indy_tails_writer, + mock_indy_rr + ): + mock_indy_rr.return_value = ("a", "b", "c") + (rr_id, rrdef_json, rre_json) = ( + await self.issuer.create_and_store_revocation_registry( + TEST_DID, + CRED_DEF_ID, + "CL_ACCUM", + "rr-tag", + 100, + "/tmp/tails/path", + ) + ) + assert (rr_id, rrdef_json, rre_json) == ("a", "b", "c") + + @async_mock.patch("indy.anoncreds.issuer_merge_revocation_registry_deltas") + async def test_merge_revocation_registry_deltas(self, mock_indy_merge): + mock_indy_merge.return_value = json.dumps({"net": "delta"}) + assert {"net": "delta"} == await self.issuer.merge_revocation_registry_deltas( + {"fro": "delta"}, + {"to": "delta"} + ) diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/manager.py b/aries_cloudagent/protocols/issue_credential/v1_0/manager.py index 0452da22ed..310ff439ec 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/manager.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/manager.py @@ -76,7 +76,6 @@ async def prepare_send( self, connection_id: str, credential_proposal: CredentialProposal, - revoc_reg_id: str = None, auto_remove: bool = None, ) -> Tuple[V10CredentialExchange, CredentialOffer]: """ @@ -86,7 +85,6 @@ async def prepare_send( connection_id: Connection to create offer for credential_proposal: The credential proposal with preview on attribute values to use if auto_issue is enabled - revoc_reg_id: ID of the revocation registry to use auto_remove: Flag to automatically remove the record on completion Returns: @@ -102,7 +100,6 @@ async def prepare_send( initiator=V10CredentialExchange.INITIATOR_SELF, role=V10CredentialExchange.ROLE_ISSUER, credential_proposal_dict=credential_proposal.serialize(), - revoc_reg_id=revoc_reg_id, ) (credential_exchange, credential_offer) = await self.create_offer( credential_exchange_record=credential_exchange, @@ -124,7 +121,6 @@ async def create_proposal( schema_version: str = None, cred_def_id: str = None, issuer_did: str = None, - revoc_reg_id: str = None, ) -> V10CredentialExchange: """ Create a credential proposal. @@ -143,7 +139,6 @@ async def create_proposal( schema_version: Schema version for credential proposal cred_def_id: Credential definition id for credential proposal issuer_did: Issuer DID for credential proposal - revoc_reg_id: ID of the revocation registry to use Returns: Resulting credential exchange record including credential proposal @@ -171,7 +166,6 @@ async def create_proposal( credential_proposal_dict=credential_proposal_message.serialize(), auto_offer=auto_offer, auto_remove=auto_remove, - revoc_reg_id=revoc_reg_id, ) await credential_exchange_record.save( self.context, reason="create credential proposal" @@ -689,7 +683,10 @@ async def receive_credential_ack(self) -> V10CredentialExchange: return credential_exchange_record async def revoke_credential( - self, credential_exchange_record: V10CredentialExchange, publish: bool = False + self, + rev_reg_id: str, + cred_rev_id: str, + publish: bool = False ): """ Revoke a previously-issued credential. @@ -697,34 +694,27 @@ async def revoke_credential( Optionally, publish the corresponding revocation registry delta to the ledger. Args: - credential_exchange_record: the active credential exchange + rev_reg_id: revocation registry id + cred_rev_id: credential revocation id publish: whether to publish the resulting revocation registry delta """ - assert ( - credential_exchange_record.revocation_id - and credential_exchange_record.revoc_reg_id - ) issuer: BaseIssuer = await self.context.inject(BaseIssuer) revoc = IndyRevocation(self.context) - registry_record = await revoc.get_issuer_rev_reg_record( - credential_exchange_record.revoc_reg_id - ) + registry_record = await revoc.get_issuer_rev_reg_record(rev_reg_id) if not registry_record: raise CredentialManagerError( - "No revocation registry record found for id {}".format( - credential_exchange_record.revoc_reg_id - ) + f"No revocation registry record found for id {rev_reg_id}" ) if publish: # create entry and send to ledger delta = json.loads( await issuer.revoke_credential( - registry_record.revoc_reg_id, + rev_reg_id, registry_record.tails_local_path, - credential_exchange_record.revocation_id, + cred_rev_id ) ) @@ -733,12 +723,10 @@ async def revoke_credential( await registry_record.publish_registry_entry(self.context) else: await registry_record.mark_pending( - self.context, credential_exchange_record.revocation_id + self.context, + cred_rev_id, ) - credential_exchange_record.state = V10CredentialExchange.STATE_REVOKED - await credential_exchange_record.save(self.context, reason="Revoked credential") - async def publish_pending_revocations(self) -> Mapping[Text, Sequence[Text]]: """ Publish pending revocations to the ledger. @@ -762,11 +750,10 @@ async def publish_pending_revocations(self) -> Mapping[Text, Sequence[Text]]: ) ) if delta: - net_delta = ( - await issuer.merge_revocation_registry_deltas(net_delta, delta) - if net_delta - else delta - ) + net_delta = await issuer.merge_revocation_registry_deltas( + net_delta, + delta + ) if net_delta else delta registry_record.revoc_reg_entry = net_delta await registry_record.publish_registry_entry(self.context) diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/models/credential_exchange.py b/aries_cloudagent/protocols/issue_credential/v1_0/models/credential_exchange.py index 1f6636a123..cf215d6b1b 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/models/credential_exchange.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/models/credential_exchange.py @@ -37,7 +37,6 @@ class Meta: STATE_ISSUED = "credential_issued" STATE_CREDENTIAL_RECEIVED = "credential_received" STATE_ACKED = "credential_acked" - STATE_REVOKED = "credential_revoked" def __init__( self, diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/routes.py b/aries_cloudagent/protocols/issue_credential/v1_0/routes.py index 5afa5b44db..6b810b43c6 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/routes.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/routes.py @@ -83,12 +83,11 @@ class V10CredentialProposalRequestSchemaBase(Schema): description="Credential issuer DID", required=False, **INDY_DID ) auto_remove = fields.Bool( - description=("Whether to remove the credential exchange record on completion"), - required=False, - default=True, - ) - revoc_reg_id = fields.Str( - description="Revocation Registry ID", required=False, **INDY_REV_REG_ID + description=( + "Whether to remove the credential exchange record on completion " + "(overrides --preserve-exchange-records configuration setting)" + ), + required=False ) comment = fields.Str(description="Human-readable comment", required=False) @@ -123,17 +122,16 @@ class V10CredentialOfferRequestSchema(Schema): "Whether to respond automatically to credential requests, creating " "and issuing requested credentials" ), - required=False, - default=False, + required=False ) auto_remove = fields.Bool( - description=("Whether to remove the credential exchange record on completion"), + description=( + "Whether to remove the credential exchange record on completion " + "(overrides --preserve-exchange-records configuration setting)" + ), required=False, default=True, ) - revoc_reg_id = fields.Str( - description="Revocation Registry ID", required=False, **INDY_REV_REG_ID - ) comment = fields.Str(description="Human-readable comment", required=False) credential_preview = fields.Nested(CredentialPreviewSchema, required=True) @@ -262,9 +260,8 @@ async def credential_exchange_send(request: web.BaseRequest): connection_id = body.get("connection_id") preview_spec = body.get("credential_proposal") if not preview_spec: - raise web.HTTPBadRequest(reason="credential_proposal must be provided.") - auto_remove = body.get("auto_remove", True) - revoc_reg_id = body.get("revoc_reg_id") + raise web.HTTPBadRequest(reason="credential_proposal must be provided") + auto_remove = body.get("auto_remove") preview = CredentialPreview.deserialize(preview_spec) try: @@ -292,7 +289,6 @@ async def credential_exchange_send(request: web.BaseRequest): connection_id, credential_proposal=credential_proposal, auto_remove=auto_remove, - revoc_reg_id=revoc_reg_id, ) await outbound_handler( credential_offer_message, connection_id=credential_exchange_record.connection_id @@ -324,8 +320,7 @@ async def credential_exchange_send_proposal(request: web.BaseRequest): comment = body.get("comment") preview_spec = body.get("credential_proposal") preview = CredentialPreview.deserialize(preview_spec) if preview_spec else None - auto_remove = body.get("auto_remove", True) - revoc_reg_id = body.get("revoc_reg_id") + auto_remove = body.get("auto_remove") try: connection_record = await ConnectionRecord.retrieve_by_id( @@ -344,7 +339,6 @@ async def credential_exchange_send_proposal(request: web.BaseRequest): comment=comment, credential_preview=preview, auto_remove=auto_remove, - revoc_reg_id=revoc_reg_id, **{t: body.get(t) for t in CRED_DEF_TAGS if body.get(t)}, ) @@ -389,8 +383,7 @@ async def credential_exchange_send_free_offer(request: web.BaseRequest): auto_issue = body.get( "auto_issue", context.settings.get("debug.auto_respond_credential_request") ) - auto_remove = body.get("auto_remove", True) - revoc_reg_id = body.get("revoc_reg_id") + auto_remove = body.get("auto_remove") comment = body.get("comment") preview_spec = body.get("credential_preview") @@ -399,8 +392,9 @@ async def credential_exchange_send_free_offer(request: web.BaseRequest): 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." + reason=( + "If auto_issue is set then credential_preview must be provided" + ) ) try: @@ -431,7 +425,6 @@ async def credential_exchange_send_free_offer(request: web.BaseRequest): credential_proposal_dict=credential_proposal_dict, auto_issue=auto_issue, auto_remove=auto_remove, - revoc_reg_id=revoc_reg_id, ) credential_manager = CredentialManager(context) @@ -573,7 +566,7 @@ async def credential_exchange_issue(request: web.BaseRequest): preview_spec = body.get("credential_preview") if not preview_spec: - raise web.HTTPBadRequest(reason="credential_preview must be provided.") + 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( @@ -607,7 +600,7 @@ async def credential_exchange_issue(request: web.BaseRequest): credential_values=credential_preview.attr_dict(decode=False), ) except IssuerRevocationRegistryFullError: - raise web.HTTPBadRequest(reason="Revocation registry is full.") + raise web.HTTPBadRequest(reason="Revocation registry is full") await outbound_handler(credential_issue_message, connection_id=connection_id) return web.json_response(cred_exch_record.serialize()) @@ -669,54 +662,34 @@ async def credential_exchange_store(request: web.BaseRequest): return web.json_response(credential_exchange_record.serialize()) -@docs( - tags=["issue-credential"], summary="Send a problem report for credential exchange" -) -@request_schema(V10CredentialProblemReportRequestSchema()) -async def credential_exchange_problem_report(request: web.BaseRequest): - """ - Request handler for sending problem report. - - Args: - request: aiohttp request object - - """ - context = request.app["request_context"] - outbound_handler = request.app["outbound_message_router"] - - credential_exchange_id = request.match_info["cred_ex_id"] - body = await request.json() - - try: - credential_exchange_record = await V10CredentialExchange.retrieve_by_id( - context, credential_exchange_id - ) - except StorageNotFoundError: - raise web.HTTPNotFound() - - error_result = ProblemReport(explain_ltxt=body["explain_ltxt"]) - error_result.assign_thread_id(credential_exchange_record.thread_id) - - await outbound_handler( - error_result, connection_id=credential_exchange_record.connection_id - ) - return web.json_response({}) - - @docs( tags=["issue-credential"], parameters=[ { - "in": "path", + "name": "rev_reg_id", + "in": "query", + "description": "revocation registry id", + "required": True + }, + { + "name": "cred_rev_id", + "in": "query", + "description": "credential revocation id", + "required": True + }, + { "name": "publish", - "description": "Whether to publish revocation to ledger immediately.", + "in": "query", + "description": ( + "(true) publish revocation to ledger immediately, or " + "(false) mark it pending" + ), "schema": {"type": "boolean"}, "required": False } ], summary="Revoke an issued credential" ) -@response_schema(V10CredentialExchangeSchema(), 200) async def credential_exchange_revoke(request: web.BaseRequest): """ Request handler for storing a credential request. @@ -730,28 +703,15 @@ async def credential_exchange_revoke(request: web.BaseRequest): """ context = request.app["request_context"] - try: - credential_exchange_id = request.match_info["cred_ex_id"] - publish = bool(json.loads(request.query.get("publish", json.dumps(False)))) - credential_exchange_record = await V10CredentialExchange.retrieve_by_id( - context, credential_exchange_id - ) - except StorageNotFoundError: - raise web.HTTPNotFound() - if ( - credential_exchange_record.state - not in (V10CredentialExchange.STATE_ISSUED, V10CredentialExchange.STATE_ACKED) - or not credential_exchange_record.revocation_id - or not credential_exchange_record.revoc_reg_id - ): - raise web.HTTPBadRequest() + rev_reg_id = request.query.get("rev_reg_id") + cred_rev_id = request.query.get("cred_rev_id") + publish = bool(json.loads(request.query.get("publish", json.dumps(False)))) credential_manager = CredentialManager(context) + await credential_manager.revoke_credential(rev_reg_id, cred_rev_id, publish) - await credential_manager.revoke_credential(credential_exchange_record, publish) - - return web.json_response(credential_exchange_record.serialize()) + return web.json_response({}) @docs(tags=["issue-credential"], summary="Publish pending revocations to ledger") @@ -772,7 +732,11 @@ async def credential_exchange_publish_revocations(request: web.BaseRequest): credential_manager = CredentialManager(context) - return web.json_response(await credential_manager.publish_pending_revocations()) + return web.json_response( + { + "results": await credential_manager.publish_pending_revocations() + } + ) @docs( @@ -798,6 +762,40 @@ async def credential_exchange_remove(request: web.BaseRequest): return web.json_response({}) +@docs( + tags=["issue-credential"], summary="Send a problem report for credential exchange" +) +@request_schema(V10CredentialProblemReportRequestSchema()) +async def credential_exchange_problem_report(request: web.BaseRequest): + """ + Request handler for sending problem report. + + Args: + request: aiohttp request object + + """ + context = request.app["request_context"] + outbound_handler = request.app["outbound_message_router"] + + credential_exchange_id = request.match_info["cred_ex_id"] + body = await request.json() + + try: + credential_exchange_record = await V10CredentialExchange.retrieve_by_id( + context, credential_exchange_id + ) + except StorageNotFoundError: + raise web.HTTPNotFound() + + error_result = ProblemReport(explain_ltxt=body["explain_ltxt"]) + error_result.assign_thread_id(credential_exchange_record.thread_id) + + await outbound_handler( + error_result, connection_id=credential_exchange_record.connection_id + ) + return web.json_response({}) + + async def register(app: web.Application): """Register routes.""" @@ -834,20 +832,20 @@ async def register(app: web.Application): credential_exchange_store, ), web.post( - "/issue-credential/records/{cred_ex_id}/revoke", + "/issue-credential/revoke", credential_exchange_revoke, ), web.post( "/issue-credential/publish-revocations", credential_exchange_publish_revocations, ), - web.post( - "/issue-credential/records/{cred_ex_id}/problem-report", - credential_exchange_problem_report, - ), web.post( "/issue-credential/records/{cred_ex_id}/remove", credential_exchange_remove, ), + web.post( + "/issue-credential/records/{cred_ex_id}/problem-report", + credential_exchange_problem_report, + ), ] ) diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_manager.py b/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_manager.py index ebd87b68bb..b913202f00 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_manager.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_manager.py @@ -720,7 +720,6 @@ async def test_issue_credential(self): credential_request=indy_cred_req, initiator=V10CredentialExchange.INITIATOR_SELF, role=V10CredentialExchange.ROLE_ISSUER, - revoc_reg_id=REV_REG_ID, thread_id=thread_id, ) @@ -797,7 +796,6 @@ async def test_issue_credential_non_revocable(self): credential_request=indy_cred_req, initiator=V10CredentialExchange.INITIATOR_SELF, role=V10CredentialExchange.ROLE_ISSUER, - revoc_reg_id=None, thread_id=thread_id, ) @@ -860,7 +858,6 @@ async def test_issue_credential_no_active_rr(self): credential_request=indy_cred_req, initiator=V10CredentialExchange.INITIATOR_SELF, role=V10CredentialExchange.ROLE_ISSUER, - revoc_reg_id=REV_REG_ID, thread_id=thread_id, ) @@ -884,9 +881,9 @@ async def test_issue_credential_no_active_rr(self): await self.manager.issue_credential( stored_exchange, comment=comment, credential_values=cred_values ) - assert x_cred_mgr.message.contains( + assert ( "has no active revocation registry" - ) + ) in x_cred_mgr.message async def test_issue_credential_rr_full(self): connection_id = "test_conn_id" @@ -903,7 +900,6 @@ async def test_issue_credential_rr_full(self): credential_request=indy_cred_req, initiator=V10CredentialExchange.INITIATOR_SELF, role=V10CredentialExchange.ROLE_ISSUER, - revoc_reg_id=REV_REG_ID, thread_id=thread_id, ) @@ -1164,18 +1160,9 @@ async def test_credential_ack(self): async def test_revoke_credential_publish(self): CRED_REV_ID = 1 - exchange = V10CredentialExchange( - credential_definition_id=CRED_DEF_ID, - role=V10CredentialExchange.ROLE_ISSUER, - revocation_id=CRED_REV_ID, - revoc_reg_id=REV_REG_ID, - ) - with async_mock.patch.object( test_module, "IndyRevocation", autospec=True - ) as revoc, async_mock.patch.object( - V10CredentialExchange, "save", autospec=True - ) as save_ex: + ) as revoc: mock_issuer_rev_reg_record = async_mock.MagicMock( revoc_reg_id=REV_REG_ID, tails_local_path=TAILS_LOCAL, @@ -1200,8 +1187,7 @@ async def test_revoke_credential_publish(self): ) self.context.injector.bind_instance(BaseIssuer, issuer) - await self.manager.revoke_credential(exchange, True) - save_ex.assert_called_once() + await self.manager.revoke_credential(REV_REG_ID, CRED_REV_ID, True) async def test_revoke_credential_no_rev_reg_rec(self): CRED_REV_ID = 1 @@ -1223,22 +1209,13 @@ async def test_revoke_credential_no_rev_reg_rec(self): self.context.injector.bind_instance(BaseIssuer, issuer) with self.assertRaises(CredentialManagerError): - await self.manager.revoke_credential(exchange) + await self.manager.revoke_credential(REV_REG_ID, CRED_REV_ID) async def test_revoke_credential_pend(self): CRED_REV_ID = 1 - exchange = V10CredentialExchange( - credential_definition_id=CRED_DEF_ID, - role=V10CredentialExchange.ROLE_ISSUER, - revocation_id=CRED_REV_ID, - revoc_reg_id=REV_REG_ID, - ) - with async_mock.patch.object( test_module, "IndyRevocation", autospec=True - ) as revoc, async_mock.patch.object( - V10CredentialExchange, "save", autospec=True - ) as save_ex: + ) as revoc: mock_issuer_rev_reg_record = async_mock.MagicMock( mark_pending=async_mock.CoroutineMock() ) @@ -1249,8 +1226,7 @@ async def test_revoke_credential_pend(self): issuer = async_mock.MagicMock(BaseIssuer, autospec=True) self.context.injector.bind_instance(BaseIssuer, issuer) - await self.manager.revoke_credential(exchange, False) - save_ex.assert_called_once() + await self.manager.revoke_credential(REV_REG_ID, CRED_REV_ID, False) mock_issuer_rev_reg_record.mark_pending.assert_called_once_with( self.context, CRED_REV_ID diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py index c86934a572..c3c1c0e3b5 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py @@ -152,7 +152,7 @@ async def test_credential_exchange_send_no_proposal(self): with self.assertRaises(test_module.web.HTTPBadRequest) as x_http: await test_module.credential_exchange_send(mock) - assert x_http.message.contains("credential_proposal") + assert "credential_proposal" in x_http.message async def test_credential_exchange_send_no_conn_record(self): conn_id = "connection-id" @@ -931,7 +931,7 @@ async def test_credential_exchange_issue_rev_reg_full(self): with self.assertRaises(test_module.web.HTTPBadRequest) as x_bad_req: await test_module.credential_exchange_issue(mock) - assert x_bad_req.message.contains("Revocation registry is full.") + assert "Revocation registry is full" in x_bad_req.message async def test_credential_exchange_store(self): mock = async_mock.MagicMock() @@ -1078,68 +1078,6 @@ async def test_credential_exchange_store_not_ready(self): with self.assertRaises(test_module.web.HTTPForbidden): await test_module.credential_exchange_store(mock) - async def test_credential_exchange_problem_report(self): - mock = async_mock.MagicMock() - mock.json = async_mock.CoroutineMock() - - mock_outbound = async_mock.CoroutineMock() - - mock.app = { - "outbound_message_router": mock_outbound, - "request_context": "context", - } - - with async_mock.patch.object( - test_module, "ConnectionRecord", autospec=True - ) as mock_connection_record, async_mock.patch.object( - test_module, "CredentialManager", autospec=True - ) as mock_credential_manager, async_mock.patch.object( - test_module, "V10CredentialExchange", autospec=True - ) as mock_cred_ex, async_mock.patch.object( - test_module, "ProblemReport", autospec=True - ) as mock_prob_report, async_mock.patch.object( - test_module.web, "json_response" - ) as mock_response: - - mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock() - - await test_module.credential_exchange_problem_report(mock) - - mock_response.assert_called_once_with({}) - mock_outbound.assert_called_once_with( - mock_prob_report.return_value, - connection_id=mock_cred_ex.retrieve_by_id.return_value.connection_id, - ) - - async def test_credential_exchange_problem_report_no_cred_record(self): - mock = async_mock.MagicMock() - mock.json = async_mock.CoroutineMock() - - mock_outbound = async_mock.CoroutineMock() - - mock.app = { - "outbound_message_router": mock_outbound, - "request_context": "context", - } - - with async_mock.patch.object( - test_module, "ConnectionRecord", autospec=True - ) as mock_connection_record, async_mock.patch.object( - test_module, "CredentialManager", autospec=True - ) as mock_credential_manager, async_mock.patch.object( - test_module, "V10CredentialExchange", autospec=True - ) as mock_cred_ex, async_mock.patch.object( - test_module, "ProblemReport", autospec=True - ) as mock_prob_report: - - # Emulate storage not found (bad cred ex id) - mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock( - side_effect=StorageNotFoundError() - ) - - with self.assertRaises(test_module.web.HTTPNotFound): - await test_module.credential_exchange_problem_report(mock) - async def test_credential_exchange_remove(self): mock = async_mock.MagicMock() mock.match_info = {"cred_ex_id": "dummy"} @@ -1181,112 +1119,110 @@ async def test_credential_exchange_remove_not_found(self): async def test_credential_exchange_revoke(self): mock = async_mock.MagicMock( - query={"publish": "false"} + query={ + "rev_reg_id": "rr_id", + "cred_rev_id": "23", + "publish": "false", + } ) - mock.app = { "request_context": "context", } with async_mock.patch.object( - test_module, "V10CredentialExchange", autospec=True - ) as mock_cred_ex, async_mock.patch.object( test_module, "CredentialManager", autospec=True ) as mock_cred_mgr, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - more_magic = async_mock.MagicMock( - state=mock_cred_ex.STATE_ACKED, - revocation_id="1", - revoc_reg_id="dummy", - serialize=async_mock.MagicMock() - ) - mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock( - return_value=more_magic - ) mock_cred_mgr.return_value.revoke_credential = async_mock.CoroutineMock() await test_module.credential_exchange_revoke(mock) - mock_response.assert_called_once_with( - more_magic.serialize.return_value - ) - - async def test_credential_exchange_revoke_not_found(self): - mock = async_mock.MagicMock( - query={"publish": "false"} - ) + mock_response.assert_called_once_with({}) + async def test_credential_exchange_publish_revocations(self): + mock = async_mock.MagicMock() mock.app = { "request_context": "context", } with async_mock.patch.object( - test_module, "V10CredentialExchange", autospec=True - ) as mock_cred_ex, async_mock.patch.object( test_module, "CredentialManager", autospec=True ) as mock_cred_mgr, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - # Emulate storage not found (bad cred ex id) - mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock( - side_effect=StorageNotFoundError() + pub_pending = async_mock.CoroutineMock() + mock_cred_mgr.return_value.publish_pending_revocations = pub_pending + + await test_module.credential_exchange_publish_revocations(mock) + + mock_response.assert_called_once_with( + {"results": pub_pending.return_value} ) - with self.assertRaises(test_module.web.HTTPNotFound): - await test_module.credential_exchange_revoke(mock) + async def test_credential_exchange_problem_report(self): + mock = async_mock.MagicMock() + mock.json = async_mock.CoroutineMock() - async def test_credential_exchange_revoke_bad_state(self): - mock = async_mock.MagicMock( - query={"publish": "false"} - ) + mock_outbound = async_mock.CoroutineMock() mock.app = { + "outbound_message_router": mock_outbound, "request_context": "context", } with async_mock.patch.object( + test_module, "ConnectionRecord", autospec=True + ) as mock_connection_record, async_mock.patch.object( + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager, async_mock.patch.object( test_module, "V10CredentialExchange", autospec=True ) as mock_cred_ex, async_mock.patch.object( - test_module, "CredentialManager", autospec=True - ) as mock_cred_mgr, async_mock.patch.object( + test_module, "ProblemReport", autospec=True + ) as mock_prob_report, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - more_magic = async_mock.MagicMock( - state=mock_cred_ex.STATE_PROPOSAL_RECEIVED, - revocation_id="1", - revoc_reg_id="dummy", - serialize=async_mock.MagicMock() - ) - mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock( - return_value=more_magic - ) - with self.assertRaises(test_module.web.HTTPBadRequest): - await test_module.credential_exchange_revoke(mock) + mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock() - async def test_credential_exchange_publish_revocations(self): + await test_module.credential_exchange_problem_report(mock) + + mock_response.assert_called_once_with({}) + mock_outbound.assert_called_once_with( + mock_prob_report.return_value, + connection_id=mock_cred_ex.retrieve_by_id.return_value.connection_id, + ) + + async def test_credential_exchange_problem_report_no_cred_record(self): mock = async_mock.MagicMock() + mock.json = async_mock.CoroutineMock() + + mock_outbound = async_mock.CoroutineMock() + mock.app = { + "outbound_message_router": mock_outbound, "request_context": "context", } with async_mock.patch.object( + test_module, "ConnectionRecord", autospec=True + ) as mock_connection_record, async_mock.patch.object( test_module, "CredentialManager", autospec=True - ) as mock_cred_mgr, async_mock.patch.object( - test_module.web, "json_response" - ) as mock_response: - mock_cred_mgr.return_value.publish_pending_revocations = ( - async_mock.CoroutineMock() - ) - - await test_module.credential_exchange_publish_revocations(mock) + ) as mock_credential_manager, async_mock.patch.object( + test_module, "V10CredentialExchange", autospec=True + ) as mock_cred_ex, async_mock.patch.object( + test_module, "ProblemReport", autospec=True + ) as mock_prob_report: - mock_response.assert_called_once_with( - mock_cred_mgr.return_value.publish_pending_revocations.return_value + # Emulate storage not found (bad cred ex id) + mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock( + side_effect=StorageNotFoundError() ) + with self.assertRaises(test_module.web.HTTPNotFound): + await test_module.credential_exchange_problem_report(mock) + async def test_register(self): mock_app = async_mock.MagicMock() mock_app.add_routes = async_mock.MagicMock() diff --git a/aries_cloudagent/revocation/indy.py b/aries_cloudagent/revocation/indy.py index 5c2af13d0a..55713b162d 100644 --- a/aries_cloudagent/revocation/indy.py +++ b/aries_cloudagent/revocation/indy.py @@ -13,8 +13,6 @@ class IndyRevocation: """Class for managing Indy credential revocation.""" - REGISTRY_CACHE = {} - def __init__(self, context: InjectionContext): """Initialize the IndyRevocation instance.""" self._context = context @@ -51,11 +49,11 @@ async def init_issuer_registry( tag=tag, ) await record.save(self._context, reason="Init revocation registry") - self.REGISTRY_CACHE[cred_def_id] = record.record_id return record async def get_active_issuer_rev_reg_record( - self, cred_def_id: str, await_create: bool = False + self, + cred_def_id: str ) -> "IssuerRevRegRecord": """Return the current active registry for issuing a given credential definition. @@ -63,21 +61,18 @@ async def get_active_issuer_rev_reg_record( Args: cred_def_id: ID of the base credential definition - await_create: Wait for the registry and tails file to be created, if needed """ - # FIXME filter issuing registries by cred def, state (active or full), pick one - if cred_def_id in self.REGISTRY_CACHE: - registry = await IssuerRevRegRecord.retrieve_by_id( - self._context, self.REGISTRY_CACHE[cred_def_id] - ) - return registry + current = await IssuerRevRegRecord.query_by_cred_def_id( + self._context, + cred_def_id, + IssuerRevRegRecord.STATE_ACTIVE + ) + return current[0] if current else None async def get_issuer_rev_reg_record( self, revoc_reg_id: str ) -> "IssuerRevRegRecord": - """Return the current active revocation registry record for a given registry ID. - - If no registry exists, then a new one will be created. + """Return a revocation registry record by identifier. Args: revoc_reg_id: ID of the revocation registry diff --git a/aries_cloudagent/revocation/models/issuer_rev_reg_record.py b/aries_cloudagent/revocation/models/issuer_rev_reg_record.py index a1121f3c05..49c11186c5 100644 --- a/aries_cloudagent/revocation/models/issuer_rev_reg_record.py +++ b/aries_cloudagent/revocation/models/issuer_rev_reg_record.py @@ -84,8 +84,10 @@ def __init__( **kwargs, ): """Initialize the issuer revocation registry record.""" - super(IssuerRevRegRecord, self).__init__( - record_id, state=state or IssuerRevRegRecord.STATE_INIT, **kwargs + super().__init__( + record_id, + state=state or IssuerRevRegRecord.STATE_INIT, + **kwargs ) self.cred_def_id = cred_def_id self.error_msg = error_msg @@ -351,6 +353,11 @@ class Meta: description="Issuer revocation registry record identifier", example=UUIDFour.EXAMPLE, ) + state = fields.Str( + required=False, + description="Issue revocation registry record state", + example=IssuerRevRegRecord.STATE_ACTIVE, + ) cred_def_id = fields.Str( required=False, description="Credential definition identifier", diff --git a/aries_cloudagent/revocation/routes.py b/aries_cloudagent/revocation/routes.py index 19f70adfc0..f228723619 100644 --- a/aries_cloudagent/revocation/routes.py +++ b/aries_cloudagent/revocation/routes.py @@ -10,7 +10,7 @@ from marshmallow import fields, Schema from ..messaging.credential_definitions.util import CRED_DEF_SENT_RECORD_TYPE -from ..messaging.valid import INDY_CRED_DEF_ID +from ..messaging.valid import INDY_CRED_DEF_ID, IndyRevRegId from ..storage.base import BaseStorage, StorageNotFoundError from .error import RevocationNotSupportedError @@ -45,7 +45,12 @@ class RevRegUpdateTailsFileUriSchema(Schema): """Request schema for updating tails file URI.""" tails_public_uri = fields.Url( - description="Public URI to the tails file", required=True + description="Public URI to the tails file", + example=( + "http://192.168.56.133:5000/revocation/registry/" + f"{IndyRevRegId.EXAMPLE}/tails-file" + ), + required=True ) @@ -97,19 +102,25 @@ async def revocation_create_registry(request: web.BaseRequest): @docs( tags=["revocation"], - summary="Get current revocation registry", - parameters=[{"in": "path", "name": "id", "description": "revocation registry id"}], + summary="Get revocation registry by credential definition id", + parameters=[ + { + "in": "path", + "name": "id", + "description": "revocation registry id" + } + ], ) @response_schema(RevRegCreateResultSchema(), 200) -async def get_current_registry(request: web.BaseRequest): +async def get_registry(request: web.BaseRequest): """ - Request handler for getting the current revocation registry. + Request handler for getting a revocation registry by identifier. Args: request: aiohttp request object Returns: - The revocation registry identifier + The revocation registry """ context = request.app["request_context"] @@ -125,6 +136,42 @@ async def get_current_registry(request: web.BaseRequest): return web.json_response({"result": revoc_registry.serialize()}) +@docs( + tags=["revocation"], + summary="Get an active revocation registry by credential definition id", + parameters=[ + { + "in": "path", + "name": "cred_def_id", + "description": "credential definition id" + } + ], +) +@response_schema(RevRegCreateResultSchema(), 200) +async def get_active_registry(request: web.BaseRequest): + """ + Request handler for getting an active revocation registry by cred def id. + + Args: + request: aiohttp request object + + Returns: + The revocation registry identifier + + """ + context = request.app["request_context"] + + cred_def_id = request.match_info["cred_def_id"] + + try: + revoc = IndyRevocation(context) + revoc_registry = await revoc.get_active_issuer_rev_reg_record(cred_def_id) + except StorageNotFoundError as e: + raise web.HTTPNotFound() from e + + return web.json_response({"result": revoc_registry.serialize()}) + + @docs( tags=["revocation"], summary="Download the tails file of revocation registry", @@ -237,7 +284,8 @@ async def register(app: web.Application): app.add_routes( [ web.post("/revocation/create-registry", revocation_create_registry), - web.get("/revocation/registry/{id}", get_current_registry), + web.get("/revocation/registry/{id}", get_registry), + web.get("/revocation/active-registry/{cred_def_id}", get_active_registry), web.get("/revocation/registry/{id}/tails-file", get_tails_file), web.patch("/revocation/registry/{id}", update_registry), web.post("/revocation/registry/{id}/publish", publish_registry), diff --git a/aries_cloudagent/revocation/tests/test_indy.py b/aries_cloudagent/revocation/tests/test_indy.py index 99835c85b9..10ed3de6c5 100644 --- a/aries_cloudagent/revocation/tests/test_indy.py +++ b/aries_cloudagent/revocation/tests/test_indy.py @@ -40,7 +40,6 @@ def setUp(self): self.storage = BasicStorage() self.context.injector.bind_instance(BaseStorage, self.storage) - IndyRevocation.REGISTRY_CACHE.clear() self.revoc = IndyRevocation(self.context) assert self.revoc._context is self.context @@ -54,9 +53,6 @@ async def test_init_issuer_registry(self): self.test_did ) - assert CRED_DEF_ID in self.revoc.REGISTRY_CACHE - self.revoc.REGISTRY_CACHE.clear() - assert result.cred_def_id == CRED_DEF_ID assert result.issuer_did == self.test_did assert result.issuance_type == IssuerRevRegRecord.ISSUANCE_BY_DEFAULT @@ -102,6 +98,9 @@ async def test_get_active_issuer_rev_reg_record(self): CRED_DEF_ID, self.test_did ) + rec.revoc_reg_id = "dummy" + rec.state = IssuerRevRegRecord.STATE_ACTIVE + await rec.save(self.context) result = await self.revoc.get_active_issuer_rev_reg_record(CRED_DEF_ID) assert rec == result @@ -110,7 +109,6 @@ async def test_get_active_issuer_rev_reg_record_none(self): CRED_DEF_ID = f"{self.test_did}:3:CL:1234:default" result = await self.revoc.get_active_issuer_rev_reg_record(CRED_DEF_ID) assert result is None - async def test_get_issuer_rev_reg_record(self): CRED_DEF_ID = f"{self.test_did}:3:CL:1234:default" diff --git a/aries_cloudagent/revocation/tests/test_routes.py b/aries_cloudagent/revocation/tests/test_routes.py index fd0578c30f..3e5907841c 100644 --- a/aries_cloudagent/revocation/tests/test_routes.py +++ b/aries_cloudagent/revocation/tests/test_routes.py @@ -116,7 +116,7 @@ async def test_create_registry_no_revo_support(self): mock_json_response.assert_not_called() - async def test_get_current_registry(self): + async def test_get_registry(self): REV_REG_ID = "{}:4:{}:3:CL:1234:default:CL_ACCUM:default".format( self.test_did, self.test_did @@ -138,13 +138,13 @@ async def test_get_current_registry(self): ) ) - result = await test_module.get_current_registry(request) + result = await test_module.get_registry(request) mock_json_response.assert_called_once_with( {"result": "dummy"} ) assert result is mock_json_response.return_value - async def test_get_current_registry_not_found(self): + async def test_get_registry_not_found(self): REV_REG_ID = "{}:4:{}:3:CL:1234:default:CL_ACCUM:default".format( self.test_did, self.test_did @@ -167,7 +167,55 @@ async def test_get_current_registry_not_found(self): ) with self.assertRaises(HTTPNotFound): - result = await test_module.get_current_registry(request) + result = await test_module.get_registry(request) + mock_json_response.assert_not_called() + + async def test_get_active_registry(self): + CRED_DEF_ID = f"{self.test_did}:3:CL:1234:default" + request = async_mock.MagicMock() + request.app = self.app + request.match_info = {"cred_def_id": CRED_DEF_ID} + + with async_mock.patch.object( + test_module, "IndyRevocation", autospec=True + ) as mock_indy_revoc, async_mock.patch.object( + test_module.web, "json_response", async_mock.Mock() + ) as mock_json_response: + mock_indy_revoc.return_value = async_mock.MagicMock( + get_active_issuer_rev_reg_record=async_mock.CoroutineMock( + return_value=async_mock.MagicMock( + serialize=async_mock.MagicMock(return_value="dummy") + ) + ) + ) + + result = await test_module.get_active_registry(request) + mock_json_response.assert_called_once_with( + {"result": "dummy"} + ) + assert result is mock_json_response.return_value + + async def test_get_active_registry_not_found(self): + CRED_DEF_ID = f"{self.test_did}:3:CL:1234:default" + request = async_mock.MagicMock() + request.app = self.app + request.match_info = {"cred_def_id": CRED_DEF_ID} + + with async_mock.patch.object( + test_module, "IndyRevocation", autospec=True + ) as mock_indy_revoc, async_mock.patch.object( + test_module.web, "json_response", async_mock.Mock() + ) as mock_json_response: + mock_indy_revoc.return_value = async_mock.MagicMock( + get_active_issuer_rev_reg_record=async_mock.CoroutineMock( + side_effect=test_module.StorageNotFoundError( + error_code="dummy" + ) + ) + ) + + with self.assertRaises(HTTPNotFound): + result = await test_module.get_active_registry(request) mock_json_response.assert_not_called() async def test_get_tails_file(self): diff --git a/demo/runners/faber.py b/demo/runners/faber.py index de08ff5c4e..c46fa22ccd 100644 --- a/demo/runners/faber.py +++ b/demo/runners/faber.py @@ -92,7 +92,7 @@ async def handle_issue_credential(self, message): ], } try: - await self.admin_POST( + cred_ex_rec = await self.admin_POST( f"/issue-credential/records/{credential_exchange_id}/issue", { "comment": ( @@ -101,6 +101,16 @@ async def handle_issue_credential(self, message): "credential_preview": cred_preview, }, ) + rev_reg_id = cred_ex_rec.get("revoc_reg_id") + cred_rev_id = cred_ex_rec.get("revocation_id") + if rev_reg_id: + self.log( + f"Revocation registry id: {rev_reg_id}" + ) + if cred_rev_id: + self.log( + f"Credential revocation id: {cred_rev_id}" + ) except ClientError: pass @@ -314,13 +324,16 @@ async def main( f"/connections/{agent.connection_id}/send-message", {"content": msg} ) elif option == "4" and revocation: - revoking_cred_id = await prompt("Enter credential exchange id: ") + rev_reg_id = await prompt("Enter revocation registry id: ") + cred_rev_id = await prompt("Enter credential revocation id: ") publish = json.dumps( await prompt("Publish now? [Y/N]: ", default="N") in ('yY') ) await agent.admin_POST( - f"/issue-credential/records/{revoking_cred_id}" - f"/revoke?publish={publish}" + "/issue-credential/revoke" + f"?publish={publish}" + f"&rev_reg_id={rev_reg_id}" + f"&cred_rev_id={cred_rev_id}" ) elif option == "5" and revocation: resp = await agent.admin_POST("/issue-credential/publish-revocations") @@ -328,7 +341,7 @@ async def main( "Published revocations for {} revocation registr{} {}".format( len(resp), "y" if len(resp) == 1 else "ies", - json.dumps([k for k in resp], indent=4) + json.dumps([k for k in resp["results"]], indent=4) ) ) elif option == "6" and revocation: diff --git a/demo/runners/performance.py b/demo/runners/performance.py index b2cacefd42..544254aca9 100644 --- a/demo/runners/performance.py +++ b/demo/runners/performance.py @@ -28,7 +28,7 @@ def __init__( self._connection_ready = None self.credential_state = {} self.credential_event = asyncio.Event() - self.cred_ex_ids = set() + self.revoc_info = {} self.ping_state = {} self.ping_event = asyncio.Event() self.sent_pings = set() @@ -76,9 +76,12 @@ async def handle_connections(self, payload): self._connection_ready.set_result(True) async def handle_issue_credential(self, payload): - cred_id = payload["credential_exchange_id"] + cred_ex_id = payload["credential_exchange_id"] + rev_reg_id = payload["revoc_reg_id"] + cred_rev_id = payload["revocation_id"] + self.credential_state[cred_id] = payload["state"] - self.cred_ex_ids.add(cred_id) + self.revoc_info[cred_ex_id] = (payload["revoc_reg_id"], payload["revocation_id"]) self.credential_event.set() async def handle_ping(self, payload): @@ -219,9 +222,12 @@ async def send_credential( }, ) - async def revoke_credential(self, cred_ex_id: str): + async def revoke_credential(self, rev_reg_id: str, cred_rev_id: str): await self.admin_POST( - f"/issue-credential/records/{cred_ex_id}/revoke?publish=true" + "/issue-credential/revoke" + "?publish=true" + f"&rev_reg_id={rev_reg_id}" + f"&cred_rev_id={rev_reg_id}" ) @@ -431,10 +437,13 @@ async def check_received_pings(agent, issue_count, pb): for line in faber.format_postgres_stats(): faber.log(line) - cred_id = next(iter(faber.cred_ex_ids)) if revoc: - print("Revoking credential and publishing", cred_id) - await faber.revoke_credential(cred_id) + (rev_reg_id, cred_rev_id) = next(iter(faber.revoc_info.values)) + print( + f"Revoking and credential reg reg id {rev_reg_id}, " + "cred rev id {cred_rev_id}; publishing revocation" + ) + await faber.revoke_credential(rev_reg_id, cred_rev_id) if show_timing: timing = await alice.fetch_timing() From 6e4117b411a0ab0b93ae976ddb1384546004ea32 Mon Sep 17 00:00:00 2001 From: sklump Date: Tue, 31 Mar 2020 00:30:34 +0000 Subject: [PATCH 2/4] work in progress: merge master Signed-off-by: sklump --- .../protocols/issue_credential/v1_0/routes.py | 81 ++++++++++ .../v1_0/tests/test_routes.py | 144 ++++++++++++++---- 2 files changed, 192 insertions(+), 33 deletions(-) diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/routes.py b/aries_cloudagent/protocols/issue_credential/v1_0/routes.py index 6b810b43c6..e7fde90166 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/routes.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/routes.py @@ -34,6 +34,8 @@ V10CredentialExchangeSchema, ) +from ....utils.tracing import trace_event, get_timer + class V10AttributeMimeTypesResultSchema(Schema): """Result schema for credential attribute MIME types by credential definition.""" @@ -251,6 +253,8 @@ async def credential_exchange_send(request: web.BaseRequest): The credential exchange record """ + r_time = get_timer() + context = request.app["request_context"] outbound_handler = request.app["outbound_message_router"] @@ -280,6 +284,12 @@ async def credential_exchange_send(request: web.BaseRequest): **{t: body.get(t) for t in CRED_DEF_TAGS if body.get(t)}, ) + trace_event( + context.settings, + credential_proposal, + outcome="credential_exchange_send.START", + ) + credential_manager = CredentialManager(context) ( @@ -294,6 +304,13 @@ async def credential_exchange_send(request: web.BaseRequest): credential_offer_message, connection_id=credential_exchange_record.connection_id ) + trace_event( + context.settings, + credential_offer_message, + outcome="credential_exchange_send.END", + perf_counter=r_time + ) + return web.json_response(credential_exchange_record.serialize()) @@ -311,6 +328,8 @@ async def credential_exchange_send_proposal(request: web.BaseRequest): The credential exchange record """ + r_time = get_timer() + context = request.app["request_context"] outbound_handler = request.app["outbound_message_router"] @@ -349,6 +368,13 @@ async def credential_exchange_send_proposal(request: web.BaseRequest): connection_id=connection_id, ) + trace_event( + context.settings, + credential_proposal, + outcome="credential_exchange_send_proposal.END", + perf_counter=r_time + ) + return web.json_response(credential_exchange_record.serialize()) @@ -372,6 +398,7 @@ async def credential_exchange_send_free_offer(request: web.BaseRequest): The credential exchange record """ + r_time = get_timer() context = request.app["request_context"] outbound_handler = request.app["outbound_message_router"] @@ -438,6 +465,13 @@ async def credential_exchange_send_free_offer(request: web.BaseRequest): await outbound_handler(credential_offer_message, connection_id=connection_id) + trace_event( + context.settings, + credential_offer_message, + outcome="credential_exchange_send_free_offer.END", + perf_counter=r_time + ) + return web.json_response(credential_exchange_record.serialize()) @@ -460,6 +494,7 @@ async def credential_exchange_send_bound_offer(request: web.BaseRequest): The credential exchange record """ + r_time = get_timer() context = request.app["request_context"] outbound_handler = request.app["outbound_message_router"] @@ -492,6 +527,13 @@ async def credential_exchange_send_bound_offer(request: web.BaseRequest): await outbound_handler(credential_offer_message, connection_id=connection_id) + trace_event( + context.settings, + credential_offer_message, + outcome="credential_exchange_send_bound_offer.END", + perf_counter=r_time + ) + return web.json_response(credential_exchange_record.serialize()) @@ -508,6 +550,8 @@ async def credential_exchange_send_request(request: web.BaseRequest): The credential exchange record """ + r_time = get_timer() + context = request.app["request_context"] outbound_handler = request.app["outbound_message_router"] @@ -541,6 +585,13 @@ async def credential_exchange_send_request(request: web.BaseRequest): ) await outbound_handler(credential_request_message, connection_id=connection_id) + + trace_event( + context.settings, + credential_request_message, + outcome="credential_exchange_send_request.END", + perf_counter=r_time + ) return web.json_response(credential_exchange_record.serialize()) @@ -558,6 +609,8 @@ async def credential_exchange_issue(request: web.BaseRequest): The credential exchange record """ + r_time = get_timer() + context = request.app["request_context"] outbound_handler = request.app["outbound_message_router"] @@ -603,6 +656,14 @@ async def credential_exchange_issue(request: web.BaseRequest): raise web.HTTPBadRequest(reason="Revocation registry is full") await outbound_handler(credential_issue_message, connection_id=connection_id) + + trace_event( + context.settings, + credential_request_message, + outcome="credential_exchange_issue.END", + perf_counter=r_time + ) + return web.json_response(cred_exch_record.serialize()) @@ -620,6 +681,8 @@ async def credential_exchange_store(request: web.BaseRequest): The credential exchange record """ + r_time = get_timer() + context = request.app["request_context"] outbound_handler = request.app["outbound_message_router"] @@ -659,6 +722,14 @@ async def credential_exchange_store(request: web.BaseRequest): ) await outbound_handler(credential_stored_message, connection_id=connection_id) + + trace_event( + context.settings, + credential_request_message, + outcome="credential_exchange_store.END", + perf_counter=r_time + ) + return web.json_response(credential_exchange_record.serialize()) @@ -774,6 +845,8 @@ async def credential_exchange_problem_report(request: web.BaseRequest): request: aiohttp request object """ + r_time = get_timer() + context = request.app["request_context"] outbound_handler = request.app["outbound_message_router"] @@ -793,6 +866,14 @@ async def credential_exchange_problem_report(request: web.BaseRequest): await outbound_handler( error_result, connection_id=credential_exchange_record.connection_id ) + + trace_event( + context.settings, + credential_request_message, + outcome="credential_exchange_problem_report.END", + perf_counter=r_time + ) + return web.json_response({}) diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py index c3c1c0e3b5..0876e62bdb 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py @@ -23,6 +23,7 @@ async def test_attribute_mime_types_get(self): mock.app = { "request_context": context, } + mock.app["request_context"].settings = {} with async_mock.patch.object(test_module.web, "json_response") as mock_response: await test_module.attribute_mime_types_get(mock) @@ -36,9 +37,11 @@ async def test_credential_exchange_list(self): "role": "dummy", "state": "dummy", } + context = RequestContext(base_context=InjectionContext(enforce_typing=False)) mock.app = { - "request_context": "context", + "request_context": context, } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "V10CredentialExchange", autospec=True @@ -59,9 +62,11 @@ async def test_credential_exchange_list(self): async def test_credential_exchange_retrieve(self): mock = async_mock.MagicMock() mock.match_info = {"cred_ex_id": "dummy"} + context = RequestContext(base_context=InjectionContext(enforce_typing=False)) mock.app = { - "request_context": "context", + "request_context": context, } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "V10CredentialExchange", autospec=True @@ -82,9 +87,11 @@ async def test_credential_exchange_retrieve(self): async def test_credential_exchange_retrieve_not_found(self): mock = async_mock.MagicMock() mock.match_info = {"cred_ex_id": "dummy"} + context = RequestContext(base_context=InjectionContext(enforce_typing=False)) mock.app = { - "request_context": "context", + "request_context": context, } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "V10CredentialExchange", autospec=True @@ -100,11 +107,12 @@ async def test_credential_exchange_retrieve_not_found(self): async def test_credential_exchange_send(self): mock = async_mock.MagicMock() mock.json = async_mock.CoroutineMock() - + context = RequestContext(base_context=InjectionContext(enforce_typing=False)) mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context", + "request_context": context, } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "ConnectionRecord", autospec=True @@ -145,10 +153,12 @@ async def test_credential_exchange_send_no_proposal(self): mock.json = async_mock.CoroutineMock( return_value={"connection_id": conn_id} ) + context = RequestContext(base_context=InjectionContext(enforce_typing=False)) mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context", + "request_context": context, } + mock.app["request_context"].settings = {} with self.assertRaises(test_module.web.HTTPBadRequest) as x_http: await test_module.credential_exchange_send(mock) @@ -162,10 +172,12 @@ async def test_credential_exchange_send_no_conn_record(self): mock.json = async_mock.CoroutineMock( return_value={"connection_id": conn_id, "credential_proposal": preview_spec} ) + context = RequestContext(base_context=InjectionContext(enforce_typing=False)) mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context", + "request_context": context, } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "ConnectionRecord", autospec=True @@ -192,10 +204,12 @@ async def test_credential_exchange_send_not_ready(self): mock.json = async_mock.CoroutineMock( return_value={"connection_id": conn_id, "credential_proposal": preview_spec} ) + context = RequestContext(base_context=InjectionContext(enforce_typing=False)) mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context", + "request_context": context, } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "ConnectionRecord", autospec=True @@ -222,10 +236,12 @@ async def test_credential_exchange_send_proposal(self): mock.json = async_mock.CoroutineMock( return_value={"connection_id": conn_id, "credential_proposal": preview_spec} ) + context = RequestContext(base_context=InjectionContext(enforce_typing=False)) mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context", + "request_context": context, } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "ConnectionRecord", autospec=True @@ -256,11 +272,12 @@ async def test_credential_exchange_send_proposal(self): async def test_credential_exchange_send_proposal_no_conn_record(self): mock = async_mock.MagicMock() mock.json = async_mock.CoroutineMock() - + context = RequestContext(base_context=InjectionContext(enforce_typing=False)) mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context", + "request_context": context, } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "ConnectionRecord", autospec=True @@ -285,11 +302,12 @@ async def test_credential_exchange_send_proposal_no_conn_record(self): async def test_credential_exchange_send_proposal_not_ready(self): mock = async_mock.MagicMock() mock.json = async_mock.CoroutineMock() - + context = RequestContext(base_context=InjectionContext(enforce_typing=False)) mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context", + "request_context": context, } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "ConnectionRecord", autospec=True @@ -509,8 +527,11 @@ async def test_credential_exchange_send_bound_offer(self): mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context", + "request_context": async_mock.patch.object( + aio_web, "BaseRequest", autospec=True + ), } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "ConnectionRecord", autospec=True @@ -550,8 +571,11 @@ async def test_credential_exchange_send_bound_offer_no_conn_record(self): mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context", + "request_context": async_mock.patch.object( + aio_web, "BaseRequest", autospec=True + ), } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "ConnectionRecord", autospec=True @@ -588,8 +612,11 @@ async def test_credential_exchange_send_bound_offer_not_ready(self): mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context", + "request_context": async_mock.patch.object( + aio_web, "BaseRequest", autospec=True + ), } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "ConnectionRecord", autospec=True @@ -625,8 +652,11 @@ async def test_credential_exchange_send_request(self): mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context", + "request_context": async_mock.patch.object( + aio_web, "BaseRequest", autospec=True + ), } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "ConnectionRecord", autospec=True @@ -662,8 +692,11 @@ async def test_credential_exchange_send_request_no_conn_record(self): mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context", + "request_context": async_mock.patch.object( + aio_web, "BaseRequest", autospec=True + ), } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "ConnectionRecord", autospec=True @@ -700,8 +733,11 @@ async def test_credential_exchange_send_request_not_ready(self): mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context", + "request_context": async_mock.patch.object( + aio_web, "BaseRequest", autospec=True + ), } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "ConnectionRecord", autospec=True @@ -737,8 +773,11 @@ async def test_credential_exchange_issue(self): mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context", + "request_context": async_mock.patch.object( + aio_web, "BaseRequest", autospec=True + ), } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "ConnectionRecord", autospec=True @@ -786,8 +825,11 @@ async def test_credential_exchange_issue_no_preview(self): mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context", + "request_context": async_mock.patch.object( + aio_web, "BaseRequest", autospec=True + ), } + mock.app["request_context"].settings = {} with self.assertRaises(test_module.web.HTTPBadRequest): await test_module.credential_exchange_issue(mock) @@ -798,8 +840,11 @@ async def test_credential_exchange_issue_no_conn_record(self): mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context", + "request_context": async_mock.patch.object( + aio_web, "BaseRequest", autospec=True + ), } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "ConnectionRecord", autospec=True @@ -845,8 +890,11 @@ async def test_credential_exchange_issue_not_ready(self): mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context", + "request_context": async_mock.patch.object( + aio_web, "BaseRequest", autospec=True + ), } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "ConnectionRecord", autospec=True @@ -891,8 +939,11 @@ async def test_credential_exchange_issue_rev_reg_full(self): mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context", + "request_context": async_mock.patch.object( + aio_web, "BaseRequest", autospec=True + ), } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "ConnectionRecord", autospec=True @@ -939,8 +990,11 @@ async def test_credential_exchange_store(self): mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context", + "request_context": async_mock.patch.object( + aio_web, "BaseRequest", autospec=True + ), } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "ConnectionRecord", autospec=True @@ -978,8 +1032,11 @@ async def test_credential_exchange_store_bad_cred_id_json(self): mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context", + "request_context": async_mock.patch.object( + aio_web, "BaseRequest", autospec=True + ), } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "ConnectionRecord", autospec=True @@ -1015,8 +1072,11 @@ async def test_credential_exchange_store_no_conn_record(self): mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context", + "request_context": async_mock.patch.object( + aio_web, "BaseRequest", autospec=True + ), } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "ConnectionRecord", autospec=True @@ -1050,8 +1110,11 @@ async def test_credential_exchange_store_not_ready(self): mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context", + "request_context": async_mock.patch.object( + aio_web, "BaseRequest", autospec=True + ), } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "ConnectionRecord", autospec=True @@ -1084,8 +1147,11 @@ async def test_credential_exchange_remove(self): mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context", + "request_context": async_mock.patch.object( + aio_web, "BaseRequest", autospec=True + ), } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "V10CredentialExchange", autospec=True @@ -1126,8 +1192,11 @@ async def test_credential_exchange_revoke(self): } ) mock.app = { - "request_context": "context", + "request_context": async_mock.patch.object( + aio_web, "BaseRequest", autospec=True + ), } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "CredentialManager", autospec=True @@ -1144,8 +1213,11 @@ async def test_credential_exchange_revoke(self): async def test_credential_exchange_publish_revocations(self): mock = async_mock.MagicMock() mock.app = { - "request_context": "context", + "request_context": async_mock.patch.object( + aio_web, "BaseRequest", autospec=True + ), } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "CredentialManager", autospec=True @@ -1169,8 +1241,11 @@ async def test_credential_exchange_problem_report(self): mock.app = { "outbound_message_router": mock_outbound, - "request_context": "context", + "request_context": async_mock.patch.object( + aio_web, "BaseRequest", autospec=True + ), } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "ConnectionRecord", autospec=True @@ -1202,8 +1277,11 @@ async def test_credential_exchange_problem_report_no_cred_record(self): mock.app = { "outbound_message_router": mock_outbound, - "request_context": "context", + "request_context": async_mock.patch.object( + aio_web, "BaseRequest", autospec=True + ), } + mock.app["request_context"].settings = {} with async_mock.patch.object( test_module, "ConnectionRecord", autospec=True From b9433807f1ee90c8bbad93de76221d6edcabd97d Mon Sep 17 00:00:00 2001 From: sklump Date: Tue, 31 Mar 2020 00:56:35 +0000 Subject: [PATCH 3/4] merge trace from master Signed-off-by: sklump --- .../protocols/issue_credential/v1_0/routes.py | 29 +------------------ 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/routes.py b/aries_cloudagent/protocols/issue_credential/v1_0/routes.py index 56445ca7b3..5007e3aeb4 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/routes.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/routes.py @@ -735,33 +735,6 @@ async def credential_exchange_store(request: web.BaseRequest): return web.json_response(credential_exchange_record.serialize()) -@docs( - tags=["issue-credential"], summary="Send a problem report for credential exchange" -) -@request_schema(V10CredentialProblemReportRequestSchema()) -async def credential_exchange_problem_report(request: web.BaseRequest): - """ - Request handler for sending problem report. - - Args: - request: aiohttp request object - - """ - r_time = get_timer() - - context = request.app["request_context"] - outbound_handler = request.app["outbound_message_router"] - - trace_event( - context.settings, - error_result, - outcome="credential_exchange_problem_report.END", - perf_counter=r_time - ) - - return web.json_response({}) - - @docs( tags=["issue-credential"], parameters=[ @@ -896,7 +869,7 @@ async def credential_exchange_problem_report(request: web.BaseRequest): trace_event( context.settings, - credential_request_message, + error_result, outcome="credential_exchange_problem_report.END", perf_counter=r_time ) From c2214dbb16656a1d460d824f60815c902c1c3bc1 Mon Sep 17 00:00:00 2001 From: sklump Date: Tue, 31 Mar 2020 14:19:57 +0000 Subject: [PATCH 4/4] fix performance demo, touch up docs Signed-off-by: sklump --- demo/AriesOpenAPIDemo-mobile.md | 36 +++++++++++++++-------- demo/AriesOpenAPIDemo.md | 13 ++++---- demo/collateral/revocation-3-console.png | Bin 0 -> 84605 bytes demo/runners/performance.py | 31 ++++++++++--------- 4 files changed, 45 insertions(+), 35 deletions(-) create mode 100644 demo/collateral/revocation-3-console.png diff --git a/demo/AriesOpenAPIDemo-mobile.md b/demo/AriesOpenAPIDemo-mobile.md index ac9d579436..01042854b9 100644 --- a/demo/AriesOpenAPIDemo-mobile.md +++ b/demo/AriesOpenAPIDemo-mobile.md @@ -223,24 +223,33 @@ In the Faber console, select option `1` to send a credential to the mobile agent
Click here to view screenshot - View Connection Status + Issue Credential
+The Faber agent outputs details to the console; e.g., +``` +Faber | Credential: state = credential_issued, credential_exchange_id = bb9bf750-905f-444f-b8aa-42c3a51d9464 +Faber | Revocation registry id: Jt7PhrEc2rYuS4iVcREfoA:4:Jt7PhrEc2rYuS4iVcREfoA:3:CL:44:default:CL_ACCUM:55a13dff-c104-45b5-b633-d3fd1ac43b9a +Faber | Credential revocation id: 1 +Faber | Credential: state = credential_acked, credential_exchange_id = bb9bf750-905f-444f-b8aa-42c3a51d9464 +``` +where the revocation registry id and credential revocation id only appear if revocation is active. + ### Accept the Credential The credential offer should automatically show up in the mobile agent. Accept the offered credential.
Click here to view screenshot - View Connection Status + Credential Offer
Click here to view screenshot - View Connection Status + Credential Details
Click here to view screenshot - View Connection Status + Credential Acceptance
## Issue a Presentation Request @@ -251,7 +260,7 @@ In the Faber console, select option `2` to send a proof request to the mobile ag
Click here to view screenshot - View Connection Status + Request Proof
## Present the Proof @@ -260,15 +269,15 @@ In the mobile agent, select the option to present the requested proof.
Click here to view screenshot - View Connection Status + Proof Request Notice
Click here to view screenshot - View Connection Status + Proof Request Details
Click here to view screenshot - View Connection Status + Proof Presentation
## Review the Proof @@ -277,16 +286,19 @@ In the Faber console window, the proof should be received as validated.
Click here to view screenshot - View Connection Status + Proof Validation
## Revoke the Credential and Send Another Proof Request -If you have enabled revocation, you can try revoking the credential pending publication (`faber` options `4` and `5`). For the revocation step, You will need the credential exchange id from the original credential issuance (not the one from the presentation exchange). +If you have enabled revocation, you can try revoking the credential pending publication (`faber` options `4` and `5`). For the revocation step, You will need the revocation registry identifier and the credential revocation identifier, as the Faber agent logged them to the console at credential issue. -Once that is done, try sending another proof request and see what happens! +Once that is done, try sending another proof request and see what happens! Experiment with immediate and pending publication. -Note - screenshots not yet provided for this last step. +
+ Click here to view screenshot + Revocation +
## Conclusion diff --git a/demo/AriesOpenAPIDemo.md b/demo/AriesOpenAPIDemo.md index 39651fa69a..223039740b 100644 --- a/demo/AriesOpenAPIDemo.md +++ b/demo/AriesOpenAPIDemo.md @@ -424,23 +424,22 @@ First, get the connection Id for Faber's connection with Alice. You can copy tha Next, for the following fields, scroll through the Swagger page, execute, copy the corresponding value and fill in to the JSON for: -- `issuer_did` the Faber public DID (**`GET /wallet/DID/public`**), -- `schema_id` the ID of the schema Faber created (**`GET /schemas/created`**) and, +- `issuer_did` the Faber public DID (**`GET /wallet/DID/public`**), and - `cred_def_id` the ID of the schema Faber created (**`GET /credential-definitions/created`**) We now have (almost) all the information we need to fill in the JSON. The good news is that the hard part is done. For the rest of the fields: -- `schema_version` set to the second last segment of the `schema_id`, in this case `degree schema` +- `schema_id` the ID of the schema Faber created (**`GET /schemas/created`**) +- `schema_name` set to the second last segment of the `schema_id`, in this case `degree schema` - `auto_remove` set to `true` (no quotes), see note below -- `revoc_reg_id` set to `null` (no quotes), see note below - `comment` a string that let's Alice know something about the credential being offered. - `schema_issuer_did:` reuse the value in `issuer_did`, -- `schema_name` set to the last segment of the `schema_id`, a three part version number that was randomly generated on startup of the Faber agent. +- `schema_version` set to the last segment of the `schema_id`, a three part version number that was randomly generated on startup of the Faber agent. -The `revoc_reg_id` being `null` means that we won't be using a revocation registry and therefore can't revoke the credentials we issue. +Note that the latter fields are optional: given a credential definition identifier, the Faber (as Issuer) can infer schema info; the `auto_remove` setting overrides the configured default via the presence or absence of command line argument `--preserve-exchange-records`. -By setting `auto-remove` to true, ACA-Py will automatically remove the credential exchange record after the protocol completes. When implementing a controller, this is the likely setting to use to free up space in the agent storage, but implies if a record of the issuance of the credential is needed, the controller must save it somewhere. For example, Faber College might extend their Student Information System, where they track all their students, to record when credentials are issued to students, and the IDs of the issued credentials. +By setting `auto_remove` to true, ACA-Py will automatically remove the credential exchange record after the protocol completes. When implementing a controller, this is the likely setting to use to free up space in the agent storage, but implies if a record of the issuance of the credential is needed, the controller must save it somewhere. For example, Faber College might extend their Student Information System, where they track all their students, to record when credentials are issued to students, and the IDs of the issued credentials. ### Faber - Issuing the Credential diff --git a/demo/collateral/revocation-3-console.png b/demo/collateral/revocation-3-console.png new file mode 100644 index 0000000000000000000000000000000000000000..78c7acc9f41e0a73ecca1c776c88e7bf01a908cf GIT binary patch literal 84605 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

;2$> z2Mtf0&mGBRWSqfWw;*#>3WdohV{SZV`6PsGzaWmt0cAyS4K(K>VR3?(^@Qoi8h%O} zJ`9QetX|HRX}yER43FzG@rC?upjpb&mJ=CV1+l~=d(o$*P5Oq5mGgmS=28^5d21Ft z9;H3j(VCXpq-WD?E;l0U-fC^QNk>_^Z#m*4IZS3yuOqtEAH7CPr!O_MI}H+?+CCf9 zbi3jp&&#Fe++`f{5?3aNB49wnBIEDz*)L?nLHWl~n|a>4Q_VUVCiZ)@--M+2CyJM9 zx%I2bbJfXW(9CpuNmauS<@03tE0&#lna7c;Zxe2&B&e&IXbUFyWsxnhCZif!Qjob4 zSa0jm3cTchdLiJ6`$DeDfv0Y3eTh|gmeE-c8{(hG6K^mG=0#|cwiSJze+l-gifBE%W`y{VJhi zJ2p}7qGh3*4W~FQHMVLq-mT2$$}gwjz3sKTYXgDR0Zz-2264nx?d-l?ue7~bulaH_R&fjO{mjvcqu3!4{F6-9hV)8ndb)K)E<}4gj(UXlTjmg;=G+^P zRMN%4$;F1jsK+5e@JM5_FIMVN_rJNXxzQ%R5i@i1ej0AlUiR7CUA2d-#k-ZPG`u}x z-H4L6)oSx4I%)AZ55_~=VtKm&F(A;+toZlT420(sd<@0g<^0bBS!!T^c5xqE$xik+ zdbUZj#h8*WFx#xl)=;6ySC{~Dv2XH9p0cugb@?cuijto*na$3p1~}38meSm=w+jC9 zBQaqU6tliwxX5*8f#4GZf*@-w`0nqN6ID)*#nfed@4(L?H%=ik6%SWzm~(&UU7!5> z+#wFJVrc=0zh0t@hJ|78{>KmZb;g?cB{WvrM}v7fhpBi!84DSS*PxaQB|oW`JnFBo z+-+akBC_2&lG1`~6ycQ=0vu{<2pMXVlk*$nb7AL_(^D6Go1cGz48`of)+j*0q`Z?A z>|lQUDQVPy(o|#ed`KVV<|wABREx;ksa8NqJANa3Rtf60yrjHs6aNc>obe3ULrnV5 z+%LegxiOd5va*SZZIf79@|0C5z66dl^_V~v=q-*0ktT%Urtub)n z7Z*C?p3&bh%gzL+dQm@Uh!GLhzja-z1Cu{VTumfU zTfHjzhIO~Ev>pQmedvr>_Ec_6{@>L@6)T7}#YYn5v{uap(I$UZ=QN$xo4=}&9xD@O zm}#PqHd#?@WXLxL0l@eu*yZZ@-_mD?(KU2#W~FAJ0FW$@684 z#;W}YeS0qm@Hq;lHIWvJVv5_rLZN@!3oJ|J2dxB6bV%oZf7R(tFwcWg#U&4kEO|1U z&xgI4QwgSumbg&9trf4^VOQi@2%%)k6u9S)(F7PJDgN=|O>`NqDQzIAJ?F*Md7xTl zdaEa7WyE&6d5YG2EGKP^^^=Q%tzjGNQKFWX-yf@lNn28RFn_ZG&w-u)oHF5Mn*lKk z>@46dkDTxo#fRW9&x>jYyGbOQ9(P15K|m z^Ax3~$SSY$6#LE0q4@q|S(8i6mj!WdRdhmywb$WMsgH|1fnU2L72zMc!plea$W3Aj zF~BhMP9tQOW#Mc1oAxw!5~Bg4{>-`>znap7QxrT`CuOm@*-$A`h8 zk_Bctc5=_9saJp%og1`m2ue$B6Qb2I`V%eFw}UaBKw02cQj(a@VvLY1@AY%<%}?F& zi}NDiT;}9ONSeY?^qh?UUp{FrC&7pCpO}PMw;G=5eTec#I7BqerBn17Tc%B^m{Xm zYrm|MCkOPc-YD7^J@$zx-Rni+B}~{ba+W;ix$Rs(jN#7hKQI(HJ5KC!`NOrs(S|9P zK#+n>DOb!$P<3UvVc#06Lde~hZKvQ~J;_2K+9&Y2n7iGLsi@Pz95LC!`-Pwt-J`eg zNk*z#$t)ojDZZGedK{><4Gkxb5G52|fMpRBR7I#uScx*`EJhwags9+f^Vez*#lpCs zUN|H01|c;`kZw{s6o8Ycdp~NTO;`!0@@XX~vWOUd1fhWA96m#MqIQ&t8gL!^!n&L` z`LLcqacM^FU|(sZ_+pe(;=3xm;>UYNIHF?Cd71vc!eWU@m2Xh9OR?5<+qPqMT`Y92 zJAVEOdPuLk>PNTqwt59K8Xi^j9yM`a0;`Re*l*^Z<> z1Z_s*X+_KT;LZ)O_6PnX6lR$OQ%k_-dB+LB-JK?W?*`KrL*bXX-N@Q`Y-q1N zXuW_jD%M9IWGQiTa)S!^v##xWo5QkK^gGn(on-1@$y|JjX=C9z)v_?XAakQ-y?!SC zTS=}>e*1C<=nJ8mwt&Ps%p*GjPZJu2tE-)FnuIV{>BA2`r?4N$ETNFCoVq8tK`(!S z{Q?q$W%!hrN&fe=;(!$wT4bu9=2 zNee$KRx;Ahf|Q190>>ki%*Of@;xhIbmhaP^dta`? z-cT}m?6w&|*H{U|2pchpLgMYFHm~jf?Fd%JOoB#gcp83N7QNg>YS2T^|IH-TSkI3i zNLp{TfBi)b5b0_tW;U@NZ!JK~t^SvUna(+2M`X>`s~U^2y1(o5Z z!e*4iEAUZc@K>9Ktn*d_?HJz%NL9z0M5zPM>#}Op7+2x>sreT-xJ)z8H|S?GpNHUZ z&8K8lADzhQlm-zUhwEW(Fnu8L+JYzjufL7}UI^mO$b_QJ{GEeFO^R|}TJd!^wyLiE zc@(W{lDiI5J%-L*SHp!Zkwlu!@$?S0%2MaVMnB`$9wj=FrUJ5G^A^^gy5t|jqAHs9U0i~3vfh}HZt3#P@O=@y|;j1c#r zSS_g*)H{VO8h`v~bJne^B0eY=bsbSp8l_zdHud)c;&I;*BS-A0=xU~?4E0LBf1$Ql z2fT&PbnzGu%Xb-Rx9!*!+Y2ukPU@!=vV_rUooZhizFMI8*u`T1mnM%#3QzJI+z_rKl%dKT;rB67D#CgI&?bF`S0 zG1fj!Kx2YqcgwgTzqP_-3GeG`fEef+|9bV}|ET1iG-^7N93>s<8b1iHFRFGg*?hI- z59TQE`bnl0TeCn*#C3_JjH}-+5H)v_u#3}<>8CY}tVGdPC_S#G0lWsYOG;pMeL#26 zmU#r6@T!ymW=d9X-oQ!8CY0WSb(nsP{r8r}i`IJXavb=HB*Kl4A!Dd28RRdNQ zqdZ2n*Nc?c%kXD9H?ic?iViqjy9>7$mXpEwx#RadO-)&PP3;tylA0r@Y@W&=+j94N z8#e2-X1p{saBW-;(H936C-(i+S?8m;Nc3*zw|CzrNp{6PyC7V5ltXcWN^FCr9+`vz zYp{pM0 zQVjMOR0X0ll^>L3RY_+!P#87zi$KinaWeT!(G%>8D4{SXHT{!0)6oOUAWDL|V}s=U zga(ray=>iCtiK0>4Y3Qj?s{JyXA}PVfx_+c*2yZ`F;?7^d+4aQ0k28_|8r$djU#lh zOO2xIQ+Ru8YaRb3{k>&>5D;6LTFFI^uYn$HozKw!RZq0_s}x+LjNqVbzg?oC`p(-$ zwWh&++2vY0^7T$DBT|2$gutQ!wQEwvo(_iGl8wJw;kQjq z%ox9M<{l$2txiu=6(oPN?~Qk+hGClh9+!H>GHBVzJ3hViMEK^rZgVyVn3&=}0sw~$ z^~bjJPlW};+qFN0;vSONalUoX#d?nF*qCN^hsQ9FMT1k~gPdgdfu@R^mJ~e0PguG9 z1|8KtJ3X7(ne=0)u_k``37q^+FtoZ@*W+q~Ip2v4mc?m_7~vR~hn%DGUd!dl=N|=- zL5))Zeww0``F$Pr$1|y~D+0kTdclM;} z2bXy>BRhl$x__rUpk~UX?Ct4S$0ZFtDCUZ_(YpSD?9cQ@A>tl(uR!i)h$oM+pRIA_ z`9oS-J^+HlKi-sRx?Dp>!a<`MMB}g=T8Pznl?uZY=JKhKXHN{6^ic+6${FO!Jx0Rq zBy7zk{Q?8ou&p8XCw^iVzE!)TibP$?Q}|c%Yju|)lfQ>Q(Nq%VV%I%w`V!F;DJfS< z)lvy`owFYoEO(6U7>21Y`xSBF_um8IS{c~f)rUSl1yLQY#}s$^NQGw+$>PRjcXpOe z-D!d2Fs|>6Ug5W&)?OmLVprtGNx7JiH{Xm7X6Q(rof8nup^}XNJ0fSYgwSrG z!FEsWz0FL2s`zXf%Hh1GMf&A|xB~bgA+TrFF4$Cgm{*bcC3$eHo?SqNa)6^_YW@Li zMS^@2Kl|hS2e6m~!0M^_xvg;sqj@5j)XNbrQM9OJa}X0hn?aGUb0yc@JdPSi=#w2z zGF7+Z`%UPBqr+QLDoh`NS1p`|;kHv8Ng*){Gn*?iqGR;Beh@f9CQ{F5JTFY&|C75M z^e8!_LC;wzQ!gTTo<}Zzac{KDGn3S~PjFRfxO|;xxuuhT%KXAYl%}h6m|dyFIOtkv zgr&j^?6{EYmMecKYjbcWFe@(FH#L5|A#K+{_cnXY`91N%njW6}Y21}!R$sdY|E~}9 z4~uI5!x4MZaNx4A8Z$OGsJ$uST%y<>^*9-YTi|Vx5g`5x&~&3o9_C={`jc%-4@g(P zQ3sWGF@Pni7Iypct7Zj4v7HB}EvC1w<`~}C^J1DjMa?J4`(hG6;2Hm1Lz?MD`Mn}; zN0v^s>;IzeEyJRG+x1Z_3ie#>-u>I_*n5B3e!>H$o_X&3zOM5+BUj29!=ylh>Zwp#(w>o+NW~!4^D@|jFT=wq z$4Ar&Oiicyjs1U!{NmL;u+5_}AC8bPW2t;}Q#4_IE-2QVUK3hHC0+{gj+_RMYFSKNAW_4uv2pfbU!r?r)CM+ zn*(c%Vdh!wgE-%6thwyHJ8*^=GQu*>`!>Qp?67ZGaIsJNqjgobgZ(CGsv(xw)`c2% z#_yxX>-C<5Um>njH(9;yl!{oTG-~&2!xQhySKe4)EdYnHU7wgzwo&}PuHx~i)MbZs zrT{I3#OL$uc+Lu|QlL$NESTmw(+LW9w-)v}iKJuO#rXM4bfNkX1JUI%N>&jnQ?|t7 zP>slB*w^s=W?6Wrljmlw@yBxI&{zKkOyr(eu&kQ|4Hfn8*nNZ`4vcI)kXI;RO;v^#N zo^k{b7s>~WpySbBMgk2WM;Ai+q$eR*WYwVc{P8e1;%8+zlnQ7tV*WnqRG-vg{S&sf5>>6g4kw262q8s zl2vxxHTL7Wnl`QSuDUBqlzmLm8ERCQ&%9=0fEt}BCdfagc{Iv{v3Kl72BlW$qTpAG zALlb<*z?*p4{kbM=16M+Vusw>1*Ud$NzF|aoI2fdX%pRpr*@9kg*K{60jocj<#uR( zNIJz;Q@&`9vdQS18Rm;+dC7)bXSg4dk~;_@kzoD=(r`TvQCfhlk?+A1lXLfheW7d^ zMZ^FMfOyrrYRB}%z^SwGI6#(TN*DORswBf2Gw-G%iTrR6sf(SByQEMxdcNZ4sj0C) zSI7wn1PeM~A+d(k=1}-Jx4VweQfz}Os4^IGBkvi+E zufXlz$hTWlmz3Pn6kuHBDF^#LcxnDFw?F&)a&oq%!Ky+7s|okDqkSx%j+oMo#y##zQNVhXp3PG_P09~ zE|#Af3R8utUKJ=ptZ(C}$l-MbezzXR3qgWirw7B}0oB0XPOer$)@?_YQkvh%$& z=z~nKc8nGMA$t6WgpvFuNd_(29V>5<;59z)D!Ya8mz2G%tNGCyC$rlh8xqw|Y{4i? zHHkJR^uw=$>Pnx2{9EK}Q+2kB=2!^H*NXIvlA}hdugJ&_&n~woyH}M<_^s-n3#h5Y z2{_gGWgs55mJv9$W|bq1-2;gXGZP9eAn-ZW2*nA`ULzA##&Bb|%AVFKMS(FecwufW zC;dul-*9b#^J<%w^uCi?y5%xA&yd)U2t41P^@Tm%eh%wF)4o|;9|=Dh<_Eap0CpV{ zRdnq7Z&sC>tDq{}K~Ad0Cz&W>Vz?X@16x-t=lTV-M2!d&{Vnz%MVD6^hy~=-^j8M; z`0A;-Rgo%JYEkS;g^U&^S$=#=m1kZPYUX?Y$xs;T6Jn=7h)O`$-y6!o7p$Bxeg4?< z{YN@@zJmDOr?jC$kkE1a8*e8181dkz$r@%ft*vlo2OOG}My zhqw(~RS1_@I84WRQ-^@$P_G1R?8NNKe{l&o1m0BD#LDeIS zJft0ATWZ;tth_!aM+C!1zPIq7s)tSXTr!Vaoyy_D7oMJogL3rPKO=@fsAiluzlCN5 zvn)G6@x-5EezmRE>7CZ~^lyzyRiRp3d*k!eE@PmhpkdbaJ2a|}_97vapsZe~{Fmz1 z4i6yefu#@~tk&YaF^l% zX}ubRL1PtbmwBRctWlPiU-Ymv){*Hj6QF8a$&GX|B#OWq_NaUw%fRDuG%-Bm3k`dj z;G}4+|E{&|nG2B;`f-)Z`+u*s?c`3%=9sr;7Y;NeGK+g;Aa7Tkm}e1oUdWhP_z+9~ zZJVOKV>_zF7rSod_uI&enqC5`5ZEx%<+i1T^+J-7XXBb&%YoTq;lXv%?398$R+AL)&s@eF256Fs zsH5PfssNd7&8Gc%Pz;qsG}d6!iv`6aOg6Pn9JkRj5EB+Y&c0u6IgP-#es+pgfR)#q zFNi9%o5vM|5IEO1(jVsIjHr7X%_H6j)}_^X)}2K;{Nw&wAHwjH+xIS|(6J~NZr?9= zfaNq9I3}*3CMPt;VC@A4w6x;kG^)`@1LE4|o1h0F*==S$9NG_$#!2zl8 zRIKV6p{kb#V-=5e8GFk=?HetR3Y8(o4ufDaT3Nd(BeTJzGQ|AGuBh6u3xf>g0afO= zGMW)9N>cwtL>oPycQk>zC#f7PqUC2oR8>*;3ShR@Oqh+Zxfxt7xt`ieTT{w-prvDY zkCNB0NZeu$#w;IAexF)grrJ*;cn2F_QuE=3&lKA2aJ9ZpK2k~w&S*gTXC>{=IoMm$alZ6UoQJ-~yXeR`0SJ2m1QLR$>V`2Zaoc73Q{| zf!R`}z*LD~cl9WT5TmbHZo=%^$C=@06dX4nJXzTS zuOx9>1fP0p)Q?^4m&Rd2I2%foJR>5(X=KL|%{m5O+(bg-!rABpzp|Wo4wK1x(+k#k znN?kE+8t3|Cl)lawG);`lzYG*31t{6aNny<>HLCE{2{%2DLPVCx;?{}!R%YySo~+u z#A~b8>}oHE>q{z>mQV;7uQ=HjB2RUSlqzQ5vYLp`K=Q@&Q|>+T%K7t%sP3h&R$kh4 z(n!17KgmL8>&J|l7~0l$nh`HIw#P&NNhPc7%zr;~8zqy?KR4@HhKEK$I+{@$jg9Vnek zBN?)lk#By#XM!9pKrD!f-lmF-GI&Qax4UCSQtj&IyUrnrZ zQ2lFYqg{T}d7)mCX_V91Lp=l<5F4={BHT@~>oh~3WG`P1Up>@05oW*DKVF>F*eEB^ zX?$^j0)W)VFD>NiaUGi`Tz#lb^_4{Eq-zl{&+!gT&_#~&E+v{`2qAWlxjEE7U|pCC zepz{+%l@K-DlcLJ;%chM>SKe|ic~Kg-D!={Xf=rEbkdS+Xwm}@-_nHnym~p$`{PG; zWeaj~nSsFN%nz!X0rNC8?dt|E0ndV{l5OoxW81%$6iWHST?T;e|5SQZmH zvWKtojKy_JyCLf=9cn5;)RZSJ|4|S-^&LN=#pi2@is6IiGehJYiUrk` zq$lEyEhtrsaUihRtY_P!2410uMH}w@D-W2|LJ6@F=i)~!pW$Sa?buLqn^#=~`tzwt z#GU1Qinhj9V=KZBdy}W7QF%3ScKLJx@tBBREP|~9PTBK|-|LIFk<&>P3KHHr5uD*7 z7&9G7qTVM?YT`5usi>i$xAYc?flJ7FNtpZdBTp~j@_p<=?T-el7cykNY098fYF{p1 z063VLUc)# zCzTpLG^a04c)vM*E$jp-6}Rt!CK$rbwvg-sL-WYP))2og#nzr!M>8FcBw}YaUfVfJbD@;@pRwF zG~3r1n410Y1Qf3-k{4ocbpgOOij>+gsFebp{r&`FP|qwepg>kAT~$>|EM5)4UHm+b zCIWxpiN5hlZ{{6%S#wevVzFab znza}vLn)!r&WQ)lFfiUSD*0XeowR3@WhoZ2<4wn4@>+F+!hch_ZuZR3X=&_D9$Y9M z++t^5N77q#MDgxWCoUfDYKT+P597p}eWhb-*0LXfWFzQ%dIX`PmxRZ=SFx|Z$5x@1 zruxZ{KAY+|tgMj_Nbm(OxQ8^0RLclel!&;@+0&n>)0)|s4H2vqe`eF$#OBY;Q{OF< ze73Gj`==TQObNMlbiO+pN(pS7C6!Wf&l#)_54A534`m>WKCSodt2krq!Fcarc~9VT z6C7L?=N!IaMMcFj1|DzquGG4PXWP&Dm#2;`Wlv(flesA(sa&Kf)=MQUJ~%%QnKDcf z{T!+%JW1Zfeb4iredr~a;5!%IHtF9=UOLNiw!Z!jR)8ZT{%fP?{zu&MzQ9sL+7+ku zLm@VmFU9Ufgpld^BDi+oVCH#D;Vj*? zA8mRR<3R0hN=j1Pl5ek3=Adk5-(A0Em0Txo?&JHpS&C}9UR+oz2gf2@%2e`xxrZtM zvyRo+3+mbYI2k`AG}WS)aR*#JluUe!;8&cR_hP}5@peFm-BId74tt3#{^u(-#b;#8 z4R*VLrzkboC_~80p{*Y10J(LdVWK)Mk7dB5gt=lfg??q%9sRMV2d{9oiFPdfwW)G# ztccnhls@d~N42kAV0|WUTS^4@WdHe7P@3#ocD6$sAhN2#{@}@QYCdxo=ie}SzOJkF zI7Axmkg#Xohc1*op=#i>epF=3YmmrJ*GDBptF;Ww82m)3Wt!(-2_n%q3T9;7VAPV= zx=?CK3*H^5kp)M!r4H?RvkyO?ny*o54!4L8drfQ2PCTb*91jQ`EsrLNfD8(jG&p(YLB~TkMtC%&S8dY z&Nc^WIIqGh>0SlMKmI^@S`%?QAGDqGoECSre`8Q!m=Ga6DU`k;42r`D`7~7Yp0Os6 zzq`ut0D9^V>A)dgqG7u|z(oVQ(i5a)E2>rniDXSetI6W54zI z$U1J@(7ry(bIk6x7BP&@M`@9c>r4+K5Md$Zk+MywyzB!}>e41<5R|W4)9t*dg*|7A zZn3>v?_47;uIfT6(QYZ(LiJinfDx9spGKAw3?Dvog~aF*Dop|0s+-{_ZFT)mfpG*OpC!LJhdz zS1&#sa6qpD5QF{FhXvzHmI`|yN{3p-CV(~IeN?8qPkDVmkYq`Xy0_v z<$=~F4q}CG5jVz-vJj0jq!NdHv=4XV1l;oOEt0{O5+~1ebViAx{D{(KlE(lOQQnwY zz}tnMAxbYXL)V}}z>BI)q!3>v>bJk-;X#|IjRLaY;cz~>*4oBY;HpqF@>Dof2|DY) zTq+3TRD=Kn+gOO()o2#rax3i6(94|^N@Ghss$VTuo@vHz$cB&&;U)y;!(VA7gmPi{ z-r4&`RqaKS{O8|32ni{)8NK7x;$)GSCV^#Rnr&pidzz}7Qjz%6X_iACd1qX|fux0S2o6cykZn;igErVv<6h+wze zS~j(J9hj9mMoGi4XfDmH1*~bc%WA?AKy4;Z$ z<6OIjt%EHuvLH}@kCV{v5d!1aV7f;GO$!YS@}nQ44M$DZYlH=P8S9|AP(jmz_9K z|7Yn%p#0l9gMHWkZnyx9FlW{>M-}e&bU2Rh(gnJtgbw!4B(R37Nkb>fjM}HsjEAWZ zhnyOc5&45eV_g;VZ!TC#X(vyM**-dyyZs7A6UCrT~TC2)A z^_%PF@jO2{+PhS>wReaL!!T@ijnH~<%5N~sc3e>`a0{hC%?m3PB%BxiQn}Lb#v1#P zcu-cJT=}v7O7-mp6YOw6dQ>xdpLuPHZ-}&^4K+>=y%&%kj;Z7#4tWINE2?tsWCA|F z&qNgiy%_Dp;ufukuR4oeR{{PLA5hXYG}7_rPCC%0o5;K< z58V|$n3FD=u90+yoGau>MmD*l`#A4{XCJAx{Os?{x&3`<(Q9l9xQQduo8-N2vKlUM zdRG8OzkJD2k`|WYxLnBi{aFkV5O*y_3qTI5i<{e=Nd#IU-BJ+?q94O9nN7K|rUBs>b|TW46eXiRu+-uL zjLkz`K72Ne?|;Nlim4mLh&F51mf*t4N0T&K!FQN^p@`ShLA!gVvDUR8O9Fj~?@i5W z)dv`1&8AP^16pX6f}AllfF596jT+A-W7B<0T?A!O zle~j`S4~4ALvRvcfB5mI{h#6ASG)4>_=d6ZmRxK5-~tKMl=s!0Y@B5Tjj%t2Yy7@? zHw-#~pZINw()94Z@f92@of#4}JU%bigE#jt@6E-VzKM#-ZNQUo-Fw#qXA4+rch1MZ9bw`jdwr{77%cRY2q8 zN5lIK+On_}WUfKJ^&~>DS6)7!$w08T&cf%DNng1y3YRMj(*Q)o`_~awa@D-BIt{0t zhz^NP>D~5*CJAtg&C;h&h9d3LvZ$j-IyEowPwT%y8TPhz?-L|PUnlEC6AU@GWYb@z zT)zta%u;mRB~jt!4iB`PU9Tsn`q4deIy5<~0GUVZmfB7jeZ8jZP=*RLErChv^+41H zi+JjdRc6dee}1jW3kQG;*j|zrHx&cDx^QSMp<<*`H(Od5E6_ou>^0Ncc`&M>PRb`> zP#IMDyY+eOW!Dj#pHF?>_IO2kZ3;CJ>}Ji;h@AgFS%9f!f7!tQ!~#5*y|F2%zPvMa zfz!@-)5VNt7GJ%WUES2MP|IVe2La}3LraFaB*VvU+_(1~_QtxRB+6|kj~gc@x@)hZ z-UT@bxRHPNgmIWoaQ?7c?Eun_cU0iKi#(Qt-jAocj})BiO9wGnt6C` zU&LMMw=jiLT#}VykpL^@`<4$si$q03q8R_pkz2q(O<1h-N|!KR zr4Oy#iwPhFQb;_u;%iG3Udf}JfSHsi_GG-OdN+ECeXto#KzjDR4$Ax0DX8l86b%>QGbV+2MY^fx>|Ge2=L6!ct<9ffDjF$N=e<4` z@mA{Ou4^hN8x7^*{uVL_X3W>)!t=jYie3K?vlaHC(`n8hB z{@&5rkSQis0t$TdmHz-w*LoHBDf+`d1V96W(9w}Znj`{gn}C&OpGC`fyXV)7dZ}^6*vKelM!_7bsNmoqM69_E+-NqOZVtK2!QP zd7fSsmLHZLIYav?-6;2eKBnL^REz2%u7=%Qm$Cn09nU)*0a)w5Cft34`nZ>!uuPAqFGDC(oGqZv}bQ>WT0&`tTo~z zk)#^CMv!<}+TXxg4m&>Ghh%;;cB80^0ZKj@t7Ka0gqMIL3v$TliVYd`ak|kL&^eo8 zU2i)PnRCtE{$rncDvs6Qiy`SQi5$O9F@gc>6q7j9 z!}Ech+6l=q*3PT(*kPsnhz!!_z{#`E|jifSt{qwr-sE#iHUN`_LvkwhX zHIk*1Y&l!+7xZ$yKRo~F*b7!ost#OiNVFV7GbN(wuXsYaEt_zJ1%`w2bkSqp5H)*H z^iDBNI$jI_D+@ofAASs-)X*=xt{f8}NW_4&4K&8!!ysADQw3DRGu8Z|gPvyY9mTtY zu0_mYZlGjvik3;`R5cT%1ID#G?`gRKXo?8PF}KRaeayhhPW*{0y)hWcB=;rIZ{=Z+ z0S`tiLTP6(HaAZ3*yH=7Yo8fUzhu`PV~zqxWowzSnSs#*>NzmfNI}>a90dEikZ7I0 zsiXBYUuX{&bbeswjvyW{gs!TCwrr>~kMz(INAeUC55VS%U>q%fKAut&=oQl-X3m(J zt%)`Ks1M-LUv8t%k3|I=fCM0ej(!nS5h$autCylnUT%p9Jsr)vnJ_@%sR4%haa6Sp z=+7?DjDN%_fn<9xO)%D=ou8uQzSfEAT1~7-x0E6$qweoBw44C^fIK^^i_3|p!Q_j4 zb{Iky%scyg>cYCy?X19gG0_NYnZ?rLGDG#8)r!@%sDjiW zjbcU*OkE+)(rdT?EV866^fRZ;OKEYvaeJ<{%zC$SpenTS+tWg~a6H(bz)iGoZxkrs z{qhU~W6Z16G5l(}5+>vn*rAxors8aeu7aQ6rT^%&ne1p?at!88dqQ)dwI}KAiPR$h zr)7l}?tQ|%!>|5W85Dw4yW$NPgcm_4t$NTl74C#X2wb1=br&AtLEq^LE2%~MCc(*$ ze7tY_=9OvV{M#96iCLmzj!WG%IY6I{6WN$+cQ zlv1fSTj{AzB-H8Cudok|gaM4{;22FcG^iK#=dSKD;;kX30DYUSmB3EZ+^32k(CuBq z9MV=QG#wQ`*DBG?^Z*`Uwfe7N5Z?s0DBxoOWylh@Z&!v^F`~XvFqbqd>XksGkMjj0 zk`&qd4z07Kfy$Jm|J}xlWj=txkR_rS1EES^n<(>uSyRs;dPrx2{0bFHN=fY+vN0EA-9cq6lIuq$+1#{a*1QxN^)hsAz@)Klt^@Q;wtLabxlF zc6aaeRujiFx!RDneY`eMX{xqyTR9Qs`SP7eNl9%a6DFA*D; z!lDxP&2HZ()}sE}{bMoBfMGV{A?7Q?VB;<6pgwnFk3uUz(=BD^hiN4Uu;i6rO#*cT zlt%uRQG7_8Ps8Dj9P`m^;EQwyv@d_3;q^s|tS==0FIGG&5oCfKb9x9tX8 z?zd3le9V-5k1qD2U+<^239ZsC@I|a0z@^@uGgV$-T%Cc-b}7#-&)98RY+j0GFV1q$ z?b+yGd3LXYyBr9waDyaZYhn>|{b-g3)GS2?PyKMKzmL2!i^~%(~UcwIoy!j5Weg3#vZ`}qf@E{u+< zTl}6-O(PE6ZHm6A5aXdSY|XoJraqrTM^xH!y+W!HC;mC}yxAR)(dj%pPUgq}N^1g2 z;zwonwPLE}qZ>eKf@P`zCtVi9&<|LtIT7-v+WKI*@y15Rl@c+VEK<~}BB+=NCNA%d zguveBPu{=w|NkZ}pWbwHO^*(IUjaMB;4pr7v1Iwbk61X;f1;T7?JH(@R^b3cQ-TXUE{JyrZG$|2ySO{8lqBvfrAyUt6N~Q@4jh{oI)j%IBx2g6Hdo z97*UD&yJCWmcgl^0`wR+1{~vJwX<|m z(q1M7q7J-jFy_!TGC6SOxs# zv{as>ui}a4K5&j(KI%NGJeU;)^TQVwVfBX4VQ6te+RDUNGsXkdxOCAuNK4;DO9Ajdq$BEneYhy^r)S|)IeR@M6ee8^aSlC}saVnK@7kT| zsd`8_%sV2RfOCpH9ZN2rrZ6T6J?W zxy2#7&3sxOriYT0Wsb6Jl+nb?W3#5O(BzaPq64kBRqrmsb|7K2WYX1mBTW}f zq5jQ;=9YsCh;)QXci;NKE)>$EfAI~?BkYx^MCGgtu0z{%uRs4M0p0(yb)HF%o= z1ygE&w8&K)FLdhXrb2Vx5Yysz3(DY{GDKgP%PQx(*BcZs z3?$<^pxKg>g_zJ9GVi~EZb_#T z`-sNp%UU2_5h|`dlkHzg0Z_c5(i_mZ87o;$T&)%n?1MMnS-0rudAh)lC(_hvF+=t1 z>m8O_a+#j|?;t39DyHTVWoMy7@Q$)BySnw(!&0%VCv(w`azBsDVH=mkrRvH83V_@2Ub{vXI%KAO zw2iM(&$1`cbzW=whilRl8A0$@WFr-zg1R_j!c_t>$yT|0<9v%(M}|JRMSph%)w&L4 zNr14*N-*QkP51%Y6TFN`Tb=EclK=~*y&S?9U!5WV`heF#SK`WJ8YcWIFc%qVtZ@}5 zinZ_C9z;lB*l>{(>JU?%4v>F7Y_)oxCg47+tKYGp;wG4>blsUm;HP=54D*Dkr5=T^ zA;8IsLo&|(DoUA)U;5TxHa1pvUx+3_FG>$A2;DOPU(lmhk=wv@DE|j2xcECO0DF2m zr}YEAYOf3m*l>E%E?pFddGg*H#c`X}>#R&g=cv)hR$KjK14T%{m`pG+kGprhbOF9j zPC8xkrKl^WmkH-GiJYpa>#UmQzM<~XJuSF90iJ-eku=Q@17GWPz*|G`t3lTor}fId z;Z|M;RDhSqWb>M~+q2)K#Pwy+%!F7vm2E1N3)Nb?%o31?4v)%vc)oXJ>i_I|G{BJS z;x1Y82D^=GWSbK;=^^S5ac?3(+D3z!KU=xmP|%D;2S z6?vt_yu4kM2O$-2lzm@UWA5;E&$z+hH$w(i>wXWCrTqV&Zf4Hqb{T_*>TaX@VAfYd zAC&~mmer!VD;}gcFWY*}?@p7)hevahBuCM>Kv(d#+FxcxgkB;Twrw3sP+D>8R$#8Kj8gi1|V1L{mO~FeL}~dErJ2n417zrQEbY+0&f>k7>r};QW)6* zC{Q#VU>FhHNnl#}BMyA(uip2zx;Fq>XgK#)C%}DpS+Tn|#hG^#>AROTK{UVlYTg@| zWGhNcVzz)>^p3neeq*RD>{7ZZfL_6QULk1 zNCb!VF;TWNx5(NhkiiQe91j%|4z+*|NyIk;us5pmRX$M}LPf(5rB5bNgT5m`wKNf5 z|A>Xr_ye$b55P3%*R$BUk9u&n;{gG5vXxy)!__ue+h>}UBYV#~0Y+Gvh<@X;(%2=n zLioQH&aAGxQG7RRLV|(WY`-{xo$2`_A>a5o-%NAJPW)5-@$nB$iBA9vlfItwoAu0{ zkSXUn-3VAD}5N@%&M3p+dJs z!Rsycj4}1sM%%>BQH?AI5(!8C$AyQ4RKbpT-4PvtG%QSc!jfH&u$F=cR#3WMcm5Y# z`kw_fkEr0t&I^xCGMVqffy{>B@Rzf(@Y!YSDsx#ZTkA%6{mZO3nq33N##4nd!hP?*$-@7Pbst+2*KYH29CsevH!thTF$^ zNSei}WX$yEUL0J2hfF@0*Glie=KjB=GXll`SJIi;dvWlv8w-v@*ATEFpii<@CCC;9 z&^`Kbmt1a0I^g|wtu13Bq}Bt~KI+#5U<=iT5e@1!5J%BUocnzXz+|x22V4|VMQ!NY zRJ{jWALm2FJtIG+m7G}%6O|r`!P_`2*5`X5BHwb7rq|H9o7}p@b?wbH{FOW zYjlKE1LbnbQk}Z?&p;J`J@i`D0YEMIelp!=iOA(E7{RnNufRNd~v+Tz|%Z7TXI~Y3~faLm%``&RWH+i`Go39|S@OJmEmK^mpAuL+r5-*^kXF*Eu`ObC7;hzba-~BTK3bAFmYAN zyWoZkaaRJsU*yJ)F`Oh-8}U-u0IU?oe__t@B5XZ$F%rI_Z3vK9RW}S+Qf-rKB0_2{ z%YUW5Wu`CtV8q9z_D!fv!RG?fF?*9<#sTx&{a0^H(v&R$1hbNPY@M&3A`H4KtL5|XUr74)VM>bBA z_VN=uA2?{~ zyt$>V{xa5j@+^BjD?T3?H&(1~t-KsG+KSe7I^1^Vcpa|aNZZTiTZ^@cZk$rku*OHm zR#myZJQ&;S4XhOz2zo1czsY1>%#|TW}^5|Jiu&+ znIV7*KX>bV45=@KPNuC-H@O91k0eBt4r|D!4?SU$%l^bQkVrFxkvZ&%JsS7z=ulnj z%ec3<8^!`iA0tZdbjU>{3FNr0M{KURi}*69F5L5&=w+ryg{7n&J!U$>a?l~DpGksn zu!Z?+?$y-nFmXY?eE)-UD`9fUbk7mAQEZU;D!UGb3bYn>5<3x4_*NhMq4V0 z=CsAkM9~53y8Yfhr*zuRj8xK2%^Z!IF!5K1!x*6K2@Qbb`zWaBWq|<;3t(}F50vpP z;!B&7q(m=L+J07y;%o(8#3wTtPdg|1$480_`at91p91Dq#Yd)#CT5as3gTwkfV48} zNT(mpH2R?x}Dgj(#CkzRT*VI5@OP&5uhpWJ8u9 z4fqQJVC0a0F`F^~fr5*?m~0mzx2-n=$!^uc?rPoZw3RxfuWp}(aO=f+<{;$|4RurY zNJ8>rY%;0GR5(n_c`b|PbUV`h`Bkdxm|{8lh9v^9FuHur-{&J4QkGX(3rqaqkmvGU zA3Tu>#i^XupDc6jr-&!hJ{i=|OICpOaL=kMdF3K7JI~}9hTY`N{qamG)SMub$kku- zBcP@Wh-ur+NlrUCrnG zF6ObNxMi&#?PjI@nla%Bt5y2wpEK`-^h_R18oF(FZJa=cD`Zbx6YMfx=nMLq3?%K2 z9&n_0)Pw_2u%jre+E`ee6;izONCFsnP{)D8FzEf$J0xzlc>)a#tzcdb0oFjb4EcPw zMdK9{%h}3B%FtmvydHa`zXmO0x=~GE6!i)J+N3zW7B?A61g#`9vfOM>Z-yD8&C8rX zOxa&Ufi+|7J-ode@>5-fGnO--_Ah%4rFYT=%|{A>>Dbueh(4PKB{ya(|T4 zr$jTIg?q zaESPpY+%1FvV!$iyczf6x0(!g&W{P*FdfJm?|l?*^45li1!U=quTZ$W^lRzXxWuPd zZg|6X5G`o4-KLR*{!__qXPXB!nYG>={kcTp@MrnjJ#4Ydy05hgbvyjwpKh<%ib?o_ zK3?J+FBCFZ4qI7WnyIDiGh4j%&OJ={`Z<@xKSy%Va5)sTyP^3`SM{KVyMp?`Y*Bd_ zZnPyxKrGHi^385LYhUIutUr{S-Kn^16#8i`8(8C`a-oeC0-LO-P ztY<#*BaT;Z*z5LoSDsS-kkRpR?lKL!NZ!3szxnRPAUTxsWO(*dt1}QD5X!^VnEYhUaX0mAnZ#N=5Ju4WsBvwr z05>_CFbp5ki;g#)d z;jeB?KqO#=Nl!sdnFlOtkgQUAoNBfT$O36e^SLOlHr9l#wUOHs-JK|I*U#MCC!v9!b@q$&mj2l< zd)L$Sh9=)#(-YI8Pey(#gutI#s`^Cvkw|mYX_%x}YikrBHR?T$a8Ix)XvPUu2w@S3 zL}OvA8Us_I2m@L}dwWi@L_@%6w!h*FV3Df(LmdViIp>+LgIO~rE)$>;>RfXK5~$O~ zI+oEq{ezk3g_Fem&|v#7q;=4=s5YtlF6(ZC1xYxhjycLP+$1Xk+nM9aKR36QryJ#7 z4lwl*cp;`0*3{U)h2~V{h3-8BF~J4O+zs8*_B5jA$QfEb!R#si14QO1L4pMVI*9JUe-j zdEVY*WTuRyY8dpck3i+wEf8Tb_;fDad}o6QnhhE$rlXXAI-EF<+emq-@wmP=EGI7v zLH4211H>4a9p>8mWAL&L`@^eM=e>l5S`o_5pYWR!(maEI+1W$W4yNGuJJ6$siW9BVa1=!I3x$?c11 zNf_=i?7plg}eI#xn3yryF@$seV ze)c<^b5ZiJG{kVo5BFvG`GmM(WWV@=V$uhJkLNSGJ4juwE${_wtt}5$^Syej=rwY6 zOAJePsIXfV3~yrxDCySBzlDgf(Ij7eE_|45?&N^Yj1E6~4cb6*D|)PD(}LJAbsSno zP`la(K~025!3MmW7E1RD@|WGE9J^|ta~(|LI@=_aN7n5Fi38GcI&xGLgdh@npWf8! z7p7HcH-B>PSNj*bNwpN?{~VGb6W5>fIQ-l4a~Cm`9Y49L;62)cx2a>k**OhME) zCH2u#Zq4Gw?noE!4;fN#9}LvTw>!f}<4NY{UMPucjt%9Ks2p}N z_3|5-*ii6|#OxLjqgetcH5R>5_M9!kO!6BWzpF13gD zOW0TmVJu3HPcYIVgG=U@(LVmFr%}REAqdD_r&*}P)|-2eG0aLU#66=3cIT8JG~m~g zK=ekPGo(ZV-5ZjTw?iHJ)j-}{4ay+e4@Uou#Li6WqYRjO8J{k zUFQQnPD@i-*IH<{=6EPKe38(7t9w2ycCX?fTN|#7y8ZaFXv{&$n}oU_S`56_RXamN zUn!n$y5*==AB%x_d%R2y-6C!oQ*qjFP&Ej_J^M-wQs)Axg^)+FwIP=1fkGIUb=qe3 zb7=3MzPm`is-_kesAbwpMsM)X;A56gA4@=jP}?pn6?V_NV;}eK*-~cie(rU$u@54arUs26-JicE?rSEG^ZSN{QXMJN^4(6NJpY3Xxms8#P4P^afJ}_V~l6pUq?_) zLX_T{HY?KqeRL>W@v#-ieQx9-2%p;fbp98rA#-_z?nD)zMVCCn%EXiMJsE!F6PluU zG~dO>_x^c*?{TH5drpZTSzTdvLGm@`|xa4z540n+Fp|#?4~0dOwuQzlb;fK>MO*+D*}zf zo=9}%6B=3)X270#0r|R+51e6A^EFa6w&bvy_+UrW`#2!m|pLW z@^n`EhL+xlX^xKCnMHZh?50?9fZPk66b1g-#bew(cw}a#y}n& zc7O?H70#y78uMIiyW}cxK_?9%OwJ#vOyf`{5ol;iY_!k;a~NCC-|o&x@xO=Z+g$%; zn0}X;ht&X^lfeN{08nXqTQn$tPZHARkt477lc}st;WA5k5BKgU2B4xUIH?W-0SA1{ zZ%{l9`Y4f{KBc8UqGPez|KZkCA~Wbs`^f0)=XZ2LAHQF?Bx6`s`WtR9@*{600R}}k zR@(NJCA&f(1g3^n#Pp9gNv~u-j`GWll3xUJ4)t)&A_$3#X`X95ol+GE5n~HdWVq~@ z`c$EW67Wy;WybZFxy!9E>s`kV9cSX7Qe_=qrJ38kcbXy- z@>I6Xxox00vG~9%ow`RE%bAf7EjodV)1olM>NS-w{_3m=o8i5qvzz+6#qq~Ofd%Gv z``?Qz-7eo1o((tE$K$fH;*A29E7#U`Z|Jpxi>@UEG)X^KCOMH2nGK(>t_aUsH2pfS zHmy|R^H(63(lgA9A4FG3W1qyKO3M{J+REMNnaxj-zXbBIdK2bV32Gm1?rEhx5J?)n zugJIkrA=*S&r;G6=f_P zKIZ7m2@F)U9P20-j%rqGPTa{Rw(#5@E25vgI<&o3E8Gi_?_;mLmp@KE!kjKAfCH0Lv{!?bv7#G!P(F{TI zL8V^HGL0M2H{{ltC4&0o2Gf_wB8|y!@%(EkT+Ceu3ve4pk^-h;BA|iqdLFrj;TE@& zZbI=tR30uw27Bk;>uNb~>iNEH9UapQ(murafOjJwYMIw@GK275uJ;?luQ5?1OSeZ{ zYw=_`aoaD?l@v}}X5zWG`gVP|T0!swKSmE=Y#(S5QQ}#d0pU% zxbeR!7z4ImI0^PKaY^;R#|}T18JAK=148Iq!g4V2=koR2H6?TQyjEg@Wv7H(@H*|w zIVw-*l^P*kN z<~9%GxGft-I#9b;Ios>rv{x#0Z_tg6jog)rhC(*|&;FK*pq0AWr?S?g)-8q$8fu;Q zl361#D7X$d8*)ub+6~fneIcwiXSp1QzUHhi&wmllSu-Ll*P3zLzrQ(0rV`rO7{l5j zS+_$l_rCoZlwElv$oJ*kDr?KmhlbE4$e%Jy>Iyl&*&O44Q8_9amd1rrEhF)yFa96i z-a0I*y6c!HM;|yp+7+!2_+{j| z-l~4vL*%Jj`}XwiCpPOH=2byI#_Od8PbVj&g;(6GWwZs+sN}zJKtwtS_W@dq z{3Kh!X2bp@cgdr;-e3t72kgy)^mvQz)Mai&P4v55lfB_7A!+p%(?d<-aR|@JIW35@ zpBc-+p+QTszw==kk_oNSgk%7m`P+?^lh{Ou(p03(`uvmmHJ=2{CpIL3N4=;=J$xnQ zFpsN{Q+iMCSFuR%E9(+QnAD{%5?;EZgyrW#jjUe2#-SztG%XqulvrA?crQEISER{+ zphKp|d7W{S@@$XoJm(jVvTjIAxN{#JqB}E9&w_hog%(%+%+@2*j+5eA&E7wpt*8lA zE-9l?CZ)qvY&l|!XS(17+Nlvkz7%MAdHjwV_rsx4V?2S(qFs;5k#??D1wJT3>K+y~ z;+E_RT8h`M6XwK+Ct)msfEfGJ=TdBf3#yyc^K61s=5$_46M+~$ZF}CStk zyb^6T=VD^iyd={s&Zj3etGTMxs3QkxHSN4NRVO*Rw`-P_pz8_~8TFQ}j?WotMqfZo zpYq2@AJrJr-uVuqSYpye9N_WpaF_*ohoTvr>MW)q>sdwQFq~5{eR0++m=>;%y^R&r z`+=#SIKEl@-Pk43qJxT`GC^L=2`>{#p{qp72gnC|>Cvqv& zPTgg$AfEE#W@Et1z^Nfh*0F7~iKFqySo44J`*e;~Ktgcoof&(7&;xXw!d%nWW6B)b z%RH(be0kuG_<8yg4&Ib@J&suQNDBu4D5FX#ixb6=#c`eS%6sbHa#o+knMg0NZZ`bvsK=83+6t$wGdErx_bT=u)GpFdI*s&4jDOX zz^K7te+n!X=axxCd=7q3bno}_EWfw=1k+L9@~CB@k=*VX)^#R_lTa1KA{7US$tq=r4_euQ$+FK8pb~0%?0L$!b)~ZIU9YrMioP7#FDD z!Jl4x{FlJdv!nkH$I?{4ZvdQIV?0M~AF`EEB=1eE-;^`&YI2FFM5E1jheft?IA-!vB0JK5aAF0n5Hn zSjJSp>t7Y9(mu|B1pK&)r7ZmJXL^@fEItJe^|!kB0bLpvgw8AK1=YU0<^{CR?am$S zO-?otboKZ=PTDyQtZ7mL#-dj`Vu|LqkKONJLIU(qfjXeA?Q$7_c7FTHyh|Kaq%QIb z+%Wq=IK4Zv!kck%LvadD3Q*orkuX%Bn)%}cCebP^@xB*in#)Y;1cl`NWzVvUoK=}m z-EAa()1RY4WB?{}u=W*W%NwMYf38arBL7vFlArU&vvntQG05C8DH7%vyB%c5XU8Wa zVHY}kSYe@&(s-cX$1LHpfMyby$IM|OHJxMZs|p5cqZB0)fp~;%1B%CXQ#fx@$fPj# zWOQWEKO}1iFW#bSNLkmNJf=^Ual$M`8))vK_ruA#Ak%x=)eu@puU=Z!C>CZ12!_PE zx9z5T)^9JF@H|okFZrST0yq>n(K;D!4jjU?w!ADuUA%90oiWgkgL|P_CAUg(;oKjT z@EFbMCM9>9De>!$Qnh?FJTG1>1J$LlQA4rbHkWWeS?o{VDDsRvfEs6`W#-z<>^vVsCJr+aU4WqYyf{Kj>>0V`v3FMDyuH z1C$2YM9f^2l^I$# z4rsJ~3EySGs2VGQV=P1ZsYk^pKkkF)w&eBKhvr(^N;aB9g$*0~0bGl@VXZ=oRbQY8 z_wrC~#oS5wQ#*_{Z@eu@Xuo7nUq<)9OfM#R<%q1^dZIALK2RLf@s^idgAM+Iuch5i@#XJ!_<%%Uf8w{+O%EV1%aLDH( zLu*ZOh`nJ&?LTpF8dp2^yT%5Tol?`{m4#vq@omG}XdG6m6^soU9Osqn``GZ9mRhV^ z6*l5`tV!oSU@A50jEj zNBgcgNJWXlTxA~#87bR2Hb6|*BQ8LlHqw-MSsL4XeMmDg9{|F~NCq6|*5m~hHPmj@ zu02j!oqk;Z*%grezGqim9x2ZlUPS*)9N&;1hGM>WhAe}{Ih-`=;@#`6W6V05EXURt zIg1lykd0C&(qO*X{?>g{+elRecN|ZB1;Blkht&>bnlF=^S@pR;ZbvwV)$((#{%+H5 zwFm>`Gc8$_to_f8$9Udf?>UYM zQw?MQ{xhC2e9?X9B@91#9nDu9bmH92;#*F-O^A^Lq^! z?7+LHM0m7bOm$-+iwp6ICc%%^q=_W{4K0V4hPu(wi=$66$fqY=M2ooOo&eaDvAN-g zQ&#KSj-#+&qNKjnRg|I7vbNMI4C-1z{bF9f%FiRr1fW|_ zlE}pu#@%}jsk=|T1ge*`JV>}cSq#yC5ktsnsKtxf&Z$M#Vg@7XCmep8q%@qDEKlHb``DX2RelZNW}5-4RT`udr?D zfwR*8GM8hZ@OAn71ib+1JnfHD401R0z54#fA9Wo7E>QWT1XBah+|Eha7|W!1a<<X0n1Jb298GER;54JQ-axzT>>T4DQ~Yv9Hg5Bv%)%u%U26MLdBhxzl%xB zzU-n>xT&IH>7t&}XSvU#BQf)^ERU)G=;f%smjO6&(C1paOuxq7Pu+}{OXtF@npFUF zI+Pi~=1_%0giPUIkpGH|CdJ zt;fe9xI^}67%C<2?ZpO+qC)~-8&qyO=|$qsaEYgwUxERH$;V(unCj|79Q(1y-t~Rx z8QnUo)_3Sa%cSsAMh?0N!TtPOx3!6@0{aMV*YNL^`S&O9{+VtM%a%Hs|6zZ3PW*=p zre|NPT=7^#I9ubn2sg>&{upARyRqDmFkc2c))`5ryvPR4zS8tmfm?JkJ?M4cIN6vL zk1=9!FSxG~kcEWadKmcQ7nGE^U)t(O_+a#RY?akWmFj`%X<5c$u_qj)w?d`W#II_9 zJte7;1Rj~ZY|8yOI2PIx2ye^L*G{**9N88n_|Jvr;S!`k<8fQ%3}$qlsv#4EXMd#X zdgufqwzI+a;Ush}pEUaX3aD}>nM|(>DafH;uGNEBs7`cP34?nMOY zJ)YHU6)BZQRQmxn*ENJej{*5gFycAM$`?ib;2*=Mvh3G~`o|g5M11&YKyiTBB5CrG z!bM^_y!CcP{?+h|Z`QI@?^DUIC6dC-y`hl4bG{7kCJl4ZbkX98MsLd{144-7auLTy7ztW*(iaD1wAc~3vHQ! zVpRBXhBv@j081mI=P~DafdkZ^OOrtBu6Ghc*CpJcAZ7Gi+h?azfDvO#wG1}R#FgAs73{^8X zA43NQoNLEFpC_T8&8VW}*qHD6w!x_Usbijhlo{k6|Le*EB9d=$gU|i>r~U&hrX*yj z<IGtvspndyIpioXgsAf3-5HJg9B*Y_{_-TC)keW!UqyW zEgyOzOdY_qEL{CqL)IcR9ea)~zAb${Qxd0UUw%wCenBf9vi*oy9RDt#1CW0R#nYGay;UoKa|fxjvaiH)}bP>IF1119Pl*Xx?llK@`41l^V&OFS0)Y{kPp z=-1$VHl5}-knisl1OG+Qp`hDv`#FWeWDTWF%>pQ(@N?2GO7y5a>DAe!-R8c6WQ=MS zi?r`O&XcA*r2;T0pTvBtDW5|4)h)?Z`hB zE8hp*r~ZXv<*|Zr28Gg^QoiM|@N^gLp2$HCXsH;qgAVat!H`uXt^h}hRp^XfY!c#B z;=J)ToFZl~jf*YSGsKvD6F^eHz@XBrOgs&!BP^deR_8=FBA2Un{ODvz&P%kvlKQub zm9lZMK~;Gw-dit_9u`eXPT+Jh$gRLcyu@|4&UEaQgBw#~KoS0+$uBSyzgfIlk+d;+ zN6^oaYPKvUg7oeR{SK}I^0N!k`KVKO9Zi-vepIM?{Rii%Z)l^E9)Jsp$64qH*UB?6 zeVJR?lOY~_v}0p`i6@r&k66?;$ABaPQqH?#a4Jy}g6olTQ8#D2jMc8Er$N&!dJf_=ol}Pj2gDKk-DjIcY;W9tLUAFEW-pmW6C5%QB7sncwG{6HN%K;&gaDnJ^%( zV|SE>UCFWltrW>x-Uj=;IN3Vfh@dNYlg$_K5A_N7VwAozkC*0J4dbe7JhwC_LSfkg zfR+dw(0XBF&{K{)W}p~9$zYncW+GPZ!SfLA8YT~us#VJz3feWUPuk}szIsD+HjV}9 za^o5ad7)0l&bVr{t#OZ$=G+C8+Z$x!xVkEkd1EWAqd%50BR0ESRG72#+&769p^qxa zMsRj1OYRlPfbJ)dEn}b}qUi$u@#NI>BY>avmy+eg{U4MpZkX0dlDTgqCEk^Ix*piq zx%9qLR*Q3d*5ZwAi(Mbv?96_=Qmvx6Zfrq8d}U(tF7uE_HSa}%jQy}FpTRR-TL*O? z`Mliyci+nOBl+c1B|XNhgdZ1~t`xKr$QRwtUUkaLuN9cwnc5X@5bH-?ea~GUvO)G~ zH!BW64e?G1c&39np;nrkg!W+WLtn=hJ$s8b-@nkajPyGvxIvIgh~1xSg+APbe{Q}{ z#w0O4erwQ~%%4eVg2Ov0)cMV)|LJH|I59DK;r`^kT$RmE%z$U?v)Q4$clU-LIYVwK z>IJ7_*Glw~kAWhs1QLTI!3H%}DDAm(+uUIz)mOx}gJPvTqZk8;NoW&su1~-O0Kc^B zqJ8}Cm4S}6S}&plrjmQpT{oyS1$j7@Lv4>f&%y;+^+>x;d;9|^V)J{|))C!_dh2e? zaO{^peo1ChPMXGh1Air6trlHqvE04Y;A-Ju_lf<; zoE6nghbAY8V~Y1q@VbV#fGkVrW6}CaaE4&r2>iW~pInw$z#IEJUkPnR-_@1=b=U6U zdK6sU6=HAfCM7^)fxWPv7j%m#p@l!Y%^Xf+z?=Otu80qNtFn$tFCIT<{;*FNc?~3; zWQd!x`PAC{e;{KKaiW?AFf1ENu|*I3o;;?RdQ|m3H6}=e6##*l5f&=ZNcYm|?tRk* zPA0G2iPn89&131bVbzGo`F~U+@>DToRa#~u>i?k{aXO|imCHbUCH7;}&WWk|PFpdE zd(K2AfWnQhNDfW{R2blXsXCI#eVP(h%zb90yUoC0xZyGMe1y}6=a)pBN!lCSG>l|A z?wN*G3xzj*gxHn;TESw7CK9L`{N|zNp38NOWY}BIPFpSOjN%dPH6*bXR7HwMBA{z! z+OiyyUQw#euLVRA-G-kWORF%_IV-==wgX|s@s8^Jdfuq?kIcH^u-&g3Ea)-Wucz=# zuoCR2LtexvjIB|V)<*{vN)(WsyvqJ2DiaGHuBPUA4GD4Wnwo!CsF=VS^@ z&~e9x>O+ZTN`(KpL9%p_exKh*=OU0Y=^H~b1S(n%{CFJ0aehU>09ySaX3IE+kn3%Km zbVxws;C0k*G1A zTEbK6&2B2!i55T!OzX>%V#!p{pp%w3FIU-vt=qRf4>x%itF@}=A z+c5WmM(zeMOlCAmJgt|?E-5Mc_LXC@=?yNqqcdWkS)qij2ZCLWpBWDlv=ifP_tHLvrXV9%8q2J3a`%((18(<7!|F1z zA~S`*(~91*k{#Abf(H^W)I# zTpID4K?0ccP|IR(_zlAb8&brxAv%SEEJx{n+(Z6Prt(SX;BWUP{ILxqbSV1Ct=icq^(ecls zD4GP5z49GUSy_xDgvJ`@B;ZcQ^9k(Gy2}ac^rA$Exsd+<`e0UVm&@<#aqWR(Sj zNkgEZd?rWe5Azy z@%p#XNG_LwHC2F+h<9ZOWI%&ee#1ax%3r9CF%?zBw@T1va30Wh0Qo*a2a zg`BAEm==Ir1n0&9aEmx@_4{B}0Rfr5bv2a^7U*m8)C_5kBGuyB5bU6bHenFMdaTy$GDf+p7~_uJ+%7`!=59xdQv+15^y5U^Qw+7W)An+l9PMnQvGVYM2-}g{Zm@& z^uIm1BjYk~&w$qlf`UPmKa1(%j!HFuuNs3@U+zU!0C$jt;4HN5;`$`Ish$0 zw4ArASy9(Ft@z>C8vT0cowH%Y-htf&fw9N?elYNo^>%(<$>$yPGMw5YD0}7UL{&Is z2!m>iTyQOcqV)yTF3r>ZZ)adtV_|_jkN66+pbdEWtTnzE(ub|6ay~=V)Nx`qu)@qa zya>-M7Y2vcS>INMGxAc1tdD0#_BoumHz}VT!x6$?t?@R>nl~(-{p# zz)yJgRRPO!u+171PJo0`rXfJ-RoC!^S75y*b;u(yQA*J5ns*cb+Me_KE6#9=?)iIW zZoXP!X=+{=$OP&m5byB`16$Rft%nKv-3y;JaYXAxn4yz39z{+f!kmXd#U69M_lhj; zp2PM^G?nkM2}2(}!VUuig6wDPkEna#Q|T~!Y}mi^M(0o7S(dWLXJLsc$gfrP1lO-H zB=2-3mo0XeJLlh^&>1KwbPA3{Ck~hTJ-hHX1o`+XV0Ac6t7@+PD4Bv>NFzD)O4iww zaZY%Fez3K#z7Uy20=Q>jC7DzWnlaNyj&JUBMX&-4oNvrn3C76C{~bu72J(RlF>HQ! zi1jKWIkiz2qGIgDQ_HRKE8E=Sph|&gH4_n$B}I@(xtmY&Bu)Tm=0|o}^?obl{C;6S z=)`&*znw*|{u6+W)DK%6nVW-^3>5TmieiNGBq%-y!{#n!^q&ad|MQT-w28hdmN9cz zaG%>zE)<~)G_qn32}3yuwjXz~D)vsb`uV--%Yo!2uKWtVs%e>ixiJsYay0l{Z7W|W zAuLM}u)p;?Hbsy)G2wdu&bFj(N}jb-fv1PxtYxF&k9Xkn&+H)p=}}G}jU*SvQMubj zUNhw}4{LNzJ^rh#`j`1_&B^u1HmRUMxtFtUw1sHb^4WC~lw9ycH{4F^eQrk&5k~aUdJxcLDF{fzKp!LuJ>!MX=P9=(2p>LFXrSH?NrWmWZuLA zL^GaiR;^B3NH1DI%*jF=3D8qeXR8UttCR!y*O%j_1w>GBvb1wtwVrysL0F+hQzCtw zIdhw64}6IfgJtEyl-RJS)diwYf^rk7ea!O?*!Xhg&r?Ji0Q_?+*JE!1IJVE%jK0A< z@$C^h^M!A2KKjLVRWpM#@a0S0gze1sPO0nYbHNM9}N ziS?uYgQV>>x!Cl3*j$GLQ@6*v8&?ot5GI@J&(os6FnA7ZwQTZ znZxkfjYRXI_^}R>L8ghQZhCqC<-w zcm#`o^CY^cs|k~5-Z+NbGB-45r#L%GLmO{>(0O!+kiX9<5SaeCFi{kM6B&g zyi+TFp|vB63!eiq?1S1^jp9BVDX%7lDx(X<#w;g z$?3x?B+~4^k-tbzU`hbXAji1toY(9^OueBF1i!Tun^Q$4{9)PE(uI!ij2ej)V0d2) zT&D8Lth(v}140dN>=|Z>CvqXfXGMto44ey^0JOW8#3`Qcxlz9zZnpG&oh(2#BPo}W z`4CvskSVX4v@|zHh^f+v;6+JnE)ILVw>|$vNs-B8GMvW}>wpHzBWlLKaR(?&|7u4q zy8EXc^-qZK*xYaaJ)D(H>7e{?Nf?|K=)Z2QZ!G0GVok@JPPaSNOYz-Oa(y#lG;=n; zNdCI)bf7xPBY!RR8NL6)@|ABLvI%sf?QNlo01u|%CnJhec?T~zW{2PQo0>A8C|P_GJ3Te;39k{dTRKk7ua*t<8x-(xvXJ-jyoEB8!>G zOHnC!Qz$l&4uFWLWQSM!b_#?&x3!l&y`RNua>sJNR<;pxtnvnRovP{XAl@b#EP`8Ldy3P~K31jh>adqg0MK4R4$|PAw6F!uaY7O~SqJVd=ge zzgR!9CxE92q2B`C=JHH)qe6YeMu8DPB5to&saeQTT0@fSxBEl0Tc1G&a_0IkekoXl z;Znb2Dj_)u4Yid(W*s_@9n~;$5>!|!1nq4U@I_BAD7j#5Q+)5Ya~Nt@0x1OJP6aP7 zvXeRjl|8r*A}IGyAerL8B6RGpADcb7c^X6EjHBLLRO0hh;9CY+T%JU?i_DFEa+l@m z?AHfn_(;=6rw}vugczks`{nwnN+QYHikqzSSC!RMKG$M_p<6g|mWfYUhCC>w7Pj_D z3x#+O*t>v)rS5D$hj>L2KSMFj;h>k_qGh!A3%mbD7=3DNu}H)bM) zscJr!Xt|;(8>CVz*LO?2ub61{0riOvKgGn9ECVNQ2&XjzIKtt{EKABFta_8aA&6+Ja6D& zg@3ejW~Fzs$#9}&**>=B-4vm(8cjX&O+Eu zd&I0!0keS6n&hj2Jh&lzUisj zR7+}nEWk|aQ7z|jQH)dtvwjk0Xm*fC+Y~t2M`RkWPdj=CMx(Gbk75aD4VLtvW!Rym zNoP0`u?fWNTj$u3VrGAT%@_WG1^UvtBLx}KJLdIA`b?By5eNiXb+IbTx0xVSdU1{L!E;&9(|AvQ75#92Ik z8zqIpH5^_J&b!7N+K28|r8B`j%p@{VXbHBO-kLtrNkX-~L^S9gi$jH%V)R$7Pdu}I za1C7t-cmDBKUpjHO&n=CKc0L${|@(LL{^JO)sP?K$5n++W*)J#GB!^_?|I;_^-T|= z4js&@n1fN)B_h_Db`z}+FI9#F4G6M~Q}1_gj6-jsnO5n$9)O4_`2-1K2uT@WIeM5M zuBwUrh#MX&8r;ZI(q8!Ojfxe~Oiwz239<8MWaCK;mx7&U`yvs+4r)+dlUiX>)ymx% z5rHWc;*3voWZc?9(l@i0$amq-F2waX3<|RG;yQpTw>yHfu**GZ^!2Vq&Om97^ zEg*y|jPCs$+akL!KZ9YIa@LIyH3;};V_@-iMoLo3Ye?fJJ_D_7+(%#$`i0(RM$J9& z5J50I;t97|EeUd0`AwwbwMt2!z6lai9(2tiu7j;P6XPxyw682syRL<1lvLA-T}`-1 z7$eP`a?L)oML%7#h_Q_66=sZyd-_~8f5HKb!?|UlEThIOkfG{9scE*T&p$7n8fU-A zjZCbb{*AlSQ$;;Wj}SCM>29#gX&9*HUGltSD7>+41bBRK9KK8UhnI@H)V+!uVIiX* z%$Hnrshhe89crj!=21RutRFmWX|QZy$)R%-<)K@?0!|fL!!DtTH@6qd8%P9{^h3;V z5)PbxHNNUM&@N1=U8F&x3|L%g8>pMJ=x*@340^R|L;tM^xYJJ|Qk<L`hfrE!eUp z&G8BLi8LPN>pBY(I-J)$b=noN#|cV%?|4`#HqoG&hnXjTEnb*4Ryvp)SdOteuSKz6 zn$Q1a16M4#^&zKVXmVFu@3@-#QIjvzVQC-$f7V*euBLfPk}06_rd%UY!xfU8SdnZ- zLLTvQ=T=Qsa1w<60vnUHw`R!yARTzZ%HmAh{&dxeI;`;mU!e>ZElZBZ(RV3PA}&1G z^cHi&-n!4mcq0>=Us?*!==51Nn9sq3sOwZcGDyQ@7KJs?@{}1n`cA60HLivtX;Rcg z#ecI9T_x&-kf^!hf;qBaXibP8N+NQ|bbPJC-s0pGC^<9-^T2;F| zcQE=WsiZ>C)NkmgKb&PH9{GvfE!zxwmeU%3cW?9{X|Rf#Fs{lT!l1bV*$*Ct-^ANh z6*Pp`JO=Bz4_1`4y{b8|8*Rm0fgT82ljY7w;c@CXQ^w$|x#G8{m&D)7)eM3!`NM{? zCz+GI7k#2X<0D(L9zq|qCj-@m(HFpe$}QY+a9r?sQ`pLlX@P;mmd3Ot?t_IpGundW z&w93rDhG%6=g!?74g@hu#$)wnPj?;me=F?JW%hfjc;*ruN98%DEYxDJd zF~?jlTc-Af`5HwS?drx6?nc4D0>8_bl))BWdP`)F<_+q9Kg z^V&*qyRFe##535~gZsHUa!r7LzwLXx`8%jj3>d2G13~Wqch2(XD0~4!w0*EI6)gVo z=^m3>QMUKvo`ei*4qXpKF>fJ>YPV_XS}KlO3`e!hT)0n_oH%-VK>266mvkGX(n$TG(FoPnzBrZi;4!^*PJ#z)Mh>n%?9^wqzRH(#&H$?ZHfR7onFj+bwi6 zUa6MMxglkZ#`js+Z_-yaWQ%@#%P)e#(Vqu9XJ}!XyCza0KM?80W@I<%Dbgp}n?lR5 z6>)BF+F609MxW;EiZDw7*V#}r6qF3qC;0NcMQ{J;&9hCNW0GcbrLVe4=3Z+g9tqFY zW;K3H6p2FLN-5rauo`BIKdYL3wl44nPPuWL^#-L)qSwh6z!{EJ{TwPxAQ2wPR?=Bx zO~bpV-~9BY3vG9x8)KOjk<&Z_@7H311XE=k&*Gx~m@cal*jq5`KPQ?FUe}w7^Uu^a zo$fe+jMr?(L<0Lt`9+N0u92#ZE?C=V;QTOAUn+p)dZ3^au}GFD!%oa6sI|VRYTQrM zUM!#tacxu%WNdAy0gGu`PaCo31Jq{OFHUmI!6Fhf`I=*um+#PJ?oBnv!YkAyKMC9E zKIlwG2h{VJtzT&!q<%B*Y5 z8VlWxU^xJ^jL4qfl0ch)V6_@|UPb8NdIoUB0)bX+y$`gj1TO!PO)+}S)qE5`2;23o z<^vAB>D{*E?l#~@$pbyiMVp`WQA;>K;C=tLQBcMYGFCtx=FPI})x^Uqk9r<>!2wjG z_c)N|mKrDxrV9*rBU_j*n#1);!>ATc!lv+f-&d!-py+`CXS|$DXL7wj z+~Vr%yGDj4!a(YaCDv&1Zdww|1GSeMq~iQl{a-=9F11Yeu-y#?PuKO`ZC>*R>Mce2 z(NF+UIh2b{8=K!b7aT24uOsnI6UAuQr1MJN z_^@4kgu64 zb~P5Z&9jD^F-!)pcL^2g^lm()j~VDB@1@b;8GV0TM0#FCRsc$Hb*Y@BLOo@;8^`5} zyHGg1D%qb^HYakRa)#(X=|3&)meUNTW|@NQ&TQvCh5wbZq*z3xe;HcS)4eKnTu}CS zZLc29GP&sTU0v7od+Y74n%$CGizF_YVSj~@>ImH!rFk0Ma*{M-tWlv||AE6yr8K4R zqKKH0?L4MxHJN$BD`aKPZ*CW?vxHa074z#~6sOOMqIQVjU+>t>f3JuUtr>P%3T`n_ zVLiS5eB7(*whXzotF832F-tnz>L=Gh9lPT?!x*xMgOh7$?3u)(d2e5*fkA`fuT;&> zpo6XOBI$d_GvGXHV4hU!d|%PaQ%2J88N|}vu1uD0AfLro8Y^DqlADQgO?7Rcgkgf0 z^c}>biPW=O2x88;|FnucWEQPrcv$swoMtMC+EYPZhn*aMmCGsWY90BWoRyfrq<@jV zcevMNy0?JIF=f*54JDZXJI=WS_Fvf8bU|1~l?(NdOa zcBQH5z}5hiqVfk|hmgB#o685F};%$vjs;cyK6K& z8sB-$C@S$3K27iOO)i`Wj4+)D?~to9;p@BZ>85*}furgJc{yG|uH@q%U-9B|n;*~u z7Un#@_tVQ8jFw=ml;a75qXZ`wIU%o!`qyn{lPHhhg^1&1xq?RRdc2lbrIL=m(e2Wm zR_F%v-YuS)QTIEsOKPxAFqQKTOeioE7KF3zal{c93!k*G zgiZ})>SM+4XIY=m<73W58QHFGN%bc1Oi6(PO6L>zJ~aa8F^?u8NdpFKNV_k*V;4N! zj5YzDKNquYvHPb5RjsXFSJ9hOpM$f&pek!wAmp5}MQOTUIV$mBGh&DB7BlVwTWT9E z(47L#6_*J1SuTp7TEUAH)Ym6U@LpcPzJ(IbAXIE7n4&p;bH`_c@#e~Rs`0@Ksu+-6 za2;sZHY3m#^~l%x8doTdzOJs!rXTTSUunHQ&CS^|2lXJ3MbCZr!MCzk6}=ANrACud zQ&Ggl4f5rJGWZK!28`6GiR$*TI6=nXyMct3C8YtWToQ#HW!5*(A?Istsj6w5a|qRw zW0Hq+%w#)0$CgnW8CxglyMle?b*txLM}qTYH)e8_X)#zHEAj4{Y}0uZncT+4)VG=n zJ*cP6-=~edHT|AYiz`*?72+VMA=;jEn zzUKYD5PdpJ7+sBdqgS`5GjBLv9;+-2=2TXdI_TR8d11Q2d=^O+iRyf#iiCuxWtL=@ zu}gEuoB4C^@SZ#+a_|YW4oA^XUT3wXYm=bGXYvbiH$^|smIp$|fmxKZ8&ScDSQ8#o z<8s;WFH6V6j1nTX&cL`ElCv$lS524@x`fuB3m)a(kKzv%c3@2Ug1T<{<<=RiD6B?R zkrmDc@&4#n06XT*)R?2{%QeAd3t)iW>gE`bW<}l4?uyS2DC#29eO*2YZ-nejX6h7{ zw#KY&c9tB;3I(>bP>~y&c zNjF9n(EQ|U2J)#0clDPTW;t>Xj@45dDX#ECc9*W`%b8hhl=>R29Etah}F z9pk0fPWQ+Wo(?x!=n%8h8~ZtAV=!+i#~kr&l_;s(1BSVpm|98DjiX;bYmtu!rYTRt zaY8h2Gq0#*(+}R#ImxN1iq5_UynrFPXrksREX~g^(6mE?Fq~)(zr?@z)m9KWH#o7G zQiHSXF3~Lg#{2YrIOEYolWN8B_%cLR)_geufuPZ?-QydW<@Dz?RZ~lOiss64I9K;i zh-$>0Q3P-GgpS?LkhIUebLoAS9`9v1^-9|kEy$yc^I?-(_-O0$bTYnvjrg=;??UCC zqlRSBcwrrc_zZKaQ^jD+`|bf*x54rKsfFYX^I5OzF)DgW%hHulh;wz7&a9A|Dx`%| z5p;d(5^SI#wH1!ThIM&=kl@8vc0e&7ZZ=h%vBcQ6d1Oy*?3!%btiCKZq&~ogDuA=) z-$gA}u3mU|shl!@bdgVi^{XSklcp>tQ2}ylhRob~8JwwZ_!*bOzS8T)J)-gIJZGA= z$rLhgR={-|>7H)jgLIeX<;TCcak77dqJ8hW`-&Hh^`J$Ka8;JNn#e_CkL}i{@>=-% z3r*|7^}f7~$Aj(a{g2nTV42)oBV0}R>{Uy)}e0@nWxG?U&R$2cxl!&f|#1#wg+hke@En z=}jl^$9soQH#e&w>H9HzcT)jLw9e)SRo)79DV4_g@%6HLXTzs{6J~xYkoeP^wW6MJ zOxuHI?h^rIO_{*EOxH0*z8H zOTpeoE9yq&N?yk zv@ds`!PlSf*lg2gUl8+u%ivyeq|PCz&e=v~xjx%ux4jz2GzFmzR(eq6GCfWhcBkqN z46^M6gItY0?%WmptqUU_aUbiZgu26n$p>Sb{a#r=JDB-Ot{n8bllhq`ynj+Di{nGy ztgLc77Kj)F8&>YE>w7E89{jm&29CHfYUStG^RonxmpX24lB|~$mQO-zOkG840@Xt~ zbib)}1fpw{GK%h!HWXzY2&aDG$z#7I*O5xG#N;+J8J-?ccN8x(EofwyXAXXD!o_o7 zu}m0z@T%Twb+N|y-P;V4y^wLsTtD|rkH%47=FW*Xy+f3BumQjD{e17rzTTrUSPxdlR8FJ3k)Yj~soO?+(8+ULexEPv>dc zjZj-~e*Nb8^_Eso3@b)^T3Ys~OkDT~a<5_6X=C)V`VBPHW5$w{_p)L^&tJzjgr@J>r1a?(0u`iA8q zUqzbEc^JfDGM8&xcuKx^4@tVkzT2nLl5Rt+%I991VuDDJ7X0xvE$Jjdp@Oi+Ql(~? zjG#L6uAuf-1I={<=}sW^TAimt`U%3?J@nBWETH#au3b1@39Lb0jvWTc3D;j3_LHZ5 zLoVZrZn^y7!Vq$=b++*0e&LSl*7)5pN=zS@vLNWwm?cNxi6t#aYJyaBRo0L|P1E(x zXl)u4)#5p8RTCd_0?Fe!e^%obwh?Slar04MbA+1FFdNZqZAooK3%ZpxKPizRc*DI{ zO%mv-c@AljTz2PoGowA=4o$9UQlA&Qb1W06pPuVIhU=%Z(|r?+L3%FJJa|p#FOGf^ zn&T%?wKIVc-}H&<$KAjoVK^*6jG(&UgK! zx!e5|F}garr5(DjkS-nlH_zv<>OJ>nal@!M>-Z<6!!>&@y6%*m&-Tt}`u#-CVwCQL zZ^f2FyVoD6Z2N>fFDDdT-TSe~^lR%&GvNL#yW7$^Wtu)NwB=^`?xs5nXv;H5xe6q19JrIvOGSFH4CEk{fAMF@x%tS1VdTPK5FtpFOTuZykx?e8}j*yUs=thFf-}FIcNRsbQLJ z!`)4fpCCAut$z#ZcIXDAk-HQh$Cu;c9$f1rvGc3z9!(SLi zmE!NoGgdO)2TQ4WHo<2=)5rgC5$U)ddt&FKj|}9RwZI_)U$lQqIul5r4ju&2^udDq zYZ+3aX}d!A{e`Vj4oLWu@&l%M;v#5C((!00+YKz(tjME-R9^dajJ^V%^kLw9;Dng} zL2z*g9rRn|if)9eVftPP>ulRJx&43aon=^5ZQJj$Kt(`A>5vu}Kw2823z3Rl21cq+`gTd#}OU`+45yefGQevEw-Qhy6)pSgdQU#dTiib^d?wimh68 zlqzAj+LnLA$uoaHZm0~sQw+w)W?QI}qm`8!4N6azDX*o@VfGhRa!-Y4?MNR5_?k9q zL8WHWT3!NY`bG*A$iefSHkBjZZ_1Z#sOq#0g$mn%Qu}3SbkC>!i{dK9AXM)#ZBs&y z*Jm?GPQTdaV{I-k4wS0;giv)-v#!s#%wLI`@_4=K(8P0#o<71$P!kZ{M-%hP%QU@8=^W=jIRm$AJoWqT}MI~gAdKoHaqoz z!{2YHZv&FueQKcRvU!4z!3Z?;*w;5dZ@$eOJ{nOTDx5GZ+deF*5<3iXGsV|%&8Cdr z+;=+`V?{Fi$`^jD0&%^6Df~*1#ql?;#TR^wtas5^M|ORCNV%;ptaL1;W+TIO z6)p9!gSvqUGFVhmd_D>Tk>USr9JS6AzO+wag+u3DM#vFcwnZrf5^OLTr1Ys8BC_M{ zc2PhUd!tWST(IxBhdix*HOvR-6L6B>6IWpG&xHRYn7L)_5^MiM;?EWROWe0){!5^c z_WNt9ikKoR)~)Ei?KLZN=Ey5X`Ii4A!kTAPz;)YcAN9O#`qG`t%!-JZ)(c`cVj_QO zU;+7XyWZHeTMp*pw&Cqy%)E1^6KlzYe-L*EB8facRpqJ!?2sEq1*&eDl$_Pwng{Af z?7^N^DTy#sRk^dB?>vW1EUw-OqO>jQ;^~xQ$V|1Gmk#bJxwYPp z=#4Jg>C74*)&hjABX*Lya9!u6U`9(rHr@h8DwDtD2Z7;KZugG~wO(qOdqSn` zzW9(c5|0cP2wHLcctG?)8!5e0$#pijd%9b9jUM}4)wBa06P_iUP}Q_nXR)@#f)gjS zl%G3%SzVIBMcDp!3V91CI}eVp=GNnEAb4xl(8lR=M!=?XV75=k`euw`F`2H93+=sCtCa z?|;{5-v6gyjPrEaGhE+4aF3B`K<{vaUdvE*2L|rI8$;xEY#ISd_MIDqCz^KoS=Gv@ zhRExty?BFH46^smR$A8bv&%ecwX9F}#jNVMmipwc3E{`PoEbyasagvMrN! z9XWCFN~&MA%(aqjQZ-5vS7DTwW#&&Wj%tG*_aN9-wyYZ(+iL(FKa~(GfbHBYrqXvx z+2hnoR6M!XY(r<*MJgE84nm8K&-oDCV4`2qx^bE?aJoVm?hmX}@A>~ofz+q@e1j~6 zg1tI+!OI`Tv!Y(dk>CA=h9N=*V@eXaLGX{t|0o}d0S?9UUYd?)ReQ=`DHwSl74NxhL4J$gVtKMlwGs82ZyR60sj`qC zqPWEP>kgG2)i^;5ri2GxsLy<+vp*X`E`qBLA`G9viC2+!<4Xh#`$Zj{mlI9~Y%t`E z6FLRv+zpP?%~%FRtPytk#k7-X>H`~K@q6(q?LBZDs{+8`Yp0lxAUXUu^1o|q{H9^L zL;IbQbpnmky4{?pw-NX7?5gE-ei`;8nVQck8PG_(OLK60y?&aAlbc94CG2Fuk-YO9 zo@6pV1;dHEEmy8skyg)MYA0f5#6u&qCUWj_ric#bL@o+%-vf_R&yAJH);5CqDM1vN zpU=CSlmB)1grv&|ypk`8HL!mxnyc6lNDGt-I({(GN;a{eJ|{4!>w2itEGqj&n#(rZ zhJW>^wt*fwtvs4~Ts9dt8)DSXg3^6R_D;cOc~!WNak-DWrCQ^Rau$-8Ikl|DMLq#y z-IyBT?rdA&b;Jz?dWj#3?qzqIvu!d9Lb+X`y<`k+H7lh3rcs`#s%L9b3D*Ad{_}Tj zx*(*FnFXZ5`iI=u*2d2jJ!oFJCdEfoLqra9`pXO_d#YF`g}XOl5fRU{G8ZQ)fb+cn zWUWD=;cKe7!#rVJu9yl)Jdp(MS?flR(OC#`%iWTd~2nXq>4Nj_$ z>%iiO&ORb$T>Hbf>}vBp|Kb!tuNi5X)M^Swq;aZc-&})gk)uTM2cZ|IEw=G10KT(! zVQDQJiv(FNQ7zlzHDZ++-7N83DaK!&OQtR~t#R~$ofsLQTJCoKf0j-VaEGMZhew@U zpY#VmR}=3Pt3>j6zrMY2UD77og6TP%>bu=6N>h3a-3bT7lBHZFEq$-1 zWJVb`lguXGCKl>zZva0eUd_*$3C|L%l!gYJS)=zJSW&zLRqKz<488mzF3{ok^)hzn z_cOr%PZ)`#F$j;H9^6uXZU8B`JVGjiLWdq<$vq~l2w$ui{8hrxURS<)B{$3>2&==- zAL@Y^M?j(Y^tkaRnH>7J)5;>D^#&X-}aY>Nm-zk}m6xkx?N5n;3wOvwt$A*sDvqm*lY^o&zM7wyOJj z{ABEAK{I+bw@W|9!A4NAvyK6tLay+@wP->ZHG%uzuDn2B!qKbZf9Dvv+V-sXX zfr4NpSetn@5>%aN!eQd8kv(Bk$u}l57qc|PfIrl;!IsAxjgK^}jgtVe951y;TxWj} zLe!Z=`Q3p7K=A8o^00$(0R;a_dR42DlrWdyDu%BA?)vJy_Dn*kA}8%=j0c~@XXY|1 zP;gm7^+}{nWPbhY+w3-5@7y3}T0PxaIys|f8-b9vNm==W8j~#^Mm1I^sGW{N z+gNe=kKvc@C-LIV^YLyP&cYzj9Sbj-80H?b!mVvaWFQ-l!n7|QkMcEbZGS+;r(jg@ zb|@xU2o}`#mO?S(gQYq(iPml9!nCUcZr0;?Y8uO6nZ6VwCt?tC+Qv}Y!D~Hu*sS3f z+y=DhIw5(F58xnq(OwfEjU5FBQl!TE)lQGv6~Jh{{7z_$x2HQg0zt^r<`O<@gu>ef zn;#`?DcvGK?<3AB5&_rV&Avyrnn-AuyoGJvlYq=)Y0CX16C)?PH9mn@q__w-=w7m@ z4Sj>XC~N${wWuZ_z1bipYl`kMMu8z0^uNdBYG`G&fg)z}`meKVVbol1SI?60%pmFvVozMXu-Bt!qD~IY!$3wt)i5B6 z91hG9_YsFn!8r8{Tl$sVr+zPHEJi3mZ%cBkK0{ z)?3kc7@vGFf{|}U0z*LY7)U!7~@Z!3es+{@ay<`&CEUw6ava|>K>m54@D^P}z!wzBq?YEM1Sx!^UZD)%-*jTku!4Bo zTZKYg+K1kJ62A1glGQtow$=7g?lkMWi3BSGdAxJ7$P0W(_ZQ?Efb#G`H6DxYEoeP` zMU^~MYd^@nrfvG*Uc?FeYQV@_sTKqdysw%Fe&F8MaMa@tXyujf98(ZmHVkbpT5_Np zr9U3TJ~(0)pdA^P+j*~t8l6kU*8k9Ga01Lkf*h|Rf#2N8P6)zM3rU$(O|p49($Mx= z9>i8)vvn{~9lJ5Xb!*g+Id2a;QT>*K2vV7+bl->gbz$f@wppd*&8vC8J_3+ehLksF zT7o@X9bM(U@Ro@GBbi7qKSr zC2B`Uz3#|Kvkfj2TN-SY%~}`a$|K}Ud|C;4q&M%ETQC!Cm zg#S-DXJJ&^5AS>W&NRHwh?A5@B6L;Zg-GVqq#LCn==1vpM$oW&j5)W`fO1t@O}h;P zdmHs?Uug|KE;4EEiuevNjw!6L3zI6#UxAM_<$^>!eJe%8183s^alFs07W{h{(Y&i0 zdCP@n^>hzLI<0G_b5$1*{NRx|GPw^phy(>BVOlgz!KJBap)a*U_E!*K$w2O93+uJ6 zZvsF`P1(WAVU_1`>zm2II$lV~RY=rS=On#-1A0dSry#uV6R7}dW-sE*X_D;(z;h%p z2cjV6=#;hP8I4*f*#|*z`f(rmA8LLT&*(RYPt~{NBX^kD&m4ZFEHlI6zQACd(+1j} zd}CkUEsB;p9LcyZ2)NR+F#OmCWs-?`y4{mCn zKo+o2aO<#ZK8xl@zH!Lgy$yCBKZ#r?Z6O^CBZx?OC2z-1$FB|F} zh2xx!={FLZ5Sbd2zI)hX4!7w*x@Lme!Bor)&E{+q5bTucX$2F_&GaJ#|5+QbVbIaU zc=STJd!l1AgB{}lnj*MWHiul`yU1!#Z?~-*l3#wIc}Y`cvbB4TscKG*GDl9a|(&wXWN zIq+5JTwT@?1diF%+KGrtkQHy-O?$Ga+l;j5^f8p#0Ct6dshRRG>Ek4%J;?=IF7>?r z{V%>V;fdVDF^DR~Z^HdoC7#po{eRZKSan$Dqlh?z=$ZmXLKmE(-pDHfCwuSp<~n`B;_~A>2XGk5W@0nhMb19L zG696C#VlXa;Hl83rN9X=!2XAjV#bV(ricE)K}kzGt1YfdWBf1>4dG!6o{oGOQ7^@7 zfk=z?n!%^aOAeT8s{q$Cb%etB-m?quXLuJT0OCniRgA+ z3hxc#D&!k|Q@vWzbB+mx5#gDQ=wErh4|djCXhm+UtLxMo9ZiFsV#-z)6rBX;%}?PO zt~i+n!QOiK#@)H_vYy8^fE(>enC5Js%~ERTO;s0o2DOmO>odXYZaHLSs?7Dt2X`mg zd?Ld40N}li8+rq-8e4`y^QQPg6w-{*gZaV>^z#T$C7P=RUQo zn@caPCjS?y=M#q`2hCZNVNIemu9bacEl>gKYm#2x)-~FJ&3bp4Xbp}B+KMR8@(`5O zoAup}16$d3TN%(onS%I<)i`1^6?(2syNT4EaZL?!v4xrYAx|h&X2U&}{b6{2dD%mW zn*b%`zI}NA3YzdjR_~AMY7u^fz6P)rE#UVQ9}V~%p|CHk&{b>Vbx@pK2X^Os3N05) z7^P0=T_@W6Y$4iMUd~8o)kk);uCpI&LcVVDRvcv66!QVtZ!UXOlFya9c~NpF1P2p& z4RT7(#S_{+EzxzqF(aZp{XdPn2hNY&WL~8hQ>3fXB0_@>ZL*<+j@Aj1bsX?@UFFob zJMY}CVyPQ8G?jaC7!0(z#}Xoj04V4A5_1BupZ-`Auc<5xGksfJ0hA$H?4v#~R8iSE z5Z|QoC{txK9bkZaY*sVpadA2mo~xLb8GV#xCI$JC0yxL&3XO&KPDsa@CoZdrI_~S7VKt)FHLW{^4sa2&V+^ zL)}%t0GiiV;rRZ8Qzzi4Yw?c(Y7w0G|IHuxajU%M zYbhNR7^2W&nRqd}aW7sZw=k|>Vv^sW**tIWrctyaP(MfyXWpT4T@Ccy;q5KEr=_V> zpITR8S=>VU;%LjJy@9Rjg~3+2-qd;9_tUtHrlO}og)Qw)QTl&jDbX*t5!9E>KH?1V}K&r}*=z$WqRp?K}PvYr2&NIkIKi0~d zlxZ6-Mw&6ueQc(=o7lVDfixEnvWF95R6PFBhL&&=t?`6`wa++iFV~GXqql9(Gi}c> zK%_BVHc6`@;NDyx`Vh-mviLZKMRxxTrA^fj^iE3{>ge0RtnFgixw4#O5+X;&1N)rW zS!gtMbeR0gGE?txqn^%M+}u?r@3yp!zrBS;@Rq2=#;{4YMo4wtTbUN^v0oD|z=+51 zs4h*ic6}5#AA+NxNE=CszEGl+F+r7a`vrvnq5!I zf=eFl;dgLcOionG6gLcapr7u9ulaNVR;~3T7rC_063bqPw0btN2fL=~clHk~blp$J zmr-!m)eeU&j%1DxnmDm#n$^#yK3_e;VMr5fd(qSjRQ=y~u}PqpzP>GO{QNo8A!&$f zLF)+tp7*5X{X{wTrXg_Bqf9r8HM$W6?>zcw_^HuFEdG7#5Y1zs9Z&-`k%FVTwDDrVC;0whWCX+IJ~P^bT^oYSZri2K7H_dOXhr-daT4~( z4l{MlR9RCNB6-d73KyG7A*-(gksX=X0bP#nttQ*a_FQaMe2M~Z?^uD88-_5N$$ zMMicW^KS{zJG5_A67v?#9S5hpFgzU~sc26`Hbn~z)@^2qrCQ91)Vh#pmfJ&1`ds8y zv58f4Ung&pZIvh_b8~?Ugg2E6-F3=`ZyT`;ae=<@VcR%A3Mn$BQ;B#vB`IGnsv*EK z{(h)I&nUw&WbbSCk52@$o=;1xgGBjPN%tQo)2^;XP6{7QLrL808sL=;(RES3>Xh%A zv|*0?Y@8`rtTJt}Av>C`t#me2ZLtnnz*r=-*<$gAw0OLjaK*;9sSJb75~ z;Mas4nWd)m0U8Ben+&K9^5+IN73y(401b=@J6JF=!YY<)0)Q@g2Z}3Yb zsudp#;-D;YyV@TJmGAWq{%G_)43&o7I5{h7kcb)P!mld(z;8sUv)x&~$-sUY{-JPk zJ`vO~1<&_sAwM5M+S6|NTC(iStzHk<68 zYk}!4^W_E?ztA$z@6?Bkh&l3)N~aWI`sVy8e(6g}S+*oHii!2f3`slEvLrr9)GQLT zfDI}^AeBVt^6SH}zF%;a>sQ<-5uW$n3eA5qR0+Uz=NR3kIB7V!;^P_ng{@Fecpl$j zKj~fntp18f2a&;bDsy#m^@95_kwxw=(0e(g0OQ6{CBr1>-jY>HlgK%$g>yZYP9Z2g zMRDz0?97-?;Fg_l#qKxA>Et)iWq;@|_p{4jg*@ea`|V`%NsW7@nK&pJGGiKAGs25ygv79O@8)~uta|BR;^E-}AkzP}NQtaI6WPKRlMG?g{rx6(Mm5BEZ42%Zb_s8ymg6#=o_DO72d_N%Ih=YV3P~p% zZ$IX5pWBq?{v0Q(A(#XRK)m!+!w{jWBu(XV$&!ERkMp{I8_mtUragc&h4+gxAy6x45b9?0EP9*|L z9D(y`NYHa>S$P)v{$eJr40fQ`>qBWcvgIm1L~%{B;P{g#DQ~K_DwWAE-8|-kSf+Fl zL7~Ay62U==(XNWi$x%OP|LBP%pza_LqKfjVH4*hExoV)A7$8TmaYmNmXQ=flDqlK*furP_~Kc z5a}Q*+?`-S%*(6gU7?R?9{IHu5}Z1!tdnpRYrE1Qui?6Nt1)V$g~z#F8tdW4pY)qdgJ0JId8p&h*3NN7r4e=QtAA z$w58DTOW#dLNPrly1~6BY8$5k5=7T7Evdje8EUxFU+;Y+l>y8S-Hp#qc6^$g_2#r| zm=>N@alw3(gZC|B>^eo1`fC;|84(6+!b22x471O9CeEBXCMd4YQyd|I;PZyG>^-=K zBY)*SG|*gncKc_VH-nU%yjY<4=Gyd5F(YD(d>a^KLT!e;fx=eWnmTn;`;+KTkjnSm zNy7`_{-#Nklnmwmf$|;mcRvXEDN*cTgao%9rZJJL-T1(-GW#X!K*6A%^_u{GX)>Wc#lDKG(>76_kthKlkvp4+qz20 z_^Es|diVCO-(|eI0nhg_4}l0Rld+--l|XwBz${#AR=cO=KBn*ZigrHkdtV-`k21WDVT=+Xj{oSgt!7-XXvtOs})1!W+f1 z)H-Re+dWyJU)x-zSTEvQ>g3?U#UT$z4qphdnI{WNamU6fcZKA!j5x$Pl^uxsoxRjNl6XQE;uF zTwq*ijw0@|*cyBJj>>T48akhV5NR-b2*@_vRYtyrQ@7=h!f*|7N09P7{ep% z>AhfeooBXu#%wJ@(cKtxuJjb1*!hE*z}AO$j`TT&cR&9Lqx_PkYi(rKPF2TP)2`gm zix4rJd;MW`XKRv6d3;z*p$b=}D-LL!AdjUk`h&X$iWDfed8isG?-^gcYq|)+->lYVMoXbLZLLIlL-S)G-OIj_@X$kxHbF)} z1o^yS5w7h@5nnX<9fkVM`s40uTY;kqh)dyKds9V;TR$~e*NJvr|BS925n*bS^Wlo# z`&P+S?8zkC-L!E7y2WAHTkgHlRXXn14XP%6Bl30R`z8dJm$pSYg_&CJ*XMeOvao1Y z5>6MsXG$$lZLfhIAkEO3(rHLy-jT@0NREM(DA%!>+1a_sRT3RUax&ZnhB8N(L(W z((QA)|4VJ-Q$~*7qQ^|=P*sO_n)p1ft^CCZF?gf0;nL1C5Pj2C{p(pP+zMVbGf!b} z0g}-xXrOZ^oo#iN{QhawBHMR)YN{x;N@Sp2N#mF1$MW4!-6?I}W!d9^Z>7uCe;W)V zW;i=ge*_NP9`R%ubs;7&$uM_&@=?Hq^at^)epDjaAgp>9DJ}^nukZ2|<E7V+JlsZnqU%!100LXKJY{+VRpuDsYTzzT^jnLxVL4Ys z#&(_0?L=23XfMCAnI5qBCPZIez7IQ55+&RGp;HaQTy7GPwi(!lJa&%BX}V>C8hw@L z5#$MykO2O|9(Ead&ek;-iD3c}LcU?`34;mN-qfJC_Xe-1YfqfN`ubY(n#m*^YugA% zl&#(t2j{5p7G_Djy$8r|!J;>K6-y7ejny5lY2If1b&z+>PL;LSm-5^Kt!J0=cV3qX zYg;^$J#ON)N*~JKTA4lYa)Yn)v>bi`L$oZbiwkn5(sGqr%I+?|UAx2z^FC~j^aLw{ zf;j}OVpzD|6P+C%Ba(gNpvHb!9Ze=bosBgGbDL$Xx-k}T$3l&XG+$a=`1y7-JX~fgJH_8)=$^e4B7b%2AFH%>6=ONa=3Mu0|I{wuR&tY79&~WMuZ5Gvf`b61zgzo6t5X z%GpX6vSqt`@l}Tr{T0IwjMq7M8J#K5KH?e^JGcwa{eTC)&i9l$jNTTmIe7=+SlqT* z((ULtc1ON9t#vy=!v*C2dM!ELPIYGl zzs^3lUd3zo%-*ImY*zKSSg_{eQps_HW6=!iNk5QrM3g9S!UzfMebq(UHlwrU8>_S6 zZSXG%q7sg}R-%YyQcbhPa`n3w8jEG-{qR07*H>k{#lfQ;2?7siv()}IIn3fu`(|;= zn!=@rRZRVFW4foWd)Il$wkjGG(f0a(b1}x0^&?JUkzx*CZ4c0I-Niq3=S9p#b&ArK*zF(OG5!JU7 zV`e8`Jty^dR%nt}pEW%J)xO7T;)hAZ$q-o&zxt0E;TN;1H#_;A&_hoK?plsrW_nGn z%dH_e$BIuw*WbRxO5cy|-UNTea67>6pH)!Rf7ypGk+Q9+e4e5{8}A6Rj|Gu?yIR;V zdTA^sa|>6`5y%B0ZIQ3xHraOY$#=EHfvIW;C(xFh-*q$4cu?ViRGw66up2wEik2p) zSS*X=4aC=tgKQMQtzG_a62g3!cbWnz4h0u6O*g6FPE3Hxn-%^uud{r$%TaQ&Ap4P` zrR6x1jkws_)pQrB4bzJpF3tH05Y>t7 z=CmdWwHQQs$59cU$WTut!_KG-=Bf8RMsojHE1GluvjVI0*Rx3iQ+$P}&-Kz7xvvU7>yNIVNo#KWV&zD$%>G7V72;|e(S+$BXpz!8OaeZ8XN4tJ zO@GQCdsNuWe7zXxX+`iK8yGN2q~kcgdj0%yS7FhR!NaiCax3ZamDH!5E1IvmMc>u6 z`X!)#|HOEqHKIuO*d;vSd^Wx=fmC@j^I4GfNaW^m)CsR1kP+IDa_^&2WEn*A(0e~A zbI+|-?O(^gqeZB%>Eb82d7OJTjcTxSDry#?SKZgw^(+}*WlWY!fMI7+2J@mm?Lu|P zZ?W1@U_|z4YS`lZ!$cC#)np&dVHGX?#SY~x*y&iS$#<>goWkN^EPYh;dSCd~p!M~9KVNLt`j&?fM-*1|K6s@Vp@S{t7rKEfXL*??6x`FYcl zM6+l5wO|+KlDW-XIfi5ImPb;f!%9$M8}oRS>6KRj<5K9Oz(g!ta2`g$<3PdS4uz)Q zY58<`mDmAgv$x(ZT9R=BW1)RSH)IUv@8}r_JD*8@5-V=cafhtq4U@gA;u!|NSJ{z- zl6NWv3>J3}i`1uW05o`Ca>YroLP;b$W%IlHk%;t_ zJ2x_HXe4TE#HP?0dA`sZo%*O?DBHe_KBbru8 zHEGXs(5`V$&+r;83A4)li+-ItXxNZ)xRdGK3>5z_dPT+KMQbk5^X$w=liJU`On7Dw?<-px-N7Qc#yVrT5wgP#J3}S}MRP`=q zpD~cY2Og1YU(lptj0`-7ZHvCtfql9q-NO1XmbuV#b8&A|b1}@@d@eFoX5(u%*>W3<#pU+4G-8YDY%!JmVwLs$+M`s`4kFo~V zL4NYCKMzmujqfHo1pj-dRqfwQx`GwT)s!Qw_@XI1eLA(3^Oi=ReMHGiy# zL#0{UzD8{~n)(pMobOe_;pW*oM)$4-Epj968F+Y}g8K1|Z{<(?_-$|3VbL75sosY5 z^WD)Qus{4Y{Gxo&PESLjT0|+s-`KMMA5J}<7r1SSWU#JyBFx)oT3#a?&%O$cq@71Y zUVE$-m(CqOA*9f`QqJ~RCtIxU-Mx*1ng7Qd@XNIl5P8VE`>hw}V+0E@FE#=V)SKKq z&U18+YxryCfKDgwRx7&a{$H!#4_)zD+%n7|?)gnMZVD{@mUX@Vr!}MbU83s-C4n1E za=aPy`cSaABL|sQqIlr{&NmfF!f)Q71K9l-fRQ1cd4@Ot0!q~1M!yn=|CTuwJTtIqFHCKxyKTTjT!17PoPzhcw^kvgAcX7{d_Jg7>DQYcYOM zE3U~r@JjLecmpYY_p$7)^qH-Z*0kcQRHiML(+@0wmgK&tUtXE2&~DMc+Z_MI3%rMU z(|%2@|49vCFF9=jdUZW{I9dka)PM}R_66t&Q zb1=}7U?yej47ZVWT9S2-4p(Qt(Teb-Q0JyEsw(fZ8YWB*i8bh{EFVICZ0k{HsIy*u zh14XfmJd5&gQxxHdnFUP@~uppEP{{IcY=>}i}Bz;t?D`Z@#IDCOZ5)9eKh4Nk)7L< zUQVRrPkjhu%#-7>nkzQQA?Y84wO6p#bF#Vk+#PF-us3h`ek$u;-pWLkqjqm;D#)^~ zsS6%c1)e2uD|y<8K2F%<6%qHC-Y6C}GLc%g&Ksch?!=39 zS?~w&qzuUvBN##?+B2xw`6@0v)1$)sS`afp<2>(SB(Y4M#$)}s^aLa1h_K|+Nwe{& z1*bOT!+SRvp#2038pvaCMVfs4{F?n|+Q6fk7a*A_> ziAz9ELAWA=;M@KYzvprmPVm|`P5hNO-Y=!Ex+kgF@JzD`8@RK`#JLP8dBko;js`O2 zcHWUgKe<2RqBp;CaF4L_uy+tlb?&CR>MreduCr<>bJe7}y{8*1`fmNZB3XkBuixcu zmWf%W^>SJVsn{REN*d13b6>w8abVzVklEC zQn)-tVk;jXXjlO{QFn5gPqFEIUyC8SOqd*_AhcGu>GVW^x4^>}EsoLL(x$S!istrH zk3O3{r7lg{5TE;6dKm+<&xID{&)?s*HF5yy^cNP@+0Mjx)(ud6mzaY2B5BXku*+h_ zjC*TS6StYF8o&K9a3^Lm`Kc}e{a{c^-7a4ZWo{4<)*_sJ$7_%Mtrz^-&(c21vTBMeSShC`;ETg$$Q_~> z>(F+<2*s3I1ox9CtgqU(aypZlpdhiGIwUD)oEqox=!;BAaLc3>OXTVM+Lp4g1tMZ} z+D$HwJCMPR)NuD!qcEA*IU@=tO)gVRrjP=R(ctfY$SC@KuxSCq2>^V-h=qFDJ)&n%?FYdjw*oJUvv>P$8(^UPxH% zE4X;tRP|Hta@|ReV%eraxLdvntdTC}49^$inXH2IX1OIknn{F*L^)ToqR-Q#+(Kt3{m?vLuE(iRsyfDen&b&=G#2fE=#lKV+X$ z6(BC{7DT(4zxuvy*I)y5Fm;}xxkI=A1nK$Bwfci~eXkcrH|$?@4 z>%aEzXn*$a|BB4~ZJ+iNJW*I(RsoIzjAfg<=%+|C6pEojv&GF$AKAMI`NeNFERj;Kcq+wbkR3XL0-eFkvLZa^ryS7wy+rztM_uqEojPN zsCsVjupYBqz3Mt2N^ATrYs1sT-As#V3*vdA@Rtm5u`KN42)IM?qAkS^6bpY5m%RV z{us-9Rd73SytWv@TO{x2Kx*H9h4(u zrnXo=uFK-@<@O>WdLPkA4yn3f8<*#Y$+j#W-T@(8n)pG%;nw@0Z&|(-{Cr_Di#bPb zRRN5hch+8dmy~jQrP;@dwm}v2isQsw5^X5F@QOISC>6+gDcI^{FRGOKBNRp=8LA~Z zPWG3F-%?7^SdfP}xC`RPn8Syk;Z}fc>79*wG-{{1(jbuo@OI%Tl+7_ zLWMG#VPIi^wAU2PO*ax&AG6qP7iQ{?q^UIl7E}zKnU7{%$YA;{$aiJKv%%w6cSlim zau&;U*JL8-+4pyP$4|X^v3@3@7Vo}NX3S#*xxg`R#V)39r8TG;23rCiqsLl+J!qSd zyCtLT#b#^@fLZi; zplxb@=i3NtS2|b8FSY5~pwD)W#6LK<7pP{(!Mq~I@5Uk?)YG&nqSa(pmENE13KBjt ze1PaH=yQ0AYa6HOEYs5p;$?cRIheRVt_U&eesO8rJU1)gY8QV+gA8A}rGI(1kaJmY?PLqc8vy`XkAem+jd$YqxvVs`ebz}AH+C<>}xjrrXVYpL)M7L%Dkg=6|46Q_m=48kGYnj|Q?;M7QZ;C*Owy>A>oZQxfN{Rx>JHLf?$M0B zDV2>6?^-qeT`POd%?otvU&v4|1715EFJrb4wS>zI1v*!ZJ~>y98fog~=bt=_aJx}X z-CUd&hJ)<2RFAC+=S|z$bX?R`zH;xeVce}NwlC5(S4hOFS#@9>pz^>cFaI>f(;VeF zYW&v@j`=0xxStJRfz{VQBdbFss`S2VAPj(MjcZ>~z0u+vC;$Nf{7r>u z!D(U@>6P_*BgJW*%bnF2{{ou+`fUW;m+jg(FN(J&ENe+dT9On%%Sv3iRN~C}O+c_# znDs|>SK2*HRd91r*U+3g<_x)K-k587@sVHas(s4wRt~@su)bXitDH`cg7c7F&}Ynk z#)~CcRRyX_RI`S?Re_Rvu*;v!bmcKqc@Z}~MW}a48B71Jnw)g}bJZkuHTrBsIK=7j zK1+9tYu`v~L0MQestFD3+c$OW^H~bML5||>GoVYQ&AA;rGKmq&`Swaf1n}TQDlp*TenK6%-ia}-S5QV4!YlnDyJ|dm15jYKs zsl&e!FpwW?hW>}0C-9wb+d!9H>#65YZdh{wMY(Z&=VGFaEr%{_z;kK;yg~D zciAxx2oLD$QZhHJhYe%-=+F_ymw%;G=Ih9;be5V#?q(pwNE`az@nDH{W=K!*#2W5& zcGnPAG%S4X&@WZXEv3y3l=gD)2Q;>1JJ?|4HfmYI*78rC<8)5>ClR7< zdrkL~1Rv0n)HvM?*OS5RlB;S-!oF(}Dsa>3&rCL{oO{puVp!>%7%~|a-0*Md6s3x1 z+W%I302IDyK5fbz8PKb_kYX`0XUPZ7hr=X+$r;krq%x&=zhh0PR(401oWWzjq0420 z-Q7AF_jg<15nle+yI3S-Hbwl#3PTNov}C!^nw2osvwfwje71^;l@cR3v+!!M71hDWyE{*he9$Bn<7UPz~WLo%oHXB>GWw=3x*t zEZIa@$ADt0qj}F&3Jm==e;VD$oW^l(Z|mK!mmj*H`ix1B^^DyntC#gxFLfPRQb07s z5YG^v;5{oWygFJxOne}*xp30)a#HyFV$-9e-}*Zj6XW2Z+SDqI`=nRY*^*h_hWfC4GVdkhJ##Z;+hV`7b?y<3=B@)7<(qbY z51}NKbC)5gxPTfV)FOG7-%rlGiWQ4$X78${e9@J;tbTx2o3E^oJJERf#sUck&AjU* z7XPdg2*}J2l>8`%5THV@3bm`m1bfE>A?Q8)F8zOsQiBxBdUooKBDQIROF{Q z3U8{IL+~j?5a)@Iws9>!+_8M>2`p)B@{!~#oHR1SeLS|`as(x+0lL79GJlRS@VWod5f@%9A~u5>7#!UNoXLou%?QFk?C~^vxCos}TYZVyx-& zE+9A&!gX@5`!s03wjKl%3_R0;tnnPx0p)q#mdh_-_NYO~SN6kU;y@;~boTZqsvJ85 zw}JNyO?-&%$*jF-w11fV>-zI&bCbRpfo-wgou-W}M`6;}0lVK>xbL=?m!4G4QCv7# zBzq1su1_Hs+u_F){mwG?)|2v-WS6~;BCnGf3RZzm&<~7hQ{M$cXQ;PA->BM%xLe)E zKqR!?;fzXt>H{(!MeZn^BeEe;358hs1ht%)JHL{8_R>AntD@M#Xmo*srMgd3)3$k^#-tlWGiDd4wur~TiOtpE%)2Z?WSpr+VszPF=B?de-Q}8jO8f#C%75!p~>7@V>P-$ zHXi@AW;u4EUwxx`gUQrqP+WsBhk{xZU*)bR3(3&qy}(_m@Qy)qCN*+t#tuWzH{fS(c?0a-La96|do!i;Je3+$eg5i+ooN2^!d`L=`fidMOvKuMa9Pi3 z(#iBSZSIO4x2qf5YHw>0t!{UeF-U|Y{FC~>2oR!fh@vmVZRV;{-Tx6);VrGqC?0Qa z!_&^na7vG|?C-$W7?Z66!^Xxn(P6yxZyLAChyyb?*ir?h!MyTw|GOf@gZ$S2*4}vr zMYU~t953io0hJ&KC|(fBsfiKwi&?(}*2Th{I7ZLR`)R=Z^kP)>5=SI6>hC>@Ee! zD!DjKFWBGUa|x@=zg8cxxzd1Zf)YFwB^K-)ox=7`9Vu1(+vM>;8bZIRYr+i5AtCYQ zDM}JzO?cE}=zczXnZNak?IUC&;Wpxgwy=_+6Cae#$A=wSH`+kg3Y~$Lq^*9+%X<%> zL_FsD`R)^H(-#Uq^|Ft+3US@7MVqsS?kDDn#tIyar#0+A)|iKV^_2#%ta(h-U;1Y# zO&pLtx)x7tWrnN1XV;@)Re&x|lERI?{);yj*{q32HuyoHK18Qwon~y1^fw?=*^qPm z*tKrTKZ{oA998a6;J7&M0<{BYmDyy3pyowz)=5zvm25Z@7vR-Ei=L^xyUj03`LaWN z{-4B@TfdNE+nQ=+m=J3Na*BZ70j1Ax$08y+ro>kgLX)9etd%=6gU(3(h&(6ctNKW-SX=fD*fh6o2egVfr^L6))t@3izfJ75g#HI zvh%a%9Be=dl1flHtzMSjHszRls$2`1aFMQ0@`KB7pm^LTiA;mtu;Pxq{5j1JJN0tC zK-pZ3npZMXOjUztSi#e{RN-`Qg@SQFJfO0^RhjzCElFf4$*WOwp}N+t2Jo#PoTM&WN2$$$_Hw z3gWCS=_31%4&%zrdp`&7L`N_adOd6!;xw$G^F3wSP`rLZ@j7rm3Z zU#=0qMz~)+cW!GPP!odR#1CDCh9jr>_Z%ad@p&g~yj6H*auJTD_&Bn5q^(T;t*!hw zKGCxRVzR^Zzm@fV^{t;UUHKsl?g-MrdVwvXmmHI&HYz<{cv*elf6F9X3JG~-%i%(q z`vM}hH;#QSESkzNmXcnpttgCj-{Wf+p{LkxKPJltT~FY8ItdJy6scoOED>z?@#{-l zuh|(DWJ)zUhlFz2_crb&ZI8uO*Q&jyvE*6s#!1o3@O#tTSK7>a8Wl(8IL#P1*7(i< zdVHW9&zOmdaC7w3zx+;$ahfo9^rB?H57h_K!OIJ|c&)x&W1Wdo4`!~2yDsEM7^Otz{e>%fSBPX(;Rn@?Lf&fjk6!~4&eu|KBl-3n-UL>m{-;my?@lRC~2 z@h9&^s_o3kP^q%|*Gmua0Fyb$ES$?*7++N$^k|!|@;$3D1qFC1pX<3J6MrN7fDZg$ zkbUtdiGG^mr_B^=1^YDF&y_(%Z=WEQ!=gJ0X5I3z(v#x)t4^cK?gHH0c(0j9qayEs ze+I%I6?y!u;m$}Up%;;APm6PIh!)>-fV}5cDpJw!CDorQi#sg`qqp=YC-)aB^W%-7 z376strX2s7wTPEfF@|8lf-`3R)bebjeq0u;AwrAMssq=>XZD;G&HGq9hmc{v3CkpE zMHjF|iulol{==FWHiUlu4U0qG}lJd&(MKM87ey#4;#s*lKuYy~v zn6b4siP#i~L_3Ev#;P+k&A9z@4q8BS@B(T0(GJGSu9coBrc;JR+UMot!)Y=}ajM;6 z0Sq3F;y^?1knv2<&B#Ql9%dwmE8!k<&hB~??fPoB#Dtj9KZ9>GO||J3CZH5{F6%lC zrdqwD#iTZMyL-k^5MgV=fJn7jAA$~+T@Mjd;1TQIm-0m5>V| z6*9|BZqZ8Q)*(Jaj(+8pe#js1tC$)RsGVK+OGMe*A!A$b7a~?2JgV7rZ%cxtsGKlf zY??pKR~cujDr3-uTxs#fqn*={4vbK`6?`7hGVK(?x!+8(8bcMs2lozn0)U1oBp^-P zlNRylDK*HvHqpS+4ui-%D<-YW2C-@jwDrC}bmS9L6Kf~bm*=lN4n_8MJGXSP0_KJM zUa7C`V^OLLF(&!*-yJSkT)8}XYs9i%Kw(^24+g^Jh+Je|_2bYxWjAO(%9hxAX3kG2%?hrasIxl4QP%BetB7 za9;XFmPQAfibv8nU0s0!NtjMg=+~$=%W23bb3pvJqa&dKGQv%8aQCI)x}C=*QzeYuY?Thp}9qW>IQIolIn>i**%EcamenPPTvCtu!_$o;0ZcRZb+RgpjdH;o%OsfqV=SQ zTL*ipi_lOCWb~;{8o4|hk3xa%Y_H)3E8+V;uMV>5dAuOA9~fF&+U+Qci&ZVx$O973 zz2H)!GOIturF95poMS!xp9H1&Z!ZlEPyDs5#%&%aYdAb=0(~h9I4W*>`d=0bsU{1#t~leuuKsHvy*EOW=(8i2C)G^+))-avW?nj zW!5$7s+0D=Zloih>&t$~YR^!j6+l4U*^gay1U6SZekY~63!!m;VtzD{Et@mG@|ICa z7a~VIw)#i5bM`x`Z<%f6IjZyQ21{t(fD0_b9T63^C7X}%Hz~NkXiqQ1y1XP|N*q^Zh(4WBAiQF&eKOal2@u9~Lx^K>h z@hb<#p8CxH_mIB%{)7C3mD-W${I)BX-lTTD1+WB0&^i!x6PZS=&@DLr>W~;;>RvWa zJ%5Pn{HA{ZQb+%0O$~wWq4j_yEBZAOzibZu>hq5B$kXW(c%j;Z{c6wgTQ)Ax>C&TE zZ?+&?-5sgVA#;3wKbdCDM}!7kvi|0uTkT$^J#G7sF*fFDgx2B8P%RW+yw&Y*N0ve9 z8hQ_TFU$(L6GSe%9-BS$e8|S=Pu1B z#@VC!jQ}V^gBiCu(N5FEmBQ|ORMiS3XeY@EKiqvA<4&f8KX^jbO}EsF6LOqijTfzr zi*@V(dHg_4B-W5JR-RY3@VjSqp+@q|h$9pMpH;JgY=T4!4HK)Bb>@G^Un*-RG;G(>3aUo?2^c$Bb&gT|Y3;6HuyE(>((n`z5~ zf?vyw!7qp~rZzZDApoXqNa;EcL<;NAt5N1}lNX6!9y6b%?Iv^$0I06g1py8v*|0y2 zcJrp4)A?8%Vhw}nng?jjJ(X4A@qTGuY%Dp}X}AX1e{EHt25|94ft!J{1M<0BTfhd6 zc9$F7B8S&zrm70mXi4Ea7d~io07bDnIg*h<@FWPF_1uXe#r@Mct+&3uLpjU59Nflj zVj>%H2;b(4$q@!I`UavUwS-Uw05$1k!~mmaO4EpAIERM;`QdKR?!ByD%hEQeR!$_A zWQDgW;_3M8L?_ghvo!9uPbs8R$NtF1)$H1z#)K0*fj@hg^Aw1EpUbVN2pw!*>pui; zE&75a=2Yh)Zp)(+v}vSb|EN^6n}LLRy%iRto1;lOE<`>}bQ*xh|9MVDrjVk$WC*h_ zbXD^WPvu&#Rzbjn9ya09SwqcNwI7Q&)ul}Y|Ln6GXAF@^&bz-tjXR`wHlA{isqMM@ zyze)?b1sDDeKVkU2JV2Kl-!fmpg}k4)JAmXo5kbB{mx9wci_bPIvIeK2Q(%uzUEn> zzPch(6^Ug$weD8jJ=Nq;?HZEQk}C#lfAEelzuE{R3sD^et_A~Ir%TtOJ3jhZ1%3gomu~TEY2&#U zW5UPWiZy#8K@QwW$?k(B^ku*Igd66sFRAb%XRE${mG_LOCcq4_vJnHtbL(F(8biG6 zTVbj@zP^RtLjU6<@ZTo%f1A+%Z9@OI3H|>+6Z$g;{s%vkC%c7)l-Omc>H4rcv{!RdP*9Z zuPitgG%iY+i7ef|Mh*=KQ_bdA@}G?-O+u*0mr_B9X%kz$!bN!wm`Hgqc9O}VhB2YC zw6jjeW)nsO2z!R9PfKp3#MqKEUR1Z#H&3?`NrdLJ{YU`qz?~6aHtj2HT&IouyoH~Q zY{NM#lIlQ1spI0py@i!f|3w-8!mc#b;JJatk!)xnNZcY%8YWrr=zYfk{}}NfuW_!q zqZNWU(56mmoz_k*kddkI{2l1{MDti-aKWPV{s}v$WZTUzzf1gc&vJLZ{(^~uS(uGq z3nCT|686h(wpTXPWS|qO&uBWON%pOS50?)%5`_ORzQXbJV9y;mrmC1?kAiF>6aUwSdvRNKf3tQJ^JxYc(1M?~%nA$-%YiHf WxnAY_MxAEpZ_2kd