uipath-core 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. {uipath_core-0.2.1 → uipath_core-0.2.2}/PKG-INFO +1 -1
  2. {uipath_core-0.2.1 → uipath_core-0.2.2}/pyproject.toml +1 -1
  3. uipath_core-0.2.2/src/uipath/core/serialization/__init__.py +5 -0
  4. uipath_core-0.2.2/src/uipath/core/serialization/json.py +150 -0
  5. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/tracing/_utils.py +5 -53
  6. uipath_core-0.2.2/tests/serialization/test_json.py +614 -0
  7. {uipath_core-0.2.1 → uipath_core-0.2.2}/uv.lock +1 -1
  8. {uipath_core-0.2.1 → uipath_core-0.2.2}/.cursorrules +0 -0
  9. {uipath_core-0.2.1 → uipath_core-0.2.2}/.editorconfig +0 -0
  10. {uipath_core-0.2.1 → uipath_core-0.2.2}/.gitattributes +0 -0
  11. {uipath_core-0.2.1 → uipath_core-0.2.2}/.github/workflows/cd.yml +0 -0
  12. {uipath_core-0.2.1 → uipath_core-0.2.2}/.github/workflows/ci.yml +0 -0
  13. {uipath_core-0.2.1 → uipath_core-0.2.2}/.github/workflows/commitlint.yml +0 -0
  14. {uipath_core-0.2.1 → uipath_core-0.2.2}/.github/workflows/lint.yml +0 -0
  15. {uipath_core-0.2.1 → uipath_core-0.2.2}/.github/workflows/publish-dev.yml +0 -0
  16. {uipath_core-0.2.1 → uipath_core-0.2.2}/.github/workflows/test.yml +0 -0
  17. {uipath_core-0.2.1 → uipath_core-0.2.2}/.gitignore +0 -0
  18. {uipath_core-0.2.1 → uipath_core-0.2.2}/.pre-commit-config.yaml +0 -0
  19. {uipath_core-0.2.1 → uipath_core-0.2.2}/.python-version +0 -0
  20. {uipath_core-0.2.1 → uipath_core-0.2.2}/.vscode/extensions.json +0 -0
  21. {uipath_core-0.2.1 → uipath_core-0.2.2}/.vscode/launch.json +0 -0
  22. {uipath_core-0.2.1 → uipath_core-0.2.2}/.vscode/settings.json +0 -0
  23. {uipath_core-0.2.1 → uipath_core-0.2.2}/CONTRIBUTING.md +0 -0
  24. {uipath_core-0.2.1 → uipath_core-0.2.2}/LICENSE +0 -0
  25. {uipath_core-0.2.1 → uipath_core-0.2.2}/README.md +0 -0
  26. {uipath_core-0.2.1 → uipath_core-0.2.2}/justfile +0 -0
  27. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/__init__.py +0 -0
  28. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/chat/__init__.py +0 -0
  29. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/chat/async_stream.py +0 -0
  30. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/chat/citation.py +0 -0
  31. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/chat/content.py +0 -0
  32. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/chat/conversation.py +0 -0
  33. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/chat/error.py +0 -0
  34. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/chat/event.py +0 -0
  35. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/chat/exchange.py +0 -0
  36. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/chat/interrupt.py +0 -0
  37. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/chat/message.py +0 -0
  38. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/chat/meta.py +0 -0
  39. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/chat/tool.py +0 -0
  40. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/errors/__init__.py +0 -0
  41. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/errors/errors.py +0 -0
  42. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/guardrails/__init__.py +0 -0
  43. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/guardrails/_deterministic_guardrails_service.py +0 -0
  44. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/guardrails/_evaluators.py +0 -0
  45. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/guardrails/guardrails.py +0 -0
  46. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/py.typed +0 -0
  47. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/tracing/__init__.py +0 -0
  48. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/tracing/decorators.py +0 -0
  49. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/tracing/exporters.py +0 -0
  50. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/tracing/processors.py +0 -0
  51. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/tracing/span_utils.py +0 -0
  52. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/tracing/trace_manager.py +0 -0
  53. {uipath_core-0.2.1 → uipath_core-0.2.2}/src/uipath/core/tracing/types.py +0 -0
  54. {uipath_core-0.2.1 → uipath_core-0.2.2}/tests/__init__.py +0 -0
  55. {uipath_core-0.2.1 → uipath_core-0.2.2}/tests/conftest.py +0 -0
  56. {uipath_core-0.2.1 → uipath_core-0.2.2}/tests/guardrails/test_deterministic_guardrails_service.py +0 -0
  57. {uipath_core-0.2.1 → uipath_core-0.2.2}/tests/tracing/test_external_integration.py +0 -0
  58. {uipath_core-0.2.1 → uipath_core-0.2.2}/tests/tracing/test_serialization.py +0 -0
  59. {uipath_core-0.2.1 → uipath_core-0.2.2}/tests/tracing/test_span_filtering.py +0 -0
  60. {uipath_core-0.2.1 → uipath_core-0.2.2}/tests/tracing/test_span_nesting.py +0 -0
  61. {uipath_core-0.2.1 → uipath_core-0.2.2}/tests/tracing/test_span_registry.py +0 -0
  62. {uipath_core-0.2.1 → uipath_core-0.2.2}/tests/tracing/test_trace_manager.py +0 -0
  63. {uipath_core-0.2.1 → uipath_core-0.2.2}/tests/tracing/test_traced.py +0 -0
  64. {uipath_core-0.2.1 → uipath_core-0.2.2}/tests/tracing/test_tracing_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: uipath-core
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: UiPath Core abstractions
5
5
  Project-URL: Homepage, https://uipath.com
