Skip to content

Commit 2d87f00

Browse files
authored
feat: add regional and edge support (#520)
* feat: add regional and edge support
1 parent f59dbdd commit 2d87f00

File tree

3 files changed

+122
-9
lines changed

3 files changed

+122
-9
lines changed

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ token = "YYYYYYYYYYYYYYYYYY"
6868
client = Client(account, token)
6969
```
7070

71-
Alternately, a `Client` constructor without these parameters will
71+
Alternatively, a `Client` constructor without these parameters will
7272
look for `TWILIO_ACCOUNT_SID` and `TWILIO_AUTH_TOKEN` variables inside the
7373
current environment.
7474

@@ -82,6 +82,28 @@ from twilio.rest import Client
8282
client = Client()
8383
```
8484

85+
### Specify Region and/or Edge
86+
87+
```python
88+
from twilio.rest import Client
89+
90+
client = Client(region='au1', edge='sydney')
91+
```
92+
A `Client` constructor without these parameters will also look for `TWILIO_REGION` and `TWILIO_EDGE` variables inside the current environment.
93+
94+
Alternatively, you may specify the edge and/or region after constructing the Twilio client:
95+
96+
```python
97+
from twilio.rest import Client
98+
99+
client = Client()
100+
client.region = 'au1'
101+
client.edge = 'sydney'
102+
```
103+
104+
105+
This will result in the `hostname` transforming from `api.twilio.com` to `api.sydney.au1.twilio.com`.
106+
85107
### Make a Call
86108

87109
```python

tests/unit/rest/test_client.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import unittest
2+
23
from twilio.rest import (
4+
Client,
35
TwilioClient,
46
TwilioRestClient,
57
TwilioIpMessagingClient,
@@ -44,3 +46,54 @@ def test_obsolete_exception_twiliotaskrouterclient(self):
4446
def test_obsolete_exception_twiliotrunkingclient(self):
4547
self.assertRaises(ObsoleteException, TwilioTrunkingClient,
4648
"Expected raised ObsoleteException")
49+
50+
51+
class TestRegionEdgeClients(unittest.TestCase):
52+
def setUp(self):
53+
self.client = Client('username', 'password')
54+
55+
def test_set_client_edge_default_region(self):
56+
self.client.edge = 'edge'
57+
self.assertEqual(self.client.get_hostname('https://api.twilio.com'),
58+
'https://api.edge.us1.twilio.com')
59+
60+
def test_set_client_region(self):
61+
self.client.region = 'region'
62+
self.assertEqual(self.client.get_hostname('https://api.twilio.com'),
63+
'https://api.region.twilio.com')
64+
65+
def test_set_uri_region(self):
66+
self.assertEqual(self.client.get_hostname('https://api.region.twilio.com'),
67+
'https://api.region.twilio.com')
68+
69+
def test_set_client_edge_region(self):
70+
self.client.edge = 'edge'
71+
self.client.region = 'region'
72+
self.assertEqual(self.client.get_hostname('https://api.twilio.com'),
73+
'https://api.edge.region.twilio.com')
74+
75+
def test_set_client_edge_uri_region(self):
76+
self.client.edge = 'edge'
77+
self.assertEqual(self.client.get_hostname('https://api.region.twilio.com'),
78+
'https://api.edge.region.twilio.com')
79+
80+
def test_set_client_region_uri_edge_region(self):
81+
self.client.region = 'region'
82+
self.assertEqual(self.client.get_hostname('https://api.edge.uriRegion.twilio.com'),
83+
'https://api.edge.region.twilio.com')
84+
85+
def test_set_client_edge_uri_edge_region(self):
86+
self.client.edge = 'edge'
87+
self.assertEqual(self.client.get_hostname('https://api.uriEdge.region.twilio.com'),
88+
'https://api.edge.region.twilio.com')
89+
90+
def test_set_uri_edge_region(self):
91+
self.assertEqual(self.client.get_hostname('https://api.edge.region.twilio.com'),
92+
'https://api.edge.region.twilio.com')
93+
94+
def test_periods_in_query(self):
95+
self.client.region = 'region'
96+
self.client.edge = 'edge'
97+
self.assertEqual(self.client.get_hostname('https://api.twilio.com/path/to/something.json?foo=12.34'),
98+
'https://api.edge.region.twilio.com/path/to/something.json?foo=12.34')
99+

twilio/rest/__init__.py

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,28 @@
1111
from twilio import __version__
1212
from twilio.base.exceptions import TwilioException
1313
from twilio.base.obsolete import obsolete_client
14+
from twilio.compat import (
15+
urlparse,
16+
urlunparse,
17+
)
1418
from twilio.http.http_client import TwilioHttpClient
1519

1620

1721
class Client(object):
1822
""" A client for accessing the Twilio API. """
1923

2024
def __init__(self, username=None, password=None, account_sid=None, region=None,
21-
http_client=None, environment=None):
25+
http_client=None, environment=None, edge=None):
2226
"""
2327
Initializes the Twilio Client
2428
2529
:param str username: Username to authenticate with
2630
:param str password: Password to authenticate with
2731
:param str account_sid: Account Sid, defaults to Username
28-
:param str region: Twilio Region to make requests to
32+
:param str region: Twilio Region to make requests to, defaults to 'us1' if an edge is provided
2933
:param HttpClient http_client: HttpClient, defaults to TwilioHttpClient
3034
:param dict environment: Environment to look for auth details, defaults to os.environ
35+
:param str edge: Twilio Edge to make requests to, defaults to None
3136
3237
:returns: Twilio Client
3338
:rtype: twilio.rest.Client
@@ -40,7 +45,9 @@ def __init__(self, username=None, password=None, account_sid=None, region=None,
4045
""" :type : str """
4146
self.account_sid = account_sid or self.username
4247
""" :type : str """
43-
self.region = region
48+
self.edge = edge or environment.get('TWILIO_EDGE')
49+
""" :type : str """
50+
self.region = region or environment.get('TWILIO_REGION')
4451
""" :type : str """
4552

4653
if not self.username or not self.password:
@@ -116,11 +123,7 @@ def request(self, method, uri, params=None, data=None, headers=None, auth=None,
116123
if 'Accept' not in headers:
117124
headers['Accept'] = 'application/json'
118125

119-
if self.region:
120-
head, tail = uri.split('.', 1)
121-
122-
if not tail.startswith(self.region):
123-
uri = '.'.join([head, self.region, tail])
126+
uri = self.get_hostname(uri)
124127

125128
return self.http_client.request(
126129
method,
@@ -133,6 +136,41 @@ def request(self, method, uri, params=None, data=None, headers=None, auth=None,
133136
allow_redirects=allow_redirects
134137
)
135138

139+
def get_hostname(self, uri):
140+
"""
141+
Determines the proper hostname given edge and region preferences
142+
via client configuration or uri.
143+
144+
:param str uri: Fully qualified url
145+
146+
:returns: The final uri used to make the request
147+
:rtype: str
148+
"""
149+
if not self.edge and not self.region:
150+
return uri
151+
152+
parsed_url = urlparse(uri)
153+
pieces = parsed_url.netloc.split('.')
154+
prefix = pieces[0]
155+
suffix = '.'.join(pieces[-2:])
156+
region = None
157+
edge = None
158+
if len(pieces) == 4:
159+
# product.region.twilio.com
160+
region = pieces[1]
161+
elif len(pieces) == 5:
162+
# product.edge.region.twilio.com
163+
edge = pieces[1]
164+
region = pieces[2]
165+
166+
edge = self.edge or edge
167+
region = self.region or region or (edge and 'us1')
168+
169+
parsed_url = parsed_url._replace(
170+
netloc='.'.join([part for part in [prefix, edge, region, suffix] if part])
171+
)
172+
return urlunparse(parsed_url)
173+
136174
@property
137175
def accounts(self):
138176
"""

0 commit comments

Comments
 (0)