Skip to content

Commit 17ead7b

Browse files
committed
added better retry system
1 parent db8789a commit 17ead7b

14 files changed

+882
-8
lines changed

.coverage

0 Bytes
Binary file not shown.

example.py

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Example usage of the Wasender API SDK
4+
5+
This example demonstrates:
6+
1. Sending a text message
7+
2. Getting all contacts
8+
9+
Set the following environment variables before running:
10+
- WASENDER_API_KEY: Your Wasender API key
11+
- WASENDER_ACCESS_TOKEN: Your personal access token (optional)
12+
13+
Usage:
14+
python example.py
15+
"""
16+
17+
import os
18+
import asyncio
19+
from wasenderapi import create_sync_wasender, create_async_wasender
20+
from wasenderapi.models import RetryConfig
21+
from wasenderapi.errors import WasenderAPIError
22+
23+
24+
def get_api_credentials():
25+
"""Get API credentials from environment variables."""
26+
api_key = os.getenv('WASENDER_API_KEY')
27+
access_token = os.getenv('WASENDER_ACCESS_TOKEN')
28+
29+
if not api_key:
30+
raise ValueError(
31+
"WASENDER_API_KEY environment variable is required. "
32+
"Please set it to your Wasender API key."
33+
)
34+
35+
return api_key, access_token
36+
37+
38+
def sync_example():
39+
"""Example using the synchronous client."""
40+
print("=== Synchronous Client Example ===")
41+
42+
try:
43+
# Get credentials from environment
44+
api_key, access_token = get_api_credentials()
45+
46+
# Create sync client with retry configuration
47+
retry_config = RetryConfig(enabled=True, max_retries=3)
48+
client = create_sync_wasender(
49+
api_key=api_key,
50+
personal_access_token=access_token,
51+
retry_options=retry_config
52+
)
53+
54+
# Example 1: Send a text message
55+
print("\n1. Sending text message...")
56+
try:
57+
# Replace with an actual phone number for testing
58+
phone_number = "+1234567890" # Update this with a real number
59+
message_text = "Hello from Wasender API SDK! 🚀"
60+
61+
result = client.send_text(
62+
to=phone_number,
63+
text_body=message_text
64+
)
65+
66+
print(f"✅ Message sent successfully!")
67+
print(f" Status: {result.response.message}")
68+
69+
# Show rate limit info
70+
if result.rate_limit:
71+
print(f" Rate limit: {result.rate_limit.remaining}/{result.rate_limit.limit} remaining")
72+
73+
except WasenderAPIError as e:
74+
print(f"❌ Failed to send message: {e.message}")
75+
if e.status_code == 429:
76+
print(f" Rate limited. Retry after: {e.retry_after} seconds")
77+
elif e.error_details:
78+
print(f" Error details: {e.error_details}")
79+
# Example 2: Get all contacts
80+
print("\n2. Getting contacts...")
81+
try:
82+
contacts_result = client.get_contacts()
83+
84+
print(f"✅ Retrieved contacts successfully!")
85+
print(f" Total contacts: {len(contacts_result.response.data)}")
86+
87+
# Show first few contacts
88+
for i, contact in enumerate(contacts_result.response.data[:3]):
89+
contact_name = contact.name or "Unknown"
90+
contact_id = contact.id or "No ID"
91+
print(f" Contact {i+1}: {contact_name} ({contact_id})")
92+
93+
if len(contacts_result.response.data) > 3:
94+
print(f" ... and {len(contacts_result.response.data) - 3} more")
95+
96+
except WasenderAPIError as e:
97+
print(f"❌ Failed to get contacts: {e.message}")
98+
if e.status_code == 401:
99+
print(" Check your API key and access token")
100+
elif e.error_details:
101+
print(f" Error details: {e.error_details}")
102+
103+
except ValueError as e:
104+
print(f"❌ Configuration error: {e}")
105+
except Exception as e:
106+
print(f"❌ Unexpected error: {e}")
107+
108+
109+
async def async_example():
110+
"""Example using the asynchronous client."""
111+
print("\n=== Asynchronous Client Example ===")
112+
113+
try:
114+
# Get credentials from environment
115+
api_key, access_token = get_api_credentials()
116+
117+
# Create async client with retry configuration
118+
retry_config = RetryConfig(enabled=True, max_retries=3)
119+
120+
async with create_async_wasender(
121+
api_key=api_key,
122+
personal_access_token=access_token,
123+
retry_options=retry_config
124+
) as client:
125+
126+
# Example 1: Send a text message
127+
print("\n1. Sending text message (async)...")
128+
try:
129+
# Replace with an actual phone number for testing
130+
phone_number = "+1234567890" # Update this with a real number
131+
message_text = "Hello from Wasender API SDK (async)! 🚀"
132+
133+
result = await client.send_text(
134+
to=phone_number,
135+
text_body=message_text
136+
)
137+
138+
print(f"✅ Message sent successfully!")
139+
print(f" Status: {result.response.message}")
140+
141+
# Show rate limit info
142+
if result.rate_limit:
143+
print(f" Rate limit: {result.rate_limit.remaining}/{result.rate_limit.limit} remaining")
144+
145+
except WasenderAPIError as e:
146+
print(f"❌ Failed to send message: {e.message}")
147+
if e.status_code == 429:
148+
print(f" Rate limited. Retry after: {e.retry_after} seconds")
149+
elif e.error_details:
150+
print(f" Error details: {e.error_details}")
151+
# Example 2: Get all contacts
152+
print("\n2. Getting contacts (async)...")
153+
try:
154+
contacts_result = await client.get_contacts()
155+
156+
print(f"✅ Retrieved contacts successfully!")
157+
print(f" Total contacts: {len(contacts_result.response.data)}")
158+
159+
# Show first few contacts
160+
for i, contact in enumerate(contacts_result.response.data[:3]):
161+
contact_name = contact.name or "Unknown"
162+
contact_id = contact.id or "No ID"
163+
print(f" Contact {i+1}: {contact_name} ({contact_id})")
164+
165+
if len(contacts_result.response.data) > 3:
166+
print(f" ... and {len(contacts_result.response.data) - 3} more")
167+
168+
except WasenderAPIError as e:
169+
print(f"❌ Failed to get contacts: {e.message}")
170+
if e.status_code == 401:
171+
print(" Check your API key and access token")
172+
elif e.error_details:
173+
print(f" Error details: {e.error_details}")
174+
175+
except ValueError as e:
176+
print(f"❌ Configuration error: {e}")
177+
except Exception as e:
178+
print(f"❌ Unexpected error: {e}")
179+
180+
181+
def main():
182+
"""Main function to run both sync and async examples."""
183+
print("Wasender API SDK Example")
184+
print("=" * 40)
185+
186+
# Check if environment variables are set
187+
api_key = os.getenv('WASENDER_API_KEY')
188+
if not api_key:
189+
print("❌ Environment variable WASENDER_API_KEY is not set!")
190+
print("\nTo run this example, please set the required environment variables:")
191+
print(" WASENDER_API_KEY=your_api_key_here")
192+
print(" WASENDER_ACCESS_TOKEN=your_access_token_here # Optional")
193+
print("\nExample:")
194+
print(" export WASENDER_API_KEY=wsk_...")
195+
print(" export WASENDER_ACCESS_TOKEN=pat_...")
196+
print(" python example.py")
197+
return
198+
199+
print(f"✅ API Key found: {api_key[:10]}...")
200+
201+
access_token = os.getenv('WASENDER_ACCESS_TOKEN')
202+
if access_token:
203+
print(f"✅ Access Token found: {access_token[:10]}...")
204+
else:
205+
print("ℹ️ Access Token not provided (optional)")
206+
207+
# Run synchronous example
208+
sync_example()
209+
210+
# Run asynchronous example
211+
asyncio.run(async_example())
212+
213+
print("\n" + "=" * 40)
214+
print("Examples completed!")
215+
216+
217+
if __name__ == "__main__":
218+
main()

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ keywords = ["whatsapp", "api", "sdk", "wasender", "messaging", "chatbot"]
3030
dependencies = [
3131
"httpx>=0.23.0,<0.28.0",
3232
"pydantic>=2.0,<3.0",
33+
"requests>=2.25.0,<3.0.0",
3334
]
3435

3536
[project.urls]
4.75 KB
Binary file not shown.
32.9 KB
Binary file not shown.
14.7 KB
Binary file not shown.

tests/test_client.py

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import pytest
2-
from unittest.mock import AsyncMock, patch
3-
from wasenderapi import create_async_wasender, __version__ as SDK_VERSION
2+
from unittest.mock import AsyncMock, patch, Mock, MagicMock
3+
import time
4+
import asyncio
5+
from wasenderapi import create_async_wasender, create_sync_wasender, __version__ as SDK_VERSION
46
from wasenderapi.models import RetryConfig, WasenderSendResult
57
from wasenderapi.errors import WasenderAPIError
68
import json
79
import httpx
10+
import requests
811

912
@pytest.fixture
1013
async def async_client_with_mocked_post():
@@ -19,6 +22,19 @@ async def async_client_with_mocked_post():
1922
client._post_internal.return_value = mock_post_return_value
2023
return client
2124

25+
@pytest.fixture
26+
def sync_client_with_mocked_post():
27+
retry_config_disabled = RetryConfig(enabled=False)
28+
client = create_sync_wasender("test_api_key", retry_options=retry_config_disabled)
29+
client._post_internal = Mock(name="_post_internal")
30+
31+
mock_post_return_value = {
32+
"response": {"success": True, "message": "ok", "data": {"messageId": "mock_message_id"}},
33+
"rate_limit": {"limit": 1000, "remaining": 999, "reset_timestamp": 1620000000}
34+
}
35+
client._post_internal.return_value = mock_post_return_value
36+
return client
37+
2238
@pytest.fixture
2339
def success_api_response_data():
2440
return {"success": True, "message": "Message sent successfully", "data": {"messageId": "test-message-id"}}
@@ -39,6 +55,18 @@ def error_api_response_data():
3955
"errors": [{"field": "to", "message": "The 'to' field is invalid."}]
4056
}
4157

58+
@pytest.fixture
59+
def sync_client_with_retry_enabled():
60+
retry_config = RetryConfig(enabled=True, max_retries=3)
61+
client = create_sync_wasender("test_api_key", retry_options=retry_config)
62+
return client
63+
64+
@pytest.fixture
65+
async def async_client_with_retry_enabled():
66+
retry_config = RetryConfig(enabled=True, max_retries=3)
67+
client = create_async_wasender("test_api_key", retry_options=retry_config)
68+
return client
69+
4270
@pytest.mark.asyncio
4371
async def test_send_text_constructs_correct_payload_and_calls_post_internal(async_client_with_mocked_post):
4472
client = async_client_with_mocked_post
@@ -150,6 +178,7 @@ async def test_send_document(async_client_with_mocked_post, success_api_response
150178
"to": test_to,
151179
"messageType": "document",
152180
"documentUrl": test_url,
181+
"fileName": test_filename,
153182
"text": test_caption
154183
}
155184
)
@@ -244,6 +273,43 @@ async def test_send_location(async_client_with_mocked_post, success_api_response
244273
assert response.response.success == True
245274
assert response.response.message == success_api_response_data["message"]
246275

276+
@pytest.mark.asyncio
277+
async def test_send_poll(async_client_with_mocked_post, success_api_response_data, rate_limit_data):
278+
client = async_client_with_mocked_post
279+
client._post_internal.return_value = {
280+
"response": success_api_response_data,
281+
"rate_limit": rate_limit_data
282+
}
283+
284+
test_to = "1234567890"
285+
test_question = "What's your favorite color?"
286+
test_options = ["Red", "Blue", "Green", "Yellow"]
287+
test_multiple_answers = True
288+
289+
response = await client.send_poll(
290+
to=test_to,
291+
question=test_question,
292+
options=test_options,
293+
is_multiple_choice=test_multiple_answers
294+
)
295+
296+
client._post_internal.assert_called_once_with(
297+
"/send-message",
298+
{
299+
"to": test_to,
300+
"messageType": "poll",
301+
"poll": {
302+
"question": test_question,
303+
"options": test_options,
304+
"multiSelect": test_multiple_answers
305+
}
306+
}
307+
)
308+
assert isinstance(response, WasenderSendResult)
309+
assert response.response.success == True
310+
assert response.response.message == success_api_response_data["message"]
311+
assert response.rate_limit.limit == rate_limit_data["limit"]
312+
247313
@pytest.mark.asyncio
248314
async def test_api_error_raised_from_post_internal(async_client_with_mocked_post, error_api_response_data):
249315
client = async_client_with_mocked_post
@@ -263,4 +329,10 @@ async def test_api_error_raised_from_post_internal(async_client_with_mocked_post
263329

264330
assert exc_info.value.status_code == 400
265331
assert exc_info.value.api_message == original_api_message
266-
assert exc_info.value.error_details == original_error_details
332+
assert exc_info.value.error_details == original_error_details
333+
334+
335+
336+
337+
338+

0 commit comments

Comments
 (0)