Skip to content

Commit 25e6b0d

Browse files
feat(errors): introduce error handling and custom exceptions
1 parent 5108a9b commit 25e6b0d

File tree

5 files changed

+222
-24
lines changed

5 files changed

+222
-24
lines changed

src/uipath/_cli/_auth/_portal_service.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
)
1717

1818
console = ConsoleLogger()
19-
client = httpx.Client(follow_redirects=True)
19+
client = httpx.Client(
20+
follow_redirects=True,
21+
proxy="http://127.0.0.1:8080",
22+
verify=False,
23+
)
2024

2125

2226
class PortalService:

src/uipath/_services/_base_service.py

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
from .._config import Config
2222
from .._execution_context import ExecutionContext
23-
from .._utils import user_agent_value
23+
from .._utils import handle_errors, user_agent_value
2424
from .._utils.constants import HEADER_USER_AGENT
2525

2626

@@ -38,17 +38,29 @@ def __init__(self, config: Config, execution_context: ExecutionContext) -> None:
3838
self._config = config
3939
self._execution_context = execution_context
4040
self._tenant_scope_client = Client(
41-
base_url=self._config.base_url, headers=Headers(self.default_headers)
41+
base_url=self._config.base_url,
42+
headers=Headers(self.default_headers),
43+
proxy="https://127.0.0.1:8080",
44+
verify=False,
4245
)
4346
self._tenant_scope_client_async = AsyncClient(
44-
base_url=self._config.base_url, headers=Headers(self.default_headers)
47+
base_url=self._config.base_url,
48+
headers=Headers(self.default_headers),
49+
proxy="https://127.0.0.1:8080",
50+
verify=False,
4551
)
4652
org_scope_base_url = self.__get_org_scope_base_url()
4753
self._org_scope_client = Client(
48-
base_url=org_scope_base_url, headers=self.default_headers
54+
base_url=org_scope_base_url,
55+
headers=self.default_headers,
56+
proxy="https://127.0.0.1:8080",
57+
verify=False,
4958
)
5059
self._org_scope_client_async = AsyncClient(
51-
base_url=org_scope_base_url, headers=self.default_headers
60+
base_url=org_scope_base_url,
61+
headers=self.default_headers,
62+
proxy="https://127.0.0.1:8080",
63+
verify=False,
5264
)
5365

