Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .github/workflows/lint-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ jobs:
working-directory: packages/uipath-platform
run: uv run ruff format --check .

- name: Check import contracts
if: steps.check.outputs.skip != 'true'
working-directory: packages/uipath-platform
run: uv run lint-imports

lint-uipath:
name: Lint uipath
needs: detect-changed-packages
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ wheels/

.cache/*
.cache
.import_linter_cache/
site


Expand Down
1 change: 1 addition & 0 deletions packages/uipath-platform/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pytest tests/services/test_assets_service.py # Single test file
ruff check . # Lint
ruff format --check . # Format check
mypy src tests # Type check
uv run lint-imports # Import-graph layering contract
```

No justfile exists for this package — run commands directly.
Expand Down
27 changes: 26 additions & 1 deletion packages/uipath-platform/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-platform"
version = "0.2.9"
version = "0.2.10"
description = "HTTP client library for programmatic access to UiPath Platform"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down Expand Up @@ -47,6 +47,7 @@ dev = [
"pytest-cov>=4.1.0",
"pytest-mock>=3.11.1",
"pre-commit>=4.1.0",
"import-linter>=2.0",
]

[tool.hatch.build.targets.wheel]
Expand Down Expand Up @@ -126,6 +127,30 @@ exclude_lines = [
"@(abc\\.)?abstractmethod",
]

[tool.importlinter]
root_package = "uipath.platform"
exclude_type_checking_imports = true

[[tool.importlinter.contracts]]
id = "platform-layers"
name = "uipath-platform layered architecture"
type = "layers"
containers = ["uipath.platform"]
exhaustive = true
# TODO: Remove this exception in the next breaking-change release.
ignore_imports = [
"uipath.platform.common.interrupt_models -> uipath.platform.resume_triggers.interrupt_models",
]
unmatched_ignore_imports_alerting = "error"
layers = [
"_uipath : resume_triggers",
"agenthub | automation_ops | automation_tracker | connections | context_grounding | entities | external_applications | governance | guardrails | memory | pii_detection | portal | resource_catalog | semantic_proxy",
"orchestrator",
"action_center | attachments | chat | documents | identity",
"common",
"constants | errors",
]

[tool.uv]
exclude-newer = "2 days"

Expand Down
34 changes: 10 additions & 24 deletions packages/uipath-platform/src/uipath/platform/_uipath.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from functools import cached_property
from typing import Optional

from pydantic import ValidationError

from uipath.platform.automation_tracker import AutomationTrackerService

from .action_center import TasksService
Expand All @@ -12,15 +10,13 @@
from .chat import ConversationsService, UiPathLlmChatService, UiPathOpenAIService
from .common import (
ApiClient,
UiPathApiConfig,
UiPathExecutionContext,
)
from .common.auth import resolve_config_from_env
from .common.auth import build_api_config, resolve_config_from_env
from .connections import ConnectionsService
from .context_grounding import ContextGroundingService
from .documents import DocumentsService
from .entities import EntitiesService
from .errors import BaseUrlMissingError, SecretMissingError
from .external_applications import ExternalApplicationService
from .governance import GovernanceService
from .guardrails import GuardrailsService
Expand Down Expand Up @@ -60,25 +56,15 @@ def __init__(
scope: Optional[str] = None,
debug: bool = False,
) -> None:
try:
if _has_valid_client_credentials(client_id, client_secret):
assert client_id and client_secret
service = ExternalApplicationService(base_url)
token_data = service.get_token_data(client_id, client_secret, scope)
base_url, secret = service._base_url, token_data.access_token
else:
base_url, secret = resolve_config_from_env(base_url, secret)

self._config = UiPathApiConfig(
base_url=base_url,
secret=secret,
)
except ValidationError as e:
for error in e.errors():
if error["loc"][0] == "base_url":
raise BaseUrlMissingError() from e
elif error["loc"][0] == "secret":
raise SecretMissingError() from e
if _has_valid_client_credentials(client_id, client_secret):
assert client_id and client_secret
service = ExternalApplicationService(base_url)
token_data = service.get_token_data(client_id, client_secret, scope)
base_url, secret = service._base_url, token_data.access_token
else:
base_url, secret = resolve_config_from_env(base_url, secret)

self._config = build_api_config(base_url, secret)
self._execution_context = UiPathExecutionContext()

@property
Expand Down
107 changes: 79 additions & 28 deletions packages/uipath-platform/src/uipath/platform/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,41 @@
This module contains common models used across multiple services.
"""

from typing import TYPE_CHECKING, Any

from uipath.core.triggers import UiPathResumeMetadata

# TODO: Remove the interrupt-model compatibility exports in the next breaking-change release.

Check warning on line 10 in packages/uipath-platform/src/uipath/platform/common/__init__.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-python&issues=AZ9hR6ccesiRx7lWFvgN&open=AZ9hR6ccesiRx7lWFvgN&pullRequest=1788
if TYPE_CHECKING:
from .interrupt_models import (
CreateBatchTransform,
CreateDeepRag,
CreateDeepRagRaw,
CreateEphemeralIndex,
CreateEphemeralIndexRaw,
CreateEscalation,
CreateTask,
DocumentExtraction,
DocumentExtractionValidation,
InvokeProcess,
InvokeProcessRaw,
InvokeSystemAgent,
WaitBatchTransform,
WaitDeepRag,
WaitDeepRagRaw,
WaitDocumentExtraction,
WaitDocumentExtractionValidation,
WaitEphemeralIndex,
WaitEphemeralIndexRaw,
WaitEscalation,
WaitIntegrationEvent,
WaitJob,
WaitJobRaw,
WaitSystemAgent,
WaitTask,
WaitUntil,
)

from ._api_client import ApiClient
from ._base_service import BaseService, resolve_trace_id
from ._bindings import (
Expand Down Expand Up @@ -41,34 +74,6 @@
from ._user_agent import user_agent_value
from .auth import TokenData
from .dynamic_schema import jsonschema_to_pydantic
from .interrupt_models import (
CreateBatchTransform,
CreateDeepRag,
CreateDeepRagRaw,
CreateEphemeralIndex,
CreateEphemeralIndexRaw,
CreateEscalation,
CreateTask,
DocumentExtraction,
DocumentExtractionValidation,
InvokeProcess,
InvokeProcessRaw,
InvokeSystemAgent,
WaitBatchTransform,
WaitDeepRag,
WaitDeepRagRaw,
WaitDocumentExtraction,
WaitDocumentExtractionValidation,
WaitEphemeralIndex,
WaitEphemeralIndexRaw,
WaitEscalation,
WaitIntegrationEvent,
WaitJob,
WaitJobRaw,
WaitSystemAgent,
WaitTask,
WaitUntil,
)
from .paging import PagedResult
from .timeout import (
UiPathTimeoutError,
Expand Down Expand Up @@ -151,3 +156,49 @@
]

from .validation import validate_pagination_params

_INTERRUPT_MODEL_NAMES = {
"CreateBatchTransform",
"CreateDeepRag",
"CreateDeepRagRaw",
"CreateEphemeralIndex",
"CreateEphemeralIndexRaw",
"CreateEscalation",
"CreateTask",
"DocumentExtraction",
"DocumentExtractionValidation",
"InvokeProcess",
"InvokeProcessRaw",
"InvokeSystemAgent",
"WaitBatchTransform",
"WaitDeepRag",
"WaitDeepRagRaw",
"WaitDocumentExtraction",
"WaitDocumentExtractionValidation",
"WaitEphemeralIndex",
"WaitEphemeralIndexRaw",
"WaitEscalation",
"WaitIntegrationEvent",
"WaitJob",
"WaitJobRaw",
"WaitSystemAgent",
"WaitTask",
"WaitUntil",
}


def __getattr__(name: str) -> Any:
"""Resolve moved interrupt models on demand."""
if name not in _INTERRUPT_MODEL_NAMES:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

from . import interrupt_models

value = interrupt_models._resolve(name)
globals()[name] = value
return value


def __dir__() -> list[str]:
"""List common exports, including compatibility aliases."""
return sorted(set(globals()) | set(__all__))
26 changes: 25 additions & 1 deletion packages/uipath-platform/src/uipath/platform/common/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
from os import environ as env
from typing import Optional

from pydantic import BaseModel
from pydantic import BaseModel, ValidationError

from uipath.platform.constants import (
ENV_BASE_URL,
ENV_UIPATH_ACCESS_TOKEN,
ENV_UNATTENDED_USER_ACCESS_TOKEN,
)
from uipath.platform.errors import BaseUrlMissingError, SecretMissingError

from ._config import UiPathApiConfig


class TokenData(BaseModel):
Expand All @@ -35,3 +38,24 @@ def resolve_config_from_env(
or env.get(ENV_UIPATH_ACCESS_TOKEN)
)
return base_url_value, secret_value


def build_api_config(
base_url: Optional[str],
secret: Optional[str],
) -> UiPathApiConfig:
"""Build a validated API config, translating missing fields into typed errors.

Raises:
BaseUrlMissingError: If ``base_url`` is missing.
SecretMissingError: If ``secret`` is missing.
"""
try:
return UiPathApiConfig(base_url=base_url, secret=secret) # type: ignore[arg-type]
except ValidationError as e:
for error in e.errors():
if error["loc"][0] == "base_url":
raise BaseUrlMissingError() from e
elif error["loc"][0] == "secret":
raise SecretMissingError() from e
raise
Loading
Loading