diff --git a/packages/uipath-platform/src/uipath/platform/_uipath.py b/packages/uipath-platform/src/uipath/platform/_uipath.py index 87c3a17f0..729e870c6 100644 --- a/packages/uipath-platform/src/uipath/platform/_uipath.py +++ b/packages/uipath-platform/src/uipath/platform/_uipath.py @@ -11,6 +11,7 @@ from .chat import ConversationsService, UiPathLlmChatService, UiPathOpenAIService from .common import ( ApiClient, + BindingsService, ExternalApplicationService, UiPathApiConfig, UiPathExecutionContext, @@ -76,6 +77,10 @@ def __init__( raise SecretMissingError() from e self._execution_context = UiPathExecutionContext() + @cached_property + def bindings(self) -> BindingsService: + return BindingsService() + @property def api_client(self) -> ApiClient: return ApiClient(self._config, self._execution_context) diff --git a/packages/uipath-platform/src/uipath/platform/common/__init__.py b/packages/uipath-platform/src/uipath/platform/common/__init__.py index 40fc1ac34..e8c14c497 100644 --- a/packages/uipath-platform/src/uipath/platform/common/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/common/__init__.py @@ -8,11 +8,13 @@ from ._bindings import ( ConnectionResourceOverwrite, GenericResourceOverwrite, + PropertyResourceOverwrite, ResourceOverwrite, ResourceOverwriteParser, ResourceOverwritesContext, resource_override, ) +from ._bindings_service import BindingsService from ._config import UiPathApiConfig, UiPathConfig from ._endpoints_manager import EndpointManager from ._execution_context import UiPathExecutionContext @@ -99,8 +101,10 @@ "validate_pagination_params", "EndpointManager", "jsonschema_to_pydantic", + "BindingsService", "ConnectionResourceOverwrite", "GenericResourceOverwrite", + "PropertyResourceOverwrite", "ResourceOverwrite", "ResourceOverwriteParser", "ResourceOverwritesContext", diff --git a/packages/uipath-platform/src/uipath/platform/common/_bindings.py b/packages/uipath-platform/src/uipath/platform/common/_bindings.py index 01ff732a4..4e3159247 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_bindings.py +++ b/packages/uipath-platform/src/uipath/platform/common/_bindings.py @@ -80,8 +80,23 @@ def folder_identifier(self) -> str: return self.folder_key +class PropertyResourceOverwrite(ResourceOverwrite): + resource_type: Literal["property"] + values: dict[str, str] = Field(alias="values") + + model_config = ConfigDict(populate_by_name=True, extra="ignore") + + @property + def resource_identifier(self) -> str: + return "" + + @property + def folder_identifier(self) -> str: + return "" + + ResourceOverwriteUnion = Annotated[ - Union[GenericResourceOverwrite, ConnectionResourceOverwrite], + Union[GenericResourceOverwrite, ConnectionResourceOverwrite, PropertyResourceOverwrite], Field(discriminator="resource_type"), ] diff --git a/packages/uipath-platform/src/uipath/platform/common/_bindings_service.py b/packages/uipath-platform/src/uipath/platform/common/_bindings_service.py new file mode 100644 index 000000000..9cb2ffa2c --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/common/_bindings_service.py @@ -0,0 +1,113 @@ +import json +import logging +from pathlib import Path +from typing import Optional, Union, overload + +from ._bindings import PropertyResourceOverwrite, _resource_overwrites +from ._config import UiPathConfig + +logger = logging.getLogger(__name__) + + +class BindingsService: + """Service for reading Property binding resources from bindings.json. + + Provides access to connector-defined property values (e.g. SharePoint folder IDs) + that are configured at design time and resolved at runtime. + """ + + def __init__(self, bindings_file_path: Optional[Path] = None) -> None: + self._bindings_file_path = bindings_file_path or UiPathConfig.bindings_file_path + + def _load_bindings(self) -> list[dict]: + try: + with open(self._bindings_file_path, "r") as f: + data = json.load(f) + return data.get("resources", []) + except FileNotFoundError: + logger.debug("Bindings file not found: %s", self._bindings_file_path) + return [] + except (json.JSONDecodeError, OSError) as e: + logger.warning("Failed to load bindings file %s: %s", self._bindings_file_path, e) + return [] + + def _find_property_resource(self, key: str) -> Optional[dict]: + """Find a Property binding resource by exact or suffix key match.""" + resources = self._load_bindings() + for resource in resources: + if resource.get("resource", "").lower() != "property": + continue + resource_key = resource.get("key", "") + if resource_key == key or resource_key.endswith(f".{key}") or resource_key.endswith(key): + return resource + return None + + def _get_overwrite(self, key: str) -> Optional[PropertyResourceOverwrite]: + """Check context var for a runtime overwrite for the given key.""" + context_overwrites = _resource_overwrites.get() + if context_overwrites is None: + return None + # Try both "property." and the raw key + for lookup_key in (f"property.{key}", key): + overwrite = context_overwrites.get(lookup_key) + if isinstance(overwrite, PropertyResourceOverwrite): + return overwrite + return None + + @overload + def get_property(self, key: str) -> dict[str, str]: ... + + @overload + def get_property(self, key: str, sub_property: str) -> str: ... + + def get_property( + self, key: str, sub_property: Optional[str] = None + ) -> Union[str, dict[str, str]]: + """Get the value(s) of a Property binding resource. + + Args: + key: The binding key, e.g. ``"sharepoint-connection.SharePoint Invoices folder"``. + Accepts the full key or a suffix that uniquely identifies the binding. + sub_property: The name of a specific sub-property to retrieve (e.g. ``"ID"``). + If omitted, returns all sub-properties as a ``{name: value}`` dict. + + Returns: + The ``defaultValue`` of the requested sub-property when ``sub_property`` is + given, or a dict of all sub-property names mapped to their ``defaultValue`` + when ``sub_property`` is omitted. + + Raises: + KeyError: When the binding key is not found, or when ``sub_property`` is given + but does not exist on the binding. + """ + # Check for runtime overwrite first + overwrite = self._get_overwrite(key) + if overwrite is not None: + if sub_property is not None: + if sub_property not in overwrite.values: + raise KeyError( + f"Sub-property '{sub_property}' not found in Property binding '{key}'. " + f"Available: {list(overwrite.values.keys())}" + ) + return overwrite.values[sub_property] + return dict(overwrite.values) + + # Fall back to bindings.json + resource = self._find_property_resource(key) + if resource is None: + raise KeyError( + f"Property binding '{key}' not found in {self._bindings_file_path}." + ) + + value: dict = resource.get("value", {}) + all_values = {name: props.get("defaultValue", "") for name, props in value.items()} + + if sub_property is not None: + if sub_property not in all_values: + raise KeyError( + f"Sub-property '{sub_property}' not found in Property binding '{key}'. " + f"Available: {list(all_values.keys())}" + ) + return all_values[sub_property] + + return all_values diff --git a/packages/uipath/specs/bindings.spec.md b/packages/uipath/specs/bindings.spec.md index 6fecf2f70..021b103fc 100644 --- a/packages/uipath/specs/bindings.spec.md +++ b/packages/uipath/specs/bindings.spec.md @@ -43,6 +43,7 @@ The configuration supports multiple resource types: 4. **index** - Search indexes 5. **apps** - Action center apps 6. **connection** - External connections +7. **Property** - Connector-defined resource properties (e.g. SharePoint folder IDs selected at design time) --- @@ -53,7 +54,7 @@ Each resource in the `resources` array has the following structure: ```json { - "resource": "asset|process|bucket|index|connection", + "resource": "asset|process|bucket|index|connection|Property", "key": "unique_key", "value": { ... }, "metadata": { ... } @@ -64,7 +65,7 @@ Each resource in the `resources` array has the following structure: | Property | Type | Required | Description | |----------|------|----------|-------------| -| `resource` | `string` | Yes | Resource type (one of the five types) | +| `resource` | `string` | Yes | Resource type (one of the seven types) | | `key` | `string` | Yes | Unique identifier for this resource | | `value` | `object` | Yes | Resource-specific configuration | | `metadata` | `object` | No | Additional metadata for the binding | @@ -303,6 +304,88 @@ Connections define external system integrations. --- +### 7. Property + +Property bindings represent connector-defined resources that a user browses and selects at design time (e.g. a SharePoint folder, an OneDrive file). They are child resources of a parent Connection binding and contain **arbitrary sub-properties** with resolved values. + +**Key Format:** `.