5466
self._logger.debug(f"HEADERS: {self.default_headers}")
@@ -96,8 +108,9 @@ def request(
96108
headers = kwargs.get("headers", {})
97109
headers[HEADER_USER_AGENT] = user_agent_value(specific_component)
98110

99-
response = self._tenant_scope_client.request(method, url, **kwargs)
100-
response.raise_for_status()
111+
with handle_errors():
112+
response = self._tenant_scope_client.request(method, url, **kwargs)
113+
response.raise_for_status()
101114

102115
return response
103116

@@ -142,8 +155,11 @@ async def request_async(
142155
headers = kwargs.get("headers", {})
143156
headers[HEADER_USER_AGENT] = user_agent_value(specific_component)
144157

145-
response = await self._tenant_scope_client_async.request(method, url, **kwargs)
146-
response.raise_for_status()
158+
with handle_errors():
159+
response = await self._tenant_scope_client_async.request(
160+
method, url, **kwargs
161+
)
162+
response.raise_for_status()
147163

148164
return response
149165

@@ -181,8 +197,9 @@ def request_org_scope(
181197
headers = kwargs.get("headers", {})
182198
headers[HEADER_USER_AGENT] = user_agent_value(specific_component)
183199

184-
response = self._org_scope_client.request(method, url, **kwargs)
185-
response.raise_for_status()
200+
with handle_errors():
201+
response = self._org_scope_client.request(method, url, **kwargs)
202+
response.raise_for_status()
186203

187204
return response
188205

@@ -220,8 +237,9 @@ async def request_org_scope_async(
220237
headers = kwargs.get("headers", {})
221238
headers[HEADER_USER_AGENT] = user_agent_value(specific_component)
222239

223-
response = await self._org_scope_client_async.request(method, url, **kwargs)
224-
response.raise_for_status()
240+
with handle_errors():
241+
response = await self._org_scope_client_async.request(method, url, **kwargs)
242+
response.raise_for_status()
225243

226244
return response
227245

src/uipath/_utils/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from ._endpoint import Endpoint
2+
from ._errors import handle_errors
23
from ._infer_bindings import get_inferred_bindings_names, infer_bindings
34
from ._logs import setup_logging
45
from ._request_override import header_folder
@@ -14,4 +15,5 @@
1415
"infer_bindings",
1516
"header_user_agent",
1617
"user_agent_value",
18+
"handle_errors",
1719
]

src/uipath/_utils/_errors.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import contextlib
2+
3+
import httpx
4+
5+
from ..models.errors import (
6+
APIError,
7+
BadRequestError,
8+
ConflictError,
9+
ForbiddenError,
10+
NotFoundError,
11+
RateLimitError,
12+
ServerError,
13+
UnauthorizedError,
14+
)
15+
16+
17+
@contextlib.contextmanager
18+
def handle_errors():
19+
try:
20+
yield
21+
except httpx.HTTPStatusError as e:
22+
content_type = e.response.headers.get("content-type")
23+
is_json_response = content_type and content_type.startswith("application/json")
24+
25+
if is_json_response:
26+
error_body = e.response.json()
27+
else:
28+
error_body = e.response.text
29+
30+
status_code = e.response.status_code
31+
32+
if type(error_body) is dict:
33+
message = error_body.get("message")
34+
else:
35+
message = None
36+
37+
match status_code:
38+
case 400:
39+
raise BadRequestError(error_body, message=message) from e
40+
case 401:
41+
raise UnauthorizedError(error_body, message=message) from e
42+
case 403:
43+
raise ForbiddenError(error_body, message=message) from e
44+
case 404:
45+
raise NotFoundError(error_body, message=message) from e
46+
case 409:
47+
raise ConflictError(error_body, message=message) from e
48+
case 429:
49+
raise RateLimitError(error_body, message=message) from e
50+
case code if code >= 500:
51+
raise ServerError(error_body, message=message) from e
52+
case _:
53+
raise APIError(str(e), status_code, error_body) from e
54+
except httpx.TimeoutException as e:
55+
raise TimeoutError(str(e)) from e

src/uipath/models/errors.py

Lines changed: 129 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,135 @@
1-
class BaseUrlMissingError(Exception):
2-
def __init__(
3-
self,
4-
message="Authentication required. Please run \033[1muipath auth\033[22m.",
5-
):
1+
"""Custom exceptions for the UiPath SDK.
2+
3+
This module defines a hierarchy of custom exceptions used throughout the SDK
4+
to provide more specific error handling and better error messages.
5+
"""
6+
7+
from typing import Any, Optional
8+
9+
10+
class UiPathError(Exception):
11+
"""Base exception class for all UiPath SDK errors.
12+
13+
All custom exceptions in the SDK should inherit from this class.
14+
15+
Attributes:
16+
message: A human-readable error message
17+
details: Optional additional error details
18+
"""
19+
20+
def __init__(self, message: str, details: Optional[Any] = None) -> None:
621
self.message = message
22+
self.details = details
23+
724
super().__init__(self.message)
825

926

10-
class SecretMissingError(Exception):
27+
class ConfigurationError(UiPathError):
28+
"""Base class for configuration-related errors."""
29+
30+
pass
31+
32+
33+
class BaseUrlMissingError(ConfigurationError):
34+
"""Raised when the base URL is not configured."""
35+
1136
def __init__(
1237
self,
13-
message="Authentication required. Please run \033[1muipath auth\033[22m.",
14-
):
15-
self.message = message
16-
super().__init__(self.message)
38+
message: str = "Authentication required. Please run \033[1muipath auth\033[22m.",
39+
) -> None:
40+
super().__init__(message)
41+
42+
43+
class AccessTokenMissingError(ConfigurationError):
44+
"""Raised when the access token is not configured."""
45+
46+
def __init__(
47+
self,
48+
message: str = "Authentication required. Please run \033[1muipath auth\033[22m.",
49+
) -> None:
50+
super().__init__(message)
51+
52+
53+
class APIError(UiPathError):
54+
"""Base class for API-related errors.
55+
56+
Attributes:
57+
status_code: The HTTP status code of the failed request
58+
response_body: The response body from the failed request
59+
"""
60+
61+
def __init__(
62+
self,
63+
message: str,
64+
status_code: Optional[int] = None,
65+
response_body: Optional[str] = None,
66+
) -> None:
67+
self.status_code = status_code
68+
self.response_body = response_body
69+
super().__init__(
70+
message,
71+
details={"status_code": status_code, "response_body": response_body},
72+
)
73+
74+
75+
class BadRequestError(APIError):
76+
"""Raised when the API request is malformed (400)."""
77+
78+
def __init__(self, response_body: str, message: str | None = None) -> None:
79+
if message is None:
80+
message = "Bad request"
81+
super().__init__(message, status_code=400, response_body=response_body)
82+
83+
84+
class UnauthorizedError(APIError):
85+
"""Raised when authentication fails (401)."""
86+
87+
def __init__(self, response_body: str, message: str | None = None) -> None:
88+
if message is None:
89+
message = "Unauthorized"
90+
super().__init__(message, status_code=401, response_body=response_body)
91+
92+
93+
class ForbiddenError(APIError):
94+
"""Raised when the user doesn't have permission (403)."""
95+
96+
def __init__(self, response_body: str, message: str | None = None) -> None:
97+
if message is None:
98+
message = "Forbidden"
99+
super().__init__(message, status_code=403, response_body=response_body)
100+
101+
102+
class NotFoundError(APIError):
103+
"""Raised when the requested resource is not found (404)."""
104+
105+
def __init__(self, response_body: str, message: str | None = None) -> None:
106+
if message is None:
107+
message = "Not found"
108+
super().__init__(message, status_code=404, response_body=response_body)
109+
110+
111+
class ConflictError(APIError):
112+
"""Raised when the request cannot be processed due to a conflict (409)."""
113+
114+
def __init__(self, response_body: str, message: str | None = None) -> None:
115+
if message is None:
116+
message = "Conflict"
117+
super().__init__(message, status_code=409, response_body=response_body)
118+
119+
120+
class RateLimitError(APIError):
121+
"""Raised when the API rate limit is exceeded (429)."""
122+
123+
def __init__(self, response_body: str, message: str | None = None) -> None:
124+
if message is None:
125+
message = "Rate limit exceeded"
126+
super().__init__(message, status_code=429, response_body=response_body)
127+
128+
129+
class ServerError(APIError):
130+
"""Raised when the API server encounters an error (5xx)."""
131+
132+
def __init__(self, response_body: str, message: str | None = None) -> None:
133+
if message is None:
134+
message = "Server error"
135+
super().__init__(message, status_code=500, response_body=response_body)

0 commit comments

Comments
 (0)