6
6
  Project-URL: Repository, https://github.com/UiPath/uipath-core-python
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "uipath-core"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "UiPath Core abstractions"
5
5
  readme = { file = "README.md", content-type = "text/markdown" }
6
6
  requires-python = ">=3.11"
@@ -0,0 +1,5 @@
1
+ """Serialization utilities for converting Python objects to various formats."""
2
+
3
+ from .json import serialize_defaults, serialize_json
4
+
5
+ __all__ = ["serialize_defaults", "serialize_json"]
@@ -0,0 +1,150 @@
1
+ """JSON serialization utilities for converting Python objects to JSON formats."""
2
+
3
+ import json
4
+ from dataclasses import asdict, is_dataclass
5
+ from datetime import datetime, timezone
6
+ from enum import Enum
7
+ from typing import Any, cast
8
+ from zoneinfo import ZoneInfo
9
+
10
+ from pydantic import BaseModel
11
+
12
+
13
+ def serialize_defaults(
14
+ obj: Any,
15
+ ) -> dict[str, Any] | list[Any] | str | int | float | bool | None:
16
+ """Convert Python objects to JSON-serializable formats.
17
+
18
+ Handles common Python types that are not natively JSON-serializable:
19
+ - Pydantic models (v1 and v2)
20
+ - Dataclasses
21
+ - Enums
22
+ - Datetime objects
23
+ - Timezone objects
24
+ - Named tuples
25
+ - Sets and tuples
26
+
27
+ This function is designed to be used as the `default` parameter in json.dumps():
28
+ ```python
29
+ import json
30
+ result = json.dumps(obj, default=serialize_defaults)
31
+ ```
32
+
33
+ Or use the convenience function `serialize_json()` which wraps this:
34
+ ```python
35
+ result = serialize_json(obj)
36
+ ```
37
+
38
+ Args:
39
+ obj: The object to serialize
40
+
41
+ Returns:
42
+ A JSON-serializable representation of the object:
43
+ - Pydantic models: dict from model_dump()
44
+ - Dataclasses: dict from asdict()
45
+ - Enums: the enum value (recursively serialized)
46
+ - datetime: ISO format string
47
+ - timezone/ZoneInfo: timezone name
48
+ - sets/tuples: converted to lists
49
+ - named tuples: converted to dict
50
+ - Primitives (None, bool, int, float, str, list, dict): returned unchanged
51
+ - Other types: converted to string with str()
52
+
53
+ Examples:
54
+ >>> from datetime import datetime
55
+ >>> from pydantic import BaseModel
56
+ >>>
57
+ >>> class User(BaseModel):
58
+ ... name: str
59
+ ... created_at: datetime
60
+ >>>
61
+ >>> user = User(name="Alice", created_at=datetime.now())
62
+ >>> import json
63
+ >>> json.dumps(user, default=serialize_defaults)
64
+ '{"name": "Alice", "created_at": "2024-01-01T12:00:00"}'
65
+ >>> # Or use the convenience function
66
+ >>> serialize_json(user)
67
+ '{"name": "Alice", "created_at": "2024-01-01T12:00:00"}'
68
+ """
69
+ # Handle Pydantic BaseModel instances
70
+ if hasattr(obj, "model_dump") and not isinstance(obj, type):
71
+ return obj.model_dump(exclude_none=True, mode="json")
72
+
73
+ # Handle Pydantic model classes - convert to schema representation
74
+ if isinstance(obj, type) and issubclass(obj, BaseModel):
75
+ return {
76
+ "__class__": obj.__name__,
77
+ "__module__": obj.__module__,
78
+ "schema": obj.model_json_schema(),
79
+ }
80
+
81
+ # Handle Pydantic v1 models
82
+ if hasattr(obj, "dict") and not isinstance(obj, type):
83
+ return obj.dict()
84
+
85
+ # Handle objects with to_dict method
86
+ if hasattr(obj, "to_dict") and not isinstance(obj, type):
87
+ return obj.to_dict()
88
+
89
+ # Handle dataclasses
90
+ if is_dataclass(obj) and not isinstance(obj, type):
91
+ return asdict(obj)
92
+
93
+ # Handle enums - recursively serialize the value
94
+ if isinstance(obj, Enum):
95
+ return serialize_defaults(obj.value)
96
+
97
+ # Handle sets and tuples
98
+ if isinstance(obj, (set, tuple)):
99
+ # Check if it's a named tuple (has _asdict method)
100
+ if hasattr(obj, "_asdict") and callable(
101
+ obj._asdict # pyright: ignore[reportAttributeAccessIssue]
102
+ ):
103
+ return cast(
104
+ dict[str, Any],
105
+ obj._asdict(), # pyright: ignore[reportAttributeAccessIssue]
106
+ )
107
+ # Convert to list
108
+ return list(obj)
109
+
110
+ # Handle datetime objects
111
+ if isinstance(obj, datetime):
112
+ return obj.isoformat()
113
+
114
+ # Handle timezone objects
115
+ if isinstance(obj, (timezone, ZoneInfo)):
116
+ return obj.tzname(None)
117
+
118
+ # Allow JSON-serializable primitives to pass through unchanged
119
+ if obj is None or isinstance(obj, (bool, int, float, str, list, dict)):
120
+ return obj
121
+
122
+ # Fallback: convert to string
123
+ return str(obj)
124
+
125
+
126
+ def serialize_json(obj: Any) -> str:
127
+ """Serialize Python object to JSON string.
128
+
129
+ This is a convenience function that wraps json.dumps() with serialize_defaults()
130
+ as the default handler for non-JSON-serializable types.
131
+
132
+ Args:
133
+ obj: The object to serialize to JSON
134
+
135
+ Returns:
136
+ JSON string representation of the object
137
+
138
+ Examples:
139
+ >>> from datetime import datetime
140
+ >>> from pydantic import BaseModel
141
+ >>>
142
+ >>> class Task(BaseModel):
143
+ ... name: str
144
+ ... created: datetime
145
+ >>>
146
+ >>> task = Task(name="Review PR", created=datetime(2024, 1, 15, 10, 30))
147
+ >>> serialize_json(task)
148
+ '{"name": "Review PR", "created": "2024-01-15T10:30:00"}'
149
+ """
150
+ return json.dumps(obj, default=serialize_defaults)
@@ -3,14 +3,11 @@
3
3
  import inspect
