Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions src/uipath/_cli/_auth/_portal_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import click
import httpx

from ..._utils._errors import handle_errors
from .._utils._console import ConsoleLogger
from ._models import TenantsAndOrganizationInfoResponse, TokenData
from ._oidc_utils import get_auth_config
Expand All @@ -16,7 +17,12 @@
)

console = ConsoleLogger()
client = httpx.Client(follow_redirects=True, timeout=30.0)
client = httpx.Client(
follow_redirects=True,
timeout=30.0,
proxy="http://127.0.0.1:8080",
verify=False,
)


class PortalService:
Expand Down Expand Up @@ -45,21 +51,15 @@ def update_token_data(self, token_data: TokenData):

def get_tenants_and_organizations(self) -> TenantsAndOrganizationInfoResponse:
url = f"https://{self.domain}.uipath.com/{self.prt_id}/portal_/api/filtering/leftnav/tenantsAndOrganizationInfo"
response = client.get(
url, headers={"Authorization": f"Bearer {self.access_token}"}
)
if response.status_code < 400:
headers = {"Authorization": f"Bearer {self.access_token}"}

with handle_errors():
response = client.get(url, headers=headers)

result = response.json()
self._tenants_and_organizations = result

return result
elif response.status_code == 401:
console.error("Unauthorized")
else:
console.error(
f"Failed to get tenants and organizations: {response.status_code} {response.text}"
)
# Can't reach here, console.error exits, linting
raise Exception("Failed to get tenants")

def get_uipath_orchestrator_url(self) -> str:
if self._tenants_and_organizations is None:
Expand Down
3 changes: 2 additions & 1 deletion src/uipath/_cli/cli_publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import httpx
from dotenv import load_dotenv

from .._utils._errors import handle_errors
from ..telemetry import track
from ._utils._common import get_env_vars
from ._utils._console import ConsoleLogger
Expand Down Expand Up @@ -123,7 +124,7 @@ def publish(feed):
else:
url = url + "?feedId=" + feed

with open(package_to_publish_path, "rb") as f:
with open(package_to_publish_path, "rb") as f, handle_errors():
files = {"file": (package_to_publish_path, f, "application/octet-stream")}
response = client.post(url, headers=headers, files=files)

Expand Down
20 changes: 9 additions & 11 deletions src/uipath/_services/_base_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,7 @@
from logging import getLogger
from typing import Any, Literal, Union

from httpx import (
URL,
AsyncClient,
Client,
ConnectTimeout,
Headers,
Response,
TimeoutException,
)
from httpx import URL, AsyncClient, Client, Headers, Response, TimeoutException
from tenacity import (
retry,
retry_if_exception,
Expand All @@ -22,12 +14,12 @@

from .._config import Config
from .._execution_context import ExecutionContext
from .._utils import UiPathUrl, user_agent_value
from .._utils import UiPathUrl, handle_errors, user_agent_value
from .._utils.constants import HEADER_USER_AGENT


def is_retryable_exception(exception: BaseException) -> bool:
return isinstance(exception, (ConnectTimeout, TimeoutException))
return isinstance(exception, (TimeoutException))


def is_retryable_status_code(response: Response) -> bool:
Expand All @@ -46,19 +38,24 @@ def __init__(self, config: Config, execution_context: ExecutionContext) -> None:
base_url=self._url.base_url,
headers=Headers(self.default_headers),
timeout=30.0,
proxy="https://127.0.0.1:8080",
verify=False,
)

self._client_async = AsyncClient(
base_url=self._url.base_url,
headers=Headers(self.default_headers),
timeout=30.0,
proxy="https://127.0.0.1:8080",
verify=False,
)

self._overwrites_manager = OverwritesManager()
self._logger.debug(f"HEADERS: {self.default_headers}")

super().__init__()

