Skip to content

Commit 7ed8bf4

Browse files
authored
feat: async support (#676)
1 parent 8fcb6ac commit 7ed8bf4

File tree

531 files changed

+67742
-796
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

531 files changed

+67742
-796
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,26 @@ message = client.messages.create(to="+12316851234", from_="+15555555555",
134134
body="Hello there!")
135135
```
136136

137+
### Asynchronous API Requests
138+
139+
By default, the Twilio Client will make synchronous requests to the Twilio API. To allow for asynchronous, non-blocking requests, we've included an optional asynchronous HTTP client. When used with the Client and the accompanying `*_async` methods, requests made to the Twilio API will be performed asynchronously.
140+
141+
```python
142+
from twilio.http.async_http_client import AsyncTwilioHttpClient
143+
from twilio.rest import Client
144+
145+
async def main():
146+
account = "ACXXXXXXXXXXXXXXXXX"
147+
token = "YYYYYYYYYYYYYYYYYY"
148+
http_client = AsyncTwilioHttpClient()
149+
client = Client(account, token, http_client=http_client)
150+
151+
message = await client.messages.create_async(to="+12316851234", from_="+15555555555",
152+
body="Hello there!")
153+
154+
asyncio.run(main())
155+
```
156+
137157
### Enable Debug Logging
138158

139159
Log the API request and response data to the console:

requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ pygments>=2.7.4 # not directly required, pinned by Snyk to avoid a vulnerability
22
pytz
33
requests>=2.0.0
44
PyJWT>=2.0.0, <3.0.0
5+
asyncio>=3.4.3
6+
aiohttp>=3.8.4
7+
aiohttp-retry>=2.8.3

setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
"pytz",
2525
"requests >= 2.0.0",
2626
"PyJWT >= 2.0.0, < 3.0.0",
27+
"asyncio>=3.4.3",
28+
"aiohttp>=3.8.4",
29+
"aiohttp-retry>=2.8.3"
2730
],
2831
packages=find_packages(exclude=['tests', 'tests.*']),
2932
include_package_data=True,

tests/integration/flex_api/v1/test_assessments.py

Lines changed: 0 additions & 40 deletions
This file was deleted.

tests/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ Sphinx>=1.8.0
22
mock
33
pytest
44
pytest-cov
5+
aiounittest
56
flake8
67
wheel>=0.22.0
78
cryptography
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import unittest
2+
import aiounittest
3+
4+
from aiohttp import ClientSession
5+
from mock import patch, AsyncMock
6+
from twilio.http.async_http_client import AsyncTwilioHttpClient
7+
8+
9+
class MockResponse(object):
10+
"""
11+
A mock of the aiohttp.ClientResponse class
12+
"""
13+
def __init__(self, text, status):
14+
self._text = text
15+
self.status = status
16+
self.headers = {}
17+
18+
async def text(self):
19+
return self._text
20+
21+
22+
class TestAsyncHttpClientRequest(aiounittest.AsyncTestCase):
23+
def setUp(self):
24+
self.session_mock = AsyncMock(wraps=ClientSession)
25+
self.session_mock.request.return_value = MockResponse('test', 200)
26+
27+
self.session_patcher = patch('twilio.http.async_http_client.aiohttp.ClientSession')
28+
session_constructor_mock = self.session_patcher.start()
29+
session_constructor_mock.return_value = self.session_mock
30+
31+
self.client = AsyncTwilioHttpClient()
32+
33+
def tearDown(self):
34+
self.session_patcher.stop()
35+
36+
async def test_request_called_with_method_and_url(self):
37+
await self.client.request('GET', 'https://mock.twilio.com')
38+
39+
self.session_mock.request.assert_called()
40+
request_args = self.session_mock.request.call_args.kwargs
41+
self.assertIsNotNone(request_args)
42+
self.assertEqual(request_args['method'], 'GET')
43+
self.assertEqual(request_args['url'], 'https://mock.twilio.com')
44+
45+
async def test_request_called_with_basic_auth(self):
46+
await self.client.request('doesnt matter', 'doesnt matter', auth=('account_sid', 'auth_token'))
47+
48+
self.session_mock.request.assert_called()
49+
auth = self.session_mock.request.call_args.kwargs['auth']
50+
self.assertIsNotNone(auth)
51+
self.assertEqual(auth.login, 'account_sid')
52+
self.assertEqual(auth.password, 'auth_token')
53+
54+
async def test_invalid_request_timeout_raises_exception(self):
55+
with self.assertRaises(ValueError):
56+
await self.client.request('doesnt matter', 'doesnt matter', timeout=-1)
57+
58+
59+
class TestAsyncHttpClientRetries(aiounittest.AsyncTestCase):
60+
def setUp(self):
61+
self.session_mock = AsyncMock(wraps=ClientSession)
62+
self.session_mock.request.side_effect = [MockResponse('Error', 500), MockResponse('Error', 500),
63+
MockResponse('Success', 200)]
64+
65+
self.session_patcher = patch('twilio.http.async_http_client.aiohttp.ClientSession')
66+
session_constructor_mock = self.session_patcher.start()
67+
session_constructor_mock.return_value = self.session_mock
68+
69+
def tearDown(self):
70+
self.session_patcher.stop()
71+
72+
async def test_request_retries_until_success(self):
73+
client = AsyncTwilioHttpClient(max_retries=99)
74+
response = await client.request('doesnt matter', 'doesnt matter')
75+
76+
self.assertEqual(self.session_mock.request.call_count, 3)
77+
self.assertEqual(response.status_code, 200)
78+
self.assertEqual(response.text, 'Success')
79+
80+
async def test_request_retries_until_max(self):
81+
client = AsyncTwilioHttpClient(max_retries=2)
82+
response = await client.request('doesnt matter', 'doesnt matter')
83+
84+
self.assertEqual(self.session_mock.request.call_count, 2)
85+
self.assertEqual(response.status_code, 500)
86+
self.assertEqual(response.text, 'Error')
87+
88+
89+
class TestAsyncHttpClientSession(aiounittest.AsyncTestCase):
90+
def setUp(self):
91+
self.session_patcher = patch('twilio.http.async_http_client.aiohttp.ClientSession')
92+
self.session_constructor_mock = self.session_patcher.start()
93+
94+
def tearDown(self):
95+
self.session_patcher.stop()
96+
97+
def _setup_session_response(self, value):
98+
session_mock = AsyncMock(wraps=ClientSession)
99+
session_mock.request.return_value = MockResponse(value, 200)
100+
session_mock.close.return_value = None
101+
self.session_constructor_mock.return_value = session_mock
102+
103+
async def test_session_preserved(self):
104+
self._setup_session_response('response_1')
105+
106+
client = AsyncTwilioHttpClient()
107+
response_1 = await client.request('GET', 'https://api.twilio.com')
108+
109+
self._setup_session_response('response_2')
110+
response_2 = await client.request('GET', 'https://api.twilio.com')
111+
112+
# Used same session, response should be the same
113+
self.assertEqual(response_1.content, 'response_1')
114+
self.assertEqual(response_2.content, 'response_1')
115+
116+
async def test_session_not_preserved(self):
117+
self._setup_session_response('response_1')
118+
119+
client = AsyncTwilioHttpClient(pool_connections=False)
120+
response_1 = await client.request('GET', 'https://api.twilio.com')
121+
122+
self._setup_session_response('response_2')
123+
response_2 = await client.request('GET', 'https://api.twilio.com')
124+
125+
# No session used, responses should be different (not cached)
126+
self.assertEqual(response_1.content, 'response_1')
127+
self.assertEqual(response_2.content, 'response_2')

tests/unit/rest/test_client.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import unittest
2+
import aiounittest
23

4+
from mock import AsyncMock, Mock
5+
from twilio.http.response import Response
36
from twilio.rest import (
47
Client
58
)
@@ -74,3 +77,34 @@ def test_set_user_agent_extensions(self):
7477
user_agent_headers = self.client.http_client._test_only_last_request.headers['User-Agent']
7578
user_agent_extensions = user_agent_headers.split(" ")[-len(expected_user_agent_extensions):]
7679
self.assertEqual(user_agent_extensions, expected_user_agent_extensions)
80+
81+
82+
class TestClientAsyncRequest(aiounittest.AsyncTestCase):
83+
def setUp(self):
84+
self.mock_async_http_client = AsyncMock()
85+
self.mock_async_http_client.request.return_value = Response(200, 'test')
86+
self.mock_async_http_client.is_async = True
87+
self.client = Client('username', 'password', http_client=self.mock_async_http_client)
88+
89+
async def test_raise_error_if_client_not_marked_async(self):
90+
mock_http_client = Mock()
91+
mock_http_client.request.return_value = Response(200, 'doesnt matter')
92+
mock_http_client.is_async = None
93+
94+
client = Client('username', 'password', http_client=mock_http_client)
95+
with self.assertRaises(RuntimeError):
96+
await client.request_async('doesnt matter', 'doesnt matter')
97+
98+
async def test_raise_error_if_client_is_not_async(self):
99+
mock_http_client = Mock()
100+
mock_http_client.request.return_value = Response(200, 'doesnt matter')
101+
mock_http_client.is_async = False
102+
103+
client = Client('username', 'password', http_client=mock_http_client)
104+
with self.assertRaises(RuntimeError):
105+
await client.request_async('doesnt matter', 'doesnt matter')
106+
107+
async def test_request_async_called_with_method_and_url(self):
108+
await self.client.request_async('GET', 'http://mock.twilio.com')
109+
self.assertEqual(self.mock_async_http_client.request.call_args.args[0], 'GET')
110+
self.assertEqual(self.mock_async_http_client.request.call_args.args[1], 'http://mock.twilio.com')

0 commit comments

Comments
 (0)