hiws 0.1.0__tar.gz

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.
hiws-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tomas Santana
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
hiws-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,95 @@
1
+ Metadata-Version: 2.4
2
+ Name: hiws
3
+ Version: 0.1.0
4
+ Summary: A wrapper for WhatsApp's Cloud API
5
+ Home-page: https://github.com/cervant-ai/hiws
6
+ Author: Tomas Santana
7
+ Author-email: Tomas Santana <tomas@cervant.chat>
8
+ License: MIT
9
+ Project-URL: Homepage, https://github.com/cervant-ai/hiws
10
+ Project-URL: Repository, https://github.com/cervant-ai/hiws.git
11
+ Project-URL: Issues, https://github.com/cervant-ai/hiws/issues
12
+ Keywords: hiws,python,package
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.7
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Requires-Python: >=3.7
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: pydantic>=2.11.7
26
+ Requires-Dist: httpx>=0.28.1
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=6.0; extra == "dev"
29
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
30
+ Requires-Dist: black>=21.0; extra == "dev"
31
+ Requires-Dist: flake8>=3.8.0; extra == "dev"
32
+ Requires-Dist: mypy>=0.800; extra == "dev"
33
+ Dynamic: author
34
+ Dynamic: home-page
35
+ Dynamic: license-file
36
+ Dynamic: requires-python
37
+
38
+ # hiws
39
+
40
+ A Python package for interacting with WhatsApp's Cloud API.
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install hiws
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ ```python
51
+ from hiws import WhatsAppMessenger
52
+
53
+ messenger = WhatsAppMessenger(access_token="YOUR_ACCESS_TOKEN", phone_number_id="YOUR_PHONE_NUMBER_ID")
54
+
55
+ # Example usage
56
+ message_id = await messenger.send_text("Hello, World!", recipient_id="RECIPIENT_ID")
57
+
58
+ print(message_id)
59
+ ```
60
+
61
+ ## Development
62
+
63
+ To install in development mode:
64
+
65
+ ```bash
66
+ git clone https://github.com/cervant-ai/hiws.git
67
+ cd hiws
68
+ pip install -e .
69
+ ```
70
+
71
+ To install development dependencies:
72
+
73
+ ```bash
74
+ pip install -e ".[dev]"
75
+ ```
76
+
77
+ ## Testing
78
+
79
+ ```bash
80
+ pytest
81
+ ```
82
+
83
+ ```bash
84
+ pip install -e ".[dev]"
85
+ ```
86
+
87
+ ## Testing
88
+
89
+ ```bash
90
+ pytest
91
+ ```
92
+
93
+ ## License
94
+
95
+ This project is licensed under the MIT License - see the LICENSE file for details.
hiws-0.1.0/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # hiws
2
+
3
+ A Python package for interacting with WhatsApp's Cloud API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install hiws
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ from hiws import WhatsAppMessenger
15
+
16
+ messenger = WhatsAppMessenger(access_token="YOUR_ACCESS_TOKEN", phone_number_id="YOUR_PHONE_NUMBER_ID")
17
+
18
+ # Example usage
19
+ message_id = await messenger.send_text("Hello, World!", recipient_id="RECIPIENT_ID")
20
+
21
+ print(message_id)
22
+ ```
23
+
24
+ ## Development
25
+
26
+ To install in development mode:
27
+
28
+ ```bash
29
+ git clone https://github.com/cervant-ai/hiws.git
30
+ cd hiws
31
+ pip install -e .
32
+ ```
33
+
34
+ To install development dependencies:
35
+
36
+ ```bash
37
+ pip install -e ".[dev]"
38
+ ```
39
+
40
+ ## Testing
41
+
42
+ ```bash
43
+ pytest
44
+ ```
45
+
46
+ ```bash
47
+ pip install -e ".[dev]"
48
+ ```
49
+
50
+ ## Testing
51
+
52
+ ```bash
53
+ pytest
54
+ ```
55
+
56
+ ## License
57
+
58
+ This project is licensed under the MIT License - see the LICENSE file for details.
@@ -0,0 +1,436 @@
1
+ import httpx
2
+ from hiws.types.exceptions import WhatsappApiException
3
+ from hiws.types import Contact
4
+ from typing import Optional, Any, Dict
5
+
6
+
7
+ BASE_URL = "https://graph.facebook.com/{API_VERSION}"
8
+ MESSAGES_ENDPOINT = "/{phone_number_id}/messages"
9
+ DEFAULT_TIMEOUT = 15.0
10
+
11
+
12
+ class WhatsappMessenger:
13
+ """
14
+ A class to interact with the WhatsApp Cloud API.
15
+
16
+ Attributes:
17
+ access_token (str): The access token for authentication.
18
+ phone_number_id (str): The phone number ID associated with the WhatsApp Business account.
19
+ api_version (str): The version of the WhatsApp API to use.
20
+ request_timeout (float): Timeout for HTTP requests in seconds. Default is 15.0 seconds.
21
+ """
22
+
23
+ access_token: str
24
+ phone_number_id: str
25
+ api_version: str
26
+ request_timeout: float = DEFAULT_TIMEOUT
27
+
28
+ def __init__(
29
+ self,
30
+ access_token: str,
31
+ phone_number_id: str,
32
+ api_version: str = "v23.0",
33
+ request_timeout: float = DEFAULT_TIMEOUT,
34
+ ):
35
+ self.access_token = access_token
36
+ self.phone_number_id = phone_number_id
37
+ self.api_version = api_version
38
+ self.base_url = BASE_URL.format(API_VERSION=self.api_version)
39
+ self.messages_endpoint = MESSAGES_ENDPOINT.format(phone_number_id=self.phone_number_id)
40
+ self.request_timeout = request_timeout
41
+
42
+ async def send_text(
43
+ self,
44
+ recipient_phone_number: str,
45
+ text: str,
46
+ enable_link_preview: bool = True,
47
+ reply_to: Optional[str] = None
48
+ ) -> str:
49
+ """
50
+ Send a text message.
51
+ Args:
52
+ recipient_phone_number (str): The recipient's phone number in international format (without leading '+').
53
+ text (str): The text message to send.
54
+ enable_link_preview (bool): Whether to enable link preview. Default is True.
55
+ reply_to (Optional[str]): Message ID to reply to. Default is None.
56
+
57
+ Returns:
58
+ str: The message ID of the sent message.
59
+
60
+ Raises:
61
+ WhatsappApiException: If there is an error sending the message.
62
+
63
+ ## Note
64
+ WhatsApp Cloud API has a limit of 4096 characters for text messages.
65
+ Messages longer than this will not be sent.
66
+
67
+ ## Documentation
68
+ https://developers.facebook.com/docs/whatsapp/cloud-api/messages/text-messages
69
+ """
70
+ payload = {
71
+ "messaging_product": "whatsapp",
72
+ "recipient_type": "individual",
73
+ "to": recipient_phone_number,
74
+ "type": "text",
75
+ "text": {"preview_url": enable_link_preview, "body": text},
76
+ }
77
+
78
+ if reply_to:
79
+ payload["context"] = {"message_id": reply_to}
80
+
81
+ return await self._send_message_payload(payload)
82
+
83
+ async def send_image(
84
+ self,
85
+ recipient_phone_number: str,
86
+ image_link: Optional[str] = None,
87
+ media_id: Optional[str] = None,
88
+ caption: Optional[str] = None,
89
+ reply_to: Optional[str] = None
90
+ ) -> str:
91
+ """
92
+ Send an image.
93
+ Args:
94
+ recipient_phone_number (str): The recipient's phone number in international format (without leading '+').
95
+ image_link (Optional[str]): The URL of the image to send. Default is None.
96
+ media_id (Optional[str]): The media ID of a previously uploaded image. Default is None.
97
+ caption (Optional[str]): Caption for the image. Default is None.
98
+ reply_to (Optional[str]): Message ID to reply to. Default is None.
99
+ Returns:
100
+ str: The message ID of the sent message.
101
+ Raises:
102
+ ValueError: If neither image_link nor media_id is provided.
103
+ WhatsappApiException: If there is an error sending the message.
104
+ ## Note
105
+ Supported formats: JPEG, PNG.
106
+ Max file size: 5 MB.
107
+ ## Documentation
108
+ https://developers.facebook.com/docs/whatsapp/cloud-api/messages/image-messages
109
+ """
110
+ return await self._send_media(
111
+ media_type="image",
112
+ media_link=image_link,
113
+ media_id=media_id,
114
+ caption=caption,
115
+ recipient_phone_number=recipient_phone_number,
116
+ reply_to=reply_to
117
+ )
118
+
119
+ async def send_document(
120
+ self,
121
+ recipient_phone_number: str,
122
+ document_link: Optional[str] = None,
123
+ media_id: Optional[str] = None,
124
+ caption: Optional[str] = None,
125
+ filename: Optional[str] = None,
126
+ reply_to: Optional[str] = None
127
+ ) -> str:
128
+ """
129
+ Send a document.
130
+ Args:
131
+ recipient_phone_number (str): The recipient's phone number in international format (without leading '+').
132
+ document_link (Optional[str]): The URL of the document to send. Default is None.
133
+ media_id (Optional[str]): The media ID of a previously uploaded document. Default is None.
134
+ caption (Optional[str]): Caption for the document. Default is None.
135
+ filename (Optional[str]): Filename for the document. Default is None.
136
+ reply_to (Optional[str]): Message ID to reply to. Default is None.
137
+ Returns:
138
+ str: The message ID of the sent message.
139
+ Raises:
140
+ ValueError: If neither document_link nor media_id is provided.
141
+ WhatsappApiException: If there is an error sending the message.
142
+ ## Note
143
+ Supported formats: PDF, XLS, XLSX, DOC, DOCX, PPT, PPTX, TXT.
144
+ Max file size: 100 MB.
145
+ ## Documentation
146
+ https://developers.facebook.com/docs/whatsapp/cloud-api/messages/document-messages
147
+ """
148
+ return await self._send_media(
149
+ media_type="document",
150
+ media_link=document_link,
151
+ media_id=media_id,
152
+ caption=caption,
153
+ filename=filename,
154
+ recipient_phone_number=recipient_phone_number,
155
+ reply_to=reply_to
156
+ )
157
+
158
+ async def send_audio(
159
+ self,
160
+ recipient_phone_number: str,
161
+ audio_link: Optional[str] = None,
162
+ media_id: Optional[str] = None,
163
+ caption: Optional[str] = None,
164
+ reply_to: Optional[str] = None
165
+ ) -> str:
166
+ """
167
+ Send an audio message.
168
+ Args:
169
+ recipient_phone_number (str): The recipient's phone number in international format (without leading '+').
170
+ audio_link (Optional[str]): The URL of the audio to send. Default is None.
171
+ media_id (Optional[str]): The media ID of a previously uploaded audio. Default is None.
172
+ caption (Optional[str]): Caption for the audio. Default is None.
173
+ reply_to (Optional[str]): Message ID to reply to. Default is None.
174
+ Returns:
175
+ str: The message ID of the sent message.
176
+ Raises:
177
+ ValueError: If neither audio_link nor media_id is provided.
178
+ WhatsappApiException: If there is an error sending the message.
179
+ ## Note
180
+ Supported formats: AAC, AMR, MP3, M4A, OGG (only OPUS codec).
181
+ Max file size: 16 MB.
182
+ """
183
+ return await self._send_media(
184
+ media_type="audio",
185
+ media_link=audio_link,
186
+ media_id=media_id,
187
+ caption=caption,
188
+ recipient_phone_number=recipient_phone_number,
189
+ reply_to=reply_to
190
+ )
191
+
192
+ async def send_video(
193
+ self,
194
+ recipient_phone_number: str,
195
+ video_link: Optional[str] = None,
196
+ media_id: Optional[str] = None,
197
+ caption: Optional[str] = None,
198
+ reply_to: Optional[str] = None
199
+ ) -> str:
200
+ """
201
+ Send a video message.
202
+ Args:
203
+ recipient_phone_number (str): The recipient's phone number in international format (without leading '+').
204
+ video_link (Optional[str]): The URL of the video to send. Default is None.
205
+ media_id (Optional[str]): The media ID of a previously uploaded video. Default is None.
206
+ caption (Optional[str]): Caption for the video. Default is None.
207
+ reply_to (Optional[str]): Message ID to reply to. Default is None.
208
+ Returns:
209
+ str: The message ID of the sent message.
210
+ Raises:
211
+ ValueError: If neither video_link nor media_id is provided.
212
+ WhatsappApiException: If there is an error sending the message.
213
+ ## Note
214
+ Supported formats: MP4, 3GPP.
215
+ Max file size: 16 MB.
216
+ ## Documentation
217
+ https://developers.facebook.com/docs/whatsapp/cloud-api/messages/video-messages
218
+ """
219
+ return await self._send_media(
220
+ media_type="video",
221
+ media_link=video_link,
222
+ media_id=media_id,
223
+ caption=caption,
224
+ recipient_phone_number=recipient_phone_number,
225
+ reply_to=reply_to
226
+ )
227
+
228
+ async def send_contact(
229
+ self,
230
+ recipient_phone_number: str,
231
+ contact: Contact | Dict[str, Any],
232
+ ) -> str:
233
+ """
234
+ Send a contact message.
235
+ Args:
236
+ recipient_phone_number (str): The recipient's phone number in international format (without leading '+').
237
+ contact (Contact | Dict[str, Any]): The contact information to send.
238
+ Returns:
239
+ str: The message ID of the sent message.
240
+ Raises:
241
+ ValueError: If contact is invalid.
242
+ WhatsappApiException: If there is an error sending the message.
243
+ """
244
+ if isinstance(contact, Contact):
245
+ contact = contact.model_dump(mode="json", exclude_none=True)
246
+
247
+ payload = {
248
+ "messaging_product": "whatsapp",
249
+ "recipient_type": "individual",
250
+ "to": recipient_phone_number,
251
+ "type": "contact",
252
+ "contact": contact,
253
+ }
254
+
255
+ return await self._send_message_payload(payload)
256
+
257
+ async def mark_as_read(
258
+ self,
259
+ message_id: str
260
+ ) -> None:
261
+ """
262
+ Mark a message as read.
263
+ Args:
264
+ message_id (str): The ID of the message to mark as read.
265
+ Returns:
266
+ str: The message ID of the read receipt.
267
+ Raises:
268
+ WhatsappApiException: If there is an error sending the read receipt.
269
+ ## Documentation
270
+ https://developers.facebook.com/docs/whatsapp/cloud-api/guides/mark-message-as-read
271
+ """
272
+ payload = {
273
+ "messaging_product": "whatsapp",
274
+ "status": "read",
275
+ "message_id": message_id
276
+ }
277
+
278
+ await self._send_mark_as_read_payload(payload)
279
+
280
+ async def send_typing_indicator(
281
+ self,
282
+ message_id: str
283
+ ) -> None:
284
+ """
285
+ Send a typing indicator.
286
+ Args:
287
+ message_id (str): The ID of the message to which the typing indicator relates. This message will be marked as read.
288
+ Returns:
289
+ str: The message ID of the typing indicator.
290
+ """
291
+ payload = {
292
+ "messaging_product": "whatsapp",
293
+ "status": "read",
294
+ "message_id": message_id,
295
+ "typing_indicator": {
296
+ "type": "text",
297
+ }
298
+ }
299
+
300
+ await self._send_mark_as_read_payload(payload)
301
+
302
+
303
+
304
+
305
+
306
+ async def _send_media(
307
+ self,
308
+ recipient_phone_number: str,
309
+ media_type: str,
310
+ media_link: Optional[str] = None,
311
+ media_id: Optional[str] = None,
312
+ caption: Optional[str] = None,
313
+ filename: Optional[str] = None,
314
+ reply_to: Optional[str] = None
315
+ ) -> str:
316
+ if not media_link and not media_id:
317
+ raise ValueError("Either media_link or media_id must be provided.")
318
+
319
+ media_payload = {}
320
+ if media_link:
321
+ media_payload["link"] = media_link
322
+ if media_id:
323
+ media_payload["id"] = media_id
324
+ if caption:
325
+ media_payload["caption"] = caption
326
+ if filename and media_type == "document":
327
+ media_payload["filename"] = filename
328
+
329
+ payload = {
330
+ "messaging_product": "whatsapp",
331
+ "recipient_type": "individual",
332
+ "to": recipient_phone_number,
333
+ "type": media_type,
334
+ media_type: media_payload,
335
+ }
336
+
337
+ if reply_to:
338
+ payload["context"] = {"message_id": reply_to}
339
+
340
+ return await self._send_message_payload(payload)
341
+
342
+ async def _send_message_payload(self, payload: dict) -> str:
343
+ """
344
+ Send a custom payload to WhatsApp's API messages endpoint with robust error handling.
345
+
346
+ Args:
347
+ payload (dict): The payload to send.
348
+ timeout (Optional[float]): Request timeout in seconds. Defaults to DEFAULT_TIMEOUT.
349
+
350
+ Returns:
351
+ SendResponse: A mapping with at least the "message_id" and, when available, "recipient_id".
352
+
353
+ Raises:
354
+ WhatsappApiException: For network errors, non-2xx responses, or malformed responses.
355
+ """
356
+ url = f"{self.base_url}{self.messages_endpoint}"
357
+ response = await self._send_payload(self.messages_endpoint, payload)
358
+
359
+ try:
360
+ data = response.json()
361
+ except ValueError as e:
362
+ raise WhatsappApiException(
363
+ message="Failed to parse JSON response",
364
+ endpoint=self.messages_endpoint,
365
+ method="POST",
366
+ payload=payload,
367
+ status_code=response.status_code,
368
+ details=str(e),
369
+ ) from e
370
+
371
+ message_id = data.get("message_id") or data.get("id")
372
+ if not message_id:
373
+ raise WhatsappApiException(
374
+ message="Missing message_id in successful response",
375
+ endpoint=url,
376
+ method="POST",
377
+ payload=payload,
378
+ status_code=response.status_code,
379
+ response_json=data,
380
+ )
381
+
382
+ return message_id
383
+
384
+ async def _send_mark_as_read_payload(self, payload: dict) -> None:
385
+ response = await self._send_payload(self.messages_endpoint, payload)
386
+ try:
387
+ data = response.json()
388
+ except ValueError as e:
389
+ raise WhatsappApiException(
390
+ message="Failed to parse JSON response",
391
+ endpoint=self.messages_endpoint,
392
+ method="POST",
393
+ payload=payload,
394
+ status_code=response.status_code,
395
+ details=str(e),
396
+ ) from e
397
+ success = data.get("success")
398
+ if not success:
399
+ raise WhatsappApiException(
400
+ message="Failed to mark message as read",
401
+ endpoint=self.messages_endpoint,
402
+ method="POST",
403
+ payload=payload,
404
+ status_code=response.status_code,
405
+ response_json=data,
406
+ )
407
+
408
+ async def _send_payload(self, endpoint: str, payload: dict) -> httpx.Response:
409
+ url = f"{self.base_url}{endpoint}"
410
+ headers = {
411
+ "Authorization": f"Bearer {self.access_token}",
412
+ "Content-Type": "application/json",
413
+ }
414
+
415
+ try:
416
+ async with httpx.AsyncClient(timeout=self.request_timeout) as client:
417
+ response = await client.post(url, json=payload, headers=headers)
418
+ except httpx.RequestError as e:
419
+ raise WhatsappApiException(
420
+ message="Network error while sending payload",
421
+ endpoint=url,
422
+ method="POST",
423
+ payload=payload,
424
+ details=str(e),
425
+ ) from e
426
+
427
+ # Non-2xx -> raise a structured exception with parsed error body when possible
428
+ if response.status_code < 200 or response.status_code >= 300:
429
+ raise WhatsappApiException.from_httpx_response(
430
+ response,
431
+ endpoint=url,
432
+ method="POST",
433
+ payload=payload,
434
+ )
435
+
436
+ return response
@@ -0,0 +1,4 @@
1
+ from .types import Update
2
+ from .WhatsappMessenger import WhatsappMessenger
3
+
4
+ __all__ = ["Update", "WhatsappMessenger"]
@@ -0,0 +1,5 @@
1
+ from .update import Update
2
+ from .message import Message
3
+ from .message.contact import Contact
4
+
5
+ __all__ = ["Update", "Message", "Contact"]
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+
6
+ class WhatsappApiException(Exception):
7
+ """Exception raised for Instagram API errors.
8
+
9
+ Captures HTTP details, endpoint, method, payload, and parsed error body when available.
10
+ """
11
+
12
+ def __init__(
13
+ self,
14
+ message: str,
15
+ *,
16
+ status_code: Optional[int] = None,
17
+ endpoint: Optional[str] = None,
18
+ method: Optional[str] = None,
19
+ payload: Optional[Dict[str, Any]] = None,
20
+ response_text: Optional[str] = None,
21
+ response_json: Optional[Dict[str, Any]] = None,
22
+ details: Optional[str] = None,
23
+ error_code: Optional[str] = None,
24
+ ) -> None:
25
+ super().__init__(message)
26
+ self.message = message
27
+ self.status_code = status_code
28
+ self.endpoint = endpoint
29
+ self.method = method
30
+ self.payload = payload
31
+ self.response_text = response_text
32
+ self.response_json = response_json
33
+ self.details = details
34
+ self.error_code = error_code
35
+
36
+ def __str__(self) -> str:
37
+ base = self.message
38
+ parts = []
39
+ if self.status_code is not None:
40
+ parts.append(f"status={self.status_code}")
41
+ if self.endpoint:
42
+ parts.append(f"endpoint={self.endpoint}")
43
+ if self.method:
44
+ parts.append(f"method={self.method}")
45
+ if self.error_code:
46
+ parts.append(f"error_code={self.error_code}")
47
+ if self.details:
48
+ parts.append(f"details={self.details}")
49
+ if parts:
50
+ base += " (" + ", ".join(parts) + ")"
51
+ return base
52
+
53
+ @classmethod
54
+ def from_httpx_response(
55
+ cls,
56
+ response: Any,
57
+ *,
58
+ endpoint: Optional[str] = None,
59
+ method: Optional[str] = None,
60
+ payload: Optional[Dict[str, Any]] = None,
61
+ ) -> "WhatsappApiException":
62
+ """
63
+ Build an exception from an httpx.Response, attempting to parse JSON error details.
64
+ """
65
+ status = getattr(response, "status_code", None)
66
+ text = None
67
+ data: Optional[Dict[str, Any]] = None
68
+ code: Optional[str] = None
69
+ message: str = "Instagram API request failed"
70
+
71
+ try:
72
+ data = response.json()
73
+ # Graph API errors are often under { "error": { "message": "...", "code": 190, ... } }
74
+ err = data.get("error") if isinstance(data, dict) else None
75
+ if isinstance(err, dict):
76
+ message = err.get("message") or message
77
+ code_value = err.get("code")
78
+ code = str(code_value) if code_value is not None else None
79
+ except Exception:
80
+ text = getattr(response, "text", None)
81
+
82
+ return cls(
83
+ message=message,
84
+ status_code=status,
85
+ endpoint=endpoint,
86
+ method=method,
87
+ payload=payload,
88
+ response_text=text,
89
+ response_json=data,
90
+ error_code=code,
91
+ )
@@ -0,0 +1,3 @@
1
+ from .WhatsappApiException import WhatsappApiException
2
+
3
+ __all__ = ["WhatsappApiException"]
@@ -0,0 +1,81 @@
1
+ from pydantic import BaseModel, Field
2
+ from typing import List, Optional
3
+ from hiws.types.message.contact import Contact
4
+
5
+
6
+ class BaseMessage(BaseModel):
7
+ from_phone_number: str = Field(alias="from")
8
+ id: str
9
+ timestamp: str
10
+
11
+ class Text(BaseModel):
12
+ body: str
13
+
14
+ class TextMessage(BaseMessage):
15
+ text: Text
16
+ type: str = "text"
17
+
18
+ class Reaction(BaseModel):
19
+ message_id: str
20
+ emoji: str
21
+
22
+ class ReactionMessage(BaseMessage):
23
+ reaction: Reaction
24
+ type: str = "reaction"
25
+
26
+ class Media(BaseModel):
27
+ id: str
28
+ mime_type: str
29
+ caption: str
30
+ sha256: str
31
+
32
+ class ImageMessage(BaseMessage):
33
+ image: Media
34
+ type: str = "image"
35
+
36
+ class StickerMessage(BaseMessage):
37
+ sticker: Media
38
+ type: str = "sticker"
39
+
40
+ class MessageError(BaseModel):
41
+ code: int
42
+ details: str
43
+ title: str
44
+
45
+ class UnknownMessage(BaseModel):
46
+ errors: List[MessageError]
47
+ type: str = "unknown"
48
+
49
+ class Location(BaseModel):
50
+ latitude: float
51
+ longitude: float
52
+ name: Optional[str]
53
+ address: Optional[str]
54
+
55
+ class LocationMessage(BaseMessage):
56
+ location: Location
57
+ type: str = "location"
58
+
59
+ class ContactMessage(BaseMessage):
60
+ contacts: List[Contact]
61
+ type: str = "contacts"
62
+
63
+ class Button(BaseModel):
64
+ text: str
65
+ payload: Optional[str]
66
+
67
+ class QuickReplyButtonMessage(BaseMessage):
68
+ button: Button
69
+ type: str = "button"
70
+
71
+ class SystemUpdate(BaseModel):
72
+ body: str
73
+ type: str = "system"
74
+ new_wa_id: Optional[str]
75
+
76
+ class SystemMessage(BaseMessage):
77
+ system: SystemUpdate
78
+ type: str = "system"
79
+
80
+
81
+ type Message = TextMessage | ReactionMessage | ImageMessage | StickerMessage | LocationMessage | QuickReplyButtonMessage | SystemMessage | UnknownMessage
@@ -0,0 +1,53 @@
1
+ from pydantic import BaseModel, Field
2
+ from typing import List, Literal, Optional
3
+
4
+
5
+ class Address(BaseModel):
6
+ city: Optional[str]
7
+ country: Optional[str]
8
+ country_code: Optional[str]
9
+ state: Optional[str]
10
+ street: Optional[str]
11
+ type: Literal["HOME", "WORK"] = "HOME"
12
+ zip: Optional[str]
13
+
14
+
15
+ class Email(BaseModel):
16
+ email: str
17
+ type: Literal["HOME", "WORK"] = "HOME"
18
+
19
+
20
+ class Name(BaseModel):
21
+ formatted_name: Optional[str]
22
+ first_name: Optional[str]
23
+ last_name: Optional[str]
24
+ middle_name: Optional[str]
25
+ prefix: Optional[str]
26
+ suffix: Optional[str]
27
+
28
+
29
+ class Org(BaseModel):
30
+ company: Optional[str]
31
+ department: Optional[str]
32
+ title: Optional[str]
33
+
34
+
35
+ class Phone(BaseModel):
36
+ phone: str
37
+ wa_id: Optional[str]
38
+ type: Literal["HOME", "WORK", "MOBILE"] = "HOME"
39
+
40
+
41
+ class Url(BaseModel):
42
+ url: str
43
+ type: Literal["HOME", "WORK"] = "HOME"
44
+
45
+
46
+ class Contact(BaseModel):
47
+ addresses: Optional[List[Address]]
48
+ birthday: Optional[str]
49
+ emails: Optional[List[Email]]
50
+ name: Optional[Name]
51
+ org: Optional[Org]
52
+ phones: List[Phone]
53
+ urls: Optional[List[Url]]
@@ -0,0 +1,54 @@
1
+ from pydantic import BaseModel
2
+ from typing import List, Literal, Optional
3
+
4
+ class StatusPricing(BaseModel):
5
+ pricing_model: str
6
+ billable: bool
7
+ category: Optional[str]
8
+
9
+ class StatusConversationOrigin(BaseModel):
10
+ type: str
11
+
12
+ class StatusConversation(BaseModel):
13
+ id: str
14
+ expiration_timestamp: Optional[str]
15
+ origin: Optional[StatusConversationOrigin]
16
+
17
+ class StatusErrorData(BaseModel):
18
+ details: str
19
+
20
+ class StatusError(BaseModel):
21
+ code: int
22
+ title: str
23
+ message: Optional[str]
24
+ error_data: Optional[StatusErrorData]
25
+ href: Optional[str]
26
+
27
+
28
+ class BaseStatus(BaseModel):
29
+ id: str
30
+ status: Literal["delivered", "read", "failed", "sent"]
31
+ timestamp: str
32
+ recipient_id: str
33
+
34
+ class SentStatus(BaseStatus):
35
+ status: Literal["sent"]
36
+ conversation: StatusConversation
37
+ pricing: StatusPricing
38
+
39
+ class DeliveredStatus(BaseStatus):
40
+ status: Literal["delivered"]
41
+ conversation: StatusConversation
42
+ pricing: StatusPricing
43
+
44
+ class ReadStatus(BaseStatus):
45
+ status: Literal["read"]
46
+
47
+ class FailedStatus(BaseStatus):
48
+ status: Literal["failed"]
49
+ errors: List[StatusError]
50
+
51
+ type Status = SentStatus | DeliveredStatus | ReadStatus | FailedStatus
52
+
53
+
54
+
@@ -0,0 +1,41 @@
1
+ from typing import List, Optional
2
+ from pydantic import BaseModel
3
+ from hiws.types.message import Message
4
+ from hiws.types.status import Status
5
+
6
+
7
+ class Profile(BaseModel):
8
+ name: str
9
+
10
+
11
+ class RequestContact(BaseModel):
12
+ profile: Profile
13
+ wa_id: str
14
+
15
+
16
+ class Metadata(BaseModel):
17
+ display_phone_number: str
18
+ phone_number_id: str
19
+
20
+
21
+ class Value(BaseModel):
22
+ messaging_product: str
23
+ metadata: Metadata
24
+ contacts: List[RequestContact]
25
+ messages: Optional[List[Message]] = None
26
+ statuses: Optional[List[Status]] = None
27
+
28
+
29
+ class Change(BaseModel):
30
+ value: Value
31
+ field: str
32
+
33
+
34
+ class Entry(BaseModel):
35
+ id: str
36
+ changes: List[Change]
37
+
38
+
39
+ class Update(BaseModel):
40
+ object: str
41
+ entry: List[Entry]
@@ -0,0 +1,95 @@
1
+ Metadata-Version: 2.4
2
+ Name: hiws
3
+ Version: 0.1.0
4
+ Summary: A wrapper for WhatsApp's Cloud API
5
+ Home-page: https://github.com/cervant-ai/hiws
6
+ Author: Tomas Santana
7
+ Author-email: Tomas Santana <tomas@cervant.chat>
8
+ License: MIT
9
+ Project-URL: Homepage, https://github.com/cervant-ai/hiws
10
+ Project-URL: Repository, https://github.com/cervant-ai/hiws.git
11
+ Project-URL: Issues, https://github.com/cervant-ai/hiws/issues
12
+ Keywords: hiws,python,package
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.7
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Requires-Python: >=3.7
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: pydantic>=2.11.7
26
+ Requires-Dist: httpx>=0.28.1
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=6.0; extra == "dev"
29
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
30
+ Requires-Dist: black>=21.0; extra == "dev"
31
+ Requires-Dist: flake8>=3.8.0; extra == "dev"
32
+ Requires-Dist: mypy>=0.800; extra == "dev"
33
+ Dynamic: author
34
+ Dynamic: home-page
35
+ Dynamic: license-file
36
+ Dynamic: requires-python
37
+
38
+ # hiws
39
+
40
+ A Python package for interacting with WhatsApp's Cloud API.
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install hiws
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ ```python
51
+ from hiws import WhatsAppMessenger
52
+
53
+ messenger = WhatsAppMessenger(access_token="YOUR_ACCESS_TOKEN", phone_number_id="YOUR_PHONE_NUMBER_ID")
54
+
55
+ # Example usage
56
+ message_id = await messenger.send_text("Hello, World!", recipient_id="RECIPIENT_ID")
57
+
58
+ print(message_id)
59
+ ```
60
+
61
+ ## Development
62
+
63
+ To install in development mode:
64
+
65
+ ```bash
66
+ git clone https://github.com/cervant-ai/hiws.git
67
+ cd hiws
68
+ pip install -e .
69
+ ```
70
+
71
+ To install development dependencies:
72
+
73
+ ```bash
74
+ pip install -e ".[dev]"
75
+ ```
76
+
77
+ ## Testing
78
+
79
+ ```bash
80
+ pytest
81
+ ```
82
+
83
+ ```bash
84
+ pip install -e ".[dev]"
85
+ ```
86
+
87
+ ## Testing
88
+
89
+ ```bash
90
+ pytest
91
+ ```
92
+
93
+ ## License
94
+
95
+ This project is licensed under the MIT License - see the LICENSE file for details.
@@ -0,0 +1,18 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ setup.py
5
+ hiws/WhatsappMessenger.py
6
+ hiws/__init__.py
7
+ hiws.egg-info/PKG-INFO
8
+ hiws.egg-info/SOURCES.txt
9
+ hiws.egg-info/dependency_links.txt
10
+ hiws.egg-info/requires.txt
11
+ hiws.egg-info/top_level.txt
12
+ hiws/types/__init__.py
13
+ hiws/types/update.py
14
+ hiws/types/exceptions/WhatsappApiException.py
15
+ hiws/types/exceptions/__init__.py
16
+ hiws/types/message/__init__.py
17
+ hiws/types/message/contact.py
18
+ hiws/types/status/__init__.py
@@ -0,0 +1,9 @@
1
+ pydantic>=2.11.7
2
+ httpx>=0.28.1
3
+
4
+ [dev]
5
+ pytest>=6.0
6
+ pytest-asyncio>=0.23
7
+ black>=21.0
8
+ flake8>=3.8.0
9
+ mypy>=0.800
@@ -0,0 +1 @@
1
+ hiws
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["setuptools>=45", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hiws"
7
+ version = "0.1.0"
8
+ description = "A wrapper for WhatsApp's Cloud API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.7"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "Tomas Santana", email = "tomas@cervant.chat"},
14
+ ]
15
+ keywords = ["hiws", "python", "package"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.7",
22
+ "Programming Language :: Python :: 3.8",
23
+ "Programming Language :: Python :: 3.9",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ ]
27
+ dependencies = [
28
+ "pydantic>=2.11.7",
29
+ "httpx>=0.28.1",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ dev = [
34
+ "pytest>=6.0",
35
+ "pytest-asyncio>=0.23",
36
+ "black>=21.0",
37
+ "flake8>=3.8.0",
38
+ "mypy>=0.800",
39
+ ]
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/cervant-ai/hiws"
43
+ Repository = "https://github.com/cervant-ai/hiws.git"
44
+ Issues = "https://github.com/cervant-ai/hiws/issues"
45
+
46
+ [tool.black]
47
+ line-length = 88
48
+ target-version = ['py37']
49
+
50
+ [tool.mypy]
51
+ python_version = "3.7"
52
+ warn_return_any = true
53
+ warn_unused_configs = true
54
+ disallow_untyped_defs = true
hiws-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
hiws-0.1.0/setup.py ADDED
@@ -0,0 +1,41 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ with open("README.md", "r", encoding="utf-8") as fh:
4
+ long_description = fh.read()
5
+
6
+ setup(
7
+ name="hiws",
8
+ version="0.1.0",
9
+ author="Tomas Santana",
10
+ author_email="tomas@cervant.chat",
11
+ description="A simple Python wrapper for WhatsApp's Cloud API",
12
+ long_description=long_description,
13
+ long_description_content_type="text/markdown",
14
+ url="https://github.com/cervant-ai/hiws",
15
+ packages=find_packages(),
16
+ classifiers=[
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.7",
22
+ "Programming Language :: Python :: 3.8",
23
+ "Programming Language :: Python :: 3.9",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ ],
27
+ python_requires=">=3.7",
28
+ install_requires=[
29
+ "pydantic>=2.11.7",
30
+ "httpx>=0.28.1",
31
+ ],
32
+ extras_require={
33
+ "dev": [
34
+ "pytest>=6.0",
35
+ "pytest-asyncio>=0.23",
36
+ "black>=21.0",
37
+ "flake8>=3.8.0",
38
+ "mypy>=0.800",
39
+ ],
40
+ },
41
+ )