@handle_errors()
@retry(
retry=(
retry_if_exception(is_retryable_exception)
Expand Down Expand Up @@ -108,6 +105,7 @@ def request(

return response

@handle_errors()
@retry(
retry=(
retry_if_exception(is_retryable_exception)
Expand Down
4 changes: 2 additions & 2 deletions src/uipath/_uipath.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
ENV_UIPATH_ACCESS_TOKEN,
ENV_UNATTENDED_USER_ACCESS_TOKEN,
)
from .models.errors import BaseUrlMissingError, SecretMissingError
from .models.errors import AccessTokenMissingError, BaseUrlMissingError

load_dotenv(override=True)

Expand Down Expand Up @@ -55,7 +55,7 @@ def __init__(
if error["loc"][0] == "base_url":
raise BaseUrlMissingError() from e
elif error["loc"][0] == "secret":
raise SecretMissingError() from e
raise AccessTokenMissingError() from e
self._folders_service: Optional[FolderService] = None
self._buckets_service: Optional[BucketsService] = None

Expand Down
2 changes: 2 additions & 0 deletions src/uipath/_utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from ._endpoint import Endpoint
from ._errors import handle_errors
from ._infer_bindings import get_inferred_bindings_names, infer_bindings
from ._logs import setup_logging
from ._request_override import header_folder
Expand All @@ -16,4 +17,5 @@
"header_user_agent",
"user_agent_value",
"UiPathUrl",
"handle_errors",
]
50 changes: 50 additions & 0 deletions src/uipath/_utils/_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import json
from contextlib import contextmanager
from typing import Generator

import httpx

from ..models.errors import (
APIError,
UiPathError,
)


@contextmanager
def handle_errors() -> Generator[None, None, None]:
"""Context manager for handling HTTP and general errors in API calls.

This context manager wraps API calls and converts various types of errors
into appropriate UiPathError subclasses. It handles both HTTP errors
and general exceptions.

Yields:
None: The context manager yields control to the wrapped code.

Raises:
APIError: For HTTP errors with status codes and error messages.
UiPathError: For general exceptions that occur during API calls.
"""
try:
yield
except httpx.HTTPStatusError as e:
try:
error_body = e.response.json()
except Exception:
error_body = e.response.text

status_code = e.response.status_code

message: str | None = None
if isinstance(error_body, dict):
message = (
error_body.get("message")
or error_body.get("error")
or error_body.get("detail")
)
error_body = json.dumps(error_body)

raise APIError(message or str(e), status_code, error_body) from e
except Exception as e:
breakpoint()
raise UiPathError(str(e)) from e
11 changes: 9 additions & 2 deletions src/uipath/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
from .connections import Connection, ConnectionToken
from .context_grounding import ContextGroundingQueryResponse
from .context_grounding_index import ContextGroundingIndex
from .errors import BaseUrlMissingError, SecretMissingError
from .errors import (
AccessTokenMissingError,
APIError,
BaseUrlMissingError,
UiPathError,
)
from .exceptions import IngestionInProgressException
from .interrupt_models import (
CreateAction,
Expand Down Expand Up @@ -47,6 +52,8 @@
"CreateAction",
"IngestionInProgressException",
"BaseUrlMissingError",
"SecretMissingError",
"Bucket",
"AccessTokenMissingError",
"APIError",
"UiPathError",
]
70 changes: 60 additions & 10 deletions src/uipath/models/errors.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,66 @@
class BaseUrlMissingError(Exception):
def __init__(
self,
message="Authentication required. Please run \033[1muipath auth\033[22m.",
):
from typing import Any, Optional


class UiPathError(Exception):
"""Base exception class for all UiPath SDK errors.

All custom exceptions in the SDK should inherit from this class.

Attributes:
message: A human-readable error message
details: Optional additional error details
"""

def __init__(self, message: str, details: Optional[Any] = None) -> None:
self.message = message
self.details = details

super().__init__(self.message)


class SecretMissingError(Exception):
class ConfigurationError(UiPathError):
"""Base class for configuration-related errors."""

pass


class BaseUrlMissingError(ConfigurationError):
"""Raised when the base URL is not configured."""

def __init__(
self,
message="Authentication required. Please run \033[1muipath auth\033[22m.",
):
self.message = message
super().__init__(self.message)
message: str = "Authentication required. Please run \033[1muipath auth\033[22m.",
) -> None:
super().__init__(message)


class AccessTokenMissingError(ConfigurationError):
"""Raised when the access token is not configured."""

def __init__(
self,
message: str = "Authentication required. Please run \033[1muipath auth\033[22m.",
) -> None:
super().__init__(message)


class APIError(UiPathError):
"""Base class for API-related errors.

Attributes:
status_code: The HTTP status code of the failed request
response_body: The response body from the failed request
"""

def __init__(
self,
message: str,
status_code: Optional[int] = None,
response_body: Optional[str] = None,
) -> None:
self.status_code = status_code
self.response_body = response_body
super().__init__(
message,
details={"status_code": status_code, "response_body": response_body},
)
Loading