Skip to content

Commit 8b87eae

Browse files
authored
feat: add support for response headers (#902)
1 parent 0a9d6cb commit 8b87eae

File tree

4 files changed

+615
-0
lines changed

4 files changed

+615
-0
lines changed

tests/unit/base/test_version.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,142 @@ def test_delete_not_found(self):
119119
)
120120

121121
self.assertIn("Unable to delete record", str(context.exception))
122+
123+
def test_fetch_with_response_info(self):
124+
self.holodeck.mock(
125+
Response(200, '{"sid": "AC123", "name": "Test Account"}', {"X-Custom-Header": "test-value"}),
126+
Request(url="https://api.twilio.com/2010-04-01/Accounts/AC123.json"),
127+
)
128+
payload, status_code, headers = self.client.api.v2010.fetch_with_response_info(
129+
method="GET", uri="/Accounts/AC123.json"
130+
)
131+
132+
self.assertEqual(payload["sid"], "AC123")
133+
self.assertEqual(payload["name"], "Test Account")
134+
self.assertEqual(status_code, 200)
135+
self.assertIn("X-Custom-Header", headers)
136+
self.assertEqual(headers["X-Custom-Header"], "test-value")
137+
138+
def test_update_with_response_info(self):
139+
self.holodeck.mock(
140+
Response(200, '{"sid": "AC123", "name": "Updated Account"}', {"X-Update-Header": "updated"}),
141+
Request(
142+
method="POST",
143+
url="https://api.twilio.com/2010-04-01/Accounts/AC123.json",
144+
),
145+
)
146+
payload, status_code, headers = self.client.api.v2010.update_with_response_info(
147+
method="POST", uri="/Accounts/AC123.json", data={"name": "Updated Account"}
148+
)
149+
150+
self.assertEqual(payload["sid"], "AC123")
151+
self.assertEqual(payload["name"], "Updated Account")
152+
self.assertEqual(status_code, 200)
153+
self.assertIn("X-Update-Header", headers)
154+
155+
def test_delete_with_response_info(self):
156+
self.holodeck.mock(
157+
Response(204, "", {"X-Delete-Header": "deleted"}),
158+
Request(
159+
method="DELETE",
160+
url="https://api.twilio.com/2010-04-01/Accounts/AC123/Messages/MM123.json",
161+
),
162+
)
163+
success, status_code, headers = self.client.api.v2010.delete_with_response_info(
164+
method="DELETE", uri="/Accounts/AC123/Messages/MM123.json"
165+
)
166+
167+
self.assertTrue(success)
168+
self.assertEqual(status_code, 204)
169+
self.assertIn("X-Delete-Header", headers)
170+
171+
def test_create_with_response_info(self):
172+
self.holodeck.mock(
173+
Response(201, '{"sid": "MM123", "body": "Hello World"}', {"X-Create-Header": "created"}),
174+
Request(
175+
method="POST",
176+
url="https://api.twilio.com/2010-04-01/Accounts/AC123/Messages.json",
177+
),
178+
)
179+
payload, status_code, headers = self.client.api.v2010.create_with_response_info(
180+
method="POST", uri="/Accounts/AC123/Messages.json", data={"body": "Hello World"}
181+
)
182+
183+
self.assertEqual(payload["sid"], "MM123")
184+
self.assertEqual(payload["body"], "Hello World")
185+
self.assertEqual(status_code, 201)
186+
self.assertIn("X-Create-Header", headers)
187+
188+
def test_page_with_response_info(self):
189+
self.holodeck.mock(
190+
Response(200, '{"messages": [], "next_page_uri": null}', {"X-Page-Header": "page"}),
191+
Request(url="https://api.twilio.com/2010-04-01/Accounts/AC123/Messages.json"),
192+
)
193+
response, status_code, headers = self.client.api.v2010.page_with_response_info(
194+
method="GET", uri="/Accounts/AC123/Messages.json"
195+
)
196+
197+
self.assertIsNotNone(response)
198+
self.assertEqual(status_code, 200)
199+
self.assertIn("X-Page-Header", headers)
200+
201+
def test_fetch_with_response_info_error(self):
202+
self.holodeck.mock(
203+
Response(404, '{"message": "Resource not found"}'),
204+
Request(url="https://api.twilio.com/2010-04-01/Accounts/AC456.json"),
205+
)
206+
207+
with self.assertRaises(Exception) as context:
208+
self.client.api.v2010.fetch_with_response_info(
209+
method="GET", uri="/Accounts/AC456.json"
210+
)
211+
212+
self.assertIn("Unable to fetch record", str(context.exception))
213+
214+
def test_update_with_response_info_error(self):
215+
self.holodeck.mock(
216+
Response(400, '{"message": "Invalid request"}'),
217+
Request(
218+
method="POST",
219+
url="https://api.twilio.com/2010-04-01/Accounts/AC123.json",
220+
),
221+
)
222+
223+
with self.assertRaises(Exception) as context:
224+
self.client.api.v2010.update_with_response_info(
225+
method="POST", uri="/Accounts/AC123.json", data={"invalid": "data"}
226+
)
227+
228+
self.assertIn("Unable to update record", str(context.exception))
229+
230+
def test_delete_with_response_info_error(self):
231+
self.holodeck.mock(
232+
Response(404, '{"message": "Resource not found"}'),
233+
Request(
234+
method="DELETE",
235+
url="https://api.twilio.com/2010-04-01/Accounts/AC123/Messages/MM456.json",
236+
),
237+
)
238+
239+
with self.assertRaises(Exception) as context:
240+
self.client.api.v2010.delete_with_response_info(
241+
method="DELETE", uri="/Accounts/AC123/Messages/MM456.json"
242+
)
243+
244+
self.assertIn("Unable to delete record", str(context.exception))
245+
246+
def test_create_with_response_info_error(self):
247+
self.holodeck.mock(
248+
Response(400, '{"message": "Invalid request"}'),
249+
Request(
250+
method="POST",
251+
url="https://api.twilio.com/2010-04-01/Accounts/AC123/Messages.json",
252+
),
253+
)
254+
255+
with self.assertRaises(Exception) as context:
256+
self.client.api.v2010.create_with_response_info(
257+
method="POST", uri="/Accounts/AC123/Messages.json", data={"invalid": "data"}
258+
)
259+
260+
self.assertIn("Unable to create record", str(context.exception))
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import unittest
2+
from twilio.base.api_response import ApiResponse
3+
4+
5+
class TestApiResponse(unittest.TestCase):
6+
def test_initialization(self):
7+
"""Test ApiResponse initialization"""
8+
data = {'sid': 'AC123', 'friendly_name': 'Test'}
9+
response = ApiResponse(
10+
data=data,
11+
status_code=200,
12+
headers={'Content-Type': 'application/json'}
13+
)
14+
15+
self.assertEqual(response.data, data)
16+
self.assertEqual(response.status_code, 200)
17+
self.assertEqual(response.headers['Content-Type'], 'application/json')
18+
19+
def test_repr(self):
20+
"""Test string representation"""
21+
response = ApiResponse(data={'test': 'data'}, status_code=201, headers={})
22+
repr_str = repr(response)
23+
self.assertIn('201', repr_str)
24+
self.assertIn('dict', repr_str)
25+
26+
def test_str(self):
27+
"""Test human-readable string representation"""
28+
response = ApiResponse(data={'test': 'data'}, status_code=200, headers={})
29+
str_repr = str(response)
30+
self.assertIn('200', str_repr)
31+
self.assertIn('dict', str_repr)
32+
33+
def test_with_list_data(self):
34+
"""Test ApiResponse with list data"""
35+
data = [{'sid': 'AC1'}, {'sid': 'AC2'}]
36+
response = ApiResponse(data=data, status_code=200, headers={})
37+
self.assertEqual(len(response.data), 2)
38+
self.assertEqual(response.status_code, 200)
39+
40+
def test_with_boolean_data(self):
41+
"""Test ApiResponse with boolean data (delete operations)"""
42+
response = ApiResponse(data=True, status_code=204, headers={})
43+
self.assertTrue(response.data)
44+
self.assertEqual(response.status_code, 204)
45+
46+
def test_with_different_status_codes(self):
47+
"""Test ApiResponse with various status codes"""
48+
# Test 201 Created
49+
response_201 = ApiResponse(data={'created': True}, status_code=201, headers={})
50+
self.assertEqual(response_201.status_code, 201)
51+
52+
# Test 204 No Content
53+
response_204 = ApiResponse(data=True, status_code=204, headers={})
54+
self.assertEqual(response_204.status_code, 204)
55+
56+
# Test 200 OK
57+
response_200 = ApiResponse(data={'ok': True}, status_code=200, headers={})
58+
self.assertEqual(response_200.status_code, 200)
59+
60+
def test_headers_access(self):
61+
"""Test accessing various headers"""
62+
headers = {
63+
'Content-Type': 'application/json',
64+
'X-RateLimit-Remaining': '100',
65+
'X-RateLimit-Limit': '1000'
66+
}
67+
response = ApiResponse(data={'test': 'data'}, status_code=200, headers=headers)
68+
69+
self.assertEqual(response.headers['Content-Type'], 'application/json')
70+
self.assertEqual(response.headers['X-RateLimit-Remaining'], '100')
71+
self.assertEqual(response.headers['X-RateLimit-Limit'], '1000')
72+
73+
def test_empty_headers(self):
74+
"""Test ApiResponse with empty headers"""
75+
response = ApiResponse(data={'test': 'data'}, status_code=200, headers={})
76+
self.assertEqual(response.headers, {})
77+
78+
def test_data_attribute_types(self):
79+
"""Test that data attribute can hold various types"""
80+
# Dictionary data
81+
dict_response = ApiResponse(data={'key': 'value'}, status_code=200, headers={})
82+
self.assertIsInstance(dict_response.data, dict)
83+
84+
# List data
85+
list_response = ApiResponse(data=[1, 2, 3], status_code=200, headers={})
86+
self.assertIsInstance(list_response.data, list)
87+
88+
# Boolean data
89+
bool_response = ApiResponse(data=True, status_code=204, headers={})
90+
self.assertIsInstance(bool_response.data, bool)
91+
92+
# None data
93+
none_response = ApiResponse(data=None, status_code=204, headers={})
94+
self.assertIsNone(none_response.data)
95+
96+
97+
if __name__ == '__main__':
98+
unittest.main()

