1+ import base64
12import dataclasses
3+ import datetime
4+ from io import BytesIO
25import typing as t
36
47from .enums import ProcessState , WebhookEvent , MessagePriority
@@ -9,31 +12,47 @@ def snake_to_camel(snake_str):
912 return components [0 ] + "" .join (x .title () for x in components [1 :])
1013
1114
12- @dataclasses .dataclass (frozen = True )
15+ @dataclasses .dataclass (frozen = True , kw_only = True )
1316class Message :
1417 """
1518 Represents an SMS message.
1619
1720 Attributes:
18- message (str): The message text.
19- phone_numbers (List[str]): A list of phone numbers to send the message to.
20- with_delivery_report (bool): Whether to request a delivery report. Defaults to True.
21- is_encrypted (bool): Whether the message is encrypted. Defaults to False.
22- id (Optional[str]): The message ID. Defaults to None.
23- ttl (Optional[int]): The time-to-live in seconds. Defaults to None.
24- sim_number (Optional[int]): The SIM number to use. Defaults to None.
25- priority (Optional[MessagePriority]): The priority of the message. Defaults to None.
21+ phone_numbers (List[str]): Recipients (phone numbers).
22+ tex_message (Optional[TextMessage]): Text message.
23+ data_message (Optional[DataMessage]): Data message.
24+ priority (Optional[MessagePriority]): Priority.
25+ sim_number (Optional[int]): SIM card number (1-3), if not set - default SIM will be used.
26+ with_delivery_report (Optional[bool]): With delivery report.
27+ is_encrypted (Optional[bool]): Is encrypted.
28+ ttl (Optional[int]): Time to live in seconds (conflicts with `validUntil`).
29+ valid_until (Optional[str]): Valid until (conflicts with `ttl`).
30+ id (Optional[str]): ID (if not set - will be generated).
31+ device_id (Optional[str]): Optional device ID for explicit selection.
2632 """
2733
28- message : str
2934 phone_numbers : t .List [str ]
35+ text_message : t .Optional ["TextMessage" ] = None
36+ data_message : t .Optional ["DataMessage" ] = None
37+
38+ priority : t .Optional [MessagePriority ] = None
39+ sim_number : t .Optional [int ] = None
3040 with_delivery_report : bool = True
3141 is_encrypted : bool = False
3242
33- id : t .Optional [str ] = None
3443 ttl : t .Optional [int ] = None
35- sim_number : t .Optional [int ] = None
36- priority : t .Optional [MessagePriority ] = None
44+ valid_until : t .Optional [datetime .datetime ] = None
45+
46+ id : t .Optional [str ] = None
47+ device_id : t .Optional [str ] = None
48+
49+ @property
50+ def content (self ) -> str :
51+ if self .text_message :
52+ return self .text_message .text
53+ if self .data_message :
54+ return self .data_message .data
55+ raise ValueError ("Message has no content" )
3756
3857 def asdict (self ) -> t .Dict [str , t .Any ]:
3958 """
@@ -43,12 +62,89 @@ def asdict(self) -> t.Dict[str, t.Any]:
4362 Dict[str, Any]: A dictionary representation of the message.
4463 """
4564 return {
46- snake_to_camel (field .name ): getattr (self , field .name )
65+ snake_to_camel (field .name ): (
66+ getattr (self , field .name ).asdict ()
67+ if hasattr (getattr (self , field .name ), "asdict" )
68+ else getattr (self , field .name )
69+ )
4770 for field in dataclasses .fields (self )
4871 if getattr (self , field .name ) is not None
4972 }
5073
5174
75+ @dataclasses .dataclass (frozen = True )
76+ class DataMessage :
77+ """
78+ Represents a data message.
79+
80+ Attributes:
81+ data (str): Base64-encoded payload.
82+ port (int): Destination port.
83+ """
84+
85+ data : str
86+ port : int
87+
88+ def asdict (self ) -> t .Dict [str , t .Any ]:
89+ return {
90+ "data" : self .data ,
91+ "port" : self .port ,
92+ }
93+
94+ @classmethod
95+ def with_bytes (cls , data : bytes , port : int ) -> "DataMessage" :
96+ return cls (
97+ data = base64 .b64encode (data ).decode ("utf-8" ),
98+ port = port ,
99+ )
100+
101+ @classmethod
102+ def from_dict (cls , payload : t .Dict [str , t .Any ]) -> "DataMessage" :
103+ """Creates a DataMessage instance from a dictionary.
104+
105+ Args:
106+ payload: A dictionary containing the data message's data.
107+
108+ Returns:
109+ A DataMessage instance.
110+ """
111+ return cls (
112+ data = payload ["data" ],
113+ port = payload ["port" ],
114+ )
115+
116+
117+ @dataclasses .dataclass (frozen = True )
118+ class TextMessage :
119+ """
120+ Represents a text message.
121+
122+ Attributes:
123+ text (str): Message text.
124+ """
125+
126+ text : str
127+
128+ def asdict (self ) -> t .Dict [str , t .Any ]:
129+ return {
130+ "text" : self .text ,
131+ }
132+
133+ @classmethod
134+ def from_dict (cls , payload : t .Dict [str , t .Any ]) -> "TextMessage" :
135+ """Creates a TextMessage instance from a dictionary.
136+
137+ Args:
138+ payload: A dictionary containing the text message's data.
139+
140+ Returns:
141+ A TextMessage instance.
142+ """
143+ return cls (
144+ text = payload ["text" ],
145+ )
146+
147+
52148@dataclasses .dataclass (frozen = True )
53149class RecipientState :
54150 phone_number : str
@@ -124,3 +220,39 @@ def asdict(self) -> t.Dict[str, t.Any]:
124220 "url" : self .url ,
125221 "event" : self .event .value ,
126222 }
223+
224+
225+ @dataclasses .dataclass (frozen = True )
226+ class Device :
227+ """Represents a device."""
228+
229+ id : str
230+ """The unique identifier of the device."""
231+ name : str
232+ """The name of the device."""
233+
234+ @classmethod
235+ def from_dict (cls , payload : t .Dict [str , t .Any ]) -> "Device" :
236+ """Creates a Device instance from a dictionary."""
237+ return cls (
238+ id = payload ["id" ],
239+ name = payload ["name" ],
240+ )
241+
242+
243+ @dataclasses .dataclass (frozen = True )
244+ class ErrorResponse :
245+ """Represents an error response from the API."""
246+
247+ code : int
248+ """The error code."""
249+ message : str
250+ """The error message."""
251+
252+ @classmethod
253+ def from_dict (cls , payload : t .Dict [str , t .Any ]) -> "ErrorResponse" :
254+ """Creates an ErrorResponse instance from a dictionary."""
255+ return cls (
256+ code = payload ["code" ],
257+ message = payload ["message" ],
258+ )
0 commit comments