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
64 changes: 28 additions & 36 deletions .github/workflows/publish-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,11 @@ jobs:

Write-Output "Package $PROJECT_NAME version set to $DEV_VERSION"

$startMarker = "<!-- DEV_PACKAGE_START:$PROJECT_NAME -->"
$endMarker = "<!-- DEV_PACKAGE_END:$PROJECT_NAME -->"

$dependencyMessage = @"
$startMarker
### $PROJECT_NAME

``````toml
Expand All @@ -130,7 +134,13 @@ jobs:

[tool.uv.sources]
$PROJECT_NAME = { index = "testpypi" }

[tool.uv]
override-dependencies = [
"$PROJECT_NAME>=$MIN_VERSION,<$MAX_VERSION",
]
``````
$endMarker
"@

# Get the owner and repo from the GitHub repository
Expand All @@ -148,36 +158,28 @@ jobs:
$pr = Invoke-RestMethod -Uri $prUri -Method Get -Headers $headers
$currentBody = $pr.body

# Define regex patterns for matching package sections
$devPackagesHeader = "## Development Packages"
$packageHeaderPattern = "### $PROJECT_NAME\s*\n"

# Find if the package section exists using multiline regex
$packageSectionRegex = "(?ms)### $PROJECT_NAME\s*\n``````toml.*?``````"

