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 +59 -0
- attp_client/client.py +119 -0
- attp_client/consts.py +1 -0
- attp_client/errors/attp_exception.py +12 -0
- attp_client/errors/correlated_rpc_exception.py +21 -0
- attp_client/errors/dead_session.py +7 -0
- attp_client/errors/not_found.py +2 -0
- attp_client/errors/serialization_error.py +6 -0
- attp_client/errors/unauthenticated_error.py +2 -0
- attp_client/inference.py +131 -0
- attp_client/interfaces/catalogs/catalog.py +6 -0
- attp_client/interfaces/error.py +6 -0
- attp_client/interfaces/handshake/auth.py +9 -0
- attp_client/interfaces/handshake/hello.py +13 -0
- attp_client/interfaces/handshake/ready.py +11 -0
- attp_client/interfaces/inference/enums/message_data_type.py +14 -0
- attp_client/interfaces/inference/enums/message_emergency_type.py +11 -0
- attp_client/interfaces/inference/enums/message_type.py +18 -0
- attp_client/interfaces/inference/message.py +51 -0
- attp_client/interfaces/inference/tool.py +6 -0
- attp_client/interfaces/route_mappings.py +12 -0
- attp_client/misc/fixed_basemodel.py +63 -0
- attp_client/misc/serializable.py +69 -0
- attp_client/router.py +113 -0
- attp_client/session.py +316 -0
- attp_client/tools.py +59 -0
- attp_client/types/route_mapping.py +14 -0
- attp_client/utils/context_awaiter.py +40 -0
- attp_client/utils/route_mapper.py +18 -0
- attp_client/utils/serializer.py +25 -0
- attp_client-0.0.1.dist-info/METADATA +278 -0
- attp_client-0.0.1.dist-info/RECORD +33 -0
- attp_client-0.0.1.dist-info/WHEEL +4 -0
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})"
|
attp_client/inference.py
ADDED
@@ -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,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,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,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}'")
|