twilio/base/api_response.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""
2+
ApiResponse class for wrapping API responses with metadata
3+
"""
4+
from typing import Any, Dict, Generic, TypeVar
5+
6+
T = TypeVar('T')
7+
8+
9+
class ApiResponse(Generic[T]):
10+
"""
11+
Wrapper for API responses that includes HTTP metadata.
12+
13+
This class is returned by *_with_http_info methods and provides access to:
14+
- The response data (resource instance, list, or boolean)
15+
- HTTP status code
16+
- Response headers
17+
18+
Attributes:
19+
data: The response data (instance, list, or boolean for delete operations)
20+
status_code: HTTP status code from the response
21+
headers: Dictionary of response headers
22+
23+
Example:
24+
>>> response = client.accounts.create_with_http_info(friendly_name="Test")
25+
>>> print(response.status_code) # 201
26+
>>> print(response.headers['Content-Type']) # application/json
27+
>>> account = response.data
28+
>>> print(account.sid)
29+
"""
30+
31+
def __init__(self, data: T, status_code: int, headers: Dict[str, str]):
32+
"""
33+
Initialize an ApiResponse
34+
35+
Args:
36+
data: The response payload (instance, list, or boolean)
37+
status_code: HTTP status code (e.g., 200, 201, 204)
38+
headers: Dictionary of response headers
39+
"""
40+
self.data = data
41+
self.status_code = status_code
42+
self.headers = headers
43+
44+
def __repr__(self) -> str:
45+
"""String representation of the ApiResponse"""
46+
return f"ApiResponse(status_code={self.status_code}, data={type(self.data).__name__})"
47+
48+
def __str__(self) -> str:
49+
"""Human-readable string representation"""
50+
return f"<ApiResponse [{self.status_code}] with {type(self.data).__name__}>"

0 commit comments

Comments
 (0)