From 08ec66c365b4e87552f5046b509c91e76d54eef4 Mon Sep 17 00:00:00 2001 From: Terry Cruz Melo <33166112+TerryCM@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:22:35 -0500 Subject: [PATCH 1/3] Update API version to 21.0 --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/YaVendio/heyoo?shareId=XXXX-XXXX-XXXX-XXXX). --- heyoo/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/heyoo/__init__.py b/heyoo/__init__.py index aef82e6..18adb25 100644 --- a/heyoo/__init__.py +++ b/heyoo/__init__.py @@ -30,8 +30,8 @@ def __init__(self, token=None, phone_number_id=None): """ self.token = token self.phone_number_id = phone_number_id - self.base_url = "https://graph.facebook.com/v14.0" - self.v15_base_url = "https://graph.facebook.com/v15.0" + self.base_url = "https://graph.facebook.com/v21.0" + self.v15_base_url = "https://graph.facebook.com/v21.0" self.url = f"{self.base_url}/{phone_number_id}/messages" self.headers = { From 658af0dba278415f50b6f7bac66dd6bdc2429292 Mon Sep 17 00:00:00 2001 From: Terry Cruz Melo <33166112+TerryCM@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:27:00 -0500 Subject: [PATCH 2/3] Add retry logic to HTTP requests in `heyoo/__init__.py` * **Imports** - Import `Retry` and `HTTPAdapter` from `requests.adapters`. * **Session with retries** - Add `_get_session_with_retries` method to create a session with retry logic. * **Update methods** - Update `send_message`, `send_audio`, `send_image`, `send_video`, and other methods to use the session with retry logic. --- heyoo/__init__.py | 66 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/heyoo/__init__.py b/heyoo/__init__.py index 18adb25..ed6bb66 100644 --- a/heyoo/__init__.py +++ b/heyoo/__init__.py @@ -10,6 +10,7 @@ from colorama import Fore, Style from requests_toolbelt.multipart.encoder import MultipartEncoder from typing import Optional, Dict, Any, List, Union, Tuple, Callable +from requests.adapters import HTTPAdapter, Retry # Setup logging @@ -39,6 +40,14 @@ def __init__(self, token=None, phone_number_id=None): "Authorization": "Bearer {}".format(self.token), } + def _get_session_with_retries(self): + session = requests.Session() + retries = Retry(total=5, backoff_factor=1, status_forcelist=[502, 503, 504]) + adapter = HTTPAdapter(max_retries=retries) + session.mount("http://", adapter) + session.mount("https://", adapter) + return session + def send_message( self, message, recipient_id, recipient_type="individual", preview_url=True ): @@ -67,7 +76,8 @@ def send_message( "text": {"preview_url": preview_url, "body": message}, } logging.info(f"Sending message to {recipient_id}") - r = requests.post(f"{self.url}", headers=self.headers, json=data) + session = self._get_session_with_retries() + r = session.post(f"{self.url}", headers=self.headers, json=data) if r.status_code == 200: logging.info(f"Message sent to {recipient_id}") return r.json() @@ -103,7 +113,8 @@ def send_reaction( "reaction": {"message_id": message_id, "emoji": emoji}, } logging.info(f"Sending reaction to number {recipient_id} message id {message_id}") - r = requests.post(f"{self.url}", headers=self.headers, json=data) + session = self._get_session_with_retries() + r = session.post(f"{self.url}", headers=self.headers, json=data) if r.status_code == 200: logging.info(f"Reaction sent to number {recipient_id} message id {message_id}") return r.json() @@ -134,7 +145,8 @@ def reply_to_message( } logging.info(f"Replying to {message_id}") - r = requests.post(f"{self.url}", headers=self.headers, json=data) + session = self._get_session_with_retries() + r = session.post(f"{self.url}", headers=self.headers, json=data) if r.status_code == 200: logging.info(f"Message sent to {recipient_id}") return r.json() @@ -173,7 +185,8 @@ def send_template(self, template, recipient_id, components, lang: str = "en_US") }, } logging.info(f"Sending template to {recipient_id}") - r = requests.post(self.url, headers=self.headers, json=data) + session = self._get_session_with_retries() + r = session.post(self.url, headers=self.headers, json=data) if r.status_code == 200: logging.info(f"Template sent to {recipient_id}") return r.json() @@ -210,7 +223,8 @@ def send_location(self, lat, long, name, address, recipient_id): }, } logging.info(f"Sending location to {recipient_id}") - r = requests.post(self.url, headers=self.headers, json=data) + session = self._get_session_with_retries() + r = session.post(self.url, headers=self.headers, json=data) if r.status_code == 200: logging.info(f"Location sent to {recipient_id}") return r.json() @@ -263,7 +277,8 @@ def send_image( "image": {"id": image, "caption": caption}, } logging.info(f"Sending image to {recipient_id}") - r = requests.post(self.url, headers=self.headers, json=data) + session = self._get_session_with_retries() + r = session.post(self.url, headers=self.headers, json=data) if r.status_code == 200: logging.info(f"Image sent to {recipient_id}") return r.json() @@ -308,7 +323,8 @@ def send_sticker(self, sticker: str, recipient_id: str, recipient_type="individu "sticker": {"id": sticker}, } logging.info(f"Sending sticker to {recipient_id}") - r = requests.post(self.url, headers=self.headers, json=data) + session = self._get_session_with_retries() + r = session.post(self.url, headers=self.headers, json=data) if r.status_code == 200: logging.info(f"Sticker sent to {recipient_id}") return r.json() @@ -347,7 +363,8 @@ def send_audio(self, audio, recipient_id, link=True): "audio": {"id": audio}, } logging.info(f"Sending audio to {recipient_id}") - r = requests.post(self.url, headers=self.headers, json=data) + session = self._get_session_with_retries() + r = session.post(self.url, headers=self.headers, json=data) if r.status_code == 200: logging.info(f"Audio sent to {recipient_id}") return r.json() @@ -389,7 +406,8 @@ def send_video( "video": {"id": video, "caption": caption}, } logging.info(f"Sending video to {recipient_id}") - r = requests.post(self.url, headers=self.headers, json=data) + session = self._get_session_with_retries() + r = session.post(self.url, headers=self.headers, json=data) if r.status_code == 200: logging.info(f"Video sent to {recipient_id}") return r.json() @@ -422,7 +440,8 @@ def send_custom_json(self, data, recipient_id=None): data["to"] = recipient_id logging.info(f"Sending custom json to {recipient_id}") - r = requests.post(self.url, headers=self.headers, json=data) + session = self._get_session_with_retries() + r = session.post(self.url, headers=self.headers, json=data) if r.status_code == 200: logging.info(f"Custom json sent to {recipient_id}") return r.json() @@ -466,7 +485,8 @@ def send_document( } logging.info(f"Sending document to {recipient_id}") - r = requests.post(self.url, headers=self.headers, json=data) + session = self._get_session_with_retries() + r = session.post(self.url, headers=self.headers, json=data) if r.status_code == 200: logging.info(f"Document sent to {recipient_id}") return r.json() @@ -513,7 +533,8 @@ def send_contacts( "contacts": contacts, } logging.info(f"Sending contacts to {recipient_id}") - r = requests.post(self.url, headers=self.headers, json=data) + session = self._get_session_with_retries() + r = session.post(self.url, headers=self.headers, json=data) if r.status_code == 200: logging.info(f"Contacts sent to {recipient_id}") return r.json() @@ -550,7 +571,8 @@ def upload_media(self, media: str) -> Union[Dict[Any, Any], None]: headers["Content-Type"] = form_data.content_type logging.info(f"Content-Type: {form_data.content_type}") logging.info(f"Uploading media {media}") - r = requests.post( + session = self._get_session_with_retries() + r = session.post( f"{self.base_url}/{self.phone_number_id}/media", headers=headers, data=form_data, @@ -571,7 +593,8 @@ def delete_media(self, media_id: str) -> Union[Dict[Any, Any], None]: media_id[str]: Id of the media to be deleted """ logging.info(f"Deleting media {media_id}") - r = requests.delete(f"{self.base_url}/{media_id}", headers=self.headers) + session = self._get_session_with_retries() + r = session.delete(f"{self.base_url}/{media_id}", headers=self.headers) if r.status_code == 200: logging.info(f"Media {media_id} deleted") return r.json() @@ -606,7 +629,8 @@ def mark_as_read(self, message_id: str) -> Dict[Any, Any]: "message_id": message_id, } logging.info(f"Marking message {message_id} as read") - response = requests.post( + session = self._get_session_with_retries() + response = session.post( f"{self.v15_base_url}/{self.phone_number_id}/messages", headers=headers, json=json_data, @@ -654,7 +678,8 @@ def send_button(self, button: Dict[Any, Any], recipient_id: str) -> Dict[Any, An "interactive": self.create_button(button), } logging.info(f"Sending buttons to {recipient_id}") - r = requests.post(self.url, headers=self.headers, json=data) + session = self._get_session_with_retries() + r = session.post(self.url, headers=self.headers, json=data) if r.status_code == 200: logging.info(f"Buttons sent to {recipient_id}") return r.json() @@ -683,7 +708,8 @@ def send_reply_button( "type": "interactive", "interactive": button, } - r = requests.post(self.url, headers=self.headers, json=data) + session = self._get_session_with_retries() + r = session.post(self.url, headers=self.headers, json=data) if r.status_code == 200: logging.info(f"Reply buttons sent to {recipient_id}") return r.json() @@ -709,7 +735,8 @@ def query_media_url(self, media_id: str) -> Union[str, None]: """ logging.info(f"Querying media url for {media_id}") - r = requests.get(f"{self.base_url}/{media_id}", headers=self.headers) + session = self._get_session_with_retries() + r = session.get(f"{self.base_url}/{media_id}", headers=self.headers) if r.status_code == 200: logging.info(f"Media url queried for {media_id}") return r.json()["url"] @@ -739,7 +766,8 @@ def download_media( >>> whatsapp.download_media("media_url", "image/jpeg") >>> whatsapp.download_media("media_url", "video/mp4", "path/to/file") #do not include the file extension """ - r = requests.get(media_url, headers=self.headers) + session = self._get_session_with_retries() + r = session.get(media_url, headers=self.headers) content = r.content extension = mime_type.split("/")[1].split(";")[0].strip() # create a temporary file From 8928200d40cabb2a62911e708baa025c62745c2e Mon Sep 17 00:00:00 2001 From: Terry Cruz Melo <33166112+TerryCM@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:28:33 -0500 Subject: [PATCH 3/3] Add tests to check retry logic for various sending methods * **test_sending_audio.py** - Import `requests`, `HTTPAdapter`, `Retry`, and `patch` - Add `test_send_audio_retries` to check retry logic for `send_audio` method * **test_sending_button.py** - Import `requests`, `HTTPAdapter`, `Retry`, and `patch` - Add `test_send_button_retries` to check retry logic for `send_button` method * **test_sending_document.py** - Import `requests`, `HTTPAdapter`, `Retry`, and `patch` - Add `test_send_document_retries` to check retry logic for `send_document` method * **test_sending_image.py** - Import `requests`, `HTTPAdapter`, `Retry`, and `patch` - Add `test_send_image_retries` to check retry logic for `send_image` method * **test_sending_location.py** - Import `requests`, `HTTPAdapter`, `Retry`, and `patch` - Add `test_send_location_retries` to check retry logic for `send_location` method * **test_sending_message.py** - Import `requests`, `HTTPAdapter`, `Retry`, and `patch` - Add `test_send_message_retries` to check retry logic for `send_message` method * **test_sending_template_message.py** - Import `requests`, `HTTPAdapter`, `Retry`, and `patch` - Add `test_send_template_retries` to check retry logic for `send_template` method * **test_sending_video.py** - Import `requests`, `HTTPAdapter`, `Retry`, and `patch` - Add `test_send_video_retries` to check retry logic for `send_video` method --- tests/test_sending_audio.py | 16 ++++++++++++ tests/test_sending_button.py | 36 ++++++++++++++++++++++++++ tests/test_sending_document.py | 16 ++++++++++++ tests/test_sending_image.py | 16 ++++++++++++ tests/test_sending_location.py | 19 ++++++++++++++ tests/test_sending_message.py | 16 ++++++++++++ tests/test_sending_template_message.py | 13 ++++++++++ tests/test_sending_video.py | 18 ++++++++++++- 8 files changed, 149 insertions(+), 1 deletion(-) diff --git a/tests/test_sending_audio.py b/tests/test_sending_audio.py index eb7e5e0..8ec01c8 100644 --- a/tests/test_sending_audio.py +++ b/tests/test_sending_audio.py @@ -1,6 +1,9 @@ from os import getenv from heyoo import WhatsApp from dotenv import load_dotenv +import requests +from requests.adapters import HTTPAdapter, Retry +from unittest.mock import patch def test_sending_audio(): load_dotenv() @@ -14,3 +17,16 @@ def test_sending_audio(): assert(response["contacts"][0]["input"]==getenv("RECIPIENT_ID")) assert(response["contacts"][0]["wa_id"]==getenv("RECIPIENT_ID")) assert(response["messaging_product"]=="whatsapp") + +@patch.object(requests.Session, 'send') +def test_send_audio_retries(mock_send): + mock_send.side_effect = [requests.exceptions.RequestException] * 2 + [requests.Response()] + load_dotenv() + messenger = WhatsApp(token=getenv("TOKEN"), phone_number_id=getenv("PHONE_NUMBER_ID")) + + response = messenger.send_audio( + audio="https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", + recipient_id=getenv("RECIPIENT_ID"), + ) + + assert mock_send.call_count == 3 diff --git a/tests/test_sending_button.py b/tests/test_sending_button.py index 14de2f5..819ee16 100644 --- a/tests/test_sending_button.py +++ b/tests/test_sending_button.py @@ -1,6 +1,9 @@ from os import getenv from heyoo import WhatsApp from dotenv import load_dotenv +import requests +from requests.adapters import HTTPAdapter, Retry +from unittest.mock import patch def test_sending_button(): load_dotenv() @@ -34,3 +37,36 @@ def test_sending_button(): assert(response["contacts"][0]["input"]==getenv("RECIPIENT_ID")) assert(response["contacts"][0]["wa_id"]==getenv("RECIPIENT_ID")) assert(response["messaging_product"]=="whatsapp") + +@patch.object(requests.Session, 'send') +def test_send_button_retries(mock_send): + mock_send.side_effect = [requests.exceptions.RequestException] * 2 + [requests.Response()] + load_dotenv() + messenger = WhatsApp(token=getenv("TOKEN"), phone_number_id=getenv("PHONE_NUMBER_ID")) + + response = messenger.send_button( + recipient_id=getenv("RECIPIENT_ID"), + button={ + "header": "Header Testing", + "body": "Body Testing", + "footer": "Footer Testing", + "action": { + "button": "Button Testing", + "sections": [ + { + "title": "iBank", + "rows": [ + {"id": "row 1", "title": "Send Money", "description": ""}, + { + "id": "row 2", + "title": "Withdraw money", + "description": "", + }, + ], + } + ], + }, + }, + ) + + assert mock_send.call_count == 3 diff --git a/tests/test_sending_document.py b/tests/test_sending_document.py index 743af66..f40c0f1 100644 --- a/tests/test_sending_document.py +++ b/tests/test_sending_document.py @@ -1,6 +1,9 @@ from os import getenv from heyoo import WhatsApp from dotenv import load_dotenv +import requests +from requests.adapters import HTTPAdapter, Retry +from unittest.mock import patch def test_sending_document(): load_dotenv() @@ -15,3 +18,16 @@ def test_sending_document(): assert(response["contacts"][0]["input"]==getenv("RECIPIENT_ID")) assert(response["contacts"][0]["wa_id"]==getenv("RECIPIENT_ID")) assert(response["messaging_product"]=="whatsapp") + +@patch.object(requests.Session, 'send') +def test_send_document_retries(mock_send): + mock_send.side_effect = [requests.exceptions.RequestException] * 2 + [requests.Response()] + load_dotenv() + messenger = WhatsApp(token=getenv("TOKEN"), phone_number_id=getenv("PHONE_NUMBER_ID")) + + response = messenger.send_document( + document="http://www.africau.edu/images/default/sample.pdf", + recipient_id=getenv("RECIPIENT_ID"), + ) + + assert mock_send.call_count == 3 diff --git a/tests/test_sending_image.py b/tests/test_sending_image.py index c616ec2..b98215c 100644 --- a/tests/test_sending_image.py +++ b/tests/test_sending_image.py @@ -1,6 +1,9 @@ from os import getenv from heyoo import WhatsApp from dotenv import load_dotenv +import requests +from requests.adapters import HTTPAdapter, Retry +from unittest.mock import patch def test_sending_image(): load_dotenv() @@ -14,3 +17,16 @@ def test_sending_image(): assert(response["contacts"][0]["input"]==getenv("RECIPIENT_ID")) assert(response["contacts"][0]["wa_id"]==getenv("RECIPIENT_ID")) assert(response["messaging_product"]=="whatsapp") + +@patch.object(requests.Session, 'send') +def test_send_image_retries(mock_send): + mock_send.side_effect = [requests.exceptions.RequestException] * 2 + [requests.Response()] + load_dotenv() + messenger = WhatsApp(token=getenv("TOKEN"), phone_number_id=getenv("PHONE_NUMBER_ID")) + + response = messenger.send_image( + image="https://i.imgur.com/Fh7XVYY.jpeg", + recipient_id=getenv("RECIPIENT_ID"), + ) + + assert mock_send.call_count == 3 diff --git a/tests/test_sending_location.py b/tests/test_sending_location.py index 6e5a386..8eb2da5 100644 --- a/tests/test_sending_location.py +++ b/tests/test_sending_location.py @@ -1,6 +1,9 @@ from os import getenv from heyoo import WhatsApp from dotenv import load_dotenv +import requests +from requests.adapters import HTTPAdapter, Retry +from unittest.mock import patch def test_sending_location(): load_dotenv() @@ -17,3 +20,19 @@ def test_sending_location(): assert(response["contacts"][0]["input"]==getenv("RECIPIENT_ID")) assert(response["contacts"][0]["wa_id"]==getenv("RECIPIENT_ID")) assert(response["messaging_product"]=="whatsapp") + +@patch.object(requests.Session, 'send') +def test_send_location_retries(mock_send): + mock_send.side_effect = [requests.exceptions.RequestException] * 2 + [requests.Response()] + load_dotenv() + messenger = WhatsApp(token=getenv("TOKEN"), phone_number_id=getenv("PHONE_NUMBER_ID")) + + response = messenger.send_location( + lat=1.29, + long=103.85, + name="Singapore", + address="Singapore", + recipient_id=getenv("RECIPIENT_ID"), + ) + + assert mock_send.call_count == 3 diff --git a/tests/test_sending_message.py b/tests/test_sending_message.py index 83106b4..577b642 100644 --- a/tests/test_sending_message.py +++ b/tests/test_sending_message.py @@ -1,6 +1,9 @@ from os import getenv from heyoo import WhatsApp from dotenv import load_dotenv +import requests +from requests.adapters import HTTPAdapter, Retry +from unittest.mock import patch def test_sending_message(): load_dotenv() @@ -15,3 +18,16 @@ def test_sending_message(): assert(response["contacts"][0]["input"]==getenv("RECIPIENT_ID")) assert(response["contacts"][0]["wa_id"]==getenv("RECIPIENT_ID")) assert(response["messaging_product"]=="whatsapp") + +@patch.object(requests.Session, 'send') +def test_send_message_retries(mock_send): + mock_send.side_effect = [requests.exceptions.RequestException] * 2 + [requests.Response()] + load_dotenv() + messenger = WhatsApp(token=getenv("TOKEN"), phone_number_id=getenv("PHONE_NUMBER_ID")) + + response = messenger.send_message( + message="https://www.youtube.com/watch?v=K4TOrB7at0Y", + recipient_id=getenv("RECIPIENT_ID"), + ) + + assert mock_send.call_count == 3 diff --git a/tests/test_sending_template_message.py b/tests/test_sending_template_message.py index 04dc49d..7caf32f 100644 --- a/tests/test_sending_template_message.py +++ b/tests/test_sending_template_message.py @@ -1,6 +1,9 @@ from os import getenv from heyoo import WhatsApp from dotenv import load_dotenv +import requests +from requests.adapters import HTTPAdapter, Retry +from unittest.mock import patch def test_sending_template_message(): load_dotenv() @@ -11,3 +14,13 @@ def test_sending_template_message(): assert(response["contacts"][0]["input"]==getenv("RECIPIENT_ID")) assert(response["contacts"][0]["wa_id"]==getenv("RECIPIENT_ID")) assert(response["messaging_product"]=="whatsapp") + +@patch.object(requests.Session, 'send') +def test_send_template_retries(mock_send): + mock_send.side_effect = [requests.exceptions.RequestException] * 2 + [requests.Response()] + load_dotenv() + messenger = WhatsApp(token=getenv("TOKEN"), phone_number_id=getenv("PHONE_NUMBER_ID")) + + response = messenger.send_template("hello_world", getenv("RECIPIENT_ID")) + + assert mock_send.call_count == 3 diff --git a/tests/test_sending_video.py b/tests/test_sending_video.py index 3ad7a4e..7d6f530 100644 --- a/tests/test_sending_video.py +++ b/tests/test_sending_video.py @@ -1,6 +1,9 @@ from os import getenv from heyoo import WhatsApp from dotenv import load_dotenv +import requests +from requests.adapters import HTTPAdapter, Retry +from unittest.mock import patch def test_sending_video_successful(): load_dotenv() @@ -13,4 +16,17 @@ def test_sending_video_successful(): assert(response["contacts"][0]["input"]==getenv("RECIPIENT_ID")) assert(response["contacts"][0]["wa_id"]==getenv("RECIPIENT_ID")) - assert(response["messaging_product"]=="whatsapp") \ No newline at end of file + assert(response["messaging_product"]=="whatsapp") + +@patch.object(requests.Session, 'send') +def test_send_video_retries(mock_send): + mock_send.side_effect = [requests.exceptions.RequestException] * 2 + [requests.Response()] + load_dotenv() + messenger = WhatsApp(token=getenv("TOKEN"), phone_number_id=getenv("PHONE_NUMBER_ID")) + + response = messenger.send_video( + video="https://www.youtube.com/watch?v=K4TOrB7at0Y", + recipient_id=getenv("RECIPIENT_ID"), + ) + + assert mock_send.call_count == 3