We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Connectionless OOB with attachments results in attribute error
As reported by @nodlesh after errors seen in AATH:

> Since approximately April 9th the AATH OOB tests have been failing. These tests have credential V1 or V2 or proof V1 or V2 attachments. There is an error that happens on the receiver/holder agent on the receive-invitation. I’m getting an internal server error. See stack trace below.

```python
2024-04-16 14:25:11,018 aiohttp.server ERROR Error handling request
Traceback (most recent call last):
  File "/usr/local/lib/python3.9/site-packages/aiohttp/web_protocol.py", line 452, in _handle_request
    resp = await request_handler(request)
  File "/usr/local/lib/python3.9/site-packages/aiohttp/web_app.py", line 543, in _handle
    resp = await handler(request)
  File "/usr/local/lib/python3.9/site-packages/aiohttp/web_middlewares.py", line 114, in impl
    return await handler(request)
  File "/usr/local/lib/python3.9/site-packages/aries_cloudagent/admin/server.py", line 181, in ready_middleware
    return await handler(request)
  File "/usr/local/lib/python3.9/site-packages/aries_cloudagent/admin/server.py", line 218, in debug_middleware
    return await handler(request)
  File "/usr/local/lib/python3.9/site-packages/aries_cloudagent/admin/server.py", line 451, in setup_context
    return await task
  File "/usr/local/lib/python3.9/asyncio/futures.py", line 284, in __await__
    yield self  # This tells Task to wait for completion.
  File "/usr/local/lib/python3.9/asyncio/tasks.py", line 328, in __wakeup
    future.result()
  File "/usr/local/lib/python3.9/asyncio/futures.py", line 201, in result
    raise self._exception
  File "/usr/local/lib/python3.9/asyncio/tasks.py", line 256, in __step
    result = coro.send(None)
  File "/usr/local/lib/python3.9/site-packages/aiohttp_apispec/middlewares.py", line 45, in validation_middleware
    return await handler(request)
  File "/usr/local/lib/python3.9/site-packages/aries_cloudagent/protocols/out_of_band/v1_0/routes.py", line 324, in invitation_receive
    result = await oob_mgr.receive_invitation(
  File "/usr/local/lib/python3.9/site-packages/aries_cloudagent/protocols/out_of_band/v1_0/manager.py", line 817, in receive_invitation
    await self._process_request_attach(oob_record)
  File "/usr/local/lib/python3.9/site-packages/aries_cloudagent/protocols/out_of_band/v1_0/manager.py", line 836, in _process_request_attach
    await message_processor.handle_message(
  File "/usr/local/lib/python3.9/site-packages/aries_cloudagent/core/oob_processor.py", line 360, in handle_message
    oob_record.their_service = their_service.serialize()
AttributeError: can't set attribute
```

This is a result of changes made in 7f5eae76b1, part of #2862. The `setter` for an attribute turned into a property was omitted.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `aries_cloudagent/protocols/out_of_band/v1_0/models/oob_record.py`
Content:
```
1 """Record for out of band invitations."""
2 
3 import json
4 from typing import Any, Mapping, Optional, Union
5 
6 from marshmallow import fields, validate
7 
8 from .....connections.models.conn_record import ConnRecord
9 from .....core.profile import ProfileSession
10 from .....messaging.decorators.service_decorator import (
11     ServiceDecorator,
12     ServiceDecoratorSchema,
13 )
14 from .....messaging.models.base_record import BaseExchangeRecord, BaseExchangeSchema
15 from .....messaging.valid import UUID4_EXAMPLE
16 from .....storage.base import BaseStorage
17 from .....storage.error import StorageNotFoundError
18 from .....storage.record import StorageRecord
19 from ..messages.invitation import InvitationMessage, InvitationMessageSchema
20 
21 
22 class OobRecord(BaseExchangeRecord):
23     """Represents an out of band record."""
24 
25     class Meta:
26         """OobRecord metadata."""
27 
28         schema_class = "OobRecordSchema"
29 
30     RECORD_TYPE = "oob_record"
31     RECORD_TYPE_METADATA = ConnRecord.RECORD_TYPE_METADATA
32     RECORD_ID_NAME = "oob_id"
33     RECORD_TOPIC = "out_of_band"
34     TAG_NAMES = {
35         "invi_msg_id",
36         "attach_thread_id",
37         "our_recipient_key",
38         "connection_id",
39         "reuse_msg_id",
40     }
41 
42     STATE_INITIAL = "initial"
43     STATE_PREPARE_RESPONSE = "prepare-response"
44     STATE_AWAIT_RESPONSE = "await-response"
45     STATE_NOT_ACCEPTED = "reuse-not-accepted"
46     STATE_ACCEPTED = "reuse-accepted"
47     STATE_DONE = "done"
48 
49     ROLE_SENDER = "sender"
50     ROLE_RECEIVER = "receiver"
51 
52     def __init__(
53         self,
54         *,
55         state: str,
56         invi_msg_id: str,
57         role: str,
58         invitation: Union[InvitationMessage, Mapping[str, Any]],
59         their_service: Optional[Union[ServiceDecorator, Mapping[str, Any]]] = None,
60         connection_id: Optional[str] = None,
61         reuse_msg_id: Optional[str] = None,
62         oob_id: Optional[str] = None,
63         attach_thread_id: Optional[str] = None,
64         our_recipient_key: Optional[str] = None,
65         our_service: Optional[Union[ServiceDecorator, Mapping[str, Any]]] = None,
66         multi_use: bool = False,
67         trace: bool = False,
68         **kwargs,
69     ):
70         """Initialize a new OobRecord."""
71         super().__init__(oob_id, state, trace=trace, **kwargs)
72         self._id = oob_id
73         self.state = state
74         self.invi_msg_id = invi_msg_id
75         self.role = role
76         self._invitation = InvitationMessage.serde(invitation)
77         self.connection_id = connection_id
78         self.reuse_msg_id = reuse_msg_id
79         self._their_service = ServiceDecorator.serde(their_service)
80         self._our_service = ServiceDecorator.serde(our_service)
81         self.attach_thread_id = attach_thread_id
82         self.our_recipient_key = our_recipient_key
83         self.multi_use = multi_use
84         self.trace = trace
85 
86     @property
87     def oob_id(self) -> str:
88         """Accessor for the ID associated with this exchange."""
89         return self._id
90 
91     @property
92     def invitation(self) -> Optional[InvitationMessage]:
93         """Accessor; get deserialized view."""
94         return None if self._invitation is None else self._invitation.de
95 
96     @invitation.setter
97     def invitation(self, value):
98         """Setter; store de/serialized views."""
99         self._invitation = InvitationMessage.serde(value)
100 
101     @property
102     def our_service(self) -> Optional[ServiceDecorator]:
103         """Accessor; get deserialized view."""
104         return None if self._our_service is None else self._our_service.de
105 
106     @our_service.setter
107     def our_service(self, value: Union[ServiceDecorator, Mapping[str, Any]]):
108         """Setter; store de/serialized views."""
109         self._our_service = ServiceDecorator.serde(value)
110 
111     @property
112     def their_service(self) -> Optional[ServiceDecorator]:
113         """Accessor; get deserialized view."""
114         return None if self._their_service is None else self._their_service.de
115 
116     @property
117     def record_value(self) -> dict:
118         """Accessor for the JSON record value generated for this invitation."""
119         return {
120             **{
121                 prop: getattr(self, prop)
122                 for prop in (
123                     "state",
124                     "their_service",
125                     "connection_id",
126                     "role",
127                     "invi_msg_id",
128                     "multi_use",
129                 )
130             },
131             **{
132                 prop: getattr(self, f"_{prop}").ser
133                 for prop in ("invitation", "our_service", "their_service")
134                 if getattr(self, prop) is not None
135             },
136         }
137 
138     async def delete_record(self, session: ProfileSession):
139         """Perform connection record deletion actions.
140 
141         Args:
142             session (ProfileSession): session
143 
144         """
145         await super().delete_record(session)
146 
147         # Delete metadata
148         if self.connection_id:
149             storage = session.inject(BaseStorage)
150             await storage.delete_all_records(
151                 self.RECORD_TYPE_METADATA,
152                 {"connection_id": self.connection_id},
153             )
154 
155     async def metadata_get(
156         self, session: ProfileSession, key: str, default: Any = None
157     ) -> Any:
158         """Retrieve arbitrary metadata associated with this connection.
159 
160         Args:
161             session (ProfileSession): session used for storage
162             key (str): key identifying metadata
163             default (Any): default value to get; type should be a JSON
164                 compatible value.
165 
166         Returns:
167             Any: metadata stored by key
168 
169         """
170         assert self.connection_id
171         storage: BaseStorage = session.inject(BaseStorage)
172         try:
173             record = await storage.find_record(
174                 self.RECORD_TYPE_METADATA,
175                 {"key": key, "connection_id": self.connection_id},
176             )
177             return json.loads(record.value)
178         except StorageNotFoundError:
179             return default
180 
181     async def metadata_set(self, session: ProfileSession, key: str, value: Any):
182         """Set arbitrary metadata associated with this connection.
183 
184         Args:
185             session (ProfileSession): session used for storage
186             key (str): key identifying metadata
187             value (Any): value to set
188         """
189         assert self.connection_id
190         value = json.dumps(value)
191         storage: BaseStorage = session.inject(BaseStorage)
192         try:
193             record = await storage.find_record(
194                 self.RECORD_TYPE_METADATA,
195                 {"key": key, "connection_id": self.connection_id},
196             )
197             await storage.update_record(record, value, record.tags)
198         except StorageNotFoundError:
199             record = StorageRecord(
200                 self.RECORD_TYPE_METADATA,
201                 value,
202                 {"key": key, "connection_id": self.connection_id},
203             )
204             await storage.add_record(record)
205 
206     async def metadata_delete(self, session: ProfileSession, key: str):
207         """Delete custom metadata associated with this connection.
208 
209         Args:
210             session (ProfileSession): session used for storage
211             key (str): key of metadata to delete
212         """
213         assert self.connection_id
214         storage: BaseStorage = session.inject(BaseStorage)
215         try:
216             record = await storage.find_record(
217                 self.RECORD_TYPE_METADATA,
218                 {"key": key, "connection_id": self.connection_id},
219             )
220             await storage.delete_record(record)
221         except StorageNotFoundError as err:
222             raise KeyError(f"{key} not found in connection metadata") from err
223 
224     async def metadata_get_all(self, session: ProfileSession) -> dict:
225         """Return all custom metadata associated with this connection.
226 
227         Args:
228             session (ProfileSession): session used for storage
229 
230         Returns:
231             dict: dictionary representation of all metadata values
232 
233         """
234         assert self.connection_id
235         storage: BaseStorage = session.inject(BaseStorage)
236         records = await storage.find_all_records(
237             self.RECORD_TYPE_METADATA,
238             {"connection_id": self.connection_id},
239         )
240         return {record.tags["key"]: json.loads(record.value) for record in records}
241 
242     def __eq__(self, other: Any) -> bool:
243         """Comparison between records."""
244         return super().__eq__(other)
245 
246 
247 class OobRecordSchema(BaseExchangeSchema):
248     """Schema to allow serialization/deserialization of invitation records."""
249 
250     class Meta:
251         """OobRecordSchema metadata."""
252 
253         model_class = OobRecord
254 
255     oob_id = fields.Str(
256         required=True,
257         metadata={"description": "Oob record identifier", "example": UUID4_EXAMPLE},
258     )
259     state = fields.Str(
260         required=True,
261         validate=validate.OneOf(
262             OobRecord.get_attributes_by_prefix("STATE_", walk_mro=True)
263         ),
264         metadata={
265             "description": "Out of band message exchange state",
266             "example": OobRecord.STATE_AWAIT_RESPONSE,
267         },
268     )
269     invi_msg_id = fields.Str(
270         required=True,
271         metadata={
272             "description": "Invitation message identifier",
273             "example": UUID4_EXAMPLE,
274         },
275     )
276     invitation = fields.Nested(
277         InvitationMessageSchema(),
278         required=True,
279         metadata={"description": "Out of band invitation message"},
280     )
281 
282     their_service = fields.Nested(ServiceDecoratorSchema(), required=False)
283 
284     connection_id = fields.Str(
285         required=False,
286         metadata={
287             "description": "Connection record identifier",
288             "example": UUID4_EXAMPLE,
289         },
290     )
291 
292     attach_thread_id = fields.Str(
293         required=False,
294         metadata={
295             "description": "Connection record identifier",
296             "example": UUID4_EXAMPLE,
297         },
298     )
299 
300     our_recipient_key = fields.Str(
301         required=False,
302         metadata={
303             "description": "Recipient key used for oob invitation",
304             "example": UUID4_EXAMPLE,
305         },
306     )
307 
308     role = fields.Str(
309         required=False,
310         validate=validate.OneOf(
311             OobRecord.get_attributes_by_prefix("ROLE_", walk_mro=False)
312         ),
313         metadata={"description": "OOB Role", "example": OobRecord.ROLE_RECEIVER},
314     )
315 
316     multi_use = fields.Boolean(
317         required=False,
318         metadata={
319             "description": "Allow for multiple uses of the oobinvitation",
320             "example": True,
321         },
322     )
323
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/models/oob_record.py b/aries_cloudagent/protocols/out_of_band/v1_0/models/oob_record.py
--- a/aries_cloudagent/protocols/out_of_band/v1_0/models/oob_record.py
+++ b/aries_cloudagent/protocols/out_of_band/v1_0/models/oob_record.py
@@ -113,6 +113,11 @@
         """Accessor; get deserialized view."""
         return None if self._their_service is None else self._their_service.de
 
+    @their_service.setter
+    def their_service(self, value: Union[ServiceDecorator, Mapping[str, Any]]):
+        """Setter; store de/serialized vies."""
+        self._their_service = ServiceDecorator.serde(value)
+
     @property
     def record_value(self) -> dict:
         """Accessor for the JSON record value generated for this invitation."""

{"golden_diff": "diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/models/oob_record.py b/aries_cloudagent/protocols/out_of_band/v1_0/models/oob_record.py\n--- a/aries_cloudagent/protocols/out_of_band/v1_0/models/oob_record.py\n+++ b/aries_cloudagent/protocols/out_of_band/v1_0/models/oob_record.py\n@@ -113,6 +113,11 @@\n         \"\"\"Accessor; get deserialized view.\"\"\"\n         return None if self._their_service is None else self._their_service.de\n \n+    @their_service.setter\n+    def their_service(self, value: Union[ServiceDecorator, Mapping[str, Any]]):\n+        \"\"\"Setter; store de/serialized vies.\"\"\"\n+        self._their_service = ServiceDecorator.serde(value)\n+\n     @property\n     def record_value(self) -> dict:\n         \"\"\"Accessor for the JSON record value generated for this invitation.\"\"\"\n", "issue": "Connectionless OOB with attachments results in attribute error\nAs reported by @nodlesh after errors seen in AATH:\r\n\r\n> Since approximately April 9th the AATH OOB tests have been failing. These tests have credential V1 or V2 or proof V1 or V2 attachments. There is an error that happens on the receiver/holder agent on the receive-invitation. I\u2019m getting an internal server error. See stack trace below.\r\n\r\n```python\r\n2024-04-16 14:25:11,018 aiohttp.server ERROR Error handling request\r\nTraceback (most recent call last):\r\n  File \"/usr/local/lib/python3.9/site-packages/aiohttp/web_protocol.py\", line 452, in _handle_request\r\n    resp = await request_handler(request)\r\n  File \"/usr/local/lib/python3.9/site-packages/aiohttp/web_app.py\", line 543, in _handle\r\n    resp = await handler(request)\r\n  File \"/usr/local/lib/python3.9/site-packages/aiohttp/web_middlewares.py\", line 114, in impl\r\n    return await handler(request)\r\n  File \"/usr/local/lib/python3.9/site-packages/aries_cloudagent/admin/server.py\", line 181, in ready_middleware\r\n    return await handler(request)\r\n  File \"/usr/local/lib/python3.9/site-packages/aries_cloudagent/admin/server.py\", line 218, in debug_middleware\r\n    return await handler(request)\r\n  File \"/usr/local/lib/python3.9/site-packages/aries_cloudagent/admin/server.py\", line 451, in setup_context\r\n    return await task\r\n  File \"/usr/local/lib/python3.9/asyncio/futures.py\", line 284, in __await__\r\n    yield self  # This tells Task to wait for completion.\r\n  File \"/usr/local/lib/python3.9/asyncio/tasks.py\", line 328, in __wakeup\r\n    future.result()\r\n  File \"/usr/local/lib/python3.9/asyncio/futures.py\", line 201, in result\r\n    raise self._exception\r\n  File \"/usr/local/lib/python3.9/asyncio/tasks.py\", line 256, in __step\r\n    result = coro.send(None)\r\n  File \"/usr/local/lib/python3.9/site-packages/aiohttp_apispec/middlewares.py\", line 45, in validation_middleware\r\n    return await handler(request)\r\n  File \"/usr/local/lib/python3.9/site-packages/aries_cloudagent/protocols/out_of_band/v1_0/routes.py\", line 324, in invitation_receive\r\n    result = await oob_mgr.receive_invitation(\r\n  File \"/usr/local/lib/python3.9/site-packages/aries_cloudagent/protocols/out_of_band/v1_0/manager.py\", line 817, in receive_invitation\r\n    await self._process_request_attach(oob_record)\r\n  File \"/usr/local/lib/python3.9/site-packages/aries_cloudagent/protocols/out_of_band/v1_0/manager.py\", line 836, in _process_request_attach\r\n    await message_processor.handle_message(\r\n  File \"/usr/local/lib/python3.9/site-packages/aries_cloudagent/core/oob_processor.py\", line 360, in handle_message\r\n    oob_record.their_service = their_service.serialize()\r\nAttributeError: can't set attribute\r\n```\r\n\r\nThis is a result of changes made in 7f5eae76b1, part of #2862. The `setter` for an attribute turned into a property was omitted.\n", "before_files": [{"content": "\"\"\"Record for out of band invitations.\"\"\"\n\nimport json\nfrom typing import Any, Mapping, Optional, Union\n\nfrom marshmallow import fields, validate\n\nfrom .....connections.models.conn_record import ConnRecord\nfrom .....core.profile import ProfileSession\nfrom .....messaging.decorators.service_decorator import (\n    ServiceDecorator,\n    ServiceDecoratorSchema,\n)\nfrom .....messaging.models.base_record import BaseExchangeRecord, BaseExchangeSchema\nfrom .....messaging.valid import UUID4_EXAMPLE\nfrom .....storage.base import BaseStorage\nfrom .....storage.error import StorageNotFoundError\nfrom .....storage.record import StorageRecord\nfrom ..messages.invitation import InvitationMessage, InvitationMessageSchema\n\n\nclass OobRecord(BaseExchangeRecord):\n    \"\"\"Represents an out of band record.\"\"\"\n\n    class Meta:\n        \"\"\"OobRecord metadata.\"\"\"\n\n        schema_class = \"OobRecordSchema\"\n\n    RECORD_TYPE = \"oob_record\"\n    RECORD_TYPE_METADATA = ConnRecord.RECORD_TYPE_METADATA\n    RECORD_ID_NAME = \"oob_id\"\n    RECORD_TOPIC = \"out_of_band\"\n    TAG_NAMES = {\n        \"invi_msg_id\",\n        \"attach_thread_id\",\n        \"our_recipient_key\",\n        \"connection_id\",\n        \"reuse_msg_id\",\n    }\n\n    STATE_INITIAL = \"initial\"\n    STATE_PREPARE_RESPONSE = \"prepare-response\"\n    STATE_AWAIT_RESPONSE = \"await-response\"\n    STATE_NOT_ACCEPTED = \"reuse-not-accepted\"\n    STATE_ACCEPTED = \"reuse-accepted\"\n    STATE_DONE = \"done\"\n\n    ROLE_SENDER = \"sender\"\n    ROLE_RECEIVER = \"receiver\"\n\n    def __init__(\n        self,\n        *,\n        state: str,\n        invi_msg_id: str,\n        role: str,\n        invitation: Union[InvitationMessage, Mapping[str, Any]],\n        their_service: Optional[Union[ServiceDecorator, Mapping[str, Any]]] = None,\n        connection_id: Optional[str] = None,\n        reuse_msg_id: Optional[str] = None,\n        oob_id: Optional[str] = None,\n        attach_thread_id: Optional[str] = None,\n        our_recipient_key: Optional[str] = None,\n        our_service: Optional[Union[ServiceDecorator, Mapping[str, Any]]] = None,\n        multi_use: bool = False,\n        trace: bool = False,\n        **kwargs,\n    ):\n        \"\"\"Initialize a new OobRecord.\"\"\"\n        super().__init__(oob_id, state, trace=trace, **kwargs)\n        self._id = oob_id\n        self.state = state\n        self.invi_msg_id = invi_msg_id\n        self.role = role\n        self._invitation = InvitationMessage.serde(invitation)\n        self.connection_id = connection_id\n        self.reuse_msg_id = reuse_msg_id\n        self._their_service = ServiceDecorator.serde(their_service)\n        self._our_service = ServiceDecorator.serde(our_service)\n        self.attach_thread_id = attach_thread_id\n        self.our_recipient_key = our_recipient_key\n        self.multi_use = multi_use\n        self.trace = trace\n\n    @property\n    def oob_id(self) -> str:\n        \"\"\"Accessor for the ID associated with this exchange.\"\"\"\n        return self._id\n\n    @property\n    def invitation(self) -> Optional[InvitationMessage]:\n        \"\"\"Accessor; get deserialized view.\"\"\"\n        return None if self._invitation is None else self._invitation.de\n\n    @invitation.setter\n    def invitation(self, value):\n        \"\"\"Setter; store de/serialized views.\"\"\"\n        self._invitation = InvitationMessage.serde(value)\n\n    @property\n    def our_service(self) -> Optional[ServiceDecorator]:\n        \"\"\"Accessor; get deserialized view.\"\"\"\n        return None if self._our_service is None else self._our_service.de\n\n    @our_service.setter\n    def our_service(self, value: Union[ServiceDecorator, Mapping[str, Any]]):\n        \"\"\"Setter; store de/serialized views.\"\"\"\n        self._our_service = ServiceDecorator.serde(value)\n\n    @property\n    def their_service(self) -> Optional[ServiceDecorator]:\n        \"\"\"Accessor; get deserialized view.\"\"\"\n        return None if self._their_service is None else self._their_service.de\n\n    @property\n    def record_value(self) -> dict:\n        \"\"\"Accessor for the JSON record value generated for this invitation.\"\"\"\n        return {\n            **{\n                prop: getattr(self, prop)\n                for prop in (\n                    \"state\",\n                    \"their_service\",\n                    \"connection_id\",\n                    \"role\",\n                    \"invi_msg_id\",\n                    \"multi_use\",\n                )\n            },\n            **{\n                prop: getattr(self, f\"_{prop}\").ser\n                for prop in (\"invitation\", \"our_service\", \"their_service\")\n                if getattr(self, prop) is not None\n            },\n        }\n\n    async def delete_record(self, session: ProfileSession):\n        \"\"\"Perform connection record deletion actions.\n\n        Args:\n            session (ProfileSession): session\n\n        \"\"\"\n        await super().delete_record(session)\n\n        # Delete metadata\n        if self.connection_id:\n            storage = session.inject(BaseStorage)\n            await storage.delete_all_records(\n                self.RECORD_TYPE_METADATA,\n                {\"connection_id\": self.connection_id},\n            )\n\n    async def metadata_get(\n        self, session: ProfileSession, key: str, default: Any = None\n    ) -> Any:\n        \"\"\"Retrieve arbitrary metadata associated with this connection.\n\n        Args:\n            session (ProfileSession): session used for storage\n            key (str): key identifying metadata\n            default (Any): default value to get; type should be a JSON\n                compatible value.\n\n        Returns:\n            Any: metadata stored by key\n\n        \"\"\"\n        assert self.connection_id\n        storage: BaseStorage = session.inject(BaseStorage)\n        try:\n            record = await storage.find_record(\n                self.RECORD_TYPE_METADATA,\n                {\"key\": key, \"connection_id\": self.connection_id},\n            )\n            return json.loads(record.value)\n        except StorageNotFoundError:\n            return default\n\n    async def metadata_set(self, session: ProfileSession, key: str, value: Any):\n        \"\"\"Set arbitrary metadata associated with this connection.\n\n        Args:\n            session (ProfileSession): session used for storage\n            key (str): key identifying metadata\n            value (Any): value to set\n        \"\"\"\n        assert self.connection_id\n        value = json.dumps(value)\n        storage: BaseStorage = session.inject(BaseStorage)\n        try:\n            record = await storage.find_record(\n                self.RECORD_TYPE_METADATA,\n                {\"key\": key, \"connection_id\": self.connection_id},\n            )\n            await storage.update_record(record, value, record.tags)\n        except StorageNotFoundError:\n            record = StorageRecord(\n                self.RECORD_TYPE_METADATA,\n                value,\n                {\"key\": key, \"connection_id\": self.connection_id},\n            )\n            await storage.add_record(record)\n\n    async def metadata_delete(self, session: ProfileSession, key: str):\n        \"\"\"Delete custom metadata associated with this connection.\n\n        Args:\n            session (ProfileSession): session used for storage\n            key (str): key of metadata to delete\n        \"\"\"\n        assert self.connection_id\n        storage: BaseStorage = session.inject(BaseStorage)\n        try:\n            record = await storage.find_record(\n                self.RECORD_TYPE_METADATA,\n                {\"key\": key, \"connection_id\": self.connection_id},\n            )\n            await storage.delete_record(record)\n        except StorageNotFoundError as err:\n            raise KeyError(f\"{key} not found in connection metadata\") from err\n\n    async def metadata_get_all(self, session: ProfileSession) -> dict:\n        \"\"\"Return all custom metadata associated with this connection.\n\n        Args:\n            session (ProfileSession): session used for storage\n\n        Returns:\n            dict: dictionary representation of all metadata values\n\n        \"\"\"\n        assert self.connection_id\n        storage: BaseStorage = session.inject(BaseStorage)\n        records = await storage.find_all_records(\n            self.RECORD_TYPE_METADATA,\n            {\"connection_id\": self.connection_id},\n        )\n        return {record.tags[\"key\"]: json.loads(record.value) for record in records}\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Comparison between records.\"\"\"\n        return super().__eq__(other)\n\n\nclass OobRecordSchema(BaseExchangeSchema):\n    \"\"\"Schema to allow serialization/deserialization of invitation records.\"\"\"\n\n    class Meta:\n        \"\"\"OobRecordSchema metadata.\"\"\"\n\n        model_class = OobRecord\n\n    oob_id = fields.Str(\n        required=True,\n        metadata={\"description\": \"Oob record identifier\", \"example\": UUID4_EXAMPLE},\n    )\n    state = fields.Str(\n        required=True,\n        validate=validate.OneOf(\n            OobRecord.get_attributes_by_prefix(\"STATE_\", walk_mro=True)\n        ),\n        metadata={\n            \"description\": \"Out of band message exchange state\",\n            \"example\": OobRecord.STATE_AWAIT_RESPONSE,\n        },\n    )\n    invi_msg_id = fields.Str(\n        required=True,\n        metadata={\n            \"description\": \"Invitation message identifier\",\n            \"example\": UUID4_EXAMPLE,\n        },\n    )\n    invitation = fields.Nested(\n        InvitationMessageSchema(),\n        required=True,\n        metadata={\"description\": \"Out of band invitation message\"},\n    )\n\n    their_service = fields.Nested(ServiceDecoratorSchema(), required=False)\n\n    connection_id = fields.Str(\n        required=False,\n        metadata={\n            \"description\": \"Connection record identifier\",\n            \"example\": UUID4_EXAMPLE,\n        },\n    )\n\n    attach_thread_id = fields.Str(\n        required=False,\n        metadata={\n            \"description\": \"Connection record identifier\",\n            \"example\": UUID4_EXAMPLE,\n        },\n    )\n\n    our_recipient_key = fields.Str(\n        required=False,\n        metadata={\n            \"description\": \"Recipient key used for oob invitation\",\n            \"example\": UUID4_EXAMPLE,\n        },\n    )\n\n    role = fields.Str(\n        required=False,\n        validate=validate.OneOf(\n            OobRecord.get_attributes_by_prefix(\"ROLE_\", walk_mro=False)\n        ),\n        metadata={\"description\": \"OOB Role\", \"example\": OobRecord.ROLE_RECEIVER},\n    )\n\n    multi_use = fields.Boolean(\n        required=False,\n        metadata={\n            \"description\": \"Allow for multiple uses of the oobinvitation\",\n            \"example\": True,\n        },\n    )\n", "path": "aries_cloudagent/protocols/out_of_band/v1_0/models/oob_record.py"}], "after_files": [{"content": "\"\"\"Record for out of band invitations.\"\"\"\n\nimport json\nfrom typing import Any, Mapping, Optional, Union\n\nfrom marshmallow import fields, validate\n\nfrom .....connections.models.conn_record import ConnRecord\nfrom .....core.profile import ProfileSession\nfrom .....messaging.decorators.service_decorator import (\n    ServiceDecorator,\n    ServiceDecoratorSchema,\n)\nfrom .....messaging.models.base_record import BaseExchangeRecord, BaseExchangeSchema\nfrom .....messaging.valid import UUID4_EXAMPLE\nfrom .....storage.base import BaseStorage\nfrom .....storage.error import StorageNotFoundError\nfrom .....storage.record import StorageRecord\nfrom ..messages.invitation import InvitationMessage, InvitationMessageSchema\n\n\nclass OobRecord(BaseExchangeRecord):\n    \"\"\"Represents an out of band record.\"\"\"\n\n    class Meta:\n        \"\"\"OobRecord metadata.\"\"\"\n\n        schema_class = \"OobRecordSchema\"\n\n    RECORD_TYPE = \"oob_record\"\n    RECORD_TYPE_METADATA = ConnRecord.RECORD_TYPE_METADATA\n    RECORD_ID_NAME = \"oob_id\"\n    RECORD_TOPIC = \"out_of_band\"\n    TAG_NAMES = {\n        \"invi_msg_id\",\n        \"attach_thread_id\",\n        \"our_recipient_key\",\n        \"connection_id\",\n        \"reuse_msg_id\",\n    }\n\n    STATE_INITIAL = \"initial\"\n    STATE_PREPARE_RESPONSE = \"prepare-response\"\n    STATE_AWAIT_RESPONSE = \"await-response\"\n    STATE_NOT_ACCEPTED = \"reuse-not-accepted\"\n    STATE_ACCEPTED = \"reuse-accepted\"\n    STATE_DONE = \"done\"\n\n    ROLE_SENDER = \"sender\"\n    ROLE_RECEIVER = \"receiver\"\n\n    def __init__(\n        self,\n        *,\n        state: str,\n        invi_msg_id: str,\n        role: str,\n        invitation: Union[InvitationMessage, Mapping[str, Any]],\n        their_service: Optional[Union[ServiceDecorator, Mapping[str, Any]]] = None,\n        connection_id: Optional[str] = None,\n        reuse_msg_id: Optional[str] = None,\n        oob_id: Optional[str] = None,\n        attach_thread_id: Optional[str] = None,\n        our_recipient_key: Optional[str] = None,\n        our_service: Optional[Union[ServiceDecorator, Mapping[str, Any]]] = None,\n        multi_use: bool = False,\n        trace: bool = False,\n        **kwargs,\n    ):\n        \"\"\"Initialize a new OobRecord.\"\"\"\n        super().__init__(oob_id, state, trace=trace, **kwargs)\n        self._id = oob_id\n        self.state = state\n        self.invi_msg_id = invi_msg_id\n        self.role = role\n        self._invitation = InvitationMessage.serde(invitation)\n        self.connection_id = connection_id\n        self.reuse_msg_id = reuse_msg_id\n        self._their_service = ServiceDecorator.serde(their_service)\n        self._our_service = ServiceDecorator.serde(our_service)\n        self.attach_thread_id = attach_thread_id\n        self.our_recipient_key = our_recipient_key\n        self.multi_use = multi_use\n        self.trace = trace\n\n    @property\n    def oob_id(self) -> str:\n        \"\"\"Accessor for the ID associated with this exchange.\"\"\"\n        return self._id\n\n    @property\n    def invitation(self) -> Optional[InvitationMessage]:\n        \"\"\"Accessor; get deserialized view.\"\"\"\n        return None if self._invitation is None else self._invitation.de\n\n    @invitation.setter\n    def invitation(self, value):\n        \"\"\"Setter; store de/serialized views.\"\"\"\n        self._invitation = InvitationMessage.serde(value)\n\n    @property\n    def our_service(self) -> Optional[ServiceDecorator]:\n        \"\"\"Accessor; get deserialized view.\"\"\"\n        return None if self._our_service is None else self._our_service.de\n\n    @our_service.setter\n    def our_service(self, value: Union[ServiceDecorator, Mapping[str, Any]]):\n        \"\"\"Setter; store de/serialized views.\"\"\"\n        self._our_service = ServiceDecorator.serde(value)\n\n    @property\n    def their_service(self) -> Optional[ServiceDecorator]:\n        \"\"\"Accessor; get deserialized view.\"\"\"\n        return None if self._their_service is None else self._their_service.de\n\n    @their_service.setter\n    def their_service(self, value: Union[ServiceDecorator, Mapping[str, Any]]):\n        \"\"\"Setter; store de/serialized vies.\"\"\"\n        self._their_service = ServiceDecorator.serde(value)\n\n    @property\n    def record_value(self) -> dict:\n        \"\"\"Accessor for the JSON record value generated for this invitation.\"\"\"\n        return {\n            **{\n                prop: getattr(self, prop)\n                for prop in (\n                    \"state\",\n                    \"their_service\",\n                    \"connection_id\",\n                    \"role\",\n                    \"invi_msg_id\",\n                    \"multi_use\",\n                )\n            },\n            **{\n                prop: getattr(self, f\"_{prop}\").ser\n                for prop in (\"invitation\", \"our_service\", \"their_service\")\n                if getattr(self, prop) is not None\n            },\n        }\n\n    async def delete_record(self, session: ProfileSession):\n        \"\"\"Perform connection record deletion actions.\n\n        Args:\n            session (ProfileSession): session\n\n        \"\"\"\n        await super().delete_record(session)\n\n        # Delete metadata\n        if self.connection_id:\n            storage = session.inject(BaseStorage)\n            await storage.delete_all_records(\n                self.RECORD_TYPE_METADATA,\n                {\"connection_id\": self.connection_id},\n            )\n\n    async def metadata_get(\n        self, session: ProfileSession, key: str, default: Any = None\n    ) -> Any:\n        \"\"\"Retrieve arbitrary metadata associated with this connection.\n\n        Args:\n            session (ProfileSession): session used for storage\n            key (str): key identifying metadata\n            default (Any): default value to get; type should be a JSON\n                compatible value.\n\n        Returns:\n            Any: metadata stored by key\n\n        \"\"\"\n        assert self.connection_id\n        storage: BaseStorage = session.inject(BaseStorage)\n        try:\n            record = await storage.find_record(\n                self.RECORD_TYPE_METADATA,\n                {\"key\": key, \"connection_id\": self.connection_id},\n            )\n            return json.loads(record.value)\n        except StorageNotFoundError:\n            return default\n\n    async def metadata_set(self, session: ProfileSession, key: str, value: Any):\n        \"\"\"Set arbitrary metadata associated with this connection.\n\n        Args:\n            session (ProfileSession): session used for storage\n            key (str): key identifying metadata\n            value (Any): value to set\n        \"\"\"\n        assert self.connection_id\n        value = json.dumps(value)\n        storage: BaseStorage = session.inject(BaseStorage)\n        try:\n            record = await storage.find_record(\n                self.RECORD_TYPE_METADATA,\n                {\"key\": key, \"connection_id\": self.connection_id},\n            )\n            await storage.update_record(record, value, record.tags)\n        except StorageNotFoundError:\n            record = StorageRecord(\n                self.RECORD_TYPE_METADATA,\n                value,\n                {\"key\": key, \"connection_id\": self.connection_id},\n            )\n            await storage.add_record(record)\n\n    async def metadata_delete(self, session: ProfileSession, key: str):\n        \"\"\"Delete custom metadata associated with this connection.\n\n        Args:\n            session (ProfileSession): session used for storage\n            key (str): key of metadata to delete\n        \"\"\"\n        assert self.connection_id\n        storage: BaseStorage = session.inject(BaseStorage)\n        try:\n            record = await storage.find_record(\n                self.RECORD_TYPE_METADATA,\n                {\"key\": key, \"connection_id\": self.connection_id},\n            )\n            await storage.delete_record(record)\n        except StorageNotFoundError as err:\n            raise KeyError(f\"{key} not found in connection metadata\") from err\n\n    async def metadata_get_all(self, session: ProfileSession) -> dict:\n        \"\"\"Return all custom metadata associated with this connection.\n\n        Args:\n            session (ProfileSession): session used for storage\n\n        Returns:\n            dict: dictionary representation of all metadata values\n\n        \"\"\"\n        assert self.connection_id\n        storage: BaseStorage = session.inject(BaseStorage)\n        records = await storage.find_all_records(\n            self.RECORD_TYPE_METADATA,\n            {\"connection_id\": self.connection_id},\n        )\n        return {record.tags[\"key\"]: json.loads(record.value) for record in records}\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Comparison between records.\"\"\"\n        return super().__eq__(other)\n\n\nclass OobRecordSchema(BaseExchangeSchema):\n    \"\"\"Schema to allow serialization/deserialization of invitation records.\"\"\"\n\n    class Meta:\n        \"\"\"OobRecordSchema metadata.\"\"\"\n\n        model_class = OobRecord\n\n    oob_id = fields.Str(\n        required=True,\n        metadata={\"description\": \"Oob record identifier\", \"example\": UUID4_EXAMPLE},\n    )\n    state = fields.Str(\n        required=True,\n        validate=validate.OneOf(\n            OobRecord.get_attributes_by_prefix(\"STATE_\", walk_mro=True)\n        ),\n        metadata={\n            \"description\": \"Out of band message exchange state\",\n            \"example\": OobRecord.STATE_AWAIT_RESPONSE,\n        },\n    )\n    invi_msg_id = fields.Str(\n        required=True,\n        metadata={\n            \"description\": \"Invitation message identifier\",\n            \"example\": UUID4_EXAMPLE,\n        },\n    )\n    invitation = fields.Nested(\n        InvitationMessageSchema(),\n        required=True,\n        metadata={\"description\": \"Out of band invitation message\"},\n    )\n\n    their_service = fields.Nested(ServiceDecoratorSchema(), required=False)\n\n    connection_id = fields.Str(\n        required=False,\n        metadata={\n            \"description\": \"Connection record identifier\",\n            \"example\": UUID4_EXAMPLE,\n        },\n    )\n\n    attach_thread_id = fields.Str(\n        required=False,\n        metadata={\n            \"description\": \"Connection record identifier\",\n            \"example\": UUID4_EXAMPLE,\n        },\n    )\n\n    our_recipient_key = fields.Str(\n        required=False,\n        metadata={\n            \"description\": \"Recipient key used for oob invitation\",\n            \"example\": UUID4_EXAMPLE,\n        },\n    )\n\n    role = fields.Str(\n        required=False,\n        validate=validate.OneOf(\n            OobRecord.get_attributes_by_prefix(\"ROLE_\", walk_mro=False)\n        ),\n        metadata={\"description\": \"OOB Role\", \"example\": OobRecord.ROLE_RECEIVER},\n    )\n\n    multi_use = fields.Boolean(\n        required=False,\n        metadata={\n            \"description\": \"Allow for multiple uses of the oobinvitation\",\n            \"example\": True,\n        },\n    )\n", "path": "aries_cloudagent/protocols/out_of_band/v1_0/models/oob_record.py"}]}