diff --git a/eb_dashboard.py b/eb_dashboard.py index 37f0654..2e5bd7d 100644 --- a/eb_dashboard.py +++ b/eb_dashboard.py @@ -119,6 +119,8 @@ refresh_token = "" threads_list = [] _token_refresh_lock = threading.Lock() on_retry_exhausted = "ask" # "ask" | "ignore" | "abort" — set at startup +_stored_username = "" # Credentials stored at login for automatic re-login +_stored_password = "" _threads_list_lock = threading.Lock() global_pbar = None _global_pbar_lock = threading.Lock() @@ -187,8 +189,10 @@ def new_token(): finally: if attempt < ERROR_MAX_RETRY - 1: sleep(WAIT_BEFORE_RETRY) - logging.critical("Persistent error in refresh_token") - raise httpx.RequestError(message="Persistent error in refresh_token") + # Refresh token exhausted — attempt full re-login with stored credentials + logging.warning("Refresh token exhausted. Attempting re-login with stored credentials.") + _do_login(_stored_username, _stored_password) + logging.info("Re-login successful. New tokens acquired.") def api_call_with_retry(func): @@ -213,7 +217,10 @@ def api_call_with_retry(func): if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code == 401: logging.info(f"Token expired for {func_name}. Refreshing token.") - new_token() + try: + new_token() + except (httpx.RequestError, httpx.HTTPStatusError) as token_exc: + logging.warning(f"Token refresh/re-login failed for {func_name}: {token_exc}") if attempt < ERROR_MAX_RETRY - 1: sleep(WAIT_BEFORE_RETRY) @@ -228,21 +235,19 @@ def api_call_with_retry(func): else: # All automatic batches exhausted — apply on_retry_exhausted policy with _user_interaction_lock: - console.print(f"\n[bold red]Persistent error in {func_name} after {batch_count} batches ({total_attempts} attempts).[/bold red]") - console.print(f"[red]Exception: {exc}[/red]") - if on_retry_exhausted == "ignore": ctx = getattr(thread_local_storage, "current_patient_context", {"id": "Unknown", "pseudo": "Unknown"}) logging.warning(f"[AUTO-IGNORE] Skipping {func_name} for Patient {ctx['id']} ({ctx['pseudo']}). Error: {exc}") - console.print(f"[yellow]⚠ Auto-ignore: skipping {func_name}.[/yellow]") return None elif on_retry_exhausted == "abort": logging.critical(f"[AUTO-ABORT] Stopping script after persistent error in {func_name}. Error: {exc}") - console.print(f"[bold red]Auto-abort: stopping script.[/bold red]") raise httpx.RequestError(message=f"Persistent error in {func_name} (auto-aborted)") - else: # "ask" — interactive prompt (original behaviour) + else: # "ask" — display error then interactive prompt + console.print(f"\n[bold red]Persistent error in {func_name} after {batch_count} batches ({total_attempts} attempts).[/bold red]") + console.print(f"[red]Exception: {exc}[/red]") + choice = questionary.select( f"What would you like to do for {func_name}?", choices=[ @@ -271,51 +276,56 @@ def api_call_with_retry(func): # BLOCK 3: AUTHENTICATION # ============================================================================ -def login(): +def _do_login(username, password): + """Performs the two-step authentication (IAM → RC) with the given credentials. + Updates global access_token and refresh_token on success. + Raises httpx.RequestError or httpx.HTTPStatusError on failure. + Must NOT acquire _token_refresh_lock (caller's responsibility). + """ global access_token, refresh_token + client = get_httpx_client() + client.base_url = IAM_URL + response = client.post(API_AUTH_LOGIN_ENDPOINT, + json={"username": username, "password": password}, + timeout=20) + response.raise_for_status() + master_token = response.json()["access_token"] + user_id = response.json()["userId"] + + client = get_httpx_client() + client.base_url = RC_URL + response = client.post(API_AUTH_CONFIG_TOKEN_ENDPOINT, + headers={"Authorization": f"Bearer {master_token}"}, + json={"userId": user_id, "clientId": RC_APP_ID, + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"}, + timeout=20) + response.raise_for_status() + access_token = response.json()["access_token"] + refresh_token = response.json()["refresh_token"] + + +def login(): + global _stored_username, _stored_password + user_name = (questionary.text("login :", default=DEFAULT_USER_NAME).ask()) password = (questionary.password("password :", default=DEFAULT_PASSWORD).ask()) if not (user_name and password): return "Exit" try: - client = get_httpx_client() - client.base_url = IAM_URL - response = client.post(API_AUTH_LOGIN_ENDPOINT, json={"username": user_name, "password": password}, - timeout=20) - response.raise_for_status() - master_token = response.json()["access_token"] - user_id = response.json()["userId"] + _do_login(user_name, password) except httpx.RequestError as exc: print(f"Login Error : {exc}") logging.warning(f"Login Error : {exc}") return "Error" except httpx.HTTPStatusError as exc: print(f"Login Error : {exc.response.status_code} for Url {exc.request.url}") - logging.warning( - f"Login Error : {exc.response.status_code} for Url {exc.request.url}") - return "Error" - - try: - client = get_httpx_client() - client.base_url = RC_URL - response = client.post(API_AUTH_CONFIG_TOKEN_ENDPOINT, headers={"Authorization": f"Bearer {master_token}"}, - json={"userId": user_id, "clientId": RC_APP_ID, - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"}, - timeout=20) - response.raise_for_status() - access_token = response.json()["access_token"] - refresh_token = response.json()["refresh_token"] - except httpx.RequestError as exc: - print(f"Login Error : {exc}") - logging.warning(f"Login Error : {exc}") - return "Error" - except httpx.HTTPStatusError as exc: - print(f"Login Error : {exc.response.status_code} for Url {exc.request.url}") - logging.warning(f"Login Error : {exc}") + logging.warning(f"Login Error : {exc.response.status_code} for Url {exc.request.url}") return "Error" + _stored_username = user_name + _stored_password = password print() print("Login Success") return "Success"