8000 feat: Organizations Api uptake for twilio-python (#815) · CodeAiModels/twilio-python@6e78c78 · GitHub
[go: up one dir, main page]

Skip to content

Commit 6e78c78

Browse files
authored
feat: Organizations Api uptake for twilio-python (twilio#815)
* feat: oauth sdk implementation and organization api uptake (twilio#799)
1 parent fb53889 commit 6e78c78

25 files changed

+2899
-16
lines changed

twilio/auth_strategy/__init__.py

Whitespace-only changes.

twilio/auth_strategy/auth_strategy.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from twilio.auth_strategy.auth_type import AuthType
2+
from abc import abstractmethod
3+
4+
5+
class AuthStrategy(object):
6+
def __init__(self, auth_type: AuthType):
7+
self._auth_type = auth_type
8+
9+
@property
10+
def auth_type(self) -> AuthType:
11+
return self._auth_type
12+
13+
@abstractmethod
14+
def get_auth_string(self) -> str:
15+
"""Return the authentication string."""
16+
17+
@abstractmethod
18+
def requires_authentication(self) -> bool:
19+
"""Return True if authentication is required, else False."""

twilio/auth_strategy/auth_type.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from enum import Enum
2+
3+
4+
class AuthType(Enum):
5+
ORGS_TOKEN = "orgs_stoken"
6+
NO_AUTH = "noauth"
7+
BASIC = "basic"
8+
API_KEY = "api_key"
9+
CLIENT_CREDENTIALS = "client_credentials"
10+
11+
def __str__(self):
12+
return self.value
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from auth_type import AuthType
2+
from twilio.auth_strategy.auth_strategy import AuthStrategy
3+
4+
5+
class NoAuthStrategy(AuthStrategy):
6+
def __init__(self):
7+
super().__init__(AuthType.NO_AUTH)
8+
9+
def get_auth_string(self) -> str:
10+
return ""
11+
12+
def requires_authentication(self) -> bool:
13+
return False
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import jwt
2+
import threading
3+
import logging
4+
from datetime import datetime
5+
6+
from twilio.auth_strategy.auth_type import AuthType
7+
from twilio.auth_strategy.auth_strategy import AuthStrategy
8+
from twilio.http.token_manager import TokenManager
9+
10+
11+
class TokenAuthStrategy(AuthStrategy):
12+
def __init__(self, token_manager: TokenManager):
13+
super().__init__(AuthType.ORGS_TOKEN)
14+
self.token_manager = token_manager
15+
self.token = None
16+
self.lock = threading.Lock()
17+
logging.basicConfig(level=logging.INFO)
18+
self.logger = logging.getLogger(__name__)
19+
20+
def get_auth_string(self) -> str:
21+
self.fetch_token()
22+
return f"Bearer {self.token}"
23+
24+
def requires_authentication(self) -> bool:
25+
return True
26+
27+
def fetch_token(self):
28+
if self.token is None or self.token == "" or self.is_token_expired(self.token):
29+
with self.lock:
30+
if (
31+
self.token is None
32+
or self.token == ""
33+
or self.is_token_expired(self.token)
34+
):
35+
self.logger.info("New token fetched for accessing organization API")
36+
self.token = self.token_manager.fetch_access_token()
37+
38+
def is_token_expired(self, token):
39+
try:
40+
decoded = jwt.decode(token, options={"verify_signature": False})
41+
exp = decoded.get("exp")
42+
43+
if exp is None:
44+
return True # No expiration time present, consider it expired
45+
46+
# Check if the expiration time has passed
47+
return datetime.fromtimestamp(exp) < datetime.utcnow()
48+
49+
except jwt.DecodeError:
50+
return True # Token is invalid
51+
except Exception as e:
52+
print(f"An error occurred: {e}")
53+
return True

twilio/base/client_base.py

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
from urllib.parse import urlparse, urlunparse
55

66
from twilio import __version__
7-
from twilio.base.exceptions import TwilioException
87
from twilio.http import HttpClient
98
from twilio.http.http_client import TwilioHttpClient
109
from twilio.http.response import Response
10+
from twilio.credential.credential_provider import CredentialProvider
1111

1212

1313
class ClientBase(object):
@@ -23,6 +23,7 @@ def __init__(
2323
environment: Optional[MutableMapping[str, str]] = None,
2424
edge: Optional[str] = None,
2525
user_agent_extensions: Optional[List[str]] = None,
26+
credential_provider: Optional[CredentialProvider] = None,
2627
):
2728
"""
2829
Initializes the Twilio Client
@@ -35,7 +36,9 @@ def __init__(
3536
:param environment: Environment to look for auth details, defaults to os.environ
3637
:param edge: Twilio Edge to make requests to, defaults to None
3738
:param user_agent_extensions: Additions to the user agent string
39+
:param credential_provider: credential provider for authentication method that needs to be used
3840
"""
41+
3942
environment = environment or os.environ
4043

4144
self.username = username or environment.get("TWILIO_ACCOUNT_SID")
@@ -48,9 +51,8 @@ def __init__(
4851
""" :type : str """
4952
self.user_agent_extensions = user_agent_extensions or []
5053
""" :type : list[str] """
51-
52-
if not self.username or not self.password:
53-
raise TwilioException("Credentials are required to create a TwilioClient")
54+
self.credential_provider = credential_provider or None
55+
""" :type : CredentialProvider """
5456

5557
self.account_sid = account_sid or self.username
5658
""" :type : str """
@@ -85,15 +87,27 @@ def request(
8587
8688
:returns: Response from the Twilio API
8789
"""
88-
auth = self.get_auth(auth)
8990
headers = self.get_headers(method, headers)
90-
uri = self.get_hostname(uri)
9191

92+
if self.credential_provider:
93+
94+
auth_strategy = self.credential_provider.to_auth_strategy()
95+
headers["Authorization"] = auth_strategy.get_auth_string()
96+
elif self.username is not None and self.password is not None:
97+
auth = self.get_auth(auth)
98+
else:
99+
auth = None
100+
101+
if method == "DELETE":
102+
del headers["Accept"]
103+
104+
uri = self.get_hostname(uri)
105+
filtered_data = self.copy_non_none_values(data)
92106
return self.http_client.request(
93107
method,
94108
uri,
95109
params=params,
96-
data=data,
110+
data=filtered_data,
97111
headers=headers,
98112
auth=auth,
99113
timeout=timeout,
@@ -132,21 +146,44 @@ async def request_async(
132146
"http_client must be asynchronous to support async API requests"
133147
)
134148

135-
auth = self.get_auth(auth)
136149
headers = self.get_headers(method, headers)
137-
uri = self.get_hostname(uri)
150+
if method == "DELETE":
151+
del headers["Accept"]
152+
153+
if self.credential_provider:
154+
auth_strategy = self.credential_provider.to_auth_strategy()
155+
headers["Authorization"] = auth_strategy.get_auth_string()
156+
elif self.username is not None and self.password is not None:
157+
auth = self.get_auth(auth)
158+
else:
159+
auth = None
138160

161+
uri = self.get_hostname(uri)
162+
filtered_data = self.copy_non_none_values(data)
139163
return await self.http_client.request(
140164
method,
141165
uri,
142166
params=params,
143-
data=data,
167+
data=filtered_data,
144168
headers=headers,
145169
auth=auth,
146170
timeout=timeout,
147171
allow_redirects=allow_redirects,
148172
)
149173

174+
def copy_non_none_values(self, data):
175+
if isinstance(data, dict):
176+
return {
177+
k: self.copy_non_none_values(v)
178+
for k, v in data.items()
179+
if v is not None
180+
}
181+
elif isinstance(data, list):
182+
return [
183+
self.copy_non_none_values(item) for item in data if item is not None
184+
]
185+
return data
186+
150187
def get_auth(self, auth: Optional[Tuple[str, str]]) -> Tuple[str, str]:
151188
"""
152189
Get the request authentication object

twilio/base/page.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ def load_page(self, payload: Dict[str, Any]):
7777
key = keys - self.META_KEYS
7878
if len(key) == 1:
7979
return payload[key.pop()]
80+
if "Resources" in payload:
81+
return payload["Resources"]
8082

8183
raise TwilioException("Page Records can not be deserialized")
8284

twilio/base/version.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,6 @@ async def fetch_async(
164164
timeout=timeout,
165165
allow_redirects=allow_redirects,
166166
)
167-
168167
return self._parse_fetch(method, uri, response)
169168

170169
def _parse_update(self, method: str, uri: str, response: Response) -> Any:
@@ -461,7 +460,6 @@ def create(
461460
timeout=timeout,
462461
allow_redirects=allow_redirects,
463462
)
464-
465463
return self._parse_create(method, uri, response)
466464

467465
async def create_async(
@@ -488,5 +486,4 @@ async def create_async(
488486
timeout=timeout,
489487
allow_redirects=allow_redirects,
490488
)
491-
492489
return self._parse_create(method, uri, response)

twilio/credential/__init__.py

Whitespace-only changes.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from twilio.auth_strategy.auth_type import AuthType
2+
3+
4+
class CredentialProvider:
5+
def __init__(self, auth_type: AuthType):
6+
self._auth_type = auth_type
7+
8+
@property
9+
def auth_type(self) -> AuthType:
10+
return self._auth_type
11+
12+
def to_auth_strategy(self):
13+
raise NotImplementedError("Subclasses must implement this method")
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from twilio.http.orgs_token_manager import OrgTokenManager
2+
from twilio.base.exceptions import TwilioException
3+
from twilio.credential.credential_provider import CredentialProvider
4+
from twilio.auth_strategy.auth_type import AuthType
5+
from twilio.auth_strategy.token_auth_strategy import TokenAuthStrategy
6+
7+
8+
class OrgsCredentialProvider(CredentialProvider):
9+
def __init__(self, client_id: str, client_secret: str, token_manager=None):
10+
super().__init__(AuthType.CLIENT_CREDENTIALS)
11+
12+
if client_id is None or client_secret is None:
13+
raise TwilioException("Client id and Client secret are mandatory")
14+
15+
self.grant_type = "client_credentials"
16+
self.client_id = client_id
17+
self.client_secret = client_secret
18+
self.token_manager = token_manager
19+
self.auth_strategy = None
20+
21+
def to_auth_strategy(self):
22+
if self.token_manager is None:
23+
self.token_manager = OrgTokenManager(
24+
self.grant_type, self.client_id, self.client_secret
25+
)
26+
if self.auth_strategy is None:
27+
self.auth_strategy = TokenAuthStrategy(self.token_manager)
28+
return self.auth_strategy

twilio/http/http_client.py

Lines changed: 3 additions & 3 deletions
Original file F438 line numberDiff line numberDiff line change
@@ -88,10 +88,11 @@ def request(
8888
}
8989
if headers and headers.get("Content-Type") == "application/json":
9090
kwargs["json"] = data
91+
elif headers and headers.get("Content-Type") == "application/scim+json":
92+
kwargs["json"] = data
9193
else:
9294
kwargs["data"] = data
9395
self.log_request(kwargs)
94-
9596
self._test_only_last_response = None
9697
session = self.session or Session()
9798
request = Request(**kwargs)
@@ -102,12 +103,11 @@ def request(
102103
settings = session.merge_environment_settings(
103104
prepped_request.url, self.proxy, None, None, None
104105
)
105-
106106
response = session.send(
107107
prepped_request,
108108
allow_redirects=allow_redirects,
109109
timeout=timeout,
110-
**settings
110+
**settings,
111111
)
112112

113113
self.log_response(response.status_code, response)

twilio/http/orgs_token_manager.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from twilio.http.token_manager import TokenManager
2+
from twilio.rest import Client
3+
4+
5+
class OrgTokenManager(TokenManager):
6+
"""
7+
Orgs Token Manager
8+
"""
9+
10+
def __init__(
11+
self,
12+
grant_type: str,
13+
client_id: str,
14+
client_secret: str,
15+
code: str = None,
16+
redirect_uri: str = None,
17+
audience: str = None,
18+
refreshToken: str = None,
19+
scope: str = None,
20+
):
21+
self.grant_type = grant_type
22+
self.client_id = client_id
23+
self.client_secret = client_secret
24+
self.code = code
25+
self.redirect_uri = redirect_uri
26+
self.audience = audience
27+
self.refreshToken = refreshToken
28+
self.scope = scope
29+
self.client = Client()
30+
31+
def fetch_access_token(self):
32+
token_instance = self.client.preview_iam.v1.token.create(
33+
grant_type=self.grant_type,
34+
client_id=self.client_id,
35+
client_secret=self.client_secret,
36+
code=self.code,
37+
redirect_uri=self.redirect_uri,
38+
audience=self.audience,
39+
scope=self.scope,
40+
)
41+
return token_instance.access_token

twilio/http/token_manager.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from twilio.base.version import Version
2+
3+
4+
class TokenManager:
5+
6+
def fetch_access_token(self, version: Version):
7+
pass

0 commit comments

Comments
 (0)
0