Skip to content

Commit 2ac470e

Browse files
committed
feat: add Property binding resource support to SDK
Adds support for the Property resource type in bindings.json, enabling users to read connector-defined property values (e.g. SharePoint folder IDs) without writing custom helpers. - Extend BindingResourceValue with optional description/propertyName fields - Add "property" to GenericResourceOverwrite (replaces a separate class) - Add BindingsService with get_property() that reads from bindings.json and respects runtime resource overwrites via the existing ContextVar - Expose sdk.bindings as a cached_property on the UiPath class - Update bindings.spec.md to document Property as the 7th resource type - Add unit tests covering file reads, suffix key matching, runtime overwrites (including Studio-loaded and real runtime payload formats), and error cases ResourceOverwriteParser.parse now normalises the Property key prefix to lowercase ("Property" -> "property") and accepts the real runtime flat-dict format in addition to the explicit {"values": {...}} form. Both changes are additive — existing valid inputs are unaffected.
1 parent 10bc126 commit 2ac470e

7 files changed

Lines changed: 604 additions & 9 deletions

File tree

packages/uipath-platform/src/uipath/platform/_uipath.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .chat import ConversationsService, UiPathLlmChatService, UiPathOpenAIService
1212
from .common import (
1313
ApiClient,
14+
BindingsService,
1415
ExternalApplicationService,
1516
UiPathApiConfig,
1617
UiPathExecutionContext,
@@ -76,6 +77,10 @@ def __init__(
7677
raise SecretMissingError() from e
7778
self._execution_context = UiPathExecutionContext()
7879

80+
@cached_property
81+
def bindings(self) -> BindingsService:
82+
return BindingsService()
83+
7984
@property
8085
def api_client(self) -> ApiClient:
8186
return ApiClient(self._config, self._execution_context)

packages/uipath-platform/src/uipath/platform/common/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
ResourceOverwritesContext,
1414
resource_override,
1515
)
16+
from ._bindings_service import BindingsService
1617
from ._config import UiPathApiConfig, UiPathConfig
1718
from ._endpoints_manager import EndpointManager
1819
from ._execution_context import UiPathExecutionContext
@@ -99,6 +100,7 @@
99100
"validate_pagination_params",
100101
"EndpointManager",
101102
"jsonschema_to_pydantic",
103+
"BindingsService",
102104
"ConnectionResourceOverwrite",
103105
"GenericResourceOverwrite",
104106
"ResourceOverwrite",

packages/uipath-platform/src/uipath/platform/common/_bindings.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,12 @@ def folder_identifier(self) -> str:
4444

4545

4646
class GenericResourceOverwrite(ResourceOverwrite):
47-
resource_type: Literal["process", "index", "app", "asset", "bucket", "mcpServer"]
48-
name: str = Field(alias="name")
49-
folder_path: str = Field(alias="folderPath")
47+
resource_type: Literal[
48+
"process", "index", "app", "asset", "bucket", "mcpServer", "property"
49+
]
50+
name: str = Field(default="", alias="name")
51+
folder_path: str = Field(default="", alias="folderPath")
52+
values: Optional[dict[str, str]] = Field(default=None)
5053

5154
@property
5255
def resource_identifier(self) -> str:
@@ -110,6 +113,13 @@ def parse(cls, key: str, value: dict[str, Any]) -> ResourceOverwrite:
110113
The appropriate ResourceOverwrite subclass instance
111114
"""
112115
resource_type = key.split(".")[0]
116+
if resource_type.lower() == "property":
117+
# Normalise casing ("Property" → "property") and handle the real
118+
# runtime format where sub-properties are a flat dict rather than
119+
# nested under a "values" key.
120+
resource_type = "property"
121+
if "values" not in value:
122+
value = {"values": value}
113123
value_with_type = {"resource_type": resource_type, **value}
114124
return cls._adapter.validate_python(value_with_type)
115125

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import json
2+
import logging
3+
from pathlib import Path
4+
from typing import Optional, Union, overload
5+
6+
from ._bindings import GenericResourceOverwrite, _resource_overwrites
7+
from ._config import UiPathConfig
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
class BindingsService:
13+
"""Service for reading Property binding resources from bindings.json.
14+
15+
Provides access to connector-defined property values (e.g. SharePoint folder IDs)
16+
that are configured at design time and resolved at runtime.
17+
"""
18+
19+
def __init__(self, bindings_file_path: Optional[Path] = None) -> None:
20+
self._bindings_file_path = bindings_file_path or UiPathConfig.bindings_file_path
21+
22+
def _load_bindings(self) -> list[dict]:
23+
try:
24+
with open(self._bindings_file_path, "r") as f:
25+
data = json.load(f)
26+
return data.get("resources", [])
27+
except FileNotFoundError:
28+
logger.debug("Bindings file not found: %s", self._bindings_file_path)
29+
return []
30+
except (json.JSONDecodeError, OSError) as e:
31+
logger.warning(
32+
"Failed to load bindings file %s: %s", self._bindings_file_path, e
33+
)
34+
return []
35+
36+
def _find_property_resource(self, key: str) -> Optional[dict]:
37+
"""Find a Property binding resource by exact or suffix key match."""
38+
resources = self._load_bindings()
39+
for resource in resources:
40+
if resource.get("resource", "").lower() != "property":
41+
continue
42+
resource_key = resource.get("key", "")
43+
if (
44+
resource_key == key
45+
or resource_key.endswith(f".{key}")
46+
or resource_key.endswith(key)
47+
):
48+
return resource
49+
return None
50+
51+
def _get_overwrite(self, key: str) -> Optional[GenericResourceOverwrite]:
52+
"""Check context var for a runtime overwrite for the given key.
53+
54+
Supports exact key match (``property.<key>``) and suffix match so that
55+
a short label like ``"SharePoint Invoices folder"`` resolves against a
56+
fully-qualified stored key like
57+
``"property.sharepoint-connection.SharePoint Invoices folder"``.
58+
"""
59+
context_overwrites = _resource_overwrites.get()
60+
if context_overwrites is None:
61+
return None
62+
for stored_key, overwrite in context_overwrites.items():
63+
if not (
64+
isinstance(overwrite, GenericResourceOverwrite)
65+
and overwrite.resource_type == "property"
66+
):
67+
continue
68+
# Strip the "property." prefix for comparison (case-insensitive)
69+
lower_stored = stored_key.lower()
70+
bare_key = (
71+
stored_key[len("property.") :]
72+
if lower_stored.startswith("property.")
73+
else stored_key
74+
)
75+
if bare_key == key or bare_key.endswith(f".{key}") or stored_key == key:
76+
return overwrite
77+
return None
78+
79+
@overload
80+
def get_property(self, key: str) -> dict[str, str]: ...
81+
82+
@overload
83+
def get_property(self, key: str, sub_property: str) -> str: ...
84+
85+
def get_property(
86+
self, key: str, sub_property: Optional[str] = None
87+
) -> Union[str, dict[str, str]]:
88+
"""Get the value(s) of a Property binding resource.
89+
90+
Args:
91+
key: The binding key, e.g. ``"sharepoint-connection.SharePoint Invoices folder"``.
92+
Accepts the full key or a suffix that uniquely identifies the binding.
93+
sub_property: The name of a specific sub-property to retrieve (e.g. ``"ID"``).
94+
If omitted, returns all sub-properties as a ``{name: value}`` dict.
95+
96+
Returns:
97+
The ``defaultValue`` of the requested sub-property when ``sub_property`` is
98+
given, or a dict of all sub-property names mapped to their ``defaultValue``
99+
when ``sub_property`` is omitted.
100+
101+
Raises:
102+
KeyError: When the binding key is not found, or when ``sub_property`` is given
103+
but does not exist on the binding.
104+
"""
105+
# Check for runtime overwrite first
106+
overwrite = self._get_overwrite(key)
107+
if overwrite is not None:
108+
if sub_property is not None:
109+
if sub_property not in overwrite.values:
110+
raise KeyError(
111+
f"Sub-property '{sub_property}' not found in Property binding '{key}'. "
112+
f"Available: {list(overwrite.values.keys())}"
113+
)
114+
return overwrite.values[sub_property]
115+
return dict(overwrite.values)
116+
117+
# Fall back to bindings.json
118+
resource = self._find_property_resource(key)
119+
if resource is None:
120+
raise KeyError(
121+
f"Property binding '{key}' not found in {self._bindings_file_path}."
122+
)
123+
124+
value: dict = resource.get("value", {})
125+
all_values = {
126+
name: props.get("defaultValue", "") for name, props in value.items()
127+
}
128+
129+
if sub_property is not None:
130+
if sub_property not in all_values:
131+
raise KeyError(
132+
f"Sub-property '{sub_property}' not found in Property binding '{key}'. "
133+
f"Available: {list(all_values.keys())}"
134+
)
135+
return all_values[sub_property]
136+
137+
return all_values

0 commit comments

Comments
 (0)