Skip to content

Commit 101376f

Browse files
committed
Merge branch 'main' into fix/uipath-pull-command
2 parents 2889f42 + 7290c31 commit 101376f

4 files changed

Lines changed: 85 additions & 57 deletions

File tree

‎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"
3-
version = "2.1.128"
3+
version = "2.1.129"
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.10"

‎src/uipath/utils/dynamic_schema.py‎

Lines changed: 68 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,35 @@
11
"""Json schema to dynamic pydantic model."""
22

3-
from typing import Any, Dict, List, Optional, Type, Union
3+
from enum import Enum
4+
from typing import Any, Dict, List, Type, Union
45

56
from pydantic import BaseModel, Field, create_model
67

78

89
def jsonschema_to_pydantic(
910
schema: dict[str, Any],
10-
definitions: Optional[dict[str, Any]] = None,
1111
) -> Type[BaseModel]:
1212
"""Convert a schema dict to a pydantic model.
1313
14-
Modified version of https://github.com/kreneskyp/jsonschema-pydantic to account for two unresolved issues.
14+
Modified version of https://github.com/kreneskyp/jsonschema-pydantic to account for three unresolved issues.
15+
1. Support for title
16+
2. Better representation of optionals.
17+
3. Support for optional
1518
1619
Args:
1720
schema: JSON schema.
1821
definitions: Definitions dict. Defaults to `$def`.
1922
2023
Returns: Pydantic model.
2124
"""
22-
title = schema.get("title", "DynamicModel")
23-
assert isinstance(title, str), "Title of a model must be a string."
24-
25-
description = schema.get("description", None)
26-
27-
# top level schema provides definitions
28-
if definitions is None:
29-
if "$defs" in schema:
30-
definitions = schema["$defs"]
31-
elif "definitions" in schema:
32-
definitions = schema["definitions"]
33-
else:
34-
definitions = {}
25+
dynamic_type_counter = 0
26+
combined_model_counter = 0
3527

3628
def convert_type(prop: dict[str, Any]) -> Any:
29+
nonlocal dynamic_type_counter, combined_model_counter
3730
if "$ref" in prop:
38-
ref_path = prop["$ref"].split("/")
39-
ref = definitions[ref_path[-1]]
40-
return jsonschema_to_pydantic(ref, definitions)
31+
# This is the full path. It will be updated in update_forward_refs.
32+
return prop["$ref"].split("/")[-1].capitalize()
4133

4234
if "type" in prop:
4335
type_mapping = {
@@ -52,13 +44,55 @@ def convert_type(prop: dict[str, Any]) -> Any:
5244

5345
type_ = prop["type"]
5446

55-
if type_ == "array":
47+
if "enum" in prop:
48+
dynamic_members = {
49+
f"KEY_{i}": value for i, value in enumerate(prop["enum"])
50+
}
51+
52+
base_type: Any = type_mapping.get(type_, Any)
53+
54+
class DynamicEnum(base_type, Enum):
55+
pass
56+
57+
type_ = DynamicEnum(prop.get("title", "DynamicEnum"), dynamic_members) # type: ignore[call-arg]
58+
return type_
59+
elif type_ == "array":
5660
item_type: Any = convert_type(prop.get("items", {}))
57-
assert isinstance(item_type, type)
5861
return List[item_type] # noqa F821
5962
elif type_ == "object":
6063
if "properties" in prop:
61-
return jsonschema_to_pydantic(prop, definitions)
64+
if "title" in prop and prop["title"]:
65+
title = prop["title"]
66+
else:
67+
title = f"DynamicType_{dynamic_type_counter}"
68+
dynamic_type_counter += 1
69+
70+
fields: dict[str, Any] = {}
71+
required_fields = prop.get("required", [])
72+
73+
for name, property in prop.get("properties", {}).items():
74+
pydantic_type = convert_type(property)
75+
field_kwargs = {}
76+
if "default" in property:
77+
field_kwargs["default"] = property["default"]
78+
if name not in required_fields:
79+
# Note that we do not make this optional. This is due to a limitation in Pydantic/Python.
80+
# If we convert the Optional type back to json schema, it is represented as type | None.
81+
# pydantic_type = Optional[pydantic_type]
82+
83+
if "default" not in field_kwargs:
84+
field_kwargs["default"] = None
85+
if "description" in property:
86+
field_kwargs["description"] = property["description"]
87+
if "title" in property:
88+
field_kwargs["title"] = property["title"]
89+
90+
fields[name] = (pydantic_type, Field(**field_kwargs))
91+
92+
object_model = create_model(title, **fields)
93+
if "description" in prop:
94+
object_model.__doc__ = prop["description"]
95+
return object_model
6296
else:
6397
return Dict[str, Any]
6498
else:
@@ -67,9 +101,13 @@ def convert_type(prop: dict[str, Any]) -> Any:
67101
elif "allOf" in prop:
68102
combined_fields = {}
69103
for sub_schema in prop["allOf"]:
70-
model = jsonschema_to_pydantic(sub_schema, definitions)
104+
model = convert_type(sub_schema)
71105
combined_fields.update(model.__annotations__)
72-
return create_model("CombinedModel", **combined_fields)
106+
combined_model = create_model(
107+
f"CombinedModel_{combined_model_counter}", **combined_fields
108+
)
109+
combined_model_counter += 1
110+
return combined_model
73111

74112
elif "anyOf" in prop:
75113
unioned_types = tuple(
@@ -81,31 +119,10 @@ def convert_type(prop: dict[str, Any]) -> Any:
81119
else:
82120
raise ValueError(f"Unsupported schema: {prop}")
83121

84-
fields: dict[str, Any] = {}
85-
required_fields = schema.get("required", [])
86-
87-
for name, prop in schema.get("properties", {}).items():
88-
pydantic_type = convert_type(prop)
89-
field_kwargs = {}
90-
if "default" in prop:
91-
field_kwargs["default"] = prop["default"]
92-
if name not in required_fields:
93-
# Note that we do not make this optional. This is due to a limitation in Pydantic/Python.
94-
# If we convert the Optional type back to json schema, it is represented as type | None.
95-
# pydantic_type = Optional[pydantic_type]
96-
97-
if "default" not in field_kwargs:
98-
field_kwargs["default"] = None
99-
if "description" in prop:
100-
field_kwargs["description"] = prop["description"]
101-
if "title" in prop:
102-
field_kwargs["title"] = prop["title"]
103-
104-
fields[name] = (pydantic_type, Field(**field_kwargs))
105-
106-
convert_type(schema.get("properties", {}).get("choices", {}))
107-
108-
model = create_model(title, **fields)
109-
if description:
110-
model.__doc__ = description
122+
namespace: dict[str, Any] = {}
123+
for name, definition in schema.get("$defs", schema.get("definitions", {})).items():
124+
model = convert_type(definition)
125+
namespace[name.capitalize()] = model
126+
model = convert_type(schema)
127+
model.model_rebuild(force=True, _types_namespace=namespace)
111128
return model

‎tests/cli/utils/test_dynamic_schema.py‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1+
from enum import Enum
12
from typing import List, Optional
23

34
from pydantic import BaseModel, Field
45

56
from uipath.utils.dynamic_schema import jsonschema_to_pydantic
67

78

8-
def test_dynamic_schema():
9+
def atest_dynamic_schema():
910
# Arrange
1011
class InnerSchema(BaseModel):
11-
"""Inner schema description."""
12+
"""Inner schema description including a self-reference."""
1213

13-
pass
14+
self_reference: Optional["InnerSchema"] = None
15+
16+
class CustomEnum(str, Enum):
17+
KEY_1 = "VALUE_1"
18+
KEY_2 = "VALUE_2"
1419

1520
class Schema(BaseModel):
1621
"""Schema description."""
@@ -70,7 +75,7 @@ class Schema(BaseModel):
7075
)
7176

7277
nested_object: InnerSchema = Field(
73-
default=InnerSchema(),
78+
default=InnerSchema(self_reference=None),
7479
title="Nested Object Title",
7580
description="Nested Object Description",
7681
)
@@ -85,6 +90,12 @@ class Schema(BaseModel):
8590
description="List Nested Object Description",
8691
)
8792

93+
enum: CustomEnum = Field(
94+
default=CustomEnum.KEY_1,
95+
title="Enum Title",
96+
description="Enum Description",
97+
)
98+
8899
schema_json = Schema.model_json_schema()
89100

90101
# Act

‎uv.lock‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)