Skip to content

Commit c95bc78

Browse files
authored
refact: extract auth API services to platform package (#1439)
1 parent 46e2e77 commit c95bc78

30 files changed

+1778
-1383
lines changed

packages/uipath-platform/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-platform"
3-
version = "0.0.24"
3+
version = "0.0.25"
44
description = "HTTP client library for programmatic access to UiPath Platform"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

packages/uipath-platform/src/uipath/platform/_uipath.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
FolderService,
2929
JobsService,
3030
McpService,
31+
OrchestratorSetupService,
3132
ProcessesService,
3233
QueuesService,
3334
)
@@ -161,6 +162,10 @@ def guardrails(self) -> GuardrailsService:
161162
def agenthub(self) -> AgentHubService:
162163
return AgentHubService(self._config, self._execution_context, self.folders)
163164

165+
@property
166+
def orchestrator_setup(self) -> OrchestratorSetupService:
167+
return OrchestratorSetupService(self._config, self._execution_context)
168+
164169
@property
165170
def automation_tracker(self) -> AutomationTrackerService:
166171
return AutomationTrackerService(self._config, self._execution_context)

packages/uipath-platform/src/uipath/platform/common/_external_application_service.py

Lines changed: 85 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
from urllib.parse import urlparse
44

55
import httpx
6-
from httpx import HTTPStatusError, Request
6+
from httpx import HTTPStatusError
77

88
from ..errors import EnrichedException
9-
from ._http_config import get_httpx_client_kwargs
9+
from ..identity import IdentityService
1010
from .auth import TokenData
1111
from .constants import ENV_BASE_URL
1212

@@ -21,16 +21,13 @@ def __init__(self, base_url: Optional[str]):
2121
)
2222
self._base_url = resolved_base_url
2323
self._domain = self._extract_environment_from_base_url(self._base_url)
24-
25-
def get_token_url(self) -> str:
26-
"""Get the token URL for the specified domain."""
27-
match self._domain:
28-
case "alpha":
29-
return "https://alpha.uipath.com/identity_/connect/token"
30-
case "staging":
31-
return "https://staging.uipath.com/identity_/connect/token"
32-
case _: # cloud (default)
33-
return "https://cloud.uipath.com/identity_/connect/token"
24+
domain_map = {
25+
"alpha": "https://alpha.uipath.com",
26+
"staging": "https://staging.uipath.com",
27+
}
28+
self._identity_service = IdentityService(
29+
domain_map.get(self._domain, "https://cloud.uipath.com")
30+
)
3431

3532
def _is_valid_domain_or_subdomain(self, hostname: str, domain: str) -> bool:
3633
"""Check if hostname is either an exact match or a valid subdomain of the domain.
@@ -87,53 +84,88 @@ def get_token_data(
8784
Returns:
8885
Token data if successful
8986
"""
90-
token_url = self.get_token_url()
87+
try:
88+
return self._identity_service.get_client_credentials_token(
89+
client_id=client_id,
90+
client_secret=client_secret,
91+
scope=scope,
92+
)
93+
except HTTPStatusError as e:
94+
match e.response.status_code:
95+
case 400:
96+
raise EnrichedException(
97+
HTTPStatusError(
98+
message="Invalid client credentials or request parameters.",
99+
request=e.request,
100+
response=e.response,
101+
)
102+
) from e
103+
case 401:
104+
raise EnrichedException(
105+
HTTPStatusError(
106+
message="Unauthorized: Invalid client credentials.",
107+
request=e.request,
108+
response=e.response,
109+
)
110+
) from e
111+
case _:
112+
raise EnrichedException(
113+
HTTPStatusError(
114+
message=f"Authentication failed with unexpected status: {e.response.status_code}",
115+
request=e.request,
116+
response=e.response,
117+
)
118+
) from e
119+
except httpx.RequestError as e:
120+
raise Exception(f"Network error during authentication: {e}") from e
121+
except Exception as e:
122+
raise Exception(f"Unexpected error during authentication: {e}") from e
91123

