Skip to content

Commit 6848205

Browse files
committed
Add additional util for development purposes
1 parent 7c2445c commit 6848205

File tree

1 file changed

+192
-0
lines changed

1 file changed

+192
-0
lines changed
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import os
2+
import yaml
3+
import time
4+
import requests
5+
from typing import Dict, Optional, Callable
6+
from pathlib import Path
7+
8+
9+
class ZohoTokenManager:
10+
"""Utility class to manage Zoho Desk API tokens with configurable save callback."""
11+
12+
def __init__(
13+
self,
14+
client_id: str,
15+
client_secret: str,
16+
refresh_token: Optional[str] = None,
17+
access_token: Optional[str] = None,
18+
grant_token: Optional[str] = None,
19+
expires_at: Optional[int] = None,
20+
on_save: Optional[Callable[[Dict], None]] = None,
21+
):
22+
"""Initialize the token manager with client credentials.
23+
24+
Args:
25+
client_id: Zoho API client ID
26+
client_secret: Zoho API client secret
27+
grant_token: Grant token for initial authorization if access_token and refresh_token aren't initialized
28+
refresh_token: Issued refresh token for token renewal
29+
access_token: Last issued access token if available
30+
expires_at: Optional timestamp when the token expires
31+
on_save: Optional callback function to save token updates
32+
"""
33+
self.client_id = client_id
34+
self.client_secret = client_secret
35+
self.refresh_token = refresh_token
36+
self.access_token = access_token
37+
self.grant_token = grant_token
38+
self.expires_at = expires_at
39+
self._on_save = on_save
40+
41+
def _save_tokens(self, token_data: Dict):
42+
"""Save token updates using the provided callback.
43+
44+
Args:
45+
token_data: Dictionary containing token information to save
46+
"""
47+
if self._on_save:
48+
try:
49+
self._on_save(token_data)
50+
except Exception as e:
51+
print(f"Error in token save callback: {e}")
52+
53+
def get_access_token_from_grant(self, grant_token: Optional[str] = None) -> Dict:
54+
"""Get access and refresh tokens using a grant token.
55+
56+
Args:
57+
grant_token: The grant token obtained from Zoho authorization.
58+
If None, uses the grant_token from initialization.
59+
60+
Returns:
61+
Dict containing access_token, refresh_token and other details
62+
"""
63+
if not grant_token and not self.grant_token:
64+
raise ValueError("No grant token provided")
65+
66+
token_to_use = grant_token or self.grant_token
67+
68+
url = "https://accounts.zoho.com/oauth/v2/token"
69+
params = {
70+
"grant_type": "authorization_code",
71+
"client_id": self.client_id,
72+
"client_secret": self.client_secret,
73+
"code": token_to_use,
74+
}
75+
76+
response = requests.post(url, params=params)
77+
if response.status_code != 200:
78+
raise Exception(f"Failed to get access token: {response.text}")
79+
80+
token_data = response.json()
81+
self.access_token = token_data.get("access_token")
82+
self.refresh_token = token_data.get("refresh_token")
83+
self.expires_at = time.time() + token_data.get("expires_in", 3600)
84+
85+
# Prepare token data for saving
86+
save_data = {
87+
"zoho_access_token": self.access_token,
88+
"zoho_refresh_token": self.refresh_token,
89+
"zoho_expires_at": self.expires_at,
90+
"zoho_grant_token": "", # Clear grant token after use
91+
}
92+
self._save_tokens(save_data)
93+
94+
return {
95+
"access_token": self.access_token,
96+
"refresh_token": self.refresh_token,
97+
"expires_at": self.expires_at,
98+
}
99+
100+
def refresh_access_token(self) -> Dict:
101+
"""Refresh the access token using the refresh token.
102+
103+
Returns:
104+
Dict containing the new access_token and other details
105+
"""
106+
if not self.refresh_token:
107+
raise ValueError("No refresh token available")
108+
109+
url = "https://accounts.zoho.com/oauth/v2/token"
110+
params = {
111+
"grant_type": "refresh_token",
112+
"client_id": self.client_id,
113+
"client_secret": self.client_secret,
114+
"refresh_token": self.refresh_token,
115+
}
116+
117+
response = requests.post(url, params=params)
118+
if response.status_code != 200:
119+
raise Exception(f"Failed to refresh access token: {response.text}")
120+
121+
token_data = response.json()
122+
self.access_token = token_data.get("access_token")
123+
self.expires_at = time.time() + token_data.get("expires_in", 3600)
124+
125+
# Prepare token data for saving
126+
save_data = {
127+
"zoho_access_token": self.access_token,
128+
"zoho_expires_at": self.expires_at,
129+
}
130+
self._save_tokens(save_data)
131+
132+
return {
133+
"access_token": self.access_token,
134+
"refresh_token": self.refresh_token,
135+
"expires_at": self.expires_at,
136+
}
137+
138+
def get_valid_access_token(self) -> str:
139+
"""Get a valid access token, refreshing if necessary.
140+
141+
If no refresh token is available but a grant token is, it will
142+
attempt to get a new access token using the grant token.
143+
144+
Returns:
145+
A valid access token string
146+
"""
147+
# If no refresh token but grant token is available, get tokens from grant
148+
if not self.refresh_token and self.grant_token:
149+
self.get_access_token_from_grant()
150+
return self.access_token
151+
152+
if not self.access_token:
153+
raise ValueError("No access token available")
154+
155+
if not self.refresh_token:
156+
raise ValueError("No refresh token available")
157+
158+
# If token is expired or will expire in the next 5 minutes, refresh it
159+
if time.time() > (self.expires_at - 300):
160+
self.refresh_access_token()
161+
162+
return self.access_token
163+
164+
165+
def create_yml_save_callback(config_path: Path) -> Callable[[Dict], None]:
166+
"""Create a callback function to save token updates to a YAML file.
167+
168+
Args:
169+
config_path: Path to the YAML configuration file
170+
171+
Returns:
172+
A callable that can be used as an on_save callback
173+
"""
174+
175+
def save_callback(token_updates: Dict):
176+
"""Save token updates to the YAML configuration file.
177+
178+
Args:
179+
token_updates: Dictionary of token updates to save
180+
"""
181+
# Load existing configuration
182+
with open(config_path, "r") as f:
183+
config = yaml.safe_load(f)
184+
185+
# Update configuration with token updates
186+
config.update(token_updates)
187+
188+
# Save updated configuration
189+
with open(config_path, "w") as f:
190+
yaml.dump(config, f)
191+
192+
return save_callback

0 commit comments

Comments
 (0)