Skip to content

Commit 5b7c9d8

Browse files
committed
feat: infer attachments on init
1 parent b0e1a82 commit 5b7c9d8

8 files changed

Lines changed: 157 additions & 99 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
[project]
22
name = "uipath"
3-
version = "2.4.12"
3+
version = "2.4.13"
44
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
77
dependencies = [
88
"uipath-core>=0.1.4, <0.2.0",
9-
"uipath-runtime>=0.4.0, <0.5.0",
9+
"uipath-runtime>=0.4.1, <0.5.0",
1010
"click>=8.3.1",
1111
"httpx>=0.28.1",
1212
"pyjwt>=2.10.1",

src/uipath/_cli/cli_init.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,11 @@ def write_entry_points_file(entry_points: list[UiPathRuntimeSchema]) -> Path:
173173
"$schema": "https://cloud.uipath.com/draft/2024-12/entry-point",
174174
"$id": "entry-points.json",
175175
"entryPoints": [
176-
ep.model_dump(by_alias=True, exclude_unset=True) for ep in entry_points
176+
ep.model_dump(
177+
by_alias=True,
178+
exclude_unset=True,
179+
)
180+
for ep in entry_points
177181
],
178182
}
179183

@@ -297,6 +301,7 @@ async def initialize() -> None:
297301
entrypoint_name, runtime_id="default"
298302
)
299303
schema = await runtime.get_schema()
304+
300305
entry_point_schemas.append(schema)
301306
finally:
302307
if runtime:

src/uipath/functions/runtime.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
UiPathErrorContract,
2323
UiPathRuntimeError,
2424
)
25-
from uipath.runtime.schema import UiPathRuntimeSchema
25+
from uipath.runtime.schema import UiPathRuntimeSchema, transform_attachments
2626