4
4
  import json
5
5
  from collections.abc import Callable
6
- from dataclasses import asdict, is_dataclass
7
- from datetime import datetime, timezone
8
- from enum import Enum
9
- from typing import Any, Mapping, Optional, cast
10
- from zoneinfo import ZoneInfo
6
+ from typing import Any, Mapping, Optional
11
7
 
12
8
  from opentelemetry.trace import Span
13
- from pydantic import BaseModel
9
+
10
+ from uipath.core.serialization import serialize_json
14
11
 
15
12
 
16
13
  def get_supported_params(
@@ -31,64 +28,19 @@ def get_supported_params(
31
28
  return supported
32
29
 
33
30
 
34
- def _simple_serialize_defaults(
35
- obj: Any,
36
- ) -> dict[str, Any] | list[Any] | str | int | float | bool | None:
37
- # Handle Pydantic BaseModel instances
38
- if hasattr(obj, "model_dump") and not isinstance(obj, type):
39
- return obj.model_dump(exclude_none=True, mode="json")
40
-
41
- # Handle classes - convert to schema representation
42
- if isinstance(obj, type) and issubclass(obj, BaseModel):
43
- return {
44
- "__class__": obj.__name__,
45
- "__module__": obj.__module__,
46
- "schema": obj.model_json_schema(),
47
- }
48
- if hasattr(obj, "dict") and not isinstance(obj, type):
49
- return obj.dict()
50
- if hasattr(obj, "to_dict") and not isinstance(obj, type):
51
- return obj.to_dict()
52
-
53
- # Handle dataclasses
54
- if is_dataclass(obj) and not isinstance(obj, type):
55
- return asdict(obj)
56
-
57
- # Handle enums
58
- if isinstance(obj, Enum):
59
- return _simple_serialize_defaults(obj.value)
60
-
61
- if isinstance(obj, (set, tuple)):
62
- if hasattr(obj, "_asdict") and callable(obj._asdict): # pyright: ignore[reportAttributeAccessIssue]
63
- return cast(dict[str, Any], obj._asdict()) # pyright: ignore[reportAttributeAccessIssue]
64
- return list(obj)
65
-
66
- if isinstance(obj, datetime):
67
- return obj.isoformat()
68
-
69
- if isinstance(obj, (timezone, ZoneInfo)):
70
- return obj.tzname(None)
71
-
72
- # Allow JSON-serializable primitives to pass through unchanged
73
- if obj is None or isinstance(obj, (bool, int, float, str)):
74
- return obj
75
-
76
- return str(obj)
77
-
78
-
79
31
  def format_args_for_trace_json(
80
32
  signature: inspect.Signature, *args: Any, **kwargs: Any
81
33
  ) -> str:
82
34
  """Return a JSON string of inputs from the function signature."""
83
35
  result = format_args_for_trace(signature, *args, **kwargs)
84
- return json.dumps(result, default=_simple_serialize_defaults)
36
+ return serialize_json(result)
85
37
 
86
38
 
87
39
  def format_object_for_trace_json(
88
40
  input_object: Any,
89
41
  ) -> str:
90
42
  """Return a JSON string of inputs from the function signature."""
91
- return json.dumps(input_object, default=_simple_serialize_defaults)
43
+ return serialize_json(input_object)
92
44
 
93
45
 
94
46
  def format_args_for_trace(
@@ -0,0 +1,614 @@
1
+ """Tests for serialization utilities."""
2
+
3
+ import json
4
+ from collections import namedtuple
5
+ from dataclasses import dataclass
6
+ from datetime import datetime, timezone
7
+ from enum import Enum
8
+ from typing import Any
9
+ from zoneinfo import ZoneInfo
10
+
11
+ import pytest
12
+ from pydantic import BaseModel
13
+
14
+ from uipath.core.serialization import serialize_json
15
+
16
+
17
+ def _has_tzdata() -> bool:
18
+ """Check if timezone data is available."""
19
+ try:
20
+ ZoneInfo("America/New_York")
21
+ return True
22
+ except Exception:
23
+ return False
24
+
25
+
26
+ class Color(Enum):
27
+ """Test enum."""
28
+
29
+ RED = "red"
30
+ GREEN = "green"
31
+ BLUE = 3
32
+
33
+
34
+ class Priority(Enum):
35
+ """Test enum with int values."""
36
+
37
+ LOW = 1
38
+ MEDIUM = 2
39
+ HIGH = 3
40
+
41
+
42
+ class SimpleModel(BaseModel):
43
+ """Simple Pydantic v2 model."""
44
+
45
+ name: str
46
+ value: int
47
+
48
+
49
+ class NestedModel(BaseModel):
50
+ """Pydantic model with nested model."""
51
+
52
+ id: str
53
+ inner: SimpleModel
54
+ items: list[SimpleModel]
55
+
56
+
57
+ @dataclass
58
+ class SimpleDataclass:
59
+ """Simple dataclass for testing."""
60
+
61
+ name: str
62
+ count: int
63
+
64
+
65
+ @dataclass
66
+ class NestedDataclass:
67
+ """Dataclass with nested dataclass."""
68
+
69
+ id: str
70
+ inner: SimpleDataclass
71
+
72
+
73
+ Point = namedtuple("Point", ["x", "y"])
74
+
75
+
76
+ class TestSimpleSerializeDefaults:
77
+ """Tests for serialize_defaults and serialize_json functions."""
78
+
79
+ def test_serializes_none(self) -> None:
80
+ """Test None serialization via json.dumps."""
81
+ data = {"value": None}
82
+ result = serialize_json(data)
83
+ parsed = json.loads(result)
84
+ assert parsed["value"] is None
85
+
86
+ def test_serializes_primitives(self) -> None:
87
+ """Test primitive types pass through json.dumps unchanged."""
88
+ data = {
89
+ "bool_true": True,
90
+ "bool_false": False,
91
+ "integer": 42,
92
+ "float": 3.14,
93
+ "string": "hello",
94
+ }
95
+ result = serialize_json(data)
96
+ parsed = json.loads(result)
97
+ assert parsed["bool_true"] is True
98
+ assert parsed["bool_false"] is False
99
+ assert parsed["integer"] == 42
100
+ assert parsed["float"] == 3.14
101
+ assert parsed["string"] == "hello"
102
+
103
+ def test_serializes_pydantic_model(self) -> None:
104
+ """Test Pydantic BaseModel serialization via json.dumps."""
105
+ model = SimpleModel(name="test", value=42)
106
+ result = serialize_json(model)
107
+ parsed = json.loads(result)
108
+ assert isinstance(parsed, dict)
109
+ assert parsed["name"] == "test"
110
+ assert parsed["value"] == 42
111
+
112
+ def test_serializes_nested_pydantic_model(self) -> None:
113
+ """Test nested Pydantic models via json.dumps."""
114
+ inner = SimpleModel(name="inner", value=10)
115
+ model = NestedModel(
116
+ id="123",
117
+ inner=inner,
118
+ items=[
119
+ SimpleModel(name="item1", value=1),
120
+ SimpleModel(name="item2", value=2),
121
+ ],
122
+ )
123
+ result = serialize_json(model)
124
+ parsed = json.loads(result)
125
+ assert isinstance(parsed, dict)
126
+ assert parsed["id"] == "123"
127
+ assert parsed["inner"]["name"] == "inner"
128
+ assert len(parsed["items"]) == 2
129
+ assert parsed["items"][0]["name"] == "item1"
130
+ assert parsed["items"][1]["value"] == 2
131
+
132
+ def test_serializes_pydantic_model_excludes_none(self) -> None:
133
+ """Test Pydantic model with None values excluded via json.dumps."""
134
+
135
+ class OptionalModel(BaseModel):
136
+ required: str
137
+ optional: str | None = None
138
+
139
+ model = OptionalModel(required="value")
140
+ result = serialize_json(model)
141
+ parsed = json.loads(result)
142
+ assert isinstance(parsed, dict)
143
+ assert parsed["required"] == "value"
144
+ # exclude_none=True should exclude the None field
145
+ assert "optional" not in parsed
146
+
147
+ def test_serializes_pydantic_model_class(self) -> None:
148
+ """Test Pydantic model class (not instance) serialization via json.dumps."""
149
+ data = {"model_class": SimpleModel}
150
+ result = serialize_json(data)
151
+ parsed = json.loads(result)
152
+ assert isinstance(parsed["model_class"], dict)
153
+ assert parsed["model_class"]["__class__"] == "SimpleModel"
154
+ assert parsed["model_class"]["__module__"] == "test_json"
155
+ assert "schema" in parsed["model_class"]
156
+ assert isinstance(parsed["model_class"]["schema"], dict)
157
+
158
+ def test_serializes_dataclass(self) -> None:
159
+ """Test dataclass serialization via json.dumps."""
160
+ obj = SimpleDataclass(name="test", count=5)
161
+ result = serialize_json(obj)
162
+ parsed = json.loads(result)
163
+ assert isinstance(parsed, dict)
164
+ assert parsed["name"] == "test"
165
+ assert parsed["count"] == 5
166
+
167
+ def test_serializes_nested_dataclass(self) -> None:
168
+ """Test nested dataclass serialization via json.dumps."""
169
+ inner = SimpleDataclass(name="inner", count=10)
170
+ obj = NestedDataclass(id="123", inner=inner)
171
+ result = serialize_json(obj)
172
+ parsed = json.loads(result)
173
+ assert isinstance(parsed, dict)
174
+ assert parsed["id"] == "123"
175
+ assert parsed["inner"]["name"] == "inner"
176
+ assert parsed["inner"]["count"] == 10
177
+
178
+ def test_serializes_enum_string_value(self) -> None:
179
+ """Test enum with string value via json.dumps."""
180
+ data = {"color": Color.RED}
181
+ result = serialize_json(data)
182
+ parsed = json.loads(result)
183
+ assert parsed["color"] == "red"
184
+
185
+ def test_serializes_enum_int_value(self) -> None:
186
+ """Test enum with int value via json.dumps."""
187
+ data = {"priority": Priority.HIGH}
188
+ result = serialize_json(data)
189
+ parsed = json.loads(result)
190
+ assert parsed["priority"] == 3
191
+
192
+ def test_serializes_enum_mixed_value(self) -> None:
193
+ """Test enum with mixed types via json.dumps."""
194
+ data = {"color1": Color.GREEN, "color2": Color.BLUE}
195
+ result = serialize_json(data)
196
+ parsed = json.loads(result)
197
+ assert parsed["color1"] == "green"
198
+ assert parsed["color2"] == 3
199
+
200
+ def test_serializes_datetime(self) -> None:
201
+ """Test datetime serialization via json.dumps."""
202
+ dt = datetime(2024, 1, 15, 10, 30, 45)
203
+ data = {"timestamp": dt}
204
+ result = serialize_json(data)
205
+ parsed = json.loads(result)
206
+ assert isinstance(parsed["timestamp"], str)
207
+ assert "2024-01-15" in parsed["timestamp"]
208
+ assert "10:30:45" in parsed["timestamp"]
209
+
210
+ def test_serializes_datetime_with_timezone(self) -> None:
211
+ """Test datetime with timezone via json.dumps."""
212
+ dt = datetime(2024, 1, 15, 10, 30, 45, tzinfo=timezone.utc)
213
+ data = {"timestamp": dt}
214
+ result = serialize_json(data)
215
+ parsed = json.loads(result)
216
+ assert isinstance(parsed["timestamp"], str)
217
+ assert parsed["timestamp"] == "2024-01-15T10:30:45+00:00"
218
+
219
+ def test_serializes_timezone(self) -> None:
220
+ """Test timezone object serialization via json.dumps."""
221
+ tz = timezone.utc
222
+ data = {"timezone": tz}
223
+ result = serialize_json(data)
224
+ parsed = json.loads(result)
225
+ assert parsed["timezone"] == "UTC"
226
+
227
+ @pytest.mark.skipif(not _has_tzdata(), reason="Timezone data not available")
228
+ def test_serializes_zoneinfo(self) -> None:
229
+ """Test ZoneInfo serialization via json.dumps."""
230
+ tz = ZoneInfo("America/New_York")
231
+ data = {"timezone": tz}
232
+ result = serialize_json(data)
233
+ parsed = json.loads(result)
234
+ # ZoneInfo returns timezone name or None
235
+ assert isinstance(parsed["timezone"], (str, type(None)))
236
+
237
+ def test_serializes_set(self) -> None:
238
+ """Test set serialization via json.dumps."""
239
+ obj = {1, 2, 3}
240
+ data = {"numbers": obj}
241
+ result = serialize_json(data)
242
+ parsed = json.loads(result)
243
+ assert isinstance(parsed["numbers"], list)
244
+ assert set(parsed["numbers"]) == {1, 2, 3}
245
+
246
+ def test_serializes_tuple(self) -> None:
247
+ """Test tuple serialization via json.dumps."""
248
+ obj = (1, 2, 3)
249
+ data = {"numbers": obj}
250
+ result = serialize_json(data)
251
+ parsed = json.loads(result)
252
+ assert isinstance(parsed["numbers"], list)
253
+ assert parsed["numbers"] == [1, 2, 3]
254
+
255
+ def test_serializes_named_tuple(self) -> None:
256
+ """Test named tuple serialization via json.dumps.
257
+
258
+ Note: Python's json encoder treats namedtuples as regular tuples,
259
+ so they serialize as lists [x, y] rather than dicts {"x": x, "y": y}.
260
+ The serialize_defaults function is not called for namedtuples
261
+ because they're natively JSON-serializable as tuples.
262
+ """
263
+ point = Point(x=10, y=20)
264
+ data = {"point": point}
265
+ result = serialize_json(data)
266
+ parsed = json.loads(result)
267
+ # Namedtuples serialize as lists through json.dumps
268
+ assert isinstance(parsed["point"], list)
269
+ assert parsed["point"] == [10, 20]
270
+
271
+ def test_serializes_object_with_to_dict(self) -> None:
272
+ """Test object with to_dict method via json.dumps."""
273
+
274
+ class CustomObject:
275
+ def to_dict(self) -> dict[str, Any]:
276
+ return {"custom": "value"}
277
+
278
+ obj = CustomObject()
279
+ data = {"obj": obj}
280
+ result = serialize_json(data)
281
+ parsed = json.loads(result)
282
+ assert parsed["obj"] == {"custom": "value"}
283
+
284
+ def test_serializes_unknown_object_to_str(self) -> None:
285
+ """Test unknown object falls back to str() via json.dumps."""
286
+
287
+ class CustomClass:
288
+ def __str__(self) -> str:
289
+ return "custom_string"
290
+
291
+ obj = CustomClass()
292
+ data = {"obj": obj}
293
+ result = serialize_json(data)
294
+ parsed = json.loads(result)
295
+ assert parsed["obj"] == "custom_string"
296
+
297
+ def test_with_json_dumps(self) -> None:
298
+ """Test integration with json.dumps()."""
299
+
300
+ class ComplexObject(BaseModel):
301
+ name: str
302
+ created_at: datetime
303
+ priority: Priority
304
+
305
+ obj = ComplexObject(
306
+ name="task",
307
+ created_at=datetime(2024, 1, 15, 10, 30),
308
+ priority=Priority.HIGH,
309
+ )
310
+
311
+ # Should not raise TypeError
312
+ result = serialize_json(obj)
313
+ parsed = json.loads(result)
314
+
315
+ assert parsed["name"] == "task"
316
+ assert "2024-01-15" in parsed["created_at"]
317
+ assert parsed["priority"] == 3
318
+
319
+ def test_with_json_dumps_complex_nested(self) -> None:
320
+ """Test with complex nested structure."""
321
+ data = {
322
+ "model": SimpleModel(name="test", value=42),
323
+ "dataclass": SimpleDataclass(name="dc", count=5),
324
+ "enum": Color.RED,
325
+ "datetime": datetime(2024, 1, 1),
326
+ "set": {1, 2, 3},
327
+ "tuple": (4, 5, 6),
328
+ }
329
+
330
+ result = serialize_json(data)
331
+ parsed = json.loads(result)
332
+
333
+ assert parsed["model"]["name"] == "test"
334
+ assert parsed["dataclass"]["name"] == "dc"
335
+ assert parsed["enum"] == "red"
336
+ assert "2024-01-01" in parsed["datetime"]
337
+ assert set(parsed["set"]) == {1, 2, 3}
338
+ assert parsed["tuple"] == [4, 5, 6]
339
+
340
+ def test_with_list_of_pydantic_models(self) -> None:
341
+ """Test with list of Pydantic models (common MCP scenario)."""
342
+ models = [
343
+ SimpleModel(name="first", value=1),
344
+ SimpleModel(name="second", value=2),
345
+ SimpleModel(name="third", value=3),
346
+ ]
347
+
348
+ # This should not raise TypeError
349
+ result = serialize_json(models)
350
+ parsed = json.loads(result)
351
+
352
+ assert len(parsed) == 3
353
+ assert parsed[0]["name"] == "first"
354
+ assert parsed[1]["value"] == 2
355
+
356
+ def test_recursive_enum_serialization(self) -> None:
357
+ """Test that enum values are recursively serialized via json.dumps."""
358
+
359
+ class NestedEnum(Enum):
360
+ """Enum with complex value."""
361
+
362
+ COMPLEX = {"key": "value"}
363
+
364
+ data = {"enum": NestedEnum.COMPLEX}
365
+ result = serialize_json(data)
366
+ parsed = json.loads(result)
367
+ # The enum value itself (a dict) should be returned as-is
368
+ assert parsed["enum"] == {"key": "value"}
369
+ assert isinstance(parsed["enum"], dict)
370
+
371
+ def test_dataclass_class_returns_string(self) -> None:
372
+ """Test that dataclass class (not instance) falls back to str via json.dumps."""
373
+ data = {"dataclass_class": SimpleDataclass}
374
+ result = serialize_json(data)
375
+ parsed = json.loads(result)
376
+ assert isinstance(parsed["dataclass_class"], str)
377
+ assert "SimpleDataclass" in parsed["dataclass_class"]
378
+
379
+ def test_empty_collections(self) -> None:
380
+ """Test empty collections via json.dumps."""
381
+ data: dict[str, Any] = {"empty_set": set(), "empty_tuple": (), "empty_list": []}
382
+ result = serialize_json(data)
383
+ parsed = json.loads(result)
384
+ assert parsed["empty_set"] == []
385
+ assert parsed["empty_tuple"] == []
386
+ assert parsed["empty_list"] == []
387
+
388
+ def test_with_dict_method(self) -> None:
389
+ """Test object with dict() method (Pydantic v1 compatibility) via json.dumps."""
390
+
391
+ class OldStyleModel:
392
+ def dict(self) -> dict[str, Any]:
393
+ return {"old": "style"}
394
+
395
+ obj = OldStyleModel()
396
+ data = {"obj": obj}
397
+ result = serialize_json(data)
398
+ parsed = json.loads(result)
399
+ assert parsed["obj"] == {"old": "style"}
400
+
401
+ def test_dict_of_pydantic_models(self) -> None:
402
+ """Test dictionary containing Pydantic models as values."""
403
+ data = {
404
+ "user1": SimpleModel(name="Alice", value=100),
405
+ "user2": SimpleModel(name="Bob", value=200),
406
+ "user3": SimpleModel(name="Charlie", value=300),
407
+ }
408
+
409
+ result = serialize_json(data)
410
+ parsed = json.loads(result)
411
+
412
+ assert isinstance(parsed, dict)
413
+ assert parsed["user1"]["name"] == "Alice"
414
+ assert parsed["user1"]["value"] == 100
415
+ assert parsed["user2"]["name"] == "Bob"
416
+ assert parsed["user2"]["value"] == 200
417
+ assert parsed["user3"]["name"] == "Charlie"
418
+ assert parsed["user3"]["value"] == 300
419
+
420
+ def test_dict_of_dataclass_models(self) -> None:
421
+ """Test dictionary containing dataclass instances as values."""
422
+ data = {
423
+ "item1": SimpleDataclass(name="First", count=1),
424
+ "item2": SimpleDataclass(name="Second", count=2),
425
+ "item3": SimpleDataclass(name="Third", count=3),
426
+ }
427
+
428
+ result = serialize_json(data)
429
+ parsed = json.loads(result)
430
+
431
+ assert isinstance(parsed, dict)
432
+ assert parsed["item1"]["name"] == "First"
433
+ assert parsed["item1"]["count"] == 1
434
+ assert parsed["item2"]["name"] == "Second"
435
+ assert parsed["item2"]["count"] == 2
436
+ assert parsed["item3"]["name"] == "Third"
437
+ assert parsed["item3"]["count"] == 3
438
+
439
+ def test_normal_class_fallback_to_str(self) -> None:
440
+ """Test normal class (not Pydantic, dataclass, or enum) falls back to str()."""
441
+
442
+ class RegularClass:
443
+ def __init__(self, value: str) -> None:
444
+ self.value = value
445
+
446
+ def __str__(self) -> str:
447
+ return f"RegularClass({self.value})"
448
+
449
+ obj = RegularClass("test_value")
450
+ data = {"object": obj, "name": "test"}
451
+ json_result = serialize_json(data)
452
+ parsed = json.loads(json_result)
453
+ assert parsed["object"] == "RegularClass(test_value)"
454
+ assert parsed["name"] == "test"
455
+
456
+ def test_list_of_dataclass(self) -> None:
457
+ """Test list containing dataclass instances."""
458
+ data = [
459
+ SimpleDataclass(name="First", count=1),
460
+ SimpleDataclass(name="Second", count=2),
461
+ SimpleDataclass(name="Third", count=3),
462
+ ]
463
+
464
+ result = serialize_json(data)
465
+ parsed = json.loads(result)
466
+
467
+ assert isinstance(parsed, list)
468
+ assert len(parsed) == 3
469
+ assert parsed[0]["name"] == "First"
470
+ assert parsed[0]["count"] == 1
471
+ assert parsed[1]["name"] == "Second"
472
+ assert parsed[2]["count"] == 3
473
+
474
+ def test_list_of_pydantic_models(self) -> None:
475
+ """Test list containing Pydantic model instances."""
476
+ data = [
477
+ SimpleModel(name="Alice", value=100),
478
+ SimpleModel(name="Bob", value=200),
479
+ SimpleModel(name="Charlie", value=300),
480
+ ]
481
+
482
+ result = serialize_json(data)
483
+ parsed = json.loads(result)
484
+
485
+ assert isinstance(parsed, list)
486
+ assert len(parsed) == 3
487
+ assert parsed[0]["name"] == "Alice"
488
+ assert parsed[0]["value"] == 100
489
+ assert parsed[1]["name"] == "Bob"
490
+ assert parsed[2]["value"] == 300
491
+
492
+ def test_list_of_normal_classes(self) -> None:
493
+ """Test list containing normal class instances (fallback to str)."""
494
+
495
+ class Item:
496
+ def __init__(self, id: int, label: str) -> None:
497
+ self.id = id
498
+ self.label = label
499
+
500
+ def __str__(self) -> str:
501
+ return f"Item(id={self.id}, label={self.label})"
502
+
503
+ data = [
504
+ Item(1, "First"),
505
+ Item(2, "Second"),
506
+ Item(3, "Third"),
507
+ ]
508
+
509
+ result = serialize_json(data)
510
+ parsed = json.loads(result)
511
+
512
+ assert isinstance(parsed, list)
513
+ assert len(parsed) == 3
514
+ assert parsed[0] == "Item(id=1, label=First)"
515
+ assert parsed[1] == "Item(id=2, label=Second)"
516
+ assert parsed[2] == "Item(id=3, label=Third)"
517
+
518
+ def test_list_of_mixed_types(self) -> None:
519
+ """Test list containing mixed types: Pydantic, dataclass, normal class, primitives."""
520
+
521
+ class CustomItem:
522
+ def __init__(self, name: str) -> None:
523
+ self.name = name
524
+
525
+ def __str__(self) -> str:
526
+ return f"CustomItem({self.name})"
527
+
528
+ data = [
529
+ SimpleModel(name="pydantic", value=1),
530
+ SimpleDataclass(name="dataclass", count=2),
531
+ CustomItem("custom"),
532
+ "plain_string",
533
+ 42,
534
+ True,
535
+ None,
536
+ Color.RED,
537
+ ]
538
+
539
+ result = serialize_json(data)
540
+ parsed = json.loads(result)
541
+
542
+ assert isinstance(parsed, list)
543
+ assert len(parsed) == 8
544
+ # Pydantic model
545
+ assert parsed[0]["name"] == "pydantic"
546
+ assert parsed[0]["value"] == 1
547
+ # Dataclass
548
+ assert parsed[1]["name"] == "dataclass"
549
+ assert parsed[1]["count"] == 2
550
+ # Normal class (str fallback)
551
+ assert parsed[2] == "CustomItem(custom)"
552
+ # Primitives
553
+ assert parsed[3] == "plain_string"
554
+ assert parsed[4] == 42
555
+ assert parsed[5] is True
556
+ assert parsed[6] is None
557
+ # Enum
558
+ assert parsed[7] == "red"
559
+
560
+ def test_list_of_lists_mixed(self) -> None:
561
+ """Test nested lists containing mixed types."""
562
+
563
+ class Widget:
564
+ def __init__(self, id: int) -> None:
565
+ self.id = id
566
+
567
+ def __str__(self) -> str:
568
+ return f"Widget({self.id})"
569
+
570
+ data = [
571
+ [SimpleModel(name="model1", value=1), SimpleModel(name="model2", value=2)],
572
+ [
573
+ SimpleDataclass(name="dc1", count=10),
574
+ SimpleDataclass(name="dc2", count=20),
575
+ ],
576
+ [Widget(1), Widget(2), Widget(3)],
577
+ ["string1", "string2"],
578
+ [1, 2, 3, 4],
579
+ [Color.RED, Color.GREEN, Priority.HIGH],
580
+ [True, False, None],
581
+ ]
582
+
583
+ result = serialize_json(data)
584
+ parsed = json.loads(result)
585
+
586
+ assert isinstance(parsed, list)
587
+ assert len(parsed) == 7
588
+
589
+ # First sublist: Pydantic models
590
+ assert len(parsed[0]) == 2
591
+ assert parsed[0][0]["name"] == "model1"
592
+ assert parsed[0][1]["value"] == 2
593
+
594
+ # Second sublist: Dataclasses
595
+ assert len(parsed[1]) == 2
596
+ assert parsed[1][0]["name"] == "dc1"
597
+ assert parsed[1][1]["count"] == 20
598
+
599
+ # Third sublist: Normal classes
600
+ assert len(parsed[2]) == 3
601
+ assert parsed[2][0] == "Widget(1)"
602
+ assert parsed[2][2] == "Widget(3)"
603
+
604
+ # Fourth sublist: Strings
605
+ assert parsed[3] == ["string1", "string2"]
606
+
607
+ # Fifth sublist: Integers
608
+ assert parsed[4] == [1, 2, 3, 4]
609
+
610
+ # Sixth sublist: Enums
611
+ assert parsed[5] == ["red", "green", 3]
612
+
613
+ # Seventh sublist: Booleans and None
614
+ assert parsed[6] == [True, False, None]
@@ -991,7 +991,7 @@ wheels = [
991
991
 
992
992
  [[package]]
993
993
  name = "uipath-core"
994
- version = "0.2.1"
994
+ version = "0.2.2"
995
995
  source = { editable = "." }
996
996
  dependencies = [
997
997
  { name = "opentelemetry-instrumentation" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes