Skip to content

Commit 637170a

Browse files
authored
Merge pull request #68 from UiPath/feat/add-schema-helpers
feat: add schema generation helpers
2 parents d2342da + a611555 commit 637170a

File tree

4 files changed

+465
-2
lines changed

4 files changed

+465
-2
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-runtime"
3-
version = "0.4.0"
3+
version = "0.4.1"
44
description = "Runtime abstractions and interfaces for building agents and automation scripts in the UiPath ecosystem"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

src/uipath/runtime/schema.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import copy
56
from typing import Any
67

78
from pydantic import BaseModel, ConfigDict, Field
@@ -68,9 +69,174 @@ class UiPathRuntimeSchema(BaseModel):
6869
model_config = COMMON_MODEL_SCHEMA
6970

7071

72+
def _get_job_attachment_definition() -> dict[str, Any]:
73+
"""Get the job-attachment definition schema for UiPath attachments.
74+
75+
Returns:
76+
The JSON schema definition for a UiPath job attachment.
77+
"""
78+
return {
79+
"type": "object",
80+
"required": ["ID"],
81+
"x-uipath-resource-kind": "JobAttachment",
82+
"properties": {
83+
"ID": {"type": "string"},
84+
"FullName": {"type": "string"},
85+
"MimeType": {"type": "string"},
86+
"Metadata": {
87+
"type": "object",
88+
"additionalProperties": {"type": "string"},
89+
},
90+
},
91+
}
92+
93+
94+
def transform_attachments(schema: dict[str, Any]) -> dict[str, Any]:
95+
"""Transform UiPathAttachment references in a JSON schema to use $ref.
96+
97+
This function recursively traverses a JSON schema and replaces any objects
98+
with title="UiPathAttachment" with a $ref to "#/definitions/job-attachment",
99+
adding the job-attachment definition to the schema's definitions section.
100+
101+
Args:
102+
schema: The JSON schema to transform (will not be modified in-place).
103+
104+
Returns:
105+
A new schema with UiPathAttachment references replaced by $ref.
106+
107+
Example:
108+
>>> schema = {
109+
... "type": "object",
110+
... "properties": {
111+
... "file": {
112+
... "title": "UiPathAttachment",
113+
... "type": "object",
114+
... "properties": {...}
115+
... }
116+
... }
117+
... }
118+
>>> result = transform_attachments(schema)
119+
>>> result["properties"]["file"]
120+
{"$ref": "#/definitions/job-attachment"}
121+
>>> "job-attachment" in result["definitions"]
122+
True
123+
"""
124+
result = copy.deepcopy(schema)
125+
has_attachments = False
126+
127+
def transform_recursive(obj: Any) -> Any:
128+
"""Recursively transform the schema object."""
129+
nonlocal has_attachments
130+
131+
if isinstance(obj, dict):
132+
if obj.get("title") == "UiPathAttachment" and obj.get("type") == "object":
133+
has_attachments = True
134+
return {"$ref": "#/definitions/job-attachment"}
135+
136+
return {key: transform_recursive(value) for key, value in obj.items()}
137+
138+
elif isinstance(obj, list):
139+
return [transform_recursive(item) for item in obj]
140+
141+
else:
142+
# Return primitive values as-is
143+
return obj
144+
145+
result = transform_recursive(result)
146+
147+
# add the job-attachment definition if any are present
148+
if has_attachments:
149+
if "definitions" not in result:
150+
result["definitions"] = {}
151+
result["definitions"]["job-attachment"] = _get_job_attachment_definition()
152+
153+
return result
154+
155+
156+
def transform_references(schema, root=None, visited=None):
157+
"""Recursively resolves $ref references in a JSON schema, handling circular references.
158+
159+
Returns:
160+
tuple: (resolved_schema, has_circular_dependency)
161+
"""
162+
if root is None:
163+
root = schema
164+
165+
if visited is None:
166+
visited = set()
167+
168+
has_circular = False
169+
170+
if isinstance(schema, dict):
171+
if "$ref" in schema:
172+
ref_path = schema["$ref"]
173+
174+
if ref_path in visited:
175+
# Circular dependency detected
176+
return {
177+
"type": "object",
178+
"description": f"Circular reference to {ref_path}",
179+
}, True
180+
181+
visited.add(ref_path)
182+
183+
# Resolve the reference
184+
ref_parts = ref_path.lstrip("#/").split("/")
185+
ref_schema = root
186+
for part in ref_parts:
187+
ref_schema = ref_schema.get(part, {})
188+
189+
result, circular = transform_references(ref_schema, root, visited)
190+
has_circular = has_circular or circular
191+
192+
# Remove from visited after resolution (allows the same ref in different branches)
193+
visited.discard(ref_path)
194+
195+
return result, has_circular
196+
197+
resolved_dict = {}
198+
for k, v in schema.items():
199+
resolved_value, circular = transform_references(v, root, visited)
200+
resolved_dict[k] = resolved_value
201+
has_circular = has_circular or circular
202+
return resolved_dict, has_circular
203+
204+
elif isinstance(schema, list):
205+
resolved_list = []
206+
for item in schema:
207+
resolved_item, circular = transform_references(item, root, visited)
208+
resolved_list.append(resolved_item)
209+
has_circular = has_circular or circular
210+
return resolved_list, has_circular
211+
212+
return schema, False
213+
214+
215+
def transform_nullable_types(
216+
schema: dict[str, Any] | list[Any] | Any,
217+
) -> dict[str, Any] | list[Any]:
218+
"""Process the schema to handle nullable types by removing anyOf with null and keeping the base type."""
219+
if isinstance(schema, dict):
220+
if "anyOf" in schema and len(schema["anyOf"]) == 2:
221+
types = [t.get("type") for t in schema["anyOf"]]
222+
if "null" in types:
223+
non_null_type = next(
224+
t for t in schema["anyOf"] if t.get("type") != "null"
225+
)
226+
return non_null_type
227+
228+
return {k: transform_nullable_types(v) for k, v in schema.items()}
229+
elif isinstance(schema, list):
230+
return [transform_nullable_types(item) for item in schema]
231+
return schema
232+
233+
71234
__all__ = [
72235
"UiPathRuntimeSchema",
73236
"UiPathRuntimeGraph",
74237
"UiPathRuntimeNode",
75238
"UiPathRuntimeEdge",
239+
"transform_nullable_types",
240+
"transform_references",
241+
"transform_attachments",
76242
]

0 commit comments

Comments
 (0)