2727
from .schema_gen import get_type_schema
2828
from .type_conversion import (
@@ -174,7 +174,8 @@ async def get_schema(self) -> UiPathRuntimeSchema:
174174
input_schema = {}
175175
else:
176176
input_param_name = next(iter(sig.parameters))
177-
input_schema = get_type_schema(hints.get(input_param_name))
177+
schema = get_type_schema(hints.get(input_param_name))
178+
input_schema = transform_attachments(schema)
178179

179180
# Determine output schema
180181
output_schema = get_type_schema(hints.get("return"))

src/uipath/functions/schema_gen.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import Any, Union, get_args, get_origin
88

99
from pydantic import BaseModel
10+
from uipath.runtime.schema import transform_nullable_types, transform_references
1011

1112
TYPE_MAP: dict[str, str] = {
1213
"int": "integer",
@@ -91,17 +92,17 @@ def _get_enum_schema(enum_class: type[Enum]) -> dict[str, Any]:
9192

9293

9394
def _get_pydantic_schema(model_class: type[BaseModel]) -> dict[str, Any]:
94-
"""Generate schema for Pydantic models."""
95-
properties = {}
96-
required = []
95+
"""Generate schema for Pydantic models using Pydantic's built-in schema generation."""
96+
schema = model_class.model_json_schema()
9797

98-
for field_name, field_info in model_class.model_fields.items():
99-
schema_field_name = field_info.alias or field_name
100-
properties[schema_field_name] = get_type_schema(field_info.annotation)
101-
if field_info.is_required():
102-
required.append(schema_field_name)
103-
104-
return {"type": "object", "properties": properties, "required": required}
98+
resolved_schema, _ = transform_references(schema)
99+
processed_properties = transform_nullable_types(resolved_schema)
100+
assert isinstance(processed_properties, dict)
101+
return {
102+
"type": "object",
103+
"properties": processed_properties.get("properties", {}),
104+
"required": processed_properties.get("required", []),
105+
}
105106

106107

107108
def _get_dataclass_schema(dataclass_type: type) -> dict[str, Any]:
@@ -111,6 +112,7 @@ def _get_dataclass_schema(dataclass_type: type) -> dict[str, Any]:
111112

112113
for field in fields(dataclass_type):
113114
properties[field.name] = get_type_schema(field.type)
115+
114116
# Field is required if it has no default value and no default_factory
115117
if field.default == field.default_factory == field.default.__class__.__name__:
116118
required.append(field.name)

src/uipath/platform/attachments/attachments.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ class Attachment(BaseModel):
2121
id: Optional[uuid.UUID] = Field(None, alias="ID")
2222
full_name: str = Field(..., alias="FullName")
2323
mime_type: str = Field(..., alias="MimeType")
24+
model_config = {
25+
"title": "UiPathAttachment",
26+
}
2427

2528

2629
@dataclass

tests/cli/test_init.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,3 +362,54 @@ def test_bindings_and_entrypoints_files_creation(
362362
config = json.load(f)
363363
assert "functions" in config
364364
assert config["functions"]["main"] == "main.py:main"
365+
366+
def test_schema_generation_resolves_attachments(
367+
self, runner: CliRunner, temp_dir: str
368+
) -> None:
369+
"""Test that attachments are resolved in entry-points schema"""
370+
371+
with runner.isolated_filesystem(temp_dir=temp_dir):
372+
with open("main.py", "w") as f:
373+
f.write("""
374+
from uipath.platform.attachments import Attachment
375+
from pydantic import BaseModel, Field
376+
class InputModel(BaseModel):
377+
input_file: Attachment
378+
other_field: int | None = Field(default=None)
379+
def main(input: InputModel) -> InputModel: return input""")
380+
381+
uipath_config = {"functions": {"main": "main.py:main"}}
382+
with open("uipath.json", "w") as f:
383+
json.dump(uipath_config, f)
384+
385+
result = runner.invoke(cli, ["init"], env={})
386+
387+
assert result.exit_code == 0
388+
assert "Created 'bindings.json' file" in result.output
389+
assert "Created 'entry-points.json' file" in result.output
390+
391+
# Verify entry-points.json contains attachments definition
392+
with open("entry-points.json", "r") as f:
393+
entrypoints = json.load(f)
394+
input_schema = entrypoints["entryPoints"][0]["input"]
395+
assert "definitions" in input_schema
396+
assert "job-attachment" in input_schema["definitions"]
397+
assert input_schema["definitions"]["job-attachment"]["type"] == "object"
398+
assert (
399+
input_schema["definitions"]["job-attachment"][
400+
"x-uipath-resource-kind"
401+
]
402+
== "JobAttachment"
403+
)
404+
assert all(
405+
prop_name
406+
in input_schema["definitions"]["job-attachment"]["properties"]
407+
for prop_name in ["ID", "FullName", "MimeType", "Metadata"]
408+
)
409+
410+
assert len(input_schema["properties"]) == 2
411+
assert all(
412+
prop_name in input_schema["properties"]
413+
for prop_name in ["input_file", "other_field"]
414+
)
415+
assert input_schema["required"] == ["input_file"]

tests/cli/test_input_args.py

Lines changed: 75 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -37,82 +37,78 @@ class SimpleDataClass:
3737
value: int = 42
3838

3939

40-
def test_pydantic_model_with_aliases():
41-
"""Test that Pydantic model schemas use field aliases when defined."""
42-
schema = get_type_schema(EventArguments)
43-
44-
assert schema["type"] == "object"
45-
assert "properties" in schema
46-
47-
# Check that aliases are used in property names
48-
expected_properties = {
49-
"UiPathEventConnector",
50-
"UiPathEvent",
51-
"UiPathEventObjectType",
52-
"UiPathEventObjectId",
53-
"UiPathAdditionalEventData",
54-
}
55-
actual_properties = set(schema["properties"].keys())
56-
assert actual_properties == expected_properties
57-
58-
# All fields have defaults, so none should be required
59-
assert schema["required"] == []
60-
61-
62-
def test_pydantic_model_required_fields():
63-
"""Test that required fields are correctly identified in Pydantic models."""
64-
schema = get_type_schema(RequiredFieldsModel)
65-
66-
assert schema["type"] == "object"
67-
assert "properties" in schema
68-
69-
# Check properties include both field names and aliases
70-
expected_properties = {
71-
"required_field", # field name (no alias)
72-
"optional_field", # field name (no alias)
73-
"AliasedRequired", # alias
74-
"AliasedOptional", # alias
75-
}
76-
actual_properties = set(schema["properties"].keys())
77-
assert actual_properties == expected_properties
78-
79-
# Check required fields (using aliases where defined)
80-
expected_required = {"required_field", "AliasedRequired"}
81-
actual_required = set(schema["required"])
82-
assert actual_required == expected_required
83-
84-
85-
def test_dataclass_still_works():
86-
"""Test that dataclass functionality is not broken."""
87-
schema = get_type_schema(SimpleDataClass)
88-
89-
assert schema["type"] == "object"
90-
assert "properties" in schema
91-
92-
# Dataclass should use field names (no alias support)
93-
expected_properties = {"name", "value"}
94-
actual_properties = set(schema["properties"].keys())
95-
assert actual_properties == expected_properties
96-
97-
# Field with default should not be required
98-
assert schema["required"] == ["name"]
99-
100-
101-
def test_primitive_types():
102-
"""Test that primitive type handling still works."""
103-
assert get_type_schema(str) == {"type": "string"}
104-
assert get_type_schema(int) == {"type": "integer"}
105-
assert get_type_schema(float) == {"type": "number"}
106-
assert get_type_schema(bool) == {"type": "boolean"}
107-
108-
109-
def test_optional_types():
110-
"""Test handling of Optional types."""
111-
schema = get_type_schema(Optional[str])
112-
assert schema == {"type": "string"} # Should unwrap Optional
113-
114-
115-
def test_optional_union_types():
116-
"""Test handling of Optional types."""
117-
schema = get_type_schema(str | None)
118-
assert schema == {"type": "string"} # Should unwrap Optional
40+
class TestInputArgs:
41+
def test_pydantic_model_with_aliases(self):
42+
"""Test that Pydantic model schemas use field aliases when defined."""
43+
schema = get_type_schema(EventArguments)
44+
45+
assert schema["type"] == "object"
46+
assert "properties" in schema
47+
48+
# Check that aliases are used in property names
49+
expected_properties = {
50+
"UiPathEventConnector",
51+
"UiPathEvent",
52+
"UiPathEventObjectType",
53+
"UiPathEventObjectId",
54+
"UiPathAdditionalEventData",
55+
}
56+
actual_properties = set(schema["properties"].keys())
57+
assert actual_properties == expected_properties
58+
59+
# All fields have defaults, so none should be required
60+
assert schema["required"] == []
61+
62+
def test_pydantic_model_required_fields(self):
63+
"""Test that required fields are correctly identified in Pydantic models."""
64+
schema = get_type_schema(RequiredFieldsModel)
65+
66+
assert schema["type"] == "object"
67+
assert "properties" in schema
68+
69+
# Check properties include both field names and aliases
70+
expected_properties = {
71+
"required_field", # field name (no alias)
72+
"optional_field", # field name (no alias)
73+
"AliasedRequired", # alias
74+
"AliasedOptional", # alias
75+
}
76+
actual_properties = set(schema["properties"].keys())
77+
assert actual_properties == expected_properties
78+
79+
# Check required fields (using aliases where defined)
80+
expected_required = {"required_field", "AliasedRequired"}
81+
actual_required = set(schema["required"])
82+
assert actual_required == expected_required
83+
84+
def test_dataclass_still_works(self):
85+
"""Test that dataclass functionality is not broken."""
86+
schema = get_type_schema(SimpleDataClass)
87+
88+
assert schema["type"] == "object"
89+
assert "properties" in schema
90+
91+
# Dataclass should use field names (no alias support)
92+
expected_properties = {"name", "value"}
93+
actual_properties = set(schema["properties"].keys())
94+
assert actual_properties == expected_properties
95+
96+
# Field with default should not be required
97+
assert schema["required"] == ["name"]
98+
99+
def test_primitive_types(self):
100+
"""Test that primitive type handling still works."""
101+
assert get_type_schema(str) == {"type": "string"}
102+
assert get_type_schema(int) == {"type": "integer"}
103+
assert get_type_schema(float) == {"type": "number"}
104+
assert get_type_schema(bool) == {"type": "boolean"}
105+
106+
def test_optional_types(self):
107+
"""Test handling of Optional types."""
108+
response = get_type_schema(Optional[str])
109+
assert response == {"type": "string"} # Should unwrap Optional
110+
111+
def test_optional_union_types(self):
112+
"""Test handling of Optional types."""
113+
response = get_type_schema(str | None)
114+
assert response == {"type": "string"} # Should unwrap Optional

uv.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)