@@ -1034,3 +1034,159 @@ def batch_submit_tasks(self, req: TasksSubmitReq) -> TasksSubmitResp:
10341034 raise Exception (
10351035 f"Failed to batch submit tasks, status code: { resp .status_code } , error: { resp .text } "
10361036 )
1037+
1038+
1039+ class PersistentMitoHttpClient :
1040+ """
1041+ A wrapper around MitoHttpClient that automatically handles re-authentication on 401 errors.
1042+
1043+ This client stores the username and password when connect() is called, and automatically
1044+ retries authentication once if a 401 Unauthorized error is encountered in any API call.
1045+
1046+ Example:
1047+ >>> client = PersistentMitoHttpClient("http://localhost:8080")
1048+ >>> client.connect(user="myuser", password="mypassword")
1049+ 'myuser'
1050+ >>> # Now all API calls will automatically re-authenticate on 401 errors
1051+ >>> tasks = client.query_tasks_by_filter(TasksQueryReq(...))
1052+ >>> # If the token expires and a 401 is returned, the client will:
1053+ >>> # 1. Automatically call connect() again with stored credentials
1054+ >>> # 2. Retry the original API call
1055+ >>> # 3. If it still fails, raise the error
1056+ """
1057+
1058+ def __init__ (self , coordinator_addr : str ):
1059+ """Initialize the persistent client with a coordinator address."""
1060+ self ._inner_client = MitoHttpClient (coordinator_addr )
1061+ self ._stored_username : Optional [str ] = None
1062+ self ._stored_password : Optional [str ] = None
1063+ self ._stored_credential_path : Optional [Path ] = None
1064+ self ._stored_retain : bool = False
1065+ self .logger = self ._inner_client .logger
1066+
1067+ def __del__ (self ):
1068+ """Clean up the inner client."""
1069+ if hasattr (self , "_inner_client" ):
1070+ del self ._inner_client
1071+
1072+ def connect (
1073+ self ,
1074+ credential_path : Optional [Path ] = None ,
1075+ user : Optional [str ] = None ,
1076+ password : Optional [str ] = None ,
1077+ retain : bool = False ,
1078+ ) -> str :
1079+ """
1080+ Connect to the server and store credentials for automatic re-authentication.
1081+
1082+ Args:
1083+ credential_path: Path to credential file
1084+ user: Username
1085+ password: Password
1086+ retain: Whether to retain the session
1087+
1088+ Returns:
1089+ The authenticated username
1090+ """
1091+ # Store credentials for future re-authentication
1092+ if user is not None and password is not None :
1093+ self ._stored_username = user
1094+ self ._stored_password = password
1095+ self ._stored_credential_path = credential_path
1096+ self ._stored_retain = retain
1097+
1098+ # Call the inner client's connect method
1099+ username = self ._inner_client .connect (
1100+ credential_path = credential_path , user = user , password = password , retain = retain
1101+ )
1102+ self ._stored_username = username
1103+ return username
1104+
1105+ def _should_retry_with_reauth (self , error : Exception ) -> bool :
1106+ """
1107+ Check if an error indicates a 401 Unauthorized that should trigger re-authentication.
1108+
1109+ Args:
1110+ error: The exception to check
1111+
1112+ Returns:
1113+ True if the error is a 401 and we should retry with re-authentication
1114+ """
1115+ error_msg = str (error )
1116+ # Check if error message contains status code 401
1117+ return "status code: 401" in error_msg or "401" in error_msg
1118+
1119+ def _retry_with_reauth (self , method_name : str , * args , ** kwargs ):
1120+ """
1121+ Execute a method with automatic re-authentication on 401 errors.
1122+
1123+ Args:
1124+ method_name: Name of the method to call on the inner client
1125+ *args: Positional arguments for the method
1126+ **kwargs: Keyword arguments for the method
1127+
1128+ Returns:
1129+ The result of the method call
1130+
1131+ Raises:
1132+ Exception: If the method fails even after re-authentication attempt
1133+ """
1134+ # Get the method from the inner client
1135+ method = getattr (self ._inner_client , method_name )
1136+
1137+ try :
1138+ # Try the original call
1139+ return method (* args , ** kwargs )
1140+ except Exception as e :
1141+ # Check if this is a 401 error
1142+ if self ._should_retry_with_reauth (e ):
1143+ # Check if we have stored credentials to retry with
1144+ if self ._stored_username is None or self ._stored_password is None :
1145+ # No stored credentials, can't retry
1146+ raise
1147+
1148+ # Try to re-authenticate
1149+ try :
1150+ self ._inner_client .connect (
1151+ credential_path = self ._stored_credential_path ,
1152+ user = self ._stored_username ,
1153+ password = self ._stored_password ,
1154+ retain = self ._stored_retain ,
1155+ )
1156+ except Exception :
1157+ # Re-authentication failed, raise the original error
1158+ raise e
1159+
1160+ # Re-authentication succeeded, retry the original call
1161+ try :
1162+ return method (* args , ** kwargs )
1163+ except Exception as retry_error :
1164+ # Retry failed, raise the error
1165+ raise retry_error
1166+ else :
1167+ # Not a 401 error, raise it immediately
1168+ raise
1169+
1170+ def __getattr__ (self , name : str ):
1171+ """
1172+ Dynamically wrap all methods from the inner client with retry logic.
1173+
1174+ Args:
1175+ name: The attribute name
1176+
1177+ Returns:
1178+ A wrapped version of the method if it's callable, otherwise the attribute itself
1179+ """
1180+ # Get the attribute from the inner client
1181+ attr = getattr (self ._inner_client , name )
1182+
1183+ # If it's a method and not a special/private method, wrap it with retry logic
1184+ if callable (attr ) and not name .startswith ("_" ) and name != "connect" :
1185+
1186+ def wrapped_method (* args , ** kwargs ):
1187+ return self ._retry_with_reauth (name , * args , ** kwargs )
1188+
1189+ return wrapped_method
1190+ else :
1191+ # For non-callable attributes or private methods, return as-is
1192+ return attr
0 commit comments