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
2 changes: 1 addition & 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.17"
version = "0.2.18"
description = "HTTP client library for programmatic access to UiPath Platform"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import uuid
from typing import Any, Dict, List, Optional

from uipath.core.feature_flags import FeatureFlags
from uipath.core.tracing import traced

from uipath.platform.constants import (
Expand All @@ -11,14 +12,16 @@
)

from ..common._base_service import BaseService
from ..common._bindings import resource_override
from ..common._bindings import resource_override, resource_override_applied
from ..common._config import UiPathApiConfig, UiPathConfig
from ..common._execution_context import UiPathExecutionContext
from ..common._folder_context import FolderContext, header_folder
from ..common._models import Endpoint, RequestSpec
from .task_schema import TaskSchema
from .tasks import Task, TaskRecipient, TaskRecipientType

_JIT_ESCALATION_APPS_FEATURE_FLAG = "EnableJITEscalationApps"


def _ensure_string_value(value: Any) -> str:
"""Convert any value to a string for use in field Value."""
Expand All @@ -27,18 +30,38 @@ def _ensure_string_value(value: Any) -> str:
return str(value) if value else ""


def _is_jit_debug_app_task(app_name: Optional[str], app_key: Optional[str]) -> bool:
"""Return whether this app task must be created just-in-time (JIT).

During a debug run an app task may target an app that is not deployed yet,
so neither an app key nor an action schema can be resolved from the
deployed-apps endpoint. Such a task is instead created with the app *name*
and folder path, and Action Center resolves the app itself.

Gated on the ``EnableJITEscalationApps`` feature flag. An explicit
``app_key`` always wins, since the caller already knows the deployed app.
"""
if FeatureFlags.is_flag_enabled(_JIT_ESCALATION_APPS_FEATURE_FLAG, default=False):
if app_key or not app_name:
return False
return UiPathConfig.is_studio_project
return False


def _create_spec(
data: Optional[Dict[str, Any]],
action_schema: Optional[TaskSchema],
title: str,
app_key: Optional[str] = None,
app_name: Optional[str] = None,
app_folder_key: Optional[str] = None,
app_folder_path: Optional[str] = None,
priority: Optional[str] = None,
labels: Optional[List[str]] = None,
is_actionable_message_enabled: Optional[bool] = None,
actionable_message_metadata: Optional[Dict[str, Any]] = None,
source_name: str = "Agent",
is_debug: bool = False,
) -> RequestSpec:
field_list = []
outcome_list = []
Expand Down Expand Up @@ -94,7 +117,6 @@ def _create_spec(
)

json_payload: Dict[str, Any] = {
"appId": app_key,
"title": title,
"data": data if data is not None else {},
"actionableMessageMetaData": actionable_message_metadata
Expand All @@ -119,10 +141,20 @@ def _create_spec(
),
}

if is_debug:
json_payload["appName"] = app_name
else:
json_payload["appId"] = app_key

if app_folder_path:
json_payload["folderPath"] = app_folder_path

_apply_priority_labels_and_actionable_toggle(
json_payload, priority, labels, is_actionable_message_enabled
)
_apply_task_source(json_payload, source_name)
_apply_task_source(json_payload, source_name, is_debug=is_debug)

print('Calling Create App Task', json_payload)

