From b962077067963f8448ae7e2e30876a37c9d87837 Mon Sep 17 00:00:00 2001 From: Micah Peltier Date: Fri, 4 Mar 2022 12:01:01 -0700 Subject: [PATCH 01/10] feat: add event and emitter Signed-off-by: Micah Peltier --- aries_cloudagent/core/conductor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aries_cloudagent/core/conductor.py b/aries_cloudagent/core/conductor.py index 6ed174545f..9237327c81 100644 --- a/aries_cloudagent/core/conductor.py +++ b/aries_cloudagent/core/conductor.py @@ -70,6 +70,7 @@ from .util import STARTUP_EVENT_TOPIC, SHUTDOWN_EVENT_TOPIC LOGGER = logging.getLogger(__name__) +UNDELIVERABLE_EVENT_TOPIC = "acapy::outbound-message::undeliverable" class Conductor: @@ -697,7 +698,7 @@ def handle_not_delivered( ) -> OutboundSendStatus: """Handle a message that failed delivery via outbound transports.""" queued_for_inbound = self.inbound_transport_manager.return_undelivered(outbound) - + profile.notify(UNDELIVERABLE_EVENT_TOPIC, outbound) return ( OutboundSendStatus.WAITING_FOR_PICKUP if queued_for_inbound From 6c7c4354ffaceadac10191b3dd27094d541b4f00 Mon Sep 17 00:00:00 2001 From: Micah Peltier Date: Fri, 4 Mar 2022 13:02:28 -0700 Subject: [PATCH 02/10] feat: async-ing and awaiting necessary funcitons Signed-off-by: Micah Peltier --- aries_cloudagent/core/conductor.py | 10 +++++----- aries_cloudagent/transport/outbound/manager.py | 4 +++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/aries_cloudagent/core/conductor.py b/aries_cloudagent/core/conductor.py index 9237327c81..51e39d80bf 100644 --- a/aries_cloudagent/core/conductor.py +++ b/aries_cloudagent/core/conductor.py @@ -658,7 +658,7 @@ async def queue_outbound( if self.outbound_queue: return await self._queue_external(profile, outbound) else: - return self._queue_internal(profile, outbound) + return await self._queue_internal(profile, outbound) async def _queue_external( self, @@ -682,7 +682,7 @@ async def _queue_external( return OutboundSendStatus.SENT_TO_EXTERNAL_QUEUE - def _queue_internal( + async def _queue_internal( self, profile: Profile, outbound: OutboundMessage ) -> OutboundSendStatus: """Save the message to an internal outbound queue.""" @@ -691,14 +691,14 @@ def _queue_internal( return OutboundSendStatus.QUEUED_FOR_DELIVERY except OutboundDeliveryError: LOGGER.warning("Cannot queue message for delivery, no supported transport") - return self.handle_not_delivered(profile, outbound) + return await self.handle_not_delivered(profile, outbound) - def handle_not_delivered( + async def handle_not_delivered( self, profile: Profile, outbound: OutboundMessage ) -> OutboundSendStatus: """Handle a message that failed delivery via outbound transports.""" queued_for_inbound = self.inbound_transport_manager.return_undelivered(outbound) - profile.notify(UNDELIVERABLE_EVENT_TOPIC, outbound) + await profile.notify(UNDELIVERABLE_EVENT_TOPIC, outbound) return ( OutboundSendStatus.WAITING_FOR_PICKUP if queued_for_inbound diff --git a/aries_cloudagent/transport/outbound/manager.py b/aries_cloudagent/transport/outbound/manager.py index 18fdf4b182..fe4e09745c 100644 --- a/aries_cloudagent/transport/outbound/manager.py +++ b/aries_cloudagent/transport/outbound/manager.py @@ -363,7 +363,9 @@ async def _process_loop(self): exc_info=queued.error, ) if self.handle_not_delivered and queued.message: - self.handle_not_delivered(queued.profile, queued.message) + await self.handle_not_delivered( + queued.profile, queued.message + ) continue # remove from buffer deliver = False From 7a68fcffcbcba4642bcc1087e96a5f5eb404cbf9 Mon Sep 17 00:00:00 2001 From: Micah Peltier Date: Wed, 23 Mar 2022 09:24:50 -0600 Subject: [PATCH 03/10] feat: emitting OutboundSendStatus events Signed-off-by: Micah Peltier --- aries_cloudagent/core/conductor.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/aries_cloudagent/core/conductor.py b/aries_cloudagent/core/conductor.py index 51e39d80bf..9aed9a9e77 100644 --- a/aries_cloudagent/core/conductor.py +++ b/aries_cloudagent/core/conductor.py @@ -70,7 +70,7 @@ from .util import STARTUP_EVENT_TOPIC, SHUTDOWN_EVENT_TOPIC LOGGER = logging.getLogger(__name__) -UNDELIVERABLE_EVENT_TOPIC = "acapy::outbound-message::undeliverable" +OUTBOUND_STATUS_PREFIX = "acapy::outbound-message::" class Conductor: @@ -603,6 +603,10 @@ async def outbound_message_router( outbound.reply_from_verkey = inbound.receipt.recipient_verkey # return message to an inbound session if self.inbound_transport_manager.return_to_session(outbound): + await profile.notify( + f"{OUTBOUND_STATUS_PREFIX}{OutboundSendStatus.SENT_TO_SESSION}", + outbound, + ) return OutboundSendStatus.SENT_TO_SESSION if not outbound.to_session_only: @@ -680,6 +684,10 @@ async def _queue_external( encoded_outbound_message.payload, target.endpoint ) + await profile.notify( + f"{OUTBOUND_STATUS_PREFIX}{OutboundSendStatus.SENT_TO_EXTERNAL_QUEUE}", + outbound, + ) return OutboundSendStatus.SENT_TO_EXTERNAL_QUEUE async def _queue_internal( @@ -688,6 +696,10 @@ async def _queue_internal( """Save the message to an internal outbound queue.""" try: self.outbound_transport_manager.enqueue_message(profile, outbound) + await profile.notify( + f"{OUTBOUND_STATUS_PREFIX}{OutboundSendStatus.QUEUED_FOR_DELIVERY}", + outbound, + ) return OutboundSendStatus.QUEUED_FOR_DELIVERY except OutboundDeliveryError: LOGGER.warning("Cannot queue message for delivery, no supported transport") @@ -698,12 +710,13 @@ async def handle_not_delivered( ) -> OutboundSendStatus: """Handle a message that failed delivery via outbound transports.""" queued_for_inbound = self.inbound_transport_manager.return_undelivered(outbound) - await profile.notify(UNDELIVERABLE_EVENT_TOPIC, outbound) - return ( + status = ( OutboundSendStatus.WAITING_FOR_PICKUP if queued_for_inbound else OutboundSendStatus.UNDELIVERABLE ) + await profile.notify(f"{OUTBOUND_STATUS_PREFIX}{status}", outbound) + return status def webhook_router( self, From 286f23ba1d361f1c0669cb70ba876bd794ec1e94 Mon Sep 17 00:00:00 2001 From: Micah Peltier Date: Thu, 24 Mar 2022 13:03:50 -0600 Subject: [PATCH 04/10] feat: added testing paths for events added in conductor Signed-off-by: Micah Peltier --- aries_cloudagent/core/tests/test_conductor.py | 68 ++++++++++++++++--- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/aries_cloudagent/core/tests/test_conductor.py b/aries_cloudagent/core/tests/test_conductor.py index d3f17d499b..10bc51ae8c 100644 --- a/aries_cloudagent/core/tests/test_conductor.py +++ b/aries_cloudagent/core/tests/test_conductor.py @@ -15,6 +15,7 @@ PublicKeyType, Service, ) +from ...core.event_bus import EventBus, MockEventBus from ...core.in_memory import InMemoryProfileManager from ...core.profile import ProfileManager from ...core.protocol_registry import ProtocolRegistry @@ -34,6 +35,7 @@ from ...transport.outbound.base import OutboundDeliveryError from ...transport.outbound.manager import QueuedOutboundMessage from ...transport.outbound.message import OutboundMessage +from ...transport.outbound.status import OutboundSendStatus from ...transport.wire_format import BaseWireFormat from ...transport.pack_format import PackWireFormat from ...utils.stats import Collector @@ -44,6 +46,8 @@ from .. import conductor as test_module +EVENT_PREFIX = test_module.OUTBOUND_STATUS_PREFIX + class Config: test_settings = {"admin.webhook_urls": ["http://sample.webhook.ca"]} @@ -92,6 +96,7 @@ async def build_context(self) -> InjectionContext: context.injector.bind_instance(ProtocolRegistry, ProtocolRegistry()) context.injector.bind_instance(BaseWireFormat, self.wire_format) context.injector.bind_instance(DIDResolver, DIDResolver(DIDResolverRegistry())) + context.injector.bind_instance(EventBus, MockEventBus()) return context @@ -297,6 +302,8 @@ async def test_outbound_message_handler_return_route(self): await conductor.setup() + bus = conductor.root_profile.inject(EventBus) + payload = "{}" message = OutboundMessage(payload=payload) message.reply_to_verkey = test_to_verkey @@ -310,7 +317,17 @@ async def test_outbound_message_handler_return_route(self): conductor, "queue_outbound", async_mock.CoroutineMock() ) as mock_queue: mock_return.return_value = True - await conductor.outbound_message_router(conductor.context, message) + + status = await conductor.outbound_message_router( + conductor.root_profile, message + ) + assert status == OutboundSendStatus.SENT_TO_SESSION + assert bus.events + assert ( + bus.events[0][1].topic + == f"{EVENT_PREFIX}{OutboundSendStatus.SENT_TO_SESSION}" + ) + assert bus.events[0][1].payload == message mock_return.assert_called_once_with(message) mock_queue.assert_not_awaited() @@ -324,16 +341,27 @@ async def test_outbound_message_handler_with_target(self): await conductor.setup() + bus = conductor.root_profile.inject(EventBus) + payload = "{}" target = ConnectionTarget( endpoint="endpoint", recipient_keys=(), routing_keys=(), sender_key="" ) message = OutboundMessage(payload=payload, target=target) - await conductor.outbound_message_router(conductor.context, message) - + status = await conductor.outbound_message_router( + conductor.root_profile, message + ) + assert status == OutboundSendStatus.QUEUED_FOR_DELIVERY + assert bus.events + print(bus.events) + assert ( + bus.events[0][1].topic + == f"{EVENT_PREFIX}{OutboundSendStatus.QUEUED_FOR_DELIVERY}" + ) + assert bus.events[0][1].payload == message mock_outbound_mgr.return_value.enqueue_message.assert_called_once_with( - conductor.context, message + conductor.root_profile, message ) async def test_outbound_message_handler_with_connection(self): @@ -348,11 +376,24 @@ async def test_outbound_message_handler_with_connection(self): await conductor.setup() + bus = conductor.root_profile.inject(EventBus) + payload = "{}" connection_id = "connection_id" message = OutboundMessage(payload=payload, connection_id=connection_id) - await conductor.outbound_message_router(conductor.root_profile, message) + status = await conductor.outbound_message_router( + conductor.root_profile, message + ) + + assert status == OutboundSendStatus.QUEUED_FOR_DELIVERY + assert bus.events + print(bus.events) + assert ( + bus.events[0][1].topic + == f"{EVENT_PREFIX}{OutboundSendStatus.QUEUED_FOR_DELIVERY}" + ) + assert bus.events[0][1].payload == message conn_mgr.return_value.get_connection_targets.assert_awaited_once_with( connection_id=connection_id @@ -376,21 +417,32 @@ async def test_outbound_message_handler_with_verkey_no_target(self): await conductor.setup() + bus = conductor.root_profile.inject(EventBus) + payload = "{}" message = OutboundMessage( payload=payload, reply_to_verkey=TestDIDs.test_verkey ) - await conductor.outbound_message_router( - conductor.context, + status = await conductor.outbound_message_router( + conductor.root_profile, message, inbound=async_mock.MagicMock( receipt=async_mock.MagicMock(recipient_verkey=TestDIDs.test_verkey) ), ) + assert status == OutboundSendStatus.QUEUED_FOR_DELIVERY + assert bus.events + print(bus.events) + assert ( + bus.events[0][1].topic + == f"{EVENT_PREFIX}{OutboundSendStatus.QUEUED_FOR_DELIVERY}" + ) + assert bus.events[0][1].payload == message + mock_outbound_mgr.return_value.enqueue_message.assert_called_once_with( - conductor.context, message + conductor.root_profile, message ) async def test_handle_nots(self): From 42477bc63d5d475d5c222f266298ed3bb4298dc2 Mon Sep 17 00:00:00 2001 From: Micah Peltier Date: Fri, 25 Mar 2022 10:48:13 -0600 Subject: [PATCH 05/10] feat: added event topic accessor Signed-off-by: Micah Peltier --- aries_cloudagent/transport/outbound/status.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/aries_cloudagent/transport/outbound/status.py b/aries_cloudagent/transport/outbound/status.py index 42576a52ec..34e76184b0 100644 --- a/aries_cloudagent/transport/outbound/status.py +++ b/aries_cloudagent/transport/outbound/status.py @@ -2,6 +2,8 @@ from enum import Enum +OUTBOUND_STATUS_PREFIX = "acapy::outbound-message::" + class OutboundSendStatus(Enum): """Send status of outbound messages.""" @@ -21,3 +23,7 @@ class OutboundSendStatus(Enum): # No endpoint available, and no internal queue for messages. UNDELIVERABLE = "undeliverable" + + @property + def topic(self): + return f"{OUTBOUND_STATUS_PREFIX}{self.value}" From a3fac5b0dcd7098040774110691787da0894b7a3 Mon Sep 17 00:00:00 2001 From: Micah Peltier Date: Fri, 25 Mar 2022 10:49:16 -0600 Subject: [PATCH 06/10] feat: cleanup event emission Signed-off-by: Micah Peltier --- aries_cloudagent/core/conductor.py | 34 ++++++++++++++++++------------ 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/aries_cloudagent/core/conductor.py b/aries_cloudagent/core/conductor.py index 9aed9a9e77..7589a12d3f 100644 --- a/aries_cloudagent/core/conductor.py +++ b/aries_cloudagent/core/conductor.py @@ -70,7 +70,6 @@ from .util import STARTUP_EVENT_TOPIC, SHUTDOWN_EVENT_TOPIC LOGGER = logging.getLogger(__name__) -OUTBOUND_STATUS_PREFIX = "acapy::outbound-message::" class Conductor: @@ -593,6 +592,26 @@ async def outbound_message_router( """ Route an outbound message. + Args: + profile: The active profile for the request + message: An outbound message to be sent + inbound: The inbound message that produced this response, if available + """ + status: OutboundSendStatus = await self._outbound_message_router( + profile=profile, outbound=outbound, inbound=inbound + ) + await profile.notify(status.topic, outbound) + return status + + async def _outbound_message_router( + self, + profile: Profile, + outbound: OutboundMessage, + inbound: InboundMessage = None, + ) -> OutboundSendStatus: + """ + Route an outbound message. + Args: profile: The active profile for the request message: An outbound message to be sent @@ -603,10 +622,6 @@ async def outbound_message_router( outbound.reply_from_verkey = inbound.receipt.recipient_verkey # return message to an inbound session if self.inbound_transport_manager.return_to_session(outbound): - await profile.notify( - f"{OUTBOUND_STATUS_PREFIX}{OutboundSendStatus.SENT_TO_SESSION}", - outbound, - ) return OutboundSendStatus.SENT_TO_SESSION if not outbound.to_session_only: @@ -684,10 +699,6 @@ async def _queue_external( encoded_outbound_message.payload, target.endpoint ) - await profile.notify( - f"{OUTBOUND_STATUS_PREFIX}{OutboundSendStatus.SENT_TO_EXTERNAL_QUEUE}", - outbound, - ) return OutboundSendStatus.SENT_TO_EXTERNAL_QUEUE async def _queue_internal( @@ -696,10 +707,6 @@ async def _queue_internal( """Save the message to an internal outbound queue.""" try: self.outbound_transport_manager.enqueue_message(profile, outbound) - await profile.notify( - f"{OUTBOUND_STATUS_PREFIX}{OutboundSendStatus.QUEUED_FOR_DELIVERY}", - outbound, - ) return OutboundSendStatus.QUEUED_FOR_DELIVERY except OutboundDeliveryError: LOGGER.warning("Cannot queue message for delivery, no supported transport") @@ -715,7 +722,6 @@ async def handle_not_delivered( if queued_for_inbound else OutboundSendStatus.UNDELIVERABLE ) - await profile.notify(f"{OUTBOUND_STATUS_PREFIX}{status}", outbound) return status def webhook_router( From 65433283e0a73bdd3d1f5df61a4b7c20246ec421 Mon Sep 17 00:00:00 2001 From: Micah Peltier Date: Fri, 25 Mar 2022 10:49:52 -0600 Subject: [PATCH 07/10] feat: update tests to reflect changes in conductor Signed-off-by: Micah Peltier --- aries_cloudagent/core/tests/test_conductor.py | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/aries_cloudagent/core/tests/test_conductor.py b/aries_cloudagent/core/tests/test_conductor.py index 10bc51ae8c..febef8e6ed 100644 --- a/aries_cloudagent/core/tests/test_conductor.py +++ b/aries_cloudagent/core/tests/test_conductor.py @@ -323,10 +323,7 @@ async def test_outbound_message_handler_return_route(self): ) assert status == OutboundSendStatus.SENT_TO_SESSION assert bus.events - assert ( - bus.events[0][1].topic - == f"{EVENT_PREFIX}{OutboundSendStatus.SENT_TO_SESSION}" - ) + assert bus.events[0][1].topic == status.topic assert bus.events[0][1].payload == message mock_return.assert_called_once_with(message) mock_queue.assert_not_awaited() @@ -355,10 +352,7 @@ async def test_outbound_message_handler_with_target(self): assert status == OutboundSendStatus.QUEUED_FOR_DELIVERY assert bus.events print(bus.events) - assert ( - bus.events[0][1].topic - == f"{EVENT_PREFIX}{OutboundSendStatus.QUEUED_FOR_DELIVERY}" - ) + assert bus.events[0][1].topic == status.topic assert bus.events[0][1].payload == message mock_outbound_mgr.return_value.enqueue_message.assert_called_once_with( conductor.root_profile, message @@ -389,10 +383,7 @@ async def test_outbound_message_handler_with_connection(self): assert status == OutboundSendStatus.QUEUED_FOR_DELIVERY assert bus.events print(bus.events) - assert ( - bus.events[0][1].topic - == f"{EVENT_PREFIX}{OutboundSendStatus.QUEUED_FOR_DELIVERY}" - ) + assert bus.events[0][1].topic == status.topic assert bus.events[0][1].payload == message conn_mgr.return_value.get_connection_targets.assert_awaited_once_with( @@ -435,10 +426,7 @@ async def test_outbound_message_handler_with_verkey_no_target(self): assert status == OutboundSendStatus.QUEUED_FOR_DELIVERY assert bus.events print(bus.events) - assert ( - bus.events[0][1].topic - == f"{EVENT_PREFIX}{OutboundSendStatus.QUEUED_FOR_DELIVERY}" - ) + assert bus.events[0][1].topic == status.topic assert bus.events[0][1].payload == message mock_outbound_mgr.return_value.enqueue_message.assert_called_once_with( From 78ed6cd65655e841d16c6f2ce5c6f8876fe076f0 Mon Sep 17 00:00:00 2001 From: Micah Peltier Date: Mon, 28 Mar 2022 11:13:10 -0600 Subject: [PATCH 08/10] feat: refactor, back to sync functions Signed-off-by: Micah Peltier --- aries_cloudagent/core/conductor.py | 9 ++++----- aries_cloudagent/core/tests/test_conductor.py | 2 -- aries_cloudagent/transport/outbound/manager.py | 4 +--- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/aries_cloudagent/core/conductor.py b/aries_cloudagent/core/conductor.py index 7589a12d3f..6303ab0b22 100644 --- a/aries_cloudagent/core/conductor.py +++ b/aries_cloudagent/core/conductor.py @@ -677,7 +677,7 @@ async def queue_outbound( if self.outbound_queue: return await self._queue_external(profile, outbound) else: - return await self._queue_internal(profile, outbound) + return self._queue_internal(profile, outbound) async def _queue_external( self, @@ -701,7 +701,7 @@ async def _queue_external( return OutboundSendStatus.SENT_TO_EXTERNAL_QUEUE - async def _queue_internal( + def _queue_internal( self, profile: Profile, outbound: OutboundMessage ) -> OutboundSendStatus: """Save the message to an internal outbound queue.""" @@ -710,19 +710,18 @@ async def _queue_internal( return OutboundSendStatus.QUEUED_FOR_DELIVERY except OutboundDeliveryError: LOGGER.warning("Cannot queue message for delivery, no supported transport") - return await self.handle_not_delivered(profile, outbound) + return self.handle_not_delivered(profile, outbound) async def handle_not_delivered( self, profile: Profile, outbound: OutboundMessage ) -> OutboundSendStatus: """Handle a message that failed delivery via outbound transports.""" queued_for_inbound = self.inbound_transport_manager.return_undelivered(outbound) - status = ( + return ( OutboundSendStatus.WAITING_FOR_PICKUP if queued_for_inbound else OutboundSendStatus.UNDELIVERABLE ) - return status def webhook_router( self, diff --git a/aries_cloudagent/core/tests/test_conductor.py b/aries_cloudagent/core/tests/test_conductor.py index febef8e6ed..bb42463f16 100644 --- a/aries_cloudagent/core/tests/test_conductor.py +++ b/aries_cloudagent/core/tests/test_conductor.py @@ -46,8 +46,6 @@ from .. import conductor as test_module -EVENT_PREFIX = test_module.OUTBOUND_STATUS_PREFIX - class Config: test_settings = {"admin.webhook_urls": ["http://sample.webhook.ca"]} diff --git a/aries_cloudagent/transport/outbound/manager.py b/aries_cloudagent/transport/outbound/manager.py index fe4e09745c..18fdf4b182 100644 --- a/aries_cloudagent/transport/outbound/manager.py +++ b/aries_cloudagent/transport/outbound/manager.py @@ -363,9 +363,7 @@ async def _process_loop(self): exc_info=queued.error, ) if self.handle_not_delivered and queued.message: - await self.handle_not_delivered( - queued.profile, queued.message - ) + self.handle_not_delivered(queued.profile, queued.message) continue # remove from buffer deliver = False From 17cfe720bf0a111e6ca50ac14e965c78e0615f28 Mon Sep 17 00:00:00 2001 From: Micah Peltier Date: Mon, 28 Mar 2022 11:51:35 -0600 Subject: [PATCH 09/10] chore: final un-asyncification Signed-off-by: Micah Peltier --- aries_cloudagent/core/conductor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aries_cloudagent/core/conductor.py b/aries_cloudagent/core/conductor.py index 6303ab0b22..e139a4f423 100644 --- a/aries_cloudagent/core/conductor.py +++ b/aries_cloudagent/core/conductor.py @@ -712,7 +712,7 @@ def _queue_internal( LOGGER.warning("Cannot queue message for delivery, no supported transport") return self.handle_not_delivered(profile, outbound) - async def handle_not_delivered( + def handle_not_delivered( self, profile: Profile, outbound: OutboundMessage ) -> OutboundSendStatus: """Handle a message that failed delivery via outbound transports.""" From c80ed59e6323659c2a4008903976de9d77da3cdc Mon Sep 17 00:00:00 2001 From: Micah Peltier Date: Tue, 29 Mar 2022 08:31:59 -0600 Subject: [PATCH 10/10] chore: add missing docstring Signed-off-by: Micah Peltier --- aries_cloudagent/transport/outbound/status.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aries_cloudagent/transport/outbound/status.py b/aries_cloudagent/transport/outbound/status.py index 34e76184b0..e1fba403ec 100644 --- a/aries_cloudagent/transport/outbound/status.py +++ b/aries_cloudagent/transport/outbound/status.py @@ -26,4 +26,5 @@ class OutboundSendStatus(Enum): @property def topic(self): + """Return an event topic associated with a given status.""" return f"{OUTBOUND_STATUS_PREFIX}{self.value}"