if ($currentBody -match $devPackagesHeader) {
# Development Packages section exists
if ($currentBody -match $packageSectionRegex) {
# Replace existing package section
Write-Output "Updating existing $PROJECT_NAME section"
$newBody = $currentBody -replace $packageSectionRegex, $dependencyMessage.Trim()
$markerPattern = "(?s)$([regex]::Escape($startMarker)).*?$([regex]::Escape($endMarker))"

function Get-UpdatedBody($body, $message) {
if ($body -match $markerPattern) {
return $body -replace $markerPattern, $message.Trim()
} elseif ($body -match $devPackagesHeader) {
$insertPoint = $body.IndexOf($devPackagesHeader) + $devPackagesHeader.Length
return $body.Insert($insertPoint, "`n`n$($message.Trim())")
} else {
# Append new package section after the Development Packages header
Write-Output "Adding new $PROJECT_NAME section"
$insertPoint = $currentBody.IndexOf($devPackagesHeader) + $devPackagesHeader.Length
$newBody = $currentBody.Insert($insertPoint, "`n`n$dependencyMessage")
$section = "$devPackagesHeader`n`n$($message.Trim())"
if ($body) {
$result = "$body`n`n$section"
} else {
$result = $section
}
return $result
}
} else {
# Create the Development Packages section
Write-Output "Creating Development Packages section with $PROJECT_NAME"
$packageSection = @"
## Development Packages

$dependencyMessage
"@
$newBody = if ($currentBody) { "$currentBody`n`n$packageSection" } else { $packageSection }
}

$newBody = Get-UpdatedBody $currentBody $dependencyMessage

# Update the PR description with retry logic
$maxRetries = 3
$retryCount = 0
Expand All @@ -200,17 +202,7 @@ jobs:
# Re-fetch PR body in case another job updated it
$pr = Invoke-RestMethod -Uri $prUri -Method Get -Headers $headers
$currentBody = $pr.body

# Recompute newBody with fresh data
if ($currentBody -match $packageSectionRegex) {
$newBody = $currentBody -replace $packageSectionRegex, $dependencyMessage.Trim()
} elseif ($currentBody -match $devPackagesHeader) {
$insertPoint = $currentBody.IndexOf($devPackagesHeader) + $devPackagesHeader.Length
$newBody = $currentBody.Insert($insertPoint, "`n`n$dependencyMessage")
} else {
$packageSection = "$devPackagesHeader`n`n$dependencyMessage"
$newBody = if ($currentBody) { "$currentBody`n`n$packageSection" } else { $packageSection }
}
$newBody = Get-UpdatedBody $currentBody $dependencyMessage
} else {
Write-Output "Failed to update PR description after $maxRetries attempts"
throw
Expand Down
5 changes: 5 additions & 0 deletions packages/uipath-platform/src/uipath/platform/_uipath.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .chat import ConversationsService, UiPathLlmChatService, UiPathOpenAIService
from .common import (
ApiClient,
BindingsService,
ExternalApplicationService,
UiPathApiConfig,
UiPathExecutionContext,
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
ResourceOverwritesContext,
resource_override,
)
from ._bindings_service import BindingsService
from ._config import UiPathApiConfig, UiPathConfig
from ._endpoints_manager import EndpointManager
from ._execution_context import UiPathExecutionContext
Expand Down Expand Up @@ -99,6 +100,7 @@
"validate_pagination_params",
"EndpointManager",
"jsonschema_to_pydantic",
"BindingsService",
"ConnectionResourceOverwrite",
"GenericResourceOverwrite",
"ResourceOverwrite",
Expand Down
26 changes: 22 additions & 4 deletions packages/uipath-platform/src/uipath/platform/common/_bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,22 @@ def folder_identifier(self) -> str:
"""The folder location identifier for this resource."""
pass

@property
@abstractmethod
def properties(self) -> dict[str, Any]:
"""A dictionary of properties provided by this overwrite."""
pass


class GenericResourceOverwrite(ResourceOverwrite):
resource_type: Literal["process", "index", "app", "asset", "bucket", "mcpServer"]
name: str = Field(alias="name")
folder_path: str = Field(alias="folderPath")
model_config = ConfigDict(populate_by_name=True, extra="allow")
resource_type: Literal["process", "index", "app", "asset", "bucket", "mcpserver", "property", "mcpServer"]
name: str = Field(default="", alias="name")
folder_path: str = Field(default="", alias="folderPath")

@property
def properties(self) -> dict[str, Any]:
return self.model_extra or {}

@property
def resource_identifier(self) -> str:
Expand All @@ -71,6 +82,10 @@ class ConnectionResourceOverwrite(ResourceOverwrite):
extra="ignore",
)

@property
def properties(self) -> dict[str, Any]:
return self.model_dump(by_alias=True, exclude={"resource_type"})

@property
def resource_identifier(self) -> str:
return self.connection_id
Expand Down Expand Up @@ -109,7 +124,7 @@ def parse(cls, key: str, value: dict[str, Any]) -> ResourceOverwrite:
Returns:
The appropriate ResourceOverwrite subclass instance
"""
resource_type = key.split(".")[0]
resource_type = key.split(".")[0].lower()
value_with_type = {"resource_type": resource_type, **value}
return cls._adapter.validate_python(value_with_type)

Expand Down Expand Up @@ -138,6 +153,9 @@ async def __aenter__(self) -> "ResourceOverwritesContext":
len(existing),
)
overwrites = await self.get_overwrites_callable()

logger.debug("ResourceOverwritesContext loading overwrites: %s", overwrites)

self._token = _resource_overwrites.set(overwrites)
self.overwrites_count = len(overwrites)
if overwrites:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import json
import logging
from functools import cached_property
from pathlib import Path
from typing import Any, overload

from ._bindings import ResourceOverwrite, _resource_overwrites
from ._config import UiPathConfig

logger = logging.getLogger(__name__)


class BindingsService:
"""Service for reading bindings configurations from bindings.json.

Provides access to properties configured at design time and resolved at runtime.
"""

def __init__(self, bindings_file_path: Path | None = None) -> None:
self._bindings_file_path = bindings_file_path or UiPathConfig.bindings_file_path

@cached_property
def _load_bindings(self) -> list[dict[str, Any]]:
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_resource(self, key: str) -> dict[str, Any] | None:
"""Find a binding resource by exact or suffix key match."""
resources = self._load_bindings
for resource in resources:
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) -> ResourceOverwrite | None:
"""Check context var for a runtime overwrite for the given key.

Supports exact key match and suffix match so that
a short label like ``"SharePoint Invoices folder"`` resolves against a
fully-qualified stored key like
``"property.sharepoint-connection.SharePoint Invoices folder"``.
"""
context_overwrites = _resource_overwrites.get()
if context_overwrites is None:
return None
for stored_key, overwrite in context_overwrites.items():
# Remove the `<resource_type>.` prefix correctly
parts = stored_key.split(".", 1)
bare_key = parts[1] if len(parts) > 1 else stored_key

if bare_key == key or bare_key.endswith(f".{key}") or stored_key == key:
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: str | None = None
) -> str | dict[str, str]:
"""Get the value(s) of a binding resource.

Args:
key: The binding key, e.g. ``"sharepoint-connection.SharePoint Invoices folder"`` or ``"asset.my-asset"``.
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"`` or ``"folderPath"``).
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.properties:
raise KeyError(
f"Sub-property '{sub_property}' not found in binding '{key}'. "
f"Available: {list(overwrite.properties.keys())}"
)
return overwrite.properties[sub_property]
return dict(overwrite.properties)

# Fall back to bindings.json
resource = self._find_resource(key)
if resource is None:
raise KeyError(
f"Binding '{key}' not found in {self._bindings_file_path}."
)

value: dict = resource.get("value", {})
all_values = {
name: props.get("defaultValue", "") if isinstance(props, dict) else str(props)
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 binding '{key}'. "
f"Available: {list(all_values.keys())}"
)
return all_values[sub_property]

return all_values
Loading
Loading