return RequestSpec(
method="POST",
Expand Down Expand Up @@ -159,7 +191,9 @@ def _apply_priority_labels_and_actionable_toggle(
payload["isActionableMessageEnabled"] = is_actionable_message_enabled


def _apply_task_source(payload: Dict[str, Any], source_name: str) -> None:
def _apply_task_source(
payload: Dict[str, Any], source_name: str, is_debug: bool = False
) -> None:
"""Populate ``payload["taskSource"]`` when UiPathConfig has project_id + trace_id.

Shared between AppTask and QuickForm spec builders — the taskSource block is
Expand All @@ -178,7 +212,10 @@ def _apply_task_source(payload: Dict[str, Any], source_name: str) -> None:
"JobKey": UiPathConfig.job_key,
"ProcessKey": UiPathConfig.process_uuid,
},
"jobId": UiPathConfig.job_key,
}
if is_debug:
payload["taskSource"]["isDebug"] = True


def _normalize_priority(priority: str | None) -> str | None:
Expand Down Expand Up @@ -485,17 +522,27 @@ async def create_async(
Raises:
Exception: If neither app_name nor app_key is provided for app-specific actions
"""
(key, action_schema) = (
(app_key, None)
if app_key
else await self._get_app_key_and_schema_async(
app_name, app_folder_path, app_folder_key
key: Optional[str]
action_schema: Optional[TaskSchema]
is_debug = _is_jit_debug_app_task(app_name, app_key)
if is_debug:
key, action_schema = None, None
# pass app_folder_path only when a deployed app is used
if not resource_override_applied():
app_folder_path = None
else:
(key, action_schema) = (
(app_key, None)
if app_key
else await self._get_app_key_and_schema_async(
app_name, app_folder_path, app_folder_key
)
)
)
spec = _create_spec(
title=title,
data=data,
app_key=key,
app_name=app_name,
action_schema=action_schema,
app_folder_key=app_folder_key,
app_folder_path=app_folder_path,
Expand All @@ -504,6 +551,7 @@ async def create_async(
is_actionable_message_enabled=is_actionable_message_enabled,
actionable_message_metadata=actionable_message_metadata,
source_name=source_name,
is_debug=is_debug,
)

response = await self.request_async(
Expand Down Expand Up @@ -571,15 +619,26 @@ def create(
Raises:
Exception: If neither app_name nor app_key is provided for app-specific actions
"""
(key, action_schema) = (
(app_key, None)
if app_key
else self._get_app_key_and_schema(app_name, app_folder_path, app_folder_key)
)
key: Optional[str]
action_schema: Optional[TaskSchema]
is_debug = _is_jit_debug_app_task(app_name, app_key)
if is_debug:
# pass app_folder_path only when a deployed app is used
if not resource_override_applied():
app_folder_path = None
else:
(key, action_schema) = (
(app_key, None)
if app_key
else self._get_app_key_and_schema(
app_name, app_folder_path, app_folder_key
)
)
spec = _create_spec(
title=title,
data=data,
app_key=key,
app_name=app_name,
action_schema=action_schema,
app_folder_key=app_folder_key,
app_folder_path=app_folder_path,
Expand All @@ -588,6 +647,7 @@ def create(
is_actionable_message_enabled=is_actionable_message_enabled,
actionable_message_metadata=actionable_message_metadata,
source_name=source_name,
is_debug=is_debug,
)

response = self.request(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
ResourceOverwriteParser,
ResourceOverwritesContext,
resource_override,
resource_override_applied,
)
from ._config import UiPathApiConfig, UiPathConfig
from ._endpoints_manager import EndpointManager
Expand Down Expand Up @@ -120,6 +121,7 @@
"get_ca_bundle_path",
"get_httpx_client_kwargs",
"resource_override",
"resource_override_applied",
"header_folder",
"validate_pagination_params",
"EndpointManager",
Expand Down
44 changes: 37 additions & 7 deletions packages/uipath-platform/src/uipath/platform/common/_bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,22 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
_resource_overwrites.reset(self._token)


_override_applied: ContextVar[bool] = ContextVar(
"resource_override_applied", default=False
)


def resource_override_applied() -> bool:
"""Whether `@resource_override` matched an override for the running call.

Call this from inside a function decorated with `@resource_override`. When it
returns True, the resource and folder identifier arguments already hold the
overridden values. Returns False when nothing matched, when no
`ResourceOverwritesContext` is active, or when called outside a decorated call.
"""
return _override_applied.get()


def resource_override(
resource_type: str,
resource_identifier: str = "name",
Expand All @@ -240,8 +256,12 @@ def resource_override(
def decorator(func: Callable[..., Any]):
sig = inspect.signature(func)

def process_args(args, kwargs) -> dict[str, Any]:
"""Process arguments and apply resource overrides if applicable."""
def process_args(args, kwargs) -> tuple[dict[str, Any], bool]:
"""Process arguments and apply resource overrides if applicable.

Returns the arguments to call the function with, and whether an
override was matched and applied.
"""
# convert both args and kwargs to single dict
bound = sig.bind_partial(*args, **kwargs)
bound.apply_defaults()
Expand All @@ -255,6 +275,7 @@ def process_args(args, kwargs) -> dict[str, Any]:

# Get overwrites from context variable
context_overwrites = _resource_overwrites.get()
applied = False

if context_overwrites is not None:
resource_identifier_value = all_args.get(resource_identifier)
Expand All @@ -273,6 +294,7 @@ def process_args(args, kwargs) -> dict[str, Any]:

# Apply the matched overwrite
if matched_overwrite is not None:
applied = True
old_resource = all_args.get(resource_identifier)
old_folder = all_args.get(folder_identifier)
if resource_identifier in sig.parameters:
Expand Down Expand Up @@ -302,22 +324,30 @@ def process_args(args, kwargs) -> dict[str, Any]:
func.__name__,
)

return all_args
return all_args, applied

if inspect.iscoroutinefunction(func):

@functools.wraps(func)
async def async_wrapper(*args, **kwargs):
all_args = process_args(args, kwargs)
return await func(**all_args)
all_args, applied = process_args(args, kwargs)
token = _override_applied.set(applied)
try:
return await func(**all_args)
finally:
_override_applied.reset(token)

return async_wrapper
else:

@functools.wraps(func)
def wrapper(*args, **kwargs):
all_args = process_args(args, kwargs)
return func(**all_args)
all_args, applied = process_args(args, kwargs)
token = _override_applied.set(applied)
try:
return func(**all_args)
finally:
_override_applied.reset(token)

return wrapper

Expand Down
Loading
Loading