Skip to content
Merged
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.34"
version = "0.2.35"
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 @@ -32,6 +32,13 @@
_ORIGINATOR_VALUE = "uipath-python"


def _to_param_str(value: Any) -> str:
"""Convert an activity parameter value to its wire string; booleans become lowercase."""
if isinstance(value, bool):
return "true" if value else "false"
return value if isinstance(value, str) else str(value)


class ConnectionsService(BaseService):
"""Service for managing UiPath external service connections.

Expand Down Expand Up @@ -754,7 +761,7 @@ def _build_activity_request_spec(
if value is None:
continue

value_str = str(value) if not isinstance(value, str) else value
value_str = _to_param_str(value)

if param_name in activity_metadata.parameter_location_info.query_params:
query_params[param_name] = value_str
Expand Down Expand Up @@ -812,7 +819,7 @@ def _build_activity_request_spec(
# Scalar (string/number/etc.) — send as a plain multipart
# form field, not a file part. The (None, value) shape tells
# httpx to omit `filename=...` from the Content-Disposition.
files[key] = (None, str(val))
files[key] = (None, _to_param_str(val))

files[json_section] = (
"",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1560,6 +1560,65 @@ def test_invoke_activity_with_path_parameters(
"/elements_/v3/element/instances/test-connection-123/elements/test-connector/users/user456/posts/post789"
)

def test_invoke_activity_bool_query_and_header_params_are_lowercase(
self,
httpx_mock: HTTPXMock,
service: ConnectionsService,
simple_activity_metadata: ActivityMetadata,
) -> None:
"""Test that booleans are sent as true/false, not Python's True/False."""
connection_id = "test-connection-123"
activity_input = {
"query_param": True,
"query_param2": False,
"custom_header": True,
"custom_header2": 3,
}

httpx_mock.add_response(
method="GET",
status_code=200,
json={"id": connection_id, "name": "Test", "elementInstanceId": 1},
)
httpx_mock.add_response(method="POST", status_code=200, json={})

service.invoke_activity(
activity_metadata=simple_activity_metadata,
connection_id=connection_id,
activity_input=activity_input,
)

sent_request = httpx_mock.get_requests()[1]
assert sent_request.url.params["query_param"] == "true"
assert sent_request.url.params["query_param2"] == "false"
assert sent_request.headers["custom_header"] == "true"
assert sent_request.headers["custom_header2"] == "3"

def test_invoke_activity_bool_path_param_is_lowercase(
self,
httpx_mock: HTTPXMock,
service: ConnectionsService,
path_activity_metadata: ActivityMetadata,
) -> None:
"""Test that a boolean path parameter is substituted as false, not False."""
connection_id = "test-connection-123"

httpx_mock.add_response(
method="GET",
status_code=200,
json={"id": connection_id, "name": "Test", "elementInstanceId": 1},
)
httpx_mock.add_response(method="POST", status_code=200, json={})

service.invoke_activity(
activity_metadata=path_activity_metadata,
connection_id=connection_id,
activity_input={"userId": False, "postId": 7},
)

sent_request = httpx_mock.get_requests()[1]
assert sent_request.url.path.endswith("/users/false/posts/7")

def test_invoke_activity_multipart_request(
self,
httpx_mock: HTTPXMock,
Expand Down Expand Up @@ -2343,3 +2402,39 @@ def test_invoke_activity_multipart_scalar_is_plain_form_field(
# Scalar payload must NOT carry a filename in Content-Disposition.
assert "filename=" not in payload_part
assert "{}" in payload_part

def test_invoke_activity_multipart_bool_scalar_is_lowercase(
self,
httpx_mock: HTTPXMock,
service: ConnectionsService,
) -> None:
"""Boolean multipart form fields are sent as true/false, not True/False."""
metadata = ActivityMetadata(
object_path="/elements/test-connector/upload",
method_name="POST",
content_type="multipart/form-data",
parameter_location_info=ActivityParameterLocationInfo(
multipart_params=["flag"],
body_fields=[],
),
)
connection_id = "test-connection-123"

httpx_mock.add_response(
method="GET",
status_code=200,
json={"id": connection_id, "name": "Test", "elementInstanceId": 1},
)
httpx_mock.add_response(method="POST", status_code=200, json={"ok": True})

service.invoke_activity(
activity_metadata=metadata,
connection_id=connection_id,
activity_input={"flag": True},
)

sent_request = httpx_mock.get_requests()[1]
boundary = sent_request.headers["content-type"].split("boundary=")[1]
flag_part = _multipart_part(sent_request.content, boundary, "flag")

assert flag_part.rstrip().endswith("true")
2 changes: 1 addition & 1 deletion packages/uipath-platform/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/uipath/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading