Skip to content

Commit 7f12de4

Browse files
committed
feat: add schema generation helpers
1 parent d2342da commit 7f12de4

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: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,176 @@ class UiPathRuntimeSchema(BaseModel):
6868
model_config = COMMON_MODEL_SCHEMA
6969

7070

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

0 commit comments

Comments
 (0)