attp-client 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
attp_client/catalog.py ADDED
@@ -0,0 +1,59 @@
1
+ from typing import Any, Callable, MutableMapping
2
+ from attp_client.errors.not_found import NotFoundError
3
+ from attp_client.tools import ToolsManager
4
+
5
+
6
+ class AttpCatalog:
7
+ attached_tools: MutableMapping[str, Callable[..., Any]]
8
+ tool_name_to_id_symlink: MutableMapping[str, str]
9
+
10
+ def __init__(
11
+ self,
12
+ id: int,
13
+ catalog_name: str,
14
+ manager: ToolsManager
15
+ ) -> None:
16
+ self.id = id
17
+ self.catalog_name = catalog_name
18
+ self.tool_manager = manager
19
+ self.attached_tools = {}
20
+ self.tool_name_to_id_symlink = {}
21
+
22
+ async def attach_tool(
23
+ self,
24
+ callback: Callable[..., Any],
25
+ name: str,
26
+ description: str | None = None,
27
+ schema_id: str | None = None,
28
+ *,
29
+ return_direct: bool = False,
30
+ schema_ver: str = "1.0",
31
+ timeout_ms: float = 20000,
32
+ idempotent: bool = False
33
+ ):
34
+ assigned_id = await self.tool_manager.register(
35
+ self.catalog_name,
36
+ name=name,
37
+ description=description,
38
+ schema_id=schema_id,
39
+ return_direct=return_direct,
40
+ schema_ver=schema_ver,
41
+ timeout_ms=timeout_ms,
42
+ idempotent=idempotent
43
+ )
44
+
45
+ self.attached_tools[str(assigned_id)] = callback
46
+ self.tool_name_to_id_symlink[name] = str(assigned_id)
47
+ return assigned_id
48
+
49
+ async def detatch_tool(
50
+ self,
51
+ name: str
52
+ ):
53
+ tool_id = self.tool_name_to_id_symlink.get(name)
54
+
55
+ if not tool_id:
56
+ raise NotFoundError(f"Tool {name} not marked as registered and wasn't found in the catalog {self.catalog_name}.")
57
+
58
+ await self.tool_manager.unregister(self.catalog_name, tool_id)
59
+ return tool_id
attp_client/client.py ADDED
@@ -0,0 +1,119 @@
1
+ import asyncio
2
+ from functools import cached_property
3
+ from logging import Logger, getLogger
4
+ from typing import Any, Callable, Literal
5
+ from attp_core.rs_api import AttpClientSession, Limits
6
+ from reactivex import Subject
7
+ from attp_core.rs_api import PyAttpMessage
8
+
9
+ from attp_client.catalog import AttpCatalog
10
+ from attp_client.inference import AttpInferenceAPI
11
+ from attp_client.interfaces.catalogs.catalog import ICatalogResponse
12
+ from attp_client.misc.serializable import Serializable
13
+ from attp_client.router import AttpRouter
14
+ from attp_client.session import SessionDriver
15
+ from attp_client.tools import ToolsManager
16
+ from attp_client.types.route_mapping import AttpRouteMapping, RouteType
17
+
18
+ class ATTPClient:
19
+
20
+ is_connected: bool
21
+ client: AttpClientSession
22
+ session: SessionDriver | None
23
+ routes: list[AttpRouteMapping]
24
+ inference: AttpInferenceAPI
25
+
26
+ def __init__(
27
+ self,
28
+ agt_token: str,
29
+ organization_id: int,
30
+ *,
31
+ connection_url: str | None = None,
32
+ max_retries: int = 20,
33
+ limits: Limits | None = None,
34
+ logger: Logger | None = None
35
+ ):
36
+ self.__agt_token = agt_token
37
+ self.organization_id = organization_id
38
+ self.connection_url = connection_url or "attp://localhost:6563"
39
+
40
+ self.client = AttpClientSession(self.connection_url)
41
+ self.session = None
42
+ self.max_retries = max_retries
43
+ self.limits = limits or Limits(max_payload_size=50000)
44
+ self.logger = logger
45
+
46
+ self.route_increment_index = 2
47
+
48
+ self.responder = Subject[PyAttpMessage]()
49
+ self.routes = []
50
+
51
+ async def connect(self):
52
+ # Open the connection
53
+ client = await self.client.connect(self.max_retries, self.limits)
54
+
55
+ if not client.session:
56
+ raise ConnectionError("Failed to connect to ATTP server after 10 attempts!")
57
+
58
+ self.session = SessionDriver(
59
+ client.session,
60
+ agt_token=self.__agt_token,
61
+ organization_id=self.organization_id,
62
+ # route_mappings=self.routes,
63
+ logger=self.logger or getLogger("Ascender Framework")
64
+ )
65
+ asyncio.create_task(self.session.start_listener())
66
+ # Send an authentication frame as soon as connection estabilishes with agenthub
67
+ await self.session.authenticate(self.routes)
68
+ asyncio.create_task(self.session.listen(self.responder))
69
+
70
+ self.router = AttpRouter(self.responder, self.session)
71
+ self.inference = AttpInferenceAPI(self.router)
72
+
73
+ async def close(self):
74
+ if self.session:
75
+ await self.session.close()
76
+ self.session = None
77
+ self.is_connected = False
78
+
79
+ @cached_property
80
+ def tools(self):
81
+ return ToolsManager(self.router)
82
+
83
+ async def catalog(self, catalog_name: str):
84
+ catalog = await self.router.send(
85
+ "tools:catalogs:specific",
86
+ Serializable[dict[str, str]]({"catalog_name": catalog_name}),
87
+ timeout=10,
88
+ expected_response=ICatalogResponse
89
+ )
90
+
91
+ return AttpCatalog(id=catalog.catalog_id, catalog_name=catalog_name, manager=self.tools)
92
+
93
+ def add_event_handler(
94
+ self,
95
+ pattern: str,
96
+ route_type: RouteType,
97
+ callback: Callable[..., Any],
98
+ ):
99
+ if route_type in ["connect", "disconnect"]:
100
+ self.routes.append(
101
+ AttpRouteMapping(
102
+ pattern=pattern,
103
+ route_id=0,
104
+ route_type=route_type,
105
+ callback=callback
106
+ )
107
+ )
108
+ return
109
+
110
+ self.routes.append(
111
+ AttpRouteMapping(
112
+ pattern=pattern,
113
+ route_id=self.route_increment_index,
114
+ route_type=route_type,
115
+ callback=callback
116
+ )
117
+ )
118
+
119
+ self.route_increment_index += 1
attp_client/consts.py ADDED
@@ -0,0 +1 @@
1
+ ATTP_VERSION=b"01"
@@ -0,0 +1,12 @@
1
+ from typing import Any, Mapping
2
+
3
+ from attp_client.interfaces.error import IErr
4
+
5
+
6
+ class AttpException(Exception):
7
+ def __init__(self, code: str = "UnknownError", *, detail: Mapping[str, Any]) -> None:
8
+ self.code = code
9
+ self.detail = detail
10
+
11
+ def to_ierr(self):
12
+ return IErr(detail={"code": self.code, **self.detail})
@@ -0,0 +1,21 @@
1
+ from typing import Any
2
+ from uuid import UUID
3
+
4
+ from attp_client.interfaces.error import IErr
5
+
6
+ class CorrelatedRPCException(Exception):
7
+ correlation_id: UUID
8
+ def __init__(
9
+ self,
10
+ correlation_id: bytes,
11
+ detail: dict[str, Any] | None = None,
12
+ ):
13
+ self.correlation_id = UUID(bytes=correlation_id)
14
+ self.detail = detail
15
+
16
+ @staticmethod
17
+ def from_err_object(correlation_id: bytes, err: IErr):
18
+ return CorrelatedRPCException(correlation_id, err.detail)
19
+
20
+ def __str__(self) -> str:
21
+ return f"[ATTPRPC ERR]: {self.detail}"
@@ -0,0 +1,7 @@
1
+ class DeadSessionError(Exception):
2
+ def __init__(self, organization_id: int) -> None:
3
+ super().__init__()
4
+ self.organization_id = organization_id
5
+
6
+ def __str__(self) -> str:
7
+ return f"Cannot perform any actions over dead ATTP session (organization_id={self.organization_id})"
@@ -0,0 +1,2 @@
1
+ class NotFoundError(Exception):
2
+ ...
@@ -0,0 +1,6 @@
1
+ class SerializationError(Exception):
2
+ def __init__(self, detail: str) -> None:
3
+ self.detail = detail
4
+
5
+ def __str__(self) -> str:
6
+ return f"Serialization fault: {self.detail}"
@@ -0,0 +1,2 @@
1
+ class UnauthenticatedError(Exception):
2
+ ...
@@ -0,0 +1,131 @@
1
+ import asyncio
2
+ from logging import Logger, getLogger
3
+ from typing import Any, Sequence
4
+ from uuid import UUID
5
+ from attp_client.interfaces.inference.message import IMessageResponse, IMessageDTOV2
6
+ from attp_client.misc.serializable import Serializable
7
+ from attp_client.router import AttpRouter
8
+
9
+
10
+ class AttpInferenceAPI:
11
+ """
12
+ AttpInferenceAPI provides methods to interact with the inference API of the AgentHub.
13
+ """
14
+ def __init__(
15
+ self,
16
+ router: AttpRouter,
17
+ logger: Logger = getLogger("Ascender Framework")
18
+ ) -> None:
19
+ self.router = router
20
+ self.logger = logger
21
+
22
+ async def invoke_inference(
23
+ self,
24
+ agent_id: int | None = None,
25
+ agent_name: str | None = None,
26
+ *,
27
+ input_configuration: dict[str, Any] | None = None,
28
+ messages: Sequence[IMessageDTOV2] | None = None,
29
+ stream: bool = False,
30
+ timeout: float = 200
31
+ ) -> IMessageResponse:
32
+ """
33
+ Invoke inference for a specific agent by its ID or name.
34
+
35
+ Parameters
36
+ ----------
37
+ agent_id : int | None, optional
38
+ The ID of the agent to invoke inference for, by default None
39
+ agent_name : str | None, optional
40
+ The name of the agent to invoke inference for, by default None
41
+ _description_, by default None
42
+ input_configuration : dict[str, Any] | None, optional
43
+ The configuration for the input, by default None
44
+ messages : Sequence[IMessageDTOV2] | None, optional
45
+ The messages to include in the inference, by default None
46
+ stream : bool, optional
47
+ Whether to stream the response, by default False
48
+ timeout : float, optional
49
+ The timeout for the request, by default 200
50
+
51
+ Returns
52
+ -------
53
+ IMessageResponse
54
+ The response from the inference request.
55
+
56
+ Raises
57
+ ------
58
+ ValueError
59
+ If neither 'agent_id' nor 'agent_name' is provided.
60
+ ValueError
61
+ If both 'agent_id' and 'agent_name' are provided.
62
+ """
63
+
64
+ if not agent_id and not agent_name:
65
+ raise ValueError("Required at least one identification specifier, 'agent_id' or 'agent_name'")
66
+
67
+ if agent_id and agent_name:
68
+ raise ValueError("Cannot find agent by two identification specifiers, use only one!")
69
+
70
+ response = await self.router.send("messages:inference:invoke", Serializable[dict[str, Any]]({
71
+ "agent_id": agent_id,
72
+ "agent_name": agent_name,
73
+ "input_configuration": input_configuration,
74
+ "messages": [message.model_dump(mode="json") for message in (messages or [])],
75
+ "stream": stream
76
+ }), timeout=timeout, expected_response=IMessageResponse)
77
+
78
+ return response
79
+
80
+ async def invoke_chat_inference(
81
+ self,
82
+ messages: Sequence[IMessageDTOV2],
83
+ chat_id: UUID,
84
+ stream: bool = False,
85
+ timeout: float = 200,
86
+ ) -> IMessageResponse:
87
+ """
88
+ Invoke inference for a specific chat by its chat_id.
89
+
90
+ Parameters
91
+ ----------
92
+ messages : Sequence[IMessageDTOV2]
93
+ The messages to include in the inference.
94
+ chat_id : UUID
95
+ The ID of the chat to invoke inference for.
96
+ stream : bool, optional
97
+ Whether to stream the response, by default False.
98
+ timeout : float, optional
99
+ The timeout for the request, by default 200.
100
+
101
+ Returns
102
+ -------
103
+ IMessageResponse
104
+ The response from the inference request.
105
+ """
106
+ for message in messages:
107
+ await self.router.send("messages:append", message, timeout=5)
108
+
109
+ response = await self.router.send(
110
+ "messages:chat:invoke",
111
+ Serializable[dict[str, Any]]({"chat_id": str(chat_id), "stream": stream}),
112
+ timeout=timeout,
113
+ # expected_response=IMessageResponse
114
+ )
115
+
116
+ return response
117
+
118
+ async def append_message(self, message: IMessageDTOV2 | Sequence[IMessageDTOV2]) -> None:
119
+ """
120
+ Append a message or a sequence of messages to the current chat.
121
+
122
+ Parameters
123
+ ----------
124
+ message : IMessageDTOV2 | Sequence[IMessageDTOV2]
125
+ The message or messages to append.
126
+ """
127
+ if isinstance(message, Sequence):
128
+ for msg in message:
129
+ await self.router.emit("messages:append", msg)
130
+ else:
131
+ await self.router.emit("messages:append", message)
@@ -0,0 +1,6 @@
1
+ from attp_client.misc.fixed_basemodel import FixedBaseModel
2
+
3
+
4
+ class ICatalogResponse(FixedBaseModel):
5
+ catalog_id: int
6
+ organization_id: int
@@ -0,0 +1,6 @@
1
+ from typing import Any
2
+ from attp_client.misc.fixed_basemodel import FixedBaseModel
3
+
4
+
5
+ class IErr(FixedBaseModel):
6
+ detail: dict[str, Any]
@@ -0,0 +1,9 @@
1
+ from typing import Annotated
2
+ from pydantic import Field
3
+ from typing_extensions import Doc
4
+ from attp_client.misc.fixed_basemodel import FixedBaseModel
5
+
6
+
7
+ class IAuth(FixedBaseModel):
8
+ token: Annotated[str | None, Doc("Provided AGT token for authentication")] = Field(None)
9
+ organization_id: Annotated[int, Doc("ID of organization.")]
@@ -0,0 +1,13 @@
1
+ from typing import Annotated
2
+ from pydantic import Field
3
+ from typing_extensions import Doc
4
+
5
+ from attp_client.interfaces.route_mappings import IRouteMapping
6
+ from attp_client.misc.fixed_basemodel import FixedBaseModel
7
+
8
+
9
+ class IHello(FixedBaseModel):
10
+ proto: Annotated[str, Doc("Protocol ID")] = Field("ATTP")
11
+ ver: Annotated[str, Doc("Semver for example: 1.0")] = Field("1.0")
12
+ caps: Annotated[list[str], Doc("Capability flags e.g. ['schemas/json', 'stream', 'hb']")] = Field(default_factory=list)
13
+ mapping: Annotated[list[IRouteMapping], Doc("Router pattern mappings, ATTP for optimization uses numbers and IDs for routing requests instead of human friendly patterns")]
@@ -0,0 +1,11 @@
1
+ from typing import Annotated, Sequence
2
+ from typing_extensions import Doc
3
+
4
+ from attp_client.interfaces.route_mappings import IRouteMapping
5
+ from attp_client.misc.fixed_basemodel import FixedBaseModel
6
+
7
+
8
+ class IReady(FixedBaseModel):
9
+ session: Annotated[str, Doc("Server-issued session id (per connection)")] # REQUIRED
10
+ server_time: Annotated[str | None, Doc("Server ISO time for skew hints")] = None
11
+ server_routes: Sequence[IRouteMapping]
@@ -0,0 +1,14 @@
1
+ from enum import Enum
2
+
3
+
4
+ class MessageDataTypeEnum(Enum):
5
+ """
6
+ Enumeration for data types available to be sent by users/AI
7
+ """
8
+
9
+ JPEG_IMAGE = 'jpeg_image' # .jpeg .jpg
10
+ FFMPEG_VIDEO = 'ffmpeg_video' # mp4
11
+ PLAIN_TEXT = 'plain_text' # txt, that would be also a simple chat message
12
+ WAVE_AUDIO = 'wave_audio' # wav
13
+ PLACEHOLDER_KEY = 'placeholder_key'
14
+ NONE = 'none'
@@ -0,0 +1,11 @@
1
+ from enum import Enum
2
+
3
+
4
+
5
+ class MessageEmergencyTypeEnum(Enum):
6
+ LOW = "low"
7
+ MEDIUM = "medium"
8
+ HIGH = "high"
9
+ IMMEDIATE = "immediate"
10
+
11
+
@@ -0,0 +1,18 @@
1
+ from enum import Enum
2
+
3
+
4
+ class MessageTypeEnum(Enum):
5
+ TOOL_STATE_EVENT = 'tool_state_event'
6
+
7
+ OPERATOR_CALLED_EVENT = 'operator_called_event'
8
+
9
+ SYSTEM_EVENT = 'system_event' # user is created, or added to chat or smth
10
+ SYSTEM_MESSAGE = 'system_message'
11
+
12
+ USER_MESSAGE = 'user_message'
13
+ AI_MESSAGE = 'ai_message'
14
+ #
15
+ OPERATOR_MESSAGE = 'operator_message'
16
+ CUSTOMER_MESSAGE = 'customer_message'
17
+
18
+ PLACEHOLDER_MESSAGE = 'placeholder_message' # content is the key for placeholder
@@ -0,0 +1,51 @@
1
+ from typing import Literal
2
+ from uuid import UUID
3
+ from attp_client.interfaces.inference.enums.message_emergency_type import MessageEmergencyTypeEnum
4
+ from attp_client.interfaces.inference.enums.message_type import MessageTypeEnum
5
+ from attp_client.interfaces.inference.tool import ToolV2
6
+ from attp_client.misc.fixed_basemodel import FixedBaseModel
7
+
8
+
9
+ class BaseMessage(FixedBaseModel):
10
+ def __init__(self, *args, **kwargs):
11
+ super().__init__(**kwargs)
12
+
13
+ class IAttachmentDTO(BaseMessage):
14
+ file_id: int
15
+
16
+ class IMessageDTOV2(FixedBaseModel):
17
+ """
18
+ The DTO Object of Message in AgentHub, contains message content and tool execution data.
19
+ """
20
+ content: dict | str | None
21
+ message_type: MessageTypeEnum
22
+ attachments: list[IAttachmentDTO] | None = None
23
+
24
+ reply_to_message_id: int | None = None
25
+ chat_id: UUID
26
+ agent_id: int | None = None
27
+ user_id: int | None = None
28
+ client_id: str | None = None
29
+
30
+ tool_called: ToolV2 | None = None
31
+ tool_status: Literal['started', 'finished', 'error'] | None = None
32
+ tool_started_input: str | None = None
33
+ tool_finished_output: str | None = None
34
+ tool_error_detail: str | None = None
35
+
36
+ specialist_required: MessageEmergencyTypeEnum | None = None
37
+
38
+ def to_wrap(self) -> dict:
39
+ w = self.model_dump()
40
+ del w['attachments']
41
+
42
+ return w
43
+
44
+
45
+ class IMessageResponse(IMessageDTOV2):
46
+ """
47
+ Response model of AgentHub Message, it represents [MessageDTO](agenthub/dtos/message_dto.py).
48
+ But it contains additional value as ID
49
+ """
50
+ id: int
51
+ """ID of Message in database"""
@@ -0,0 +1,6 @@
1
+ from attp_client.misc.fixed_basemodel import FixedBaseModel
2
+
3
+
4
+ class ToolV2(FixedBaseModel):
5
+ name: str
6
+ db_id: int | None = None
@@ -0,0 +1,12 @@
1
+ from attp_client.types.route_mapping import AttpRouteMapping, RouteType
2
+ from attp_client.misc.fixed_basemodel import FixedBaseModel
3
+
4
+
5
+ class IRouteMapping(FixedBaseModel):
6
+ pattern: str
7
+ route_id: int
8
+ route_type: RouteType
9
+
10
+ @staticmethod
11
+ def from_route_mapper(mapper: AttpRouteMapping):
12
+ return IRouteMapping(pattern=mapper.pattern, route_id=mapper.route_id, route_type=mapper.route_type)
@@ -0,0 +1,63 @@
1
+ from typing import Any, Self
2
+
3
+ import msgpack
4
+ from pydantic import BaseModel
5
+
6
+
7
+ class FixedBaseModel(BaseModel):
8
+ @classmethod
9
+ def serialize(cls, entity, **kwargs):
10
+ serialized_data = {
11
+ key: value
12
+ for key, value in entity.__dict__.items()
13
+ if not callable(value) and not key.startswith('_')
14
+ }
15
+
16
+ for key, value in kwargs.items():
17
+ serialized_data[key] = value
18
+
19
+ # noinspection PyArgumentList
20
+ pydantic_instance = cls(**serialized_data)
21
+
22
+ return pydantic_instance
23
+
24
+ @classmethod
25
+ def s(cls, entity, **kwargs) -> Self:
26
+ return cls.serialize(entity, **kwargs)
27
+
28
+ @classmethod
29
+ def mps(
30
+ cls,
31
+ obj: bytes,
32
+ strict: bool | None = None,
33
+ from_attributes: bool | None = None,
34
+ context: Any | None = None,
35
+ by_alias: bool | None = None,
36
+ by_name: bool | None = None,
37
+ mp_configs: dict[str, Any] | None = None
38
+ ):
39
+ """
40
+ Message Pack Serialize
41
+
42
+ Serializes and unpacks the model from the binary by utilizing Message Pack library.
43
+
44
+ Opposite method: `mpd(...)`
45
+
46
+ Parameters
47
+ ----------
48
+ obj : bytes
49
+ Binary packed by Message Pack object.
50
+ """
51
+ obj = msgpack.unpackb(obj, **(mp_configs or {}))
52
+
53
+ return cls.model_validate(obj, strict=strict, from_attributes=from_attributes, context=context, by_alias=by_alias, by_name=by_name)
54
+
55
+ def mpd(self, mp_configs: dict[str, Any] | None = None, **kwargs) -> bytes | None:
56
+ """
57
+ Message Pack Dump
58
+
59
+ Dumps and packs the model to the binary by utilizing Message Pack library.
60
+
61
+ Opposite method: `mps(...)`
62
+ """
63
+ return msgpack.packb(self.model_dump(mode="json", **kwargs), **(mp_configs or {}))
@@ -0,0 +1,69 @@
1
+ import inspect
2
+ from typing import Any, Generic, Self, TypeVar, final
3
+
4
+ import msgpack
5
+ from pydantic import TypeAdapter
6
+
7
+ from attp_client.misc.fixed_basemodel import FixedBaseModel
8
+
9
+ T = TypeVar("T")
10
+
11
+
12
+ class Serializable(Generic[T]):
13
+ def __init__(self, data: T, enable_validation: bool = False) -> None:
14
+ self.data = data
15
+ self.enable_validation = enable_validation
16
+
17
+ self.validate()
18
+
19
+ def validate(self):
20
+ if isinstance(self.data, FixedBaseModel) and self.enable_validation:
21
+ self.data = self.data.model_dump(mode="json")
22
+
23
+ @staticmethod
24
+ def deserialize(obj: bytes, mp_configs: dict[str, Any] | None = None) -> Any:
25
+ return msgpack.unpackb(obj, **(mp_configs or {}))
26
+
27
+ def serialize(self, mp_configs: dict[str, Any] | None = None) -> bytes | None:
28
+ return msgpack.packb(self.data, **(mp_configs or {}))
29
+
30
+ @final
31
+ @classmethod
32
+ def mps(
33
+ cls,
34
+ obj: bytes,
35
+ mp_configs: dict[str, Any] | None = None
36
+ ) -> Self:
37
+ """
38
+ Message Pack Serialize
39
+
40
+ Deserializes and unpacks the model from the binary by utilizing Message Pack library.
41
+
42
+ Opposite method: `mpd(...)`
43
+
44
+ Parameters
45
+ ----------
46
+ obj : bytes
47
+ Binary packed by Message Pack object.
48
+ """
49
+ return cls(data=cls.deserialize(obj, mp_configs=mp_configs))
50
+
51
+ @final
52
+ def mpd(self, mp_configs: dict[str, Any] | None = None) -> bytes | None:
53
+ """
54
+ Message Pack Dump
55
+
56
+ Dumps and packs the model to the binary by utilizing Message Pack library.
57
+
58
+ Opposite method: `mps(...)`
59
+ """
60
+ return self.serialize(mp_configs)
61
+
62
+ def __setattr__(self, name: str, value: Any) -> None:
63
+ caller = inspect.stack()[1].function
64
+ if caller == "__init__" or caller.startswith("_"):
65
+ super().__setattr__(name, value)
66
+ return
67
+
68
+ if name in ["data"]:
69
+ raise TypeError(f"'Serializable' object does not support data mutation for attribute '{name}'")