1818import socketserver
1919import threading
2020import time
21+ from collections .abc import Callable
2122from dataclasses import dataclass , field
2223from pathlib import Path
2324from typing import Any
@@ -109,6 +110,8 @@ class AuthState:
109110 _last_tenant : str | None = None
110111 _last_org : dict [str , str ] = field (default_factory = dict )
111112 _last_environment : str | None = None
113+ # Callback invoked after successful authentication
114+ _on_authenticated : Callable [[], None ] | None = None
112115 # internal
113116 _code_verifier : str | None = None
114117 _state : str | None = None
@@ -128,9 +131,30 @@ def get_auth_state() -> AuthState:
128131
129132
130133def reset_auth_state () -> None :
131- """Reset the auth state to its initial (unauthenticated) values."""
132- global _auth
133- _auth = AuthState ()
134+ """Reset the auth state to its initial (unauthenticated) values.
135+
136+ Preserves registered callbacks (e.g. ``_on_authenticated``).
137+ """
138+ _auth .status = "unauthenticated"
139+ _auth .environment = "cloud"
140+ _auth .token_data = {}
141+ _auth .tenants = []
142+ _auth .organization = {}
143+ _auth .uipath_url = None
144+ _auth ._last_tenant = None
145+ _auth ._last_org = {}
146+ _auth ._last_environment = None
147+ _auth ._code_verifier = None
148+ _auth ._state = None
149+ _auth ._port = None
150+ _auth ._callback_server = None
151+ if _auth ._wait_task and not _auth ._wait_task .done ():
152+ _auth ._wait_task .cancel ()
153+ _auth ._wait_task = None
154+ if _auth ._token_event :
155+ _auth ._token_event .set ()
156+ _auth ._token_event = None
157+ _auth ._loop = None
134158
135159
136160# ---------------------------------------------------------------------------
@@ -990,6 +1014,40 @@ def select_tenant(tenant_name: str) -> dict[str, Any]:
9901014 return {"status" : "authenticated" , "uipath_url" : auth .uipath_url }
9911015
9921016
1017+ def _update_env_file (env_contents : dict [str , str ]) -> None :
1018+ """Merge *env_contents* into the CWD ``.env`` file.
1019+
1020+ New keys take priority; existing keys not in *env_contents* are preserved.
1021+ Comments and blank lines are kept as-is.
1022+ """
1023+ env_path = Path .cwd () / ".env"
1024+ lines : list [str ] = []
1025+ seen_keys : set [str ] = set ()
1026+
1027+ if env_path .exists ():
1028+ with open (env_path ) as f :
1029+ for raw_line in f :
1030+ stripped = raw_line .strip ()
1031+ if stripped .startswith ("#" ) or "=" not in stripped :
1032+ # Preserve comments and blank lines
1033+ lines .append (raw_line )
1034+ continue
1035+ key = stripped .split ("=" , 1 )[0 ]
1036+ if key in env_contents :
1037+ lines .append (f"{ key } ={ env_contents [key ]} \n " )
1038+ else :
1039+ lines .append (raw_line )
1040+ seen_keys .add (key )
1041+
1042+ # Append new keys that weren't already in the file
1043+ for key , value in env_contents .items ():
1044+ if key not in seen_keys :
1045+ lines .append (f"{ key } ={ value } \n " )
1046+
1047+ with open (env_path , "w" ) as f :
1048+ f .writelines (lines )
1049+
1050+
9931051def _finalize_tenant (auth : AuthState , tenant_name : str ) -> None :
9941052 """Write .env and os.environ with the resolved credentials."""
9951053 org_name = auth .organization .get ("name" , "" )
@@ -1010,45 +1068,27 @@ def _finalize_tenant(auth: AuthState, tenant_name: str) -> None:
10101068 auth ._last_org = dict (auth .organization )
10111069 auth ._last_environment = auth .environment
10121070
1013- # Update os.environ
1014- os .environ ["UIPATH_ACCESS_TOKEN" ] = access_token
1015- os .environ ["UIPATH_URL" ] = uipath_url
1016- os .environ ["UIPATH_TENANT_ID" ] = tenant_id
1017- os .environ ["UIPATH_ORGANIZATION_ID" ] = org_id
1018-
1019- # Write/update .env file (preserving comments, blank lines, and ordering)
1020- env_path = Path .cwd () / ".env"
1021- lines : list [str ] = []
1022- updated_keys : set [str ] = set ()
1023- new_values = {
1024- "UIPATH_ACCESS_TOKEN" : access_token ,
1025- "UIPATH_URL" : uipath_url ,
1026- "UIPATH_TENANT_ID" : tenant_id ,
1027- "UIPATH_ORGANIZATION_ID" : org_id ,
1028- }
1029-
1030- if env_path .exists ():
1031- with open (env_path ) as f :
1032- for raw_line in f :
1033- stripped = raw_line .strip ()
1034- if "=" in stripped and not stripped .startswith ("#" ):
1035- key = stripped .split ("=" , 1 )[0 ]
1036- if key in new_values :
1037- lines .append (f"{ key } ={ new_values [key ]} \n " )
1038- updated_keys .add (key )
1039- continue
1040- lines .append (raw_line )
1041-
1042- # Append any keys that weren't already in the file
1043- for key , value in new_values .items ():
1044- if key not in updated_keys :
1045- lines .append (f"{ key } ={ value } \n " )
1071+ # Write .env using the same approach as `uipath auth`
1072+ _update_env_file (
1073+ {
1074+ "UIPATH_ACCESS_TOKEN" : access_token ,
1075+ "UIPATH_URL" : uipath_url ,
1076+ "UIPATH_TENANT_ID" : tenant_id ,
1077+ "UIPATH_ORGANIZATION_ID" : org_id ,
1078+ }
1079+ )
10461080
1047- with open (env_path , "w" ) as f :
1048- f .writelines (lines )
1081+ # Reload .env into os.environ (same as CLI root: cwd + override)
1082+ load_dotenv (
1083+ dotenv_path = os .path .join (os .getcwd (), ".env" ),
1084+ override = True ,
1085+ )
10491086
1050- # Reload all .env variables into os.environ
1051- load_dotenv (override = True )
1087+ if auth ._on_authenticated :
1088+ try :
1089+ auth ._on_authenticated ()
1090+ except Exception :
1091+ logger .exception ("Error in post-authentication callback" )
10521092
10531093
10541094def logout () -> None :
0 commit comments