@@ -73,7 +73,8 @@ def __init__(
7373 raise InvalidRequestException (
7474 message = "This request should use https protocol." ,
7575 status_code = 400 ,
76- error_type = "request" ,
76+ error_type = "invalid_request" ,
77+ field_name = "redirect_uri" ,
7778 )
7879
7980 environ ["OAUTHLIB_INSECURE_TRANSPORT" ] = "1"
@@ -128,7 +129,14 @@ def _load_token(self) -> Optional[TokenDict]:
128129 except InvalidGrantException :
129130 # Invalid/expired refresh token
130131 return None
131- except Exception :
132+ except json .JSONDecodeError :
133+ self .logger .error (f"Invalid JSON in token cache file: { self .token_cache_path } " )
134+ return None
135+ except OSError as e :
136+ self .logger .error (f"Error reading token cache file: { self .token_cache_path } : { str (e )} " )
137+ return None
138+ except Exception as e :
139+ self .logger .error (f"Unexpected error loading token: { e .__class__ .__name__ } : { str (e )} " )
132140 return None
133141 return None
134142
@@ -139,7 +147,23 @@ def _save_token(self, token: TokenDict) -> None:
139147 self .token = token
140148
141149 def authenticate (self , force_new : bool = False ) -> bool :
142- """Complete authentication flow if needed"""
150+ """Complete authentication flow if needed
151+
152+ Args:
153+ force_new: Force new authentication even if valid token exists
154+
155+ Returns:
156+ bool: True if authenticated successfully
157+
158+ Raises:
159+ InvalidRequestException: If the request syntax is invalid
160+ InvalidClientException: If the client_id is invalid
161+ InvalidGrantException: If the grant_type is invalid
162+ InvalidTokenException: If the OAuth token is invalid
163+ ExpiredTokenException: If the OAuth token has expired
164+ OAuthException: Base class for all OAuth-related exceptions
165+ SystemException: If there's a system-level failure
166+ """
143167 if not force_new and self .is_authenticated ():
144168 self .logger .debug ("Authentication token exchange completed successfully" )
145169 return True
@@ -164,18 +188,9 @@ def authenticate(self, force_new: bool = False) -> bool:
164188 callback_url = input ("Enter the full callback URL: " )
165189
166190 # Exchange authorization code for token
167- try :
168- token = self .fetch_token (callback_url )
169- self ._save_token (token )
170- return True
171- except Exception as e :
172- if "invalid_grant" in str (e ):
173- raise InvalidGrantException (
174- message = "Authorization code expired or invalid" ,
175- status_code = 400 ,
176- error_type = "invalid_grant" ,
177- ) from e
178- raise
191+ token = self .fetch_token (callback_url )
192+ self ._save_token (token )
193+ return True
179194
180195 def is_authenticated (self ) -> bool :
181196 """Check if we have valid tokens"""
@@ -192,7 +207,21 @@ def get_authorization_url(self) -> Tuple[str, str]:
192207 return (str (auth_url_tuple [0 ]), str (auth_url_tuple [1 ]))
193208
194209 def fetch_token (self , authorization_response : str ) -> TokenDict :
195- """Exchange authorization code for access token"""
210+ """Exchange authorization code for access token
211+
212+ Args:
213+ authorization_response: The full callback URL with authorization code
214+
215+ Returns:
216+ TokenDict: Dictionary containing access token and other OAuth details
217+
218+ Raises:
219+ InvalidClientException: If the client credentials are invalid
220+ InvalidTokenException: If the authorization code is invalid
221+ InvalidGrantException: If the authorization grant is invalid
222+ ExpiredTokenException: If the token has expired
223+ OAuthException: For other OAuth-related errors
224+ """
196225 try :
197226 auth = HTTPBasicAuth (self .client_id , self .client_secret )
198227 token_data = self .session .fetch_token (
@@ -208,31 +237,54 @@ def fetch_token(self, authorization_response: str) -> TokenDict:
208237 except Exception as e :
209238 error_msg = str (e ).lower ()
210239
211- if "invalid_client" in error_msg :
212- self .logger .error (
213- f"InvalidClientException: Authentication failed "
214- f"(Client ID: { self .client_id [:4 ]} ..., Error: { str (e )} )"
215- )
216- raise InvalidClientException (
217- message = "Invalid client credentials" ,
218- status_code = 401 ,
219- error_type = "invalid_client" ,
220- ) from e
221- if "invalid_token" in error_msg :
222- self .logger .error (
223- f"InvalidTokenException: Token validation failed " f"(Error: { str (e )} )"
224- )
225- raise InvalidTokenException (
226- message = "Invalid authorization code" ,
227- status_code = 401 ,
228- error_type = "invalid_token" ,
229- ) from e
240+ # Use standard error mapping from ERROR_TYPE_EXCEPTIONS
241+ # Local imports
242+ from fitbit_client .exceptions import ERROR_TYPE_EXCEPTIONS
243+ from fitbit_client .exceptions import OAuthException
244+
245+ # Check for known error types
246+ for error_type , exception_class in ERROR_TYPE_EXCEPTIONS .items ():
247+ if error_type in error_msg :
248+ # Special case for client ID to mask most of it in logs
249+ if error_type == "invalid_client" :
250+ self .logger .error (
251+ f"{ exception_class .__name__ } : Authentication failed "
252+ f"(Client ID: { self .client_id [:4 ]} ..., Error: { str (e )} )"
253+ )
254+ else :
255+ self .logger .error (
256+ f"{ exception_class .__name__ } : { error_type } error during token fetch: { str (e )} "
257+ )
230258
231- self .logger .error (f"OAuthException: { e .__class__ .__name__ } : { str (e )} " )
232- raise
259+ raise exception_class (
260+ message = str (e ),
261+ status_code = (
262+ 401 if "token" in error_type or error_type == "authorization" else 400
263+ ),
264+ error_type = error_type ,
265+ ) from e
266+
267+ # If no specific error type found, use OAuthException
268+ self .logger .error (
269+ f"OAuthException during token fetch: { e .__class__ .__name__ } : { str (e )} "
270+ )
271+ raise OAuthException (message = str (e ), status_code = 400 , error_type = "oauth" ) from e
233272
234273 def refresh_token (self , refresh_token : str ) -> TokenDict :
235- """Refresh the access token"""
274+ """Refresh the access token
275+
276+ Args:
277+ refresh_token: The refresh token to use
278+
279+ Returns:
280+ TokenDict: Dictionary containing new access token and other OAuth details
281+
282+ Raises:
283+ ExpiredTokenException: If the access token has expired
284+ InvalidGrantException: If the refresh token is invalid
285+ InvalidClientException: If the client credentials are invalid
286+ OAuthException: For other OAuth-related errors
287+ """
236288 try :
237289 auth = HTTPBasicAuth (self .client_id , self .client_secret )
238290 extra = {
@@ -248,12 +300,29 @@ def refresh_token(self, refresh_token: str) -> TokenDict:
248300 return token
249301 except Exception as e :
250302 error_msg = str (e ).lower ()
251- if "expired_token" in error_msg :
252- raise ExpiredTokenException (
253- message = "Access token expired" , status_code = 401 , error_type = "expired_token"
254- ) from e
255- if "invalid_grant" in error_msg :
256- raise InvalidGrantException (
257- message = "Refresh token invalid" , status_code = 400 , error_type = "invalid_grant"
258- ) from e
259- raise
303+
304+ # Use standard error mapping from ERROR_TYPE_EXCEPTIONS
305+ # Local imports
306+ from fitbit_client .exceptions import ERROR_TYPE_EXCEPTIONS
307+ from fitbit_client .exceptions import OAuthException
308+
309+ # Check for known error types
310+ for error_type , exception_class in ERROR_TYPE_EXCEPTIONS .items ():
311+ if error_type in error_msg :
312+ self .logger .error (
313+ f"{ exception_class .__name__ } : { error_type } error during token refresh: { str (e )} "
314+ )
315+
316+ raise exception_class (
317+ message = str (e ),
318+ status_code = (
319+ 401 if "token" in error_type or error_type == "authorization" else 400
320+ ),
321+ error_type = error_type ,
322+ ) from e
323+
324+ # If no specific error type found, use OAuthException
325+ self .logger .error (
326+ f"OAuthException during token refresh: { e .__class__ .__name__ } : { str (e )} "
327+ )
328+ raise OAuthException (message = str (e ), status_code = 400 , error_type = "oauth" ) from e
0 commit comments