Skip to content

Commit 8eb87f6

Browse files
Remove base_url param, add sandbox support
1 parent 3951a30 commit 8eb87f6

File tree

9 files changed

+155
-15
lines changed

9 files changed

+155
-15
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ client = DevoClient(api_key="your-api-key")
7373
```python
7474
client = DevoClient(
7575
api_key="your-api-key",
76-
base_url="https://api.devo.com", # Optional: custom base URL
7776
timeout=30.0, # Optional: request timeout
7877
)
7978
```

src/devo_global_comms_python/client.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class DevoClient:
5555
def __init__(
5656
self,
5757
api_key: str,
58-
base_url: Optional[str] = None,
58+
sandbox_api_key: Optional[str] = None,
5959
timeout: float = DEFAULT_TIMEOUT,
6060
max_retries: int = 3,
6161
session: Optional[requests.Session] = None,
@@ -65,7 +65,7 @@ def __init__(
6565
6666
Args:
6767
api_key: API key for authentication
68-
base_url: Base URL for the API (defaults to production)
68+
sandbox_api_key: Optional sandbox API key for testing environments
6969
timeout: Request timeout in seconds
7070
max_retries: Maximum number of retries for failed requests
7171
session: Custom requests session (optional)
@@ -76,7 +76,9 @@ def __init__(
7676
if not api_key or not api_key.strip():
7777
raise DevoMissingAPIKeyException()
7878

79-
self.base_url = base_url or self.DEFAULT_BASE_URL
79+
self.api_key = api_key.strip()
80+
self.sandbox_api_key = sandbox_api_key.strip() if sandbox_api_key else None
81+
self.base_url = self.DEFAULT_BASE_URL
8082
self.timeout = timeout
8183

8284
# Set up authentication
@@ -121,6 +123,7 @@ def request(
121123
data: Optional[Dict[str, Any]] = None,
122124
json: Optional[Dict[str, Any]] = None,
123125
headers: Optional[Dict[str, str]] = None,
126+
sandbox: bool = False,
124127
) -> requests.Response:
125128
"""
126129
Make an authenticated request to the API.
@@ -132,6 +135,7 @@ def request(
132135
data: Form data
133136
json: JSON data
134137
headers: Additional headers
138+
sandbox: Use sandbox API key for this request (default: False)
135139
136140
Returns:
137141
requests.Response: The API response
@@ -142,6 +146,10 @@ def request(
142146
"""
143147
url = f"{self.base_url.rstrip('/')}/{path.lstrip('/')}"
144148

149+
# Validate sandbox usage
150+
if sandbox and not self.sandbox_api_key:
151+
raise DevoException("Sandbox API key required when sandbox=True")
152+
145153
# Prepare headers
146154
request_headers = {
147155
"User-Agent": f"devo-python-sdk/{__version__}",
@@ -151,7 +159,13 @@ def request(
151159
request_headers.update(headers)
152160

153161
# Add authentication headers
154-
auth_headers = self.auth.get_headers()
162+
if sandbox and self.sandbox_api_key:
163+
# Use sandbox API key for this request
164+
sandbox_auth = APIKeyAuth(self.sandbox_api_key)
165+
auth_headers = sandbox_auth.get_headers()
166+
else:
167+
# Use regular API key
168+
auth_headers = self.auth.get_headers()
155169
request_headers.update(auth_headers)
156170

157171
try:

src/devo_global_comms_python/resources/email.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def send_email(
2727
body: str,
2828
sender: str,
2929
recipient: str,
30+
sandbox: bool = False,
3031
) -> "EmailSendResponse":
3132
"""
3233
Send an email using the exact API specification.
@@ -36,6 +37,7 @@ def send_email(
3637
body: Email body content
3738
sender: Sender email address
3839
recipient: Recipient email address
40+
sandbox: Use sandbox environment for testing (default: False)
3941
4042
Returns:
4143
EmailSendResponse: The email send response

src/devo_global_comms_python/resources/messages.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class MessagesResource(BaseResource):
1515
across any channel (SMS, Email, WhatsApp, RCS).
1616
"""
1717

18-
def send(self, data: "SendMessageDto") -> "SendMessageSerializer":
18+
def send(self, data: "SendMessageDto", sandbox: bool = False) -> "SendMessageSerializer":
1919
"""
2020
Send a message through any channel (omni-channel endpoint).
2121

src/devo_global_comms_python/resources/sms.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def send_sms(
5858
message: str,
5959
sender: str,
6060
hirvalidation: bool = True,
61+
sandbox: bool = False,
6162
) -> "SMSQuickSendResponse":
6263
"""
6364
Send an SMS message using the quick-send API.
@@ -67,6 +68,7 @@ def send_sms(
6768
message: The SMS message content
6869
sender: The sender phone number or sender ID
6970
hirvalidation: Enable HIR validation (default: True)
71+
sandbox: Use sandbox environment for testing (default: False)
7072
7173
Returns:
7274
SMSQuickSendResponse: The sent message details including ID and status
@@ -102,7 +104,7 @@ def send_sms(
102104
)
103105

104106
# Send request to the exact API endpoint
105-
response = self.client.post("user-api/sms/quick-send", json=request_data.dict())
107+
response = self.client.post("user-api/sms/quick-send", json=request_data.dict(), sandbox=sandbox)
106108

107109
# Parse response according to API spec
108110
from ..models.sms import SMSQuickSendResponse
@@ -112,10 +114,13 @@ def send_sms(
112114

113115
return result
114116

115-
def get_senders(self) -> "SendersListResponse":
117+
def get_senders(self, sandbox: bool = False) -> "SendersListResponse":
116118
"""
117119
Retrieve the list of available senders for the account.
118120
121+
Args:
122+
sandbox: Use sandbox environment for testing (default: False)
123+
119124
Returns:
120125
SendersListResponse: List of available senders with their details
121126
@@ -131,7 +136,7 @@ def get_senders(self) -> "SendersListResponse":
131136
logger.info("Fetching available senders")
132137

133138
# Send request to the exact API endpoint
134-
response = self.client.get("user-api/me/senders")
139+
response = self.client.get("user-api/me/senders", sandbox=sandbox)
135140

136141
# Parse response according to API spec
137142
from ..models.sms import SendersListResponse
@@ -151,6 +156,7 @@ def buy_number(
151156
is_longcode: bool = True,
152157
agreement_last_sent_date: Optional[datetime] = None,
153158
is_automated_enabled: bool = True,
159+
sandbox: bool = False,
154160
) -> "NumberPurchaseResponse":
155161
"""
156162
Purchase a phone number.
@@ -164,6 +170,7 @@ def buy_number(
164170
is_longcode: Whether this is a long code number (default: True)
165171
agreement_last_sent_date: Last date agreement was sent (optional)
166172
is_automated_enabled: Whether automated messages are enabled (default: True)
173+
sandbox: Use sandbox environment for testing (default: False)
167174
168175
Returns:
169176
NumberPurchaseResponse: Details of the purchased number including features
@@ -227,6 +234,7 @@ def get_available_numbers(
227234
type: Optional[str] = None,
228235
prefix: Optional[str] = None,
229236
region: str = "US",
237+
sandbox: bool = False,
230238
) -> "AvailableNumbersResponse":
231239
"""
232240
Get available phone numbers for purchase.
@@ -238,6 +246,7 @@ def get_available_numbers(
238246
type: Filter by type (optional)
239247
prefix: Filter by prefix (optional)
240248
region: Filter by region (Country ISO Code), default: "US"
249+
sandbox: Use sandbox environment for testing (default: False)
241250
242251
Returns:
243252
AvailableNumbersResponse: List of available numbers with their features
@@ -292,14 +301,17 @@ def get_available_numbers(
292301
return result
293302

294303
# Legacy methods for backward compatibility
295-
def send(self, to: str, body: str, from_: Optional[str] = None, **kwargs) -> "SMSQuickSendResponse":
304+
def send(
305+
self, to: str, body: str, from_: Optional[str] = None, sandbox: bool = False, **kwargs
306+
) -> "SMSQuickSendResponse":
296307
"""
297308
Legacy method for sending SMS (backward compatibility).
298309
299310
Args:
300311
to: The recipient's phone number in E.164 format
301312
body: The message body text
302313
from_: The sender's phone number (optional)
314+
sandbox: Use sandbox environment for testing (default: False)
303315
**kwargs: Additional parameters (ignored for compatibility)
304316
305317
Returns:

src/devo_global_comms_python/resources/whatsapp.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def get_accounts(
5050
limit: Optional[int] = None,
5151
is_approved: Optional[bool] = None,
5252
search: Optional[str] = None,
53+
sandbox: bool = False,
5354
) -> "GetWhatsAppAccountsResponse":
5455
"""
5556
Get all shared WhatsApp accounts.
@@ -92,7 +93,7 @@ def get_accounts(
9293

9394
return GetWhatsAppAccountsResponse.model_validate(response.json())
9495

95-
def get_template(self, name: str) -> "WhatsAppTemplate":
96+
def get_template(self, name: str, sandbox: bool = False) -> "WhatsAppTemplate":
9697
"""
9798
Get a WhatsApp template by name.
9899
@@ -126,6 +127,7 @@ def upload_file(
126127
file_content: bytes,
127128
filename: str,
128129
content_type: str,
130+
sandbox: bool = False,
129131
) -> "WhatsAppUploadFileResponse":
130132
"""
131133
Upload a file for WhatsApp messaging.
@@ -177,6 +179,7 @@ def send_normal_message(
177179
to: str,
178180
message: str,
179181
account_id: Optional[str] = None,
182+
sandbox: bool = False,
180183
) -> "WhatsAppSendMessageResponse":
181184
"""
182185
Send a normal WhatsApp message.
@@ -228,6 +231,7 @@ def create_template(
228231
self,
229232
account_id: str,
230233
template: "WhatsAppTemplateRequest",
234+
sandbox: bool = False,
231235
) -> "WhatsAppTemplateResponse":
232236
"""
233237
Create a WhatsApp template.

tests/test_client.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,12 @@ def test_client_initialization_with_api_key(self, api_key):
2020

2121
def test_client_initialization_with_custom_params(self, api_key):
2222
"""Test client initialization with custom parameters."""
23-
base_url = "https://custom.api.com"
2423
timeout = 60.0
2524
max_retries = 5
2625

27-
client = DevoClient(api_key=api_key, base_url=base_url, timeout=timeout, max_retries=max_retries)
26+
client = DevoClient(api_key=api_key, timeout=timeout, max_retries=max_retries)
2827

29-
assert client.base_url == base_url
28+
assert client.base_url == DevoClient.DEFAULT_BASE_URL
3029
assert client.timeout == timeout
3130

3231
def test_client_has_all_resources(self, api_key):

tests/test_sandbox.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Tests for sandbox functionality in the Devo Global Communications Python SDK.
4+
"""
5+
6+
from unittest.mock import Mock, patch
7+
8+
import pytest
9+
10+
from devo_global_comms_python.client import DevoClient
11+
from devo_global_comms_python.exceptions import DevoException
12+
13+
14+
class TestSandboxFunctionality:
15+
"""Test sandbox API key switching functionality."""
16+
17+
def test_client_initialization_with_sandbox_api_key(self):
18+
"""Test that client can be initialized with sandbox API key."""
19+
client = DevoClient(api_key="test-api-key", sandbox_api_key="sandbox-api-key")
20+
21+
assert client.api_key == "test-api-key"
22+
assert client.sandbox_api_key == "sandbox-api-key"
23+
24+
def test_client_initialization_without_sandbox_api_key(self):
25+
"""Test that client can be initialized without sandbox API key."""
26+
client = DevoClient(api_key="test-api-key")
27+
28+
assert client.api_key == "test-api-key"
29+
assert client.sandbox_api_key is None
30+
31+
def test_sandbox_request_without_sandbox_api_key_raises_error(self):
32+
"""Test that sandbox request without sandbox API key raises appropriate error."""
33+
client = DevoClient(api_key="test-api-key")
34+
35+
with pytest.raises(DevoException, match="Sandbox API key required when sandbox=True"):
36+
client.get("test-endpoint", sandbox=True)
37+
38+
@patch("devo_global_comms_python.client.requests.Session.request")
39+
def test_sandbox_request_uses_sandbox_api_key(self, mock_request):
40+
"""Test that sandbox request uses sandbox API key for authentication."""
41+
# Mock successful response
42+
mock_response = Mock()
43+
mock_response.ok = True
44+
mock_response.json.return_value = {"success": True}
45+
mock_request.return_value = mock_response
46+
47+
client = DevoClient(api_key="production-api-key", sandbox_api_key="sandbox-api-key")
48+
49+
# Make sandbox request
50+
client.get("test-endpoint", sandbox=True)
51+
52+
# Verify the request was made with sandbox API key
53+
mock_request.assert_called_once()
54+
call_args = mock_request.call_args
55+
headers = call_args[1]["headers"]
56+
57+
# Check that X-API-Key header contains sandbox API key
58+
assert "X-API-Key" in headers
59+
assert "sandbox-api-key" in headers["X-API-Key"]
60+
61+
@patch("devo_global_comms_python.client.requests.Session.request")
62+
def test_regular_request_uses_production_api_key(self, mock_request):
63+
"""Test that regular request uses production API key for authentication."""
64+
# Mock successful response
65+
mock_response = Mock()
66+
mock_response.ok = True
67+
mock_response.json.return_value = {"success": True}
68+
mock_request.return_value = mock_response
69+
70+
client = DevoClient(api_key="production-api-key", sandbox_api_key="sandbox-api-key")
71+
72+
# Make regular request (sandbox=False by default)
73+
client.get("test-endpoint")
74+
75+
# Verify the request was made with production API key
76+
mock_request.assert_called_once()
77+
call_args = mock_request.call_args
78+
headers = call_args[1]["headers"]
79+
80+
# Check that X-API-Key header contains production API key
81+
assert "X-API-Key" in headers
82+
assert "production-api-key" in headers["X-API-Key"]
83+
84+
@patch("devo_global_comms_python.client.requests.Session.request")
85+
def test_sms_resource_sandbox_parameter(self, mock_request):
86+
"""Test that SMS resource functions correctly pass sandbox parameter."""
87+
# Mock successful response
88+
mock_response = Mock()
89+
mock_response.ok = True
90+
mock_response.json.return_value = {"senders": []}
91+
mock_request.return_value = mock_response
92+
93+
client = DevoClient(api_key="production-api-key", sandbox_api_key="sandbox-api-key")
94+
95+
# Call SMS function with sandbox=True
96+
client.sms.get_senders(sandbox=True)
97+
98+
# Verify the request was made with sandbox API key
99+
mock_request.assert_called_once()
100+
call_args = mock_request.call_args
101+
headers = call_args[1]["headers"]
102+
103+
# Check that X-API-Key header contains sandbox API key
104+
assert "X-API-Key" in headers
105+
assert "sandbox-api-key" in headers["X-API-Key"]
106+
107+
108+
if __name__ == "__main__":
109+
pytest.main([__file__, "-v"])

tests/test_sms.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def test_send_sms_success(self, sms_resource, test_phone_number):
6666
"message": "Hello, World!",
6767
"hirvalidation": True,
6868
},
69+
sandbox=False,
6970
)
7071

7172
def test_send_sms_with_invalid_recipient(self, sms_resource):
@@ -125,7 +126,7 @@ def test_get_senders_success(self, sms_resource):
125126
assert result.senders[1].istest is True
126127

127128
# Verify the API call
128-
sms_resource.client.get.assert_called_once_with("user-api/me/senders")
129+
sms_resource.client.get.assert_called_once_with("user-api/me/senders", sandbox=False)
129130

130131
def test_buy_number_success(self, sms_resource):
131132
"""Test purchasing a phone number successfully."""

0 commit comments

Comments
 (0)