92-
data = {
93-
"grant_type": "client_credentials",
94-
"client_id": client_id,
95-
"client_secret": client_secret,
96-
"scope": scope,
97-
}
124+
async def get_token_data_async(
125+
self, client_id: str, client_secret: str, scope: Optional[str] = "OR.Execution"
126+
) -> TokenData:
127+
"""Authenticate using client credentials flow.
128+
129+
Args:
130+
client_id: The client ID for authentication
131+
client_secret: The client secret for authentication
132+
scope: The scope for the token (default: OR.Execution)
98133
134+
Returns:
135+
Token data if successful
136+
"""
99137
try:
100-
with httpx.Client(**get_httpx_client_kwargs()) as client:
101-
response = client.post(token_url, data=data)
102-
match response.status_code:
103-
case 200:
104-
return TokenData.model_validate(response.json())
105-
case 400:
106-
raise EnrichedException(
107-
HTTPStatusError(
108-
message="Invalid client credentials or request parameters.",
109-
request=Request(
110-
data=data, url=token_url, method="post"
111-
),
112-
response=response,
113-
)
138+
return await self._identity_service.get_client_credentials_token_async(
139+
client_id=client_id,
140+
client_secret=client_secret,
141+
scope=scope,
142+
)
143+
except HTTPStatusError as e:
144+
match e.response.status_code:
145+
case 400:
146+
raise EnrichedException(
147+
HTTPStatusError(
148+
message="Invalid client credentials or request parameters.",
149+
request=e.request,
150+
response=e.response,
114151
)
115-
case 401:
116-
raise EnrichedException(
117-
HTTPStatusError(
118-
message="Unauthorized: Invalid client credentials.",
119-
request=Request(
120-
data=data, url=token_url, method="post"
121-
),
122-
response=response,
123-
)
152+
) from e
153+
case 401:
154+
raise EnrichedException(
155+
HTTPStatusError(
156+
message="Unauthorized: Invalid client credentials.",
157+
request=e.request,
158+
response=e.response,
124159
)
125-
case _:
126-
raise EnrichedException(
127-
HTTPStatusError(
128-
message=f"Authentication failed with unexpected status: {response.status_code}",
129-
request=Request(
130-
data=data, url=token_url, method="post"
131-
),
132-
response=response,
133-
)
160+
) from e
161+
case _:
162+
raise EnrichedException(
163+
HTTPStatusError(
164+
message=f"Authentication failed with unexpected status: {e.response.status_code}",
165+
request=e.request,
166+
response=e.response,
134167
)
135-
except EnrichedException:
136-
raise
168+
) from e
137169
except httpx.RequestError as e:
138170
raise Exception(f"Network error during authentication: {e}") from e
139171
except Exception as e:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""UiPath Identity Service."""
2+
3+
from ._identity_service import IdentityService
4+
5+
__all__ = ["IdentityService"]
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
"""Identity service for UiPath authentication token operations."""
2+
3+
from typing import Optional
4+
5+
import httpx
6+
7+
from ..common._http_config import get_httpx_client_kwargs
8+
from ..common.auth import TokenData
9+
10+
11+
class IdentityService:
12+
"""Service for interacting with the UiPath Identity server."""
13+
14+
def __init__(self, domain: str):
15+
"""Initialize the IdentityService.
16+
17+
Args:
18+
domain: The base URL of the UiPath identity server (e.g., "https://cloud.uipath.com").
19+
"""
20+
self._domain = domain
21+
22+
def refresh_access_token(
23+
self,
24+
refresh_token: str,
25+
client_id: str,
26+
) -> TokenData:
27+
"""Refresh an access token using a refresh token.
28+
29+
Args:
30+
refresh_token: The refresh token to exchange for a new access token.
31+
client_id: The client ID of the application.
32+
33+
Returns:
34+
TokenData containing the new access token and related information.
35+
36+
Raises:
37+
httpx.HTTPStatusError: If the server returns a non-2xx response.
38+
httpx.ConnectError: If there is a network connectivity issue.
39+
"""
40+
url = f"{self._domain}/identity_/connect/token"
41+
data = {
42+
"grant_type": "refresh_token",
43+
"refresh_token": refresh_token,
44+
"client_id": client_id,
45+
}
46+
47+
with httpx.Client(**get_httpx_client_kwargs()) as client:
48+
response = client.post(url, data=data)
49+
response.raise_for_status()
50+
return TokenData.model_validate(response.json())
51+
52+
async def refresh_access_token_async(
53+
self,
54+
refresh_token: str,
55+
client_id: str,
56+
) -> TokenData:
57+
"""Refresh an access token using a refresh token.
58+
59+
Args:
60+
refresh_token: The refresh token to exchange for a new access token.
61+
client_id: The client ID of the application.
62+
63+
Returns:
64+
TokenData containing the new access token and related information.
65+
66+
Raises:
67+
httpx.HTTPStatusError: If the server returns a non-2xx response.
68+
httpx.ConnectError: If there is a network connectivity issue.
69+
"""
70+
url = f"{self._domain}/identity_/connect/token"
71+
data = {
72+
"grant_type": "refresh_token",
73+
"refresh_token": refresh_token,
74+
"client_id": client_id,
75+
}
76+
77+
async with httpx.AsyncClient(**get_httpx_client_kwargs()) as client:
78+
response = await client.post(url, data=data)
79+
response.raise_for_status()
80+
return TokenData.model_validate(response.json())
81+
82+
def get_client_credentials_token(
83+
self,
84+
client_id: str,
85+
client_secret: str,
86+
scope: Optional[str] = "OR.Execution",
87+
) -> TokenData:
88+
"""Obtain an access token using client credentials grant.
89+
90+
Args:
91+
client_id: The client ID of the application.
92+
client_secret: The client secret of the application.
93+
scope: The requested OAuth scopes (optional, default: "OR.Execution").
94+
95+
Returns:
96+
TokenData containing the access token and related information.
97+
98+
Raises:
99+
httpx.HTTPStatusError: If the server returns a non-2xx response.
100+
httpx.ConnectError: If there is a network connectivity issue.
101+
"""
102+
scope = scope or "OR.Execution"
103+
url = f"{self._domain}/identity_/connect/token"
104+
data = {
105+
"grant_type": "client_credentials",
106+
"client_id": client_id,
107+
"client_secret": client_secret,
108+
"scope": scope,
109+
}
110+
111+
with httpx.Client(**get_httpx_client_kwargs()) as client:
112+
response = client.post(url, data=data)
113+
response.raise_for_status()
114+
return TokenData.model_validate(response.json())
115+
116+
async def get_client_credentials_token_async(
117+
self,
118+
client_id: str,
119+
client_secret: str,
120+
scope: Optional[str] = "OR.Execution",
121+
) -> TokenData:
122+
"""Obtain an access token using client credentials grant.
123+
124+
Args:
125+
client_id: The client ID of the application.
126+
client_secret: The client secret of the application.
127+
scope: The requested OAuth scopes (optional, default: "OR.Execution").
128+
129+
Returns:
130+
TokenData containing the access token and related information.
131+
132+
Raises:
133+
httpx.HTTPStatusError: If the server returns a non-2xx response.
134+
httpx.ConnectError: If there is a network connectivity issue.
135+
"""
136+
scope = scope or "OR.Execution"
137+
url = f"{self._domain}/identity_/connect/token"
138+
data = {
139+
"grant_type": "client_credentials",
140+
"client_id": client_id,
141+
"client_secret": client_secret,
142+
"scope": scope,
143+
}
144+
145+
async with httpx.AsyncClient(**get_httpx_client_kwargs()) as client:
146+
response = await client.post(url, data=data)
147+
response.raise_for_status()
148+
return TokenData.model_validate(response.json())

packages/uipath-platform/src/uipath/platform/orchestrator/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
from ._folder_service import FolderService
1010
from ._jobs_service import JobsService
1111
from ._mcp_service import McpService
12+
from ._orchestrator_setup_service import OrchestratorSetupService
1213
from ._processes_service import ProcessesService
1314
from ._queues_service import QueuesService
15+
from ._server_version import get_server_version, get_server_version_async
1416
from .assets import Asset, UserAsset
1517
from .attachment import Attachment
1618
from .buckets import Bucket, BucketFile
@@ -34,6 +36,9 @@
3436
"McpService",
3537
"ProcessesService",
3638
"QueuesService",
39+
"OrchestratorSetupService",
40+
"get_server_version",
41+
"get_server_version_async",
3742
"Asset",
3843
"UserAsset",
3944
"Attachment",

0 commit comments

Comments
 (0)