planar 0.5.0__py3-none-any.whl

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 (289) hide show
  1. planar/.__init__.py.un~ +0 -0
  2. planar/._version.py.un~ +0 -0
  3. planar/.app.py.un~ +0 -0
  4. planar/.cli.py.un~ +0 -0
  5. planar/.config.py.un~ +0 -0
  6. planar/.context.py.un~ +0 -0
  7. planar/.db.py.un~ +0 -0
  8. planar/.di.py.un~ +0 -0
  9. planar/.engine.py.un~ +0 -0
  10. planar/.files.py.un~ +0 -0
  11. planar/.log_context.py.un~ +0 -0
  12. planar/.log_metadata.py.un~ +0 -0
  13. planar/.logging.py.un~ +0 -0
  14. planar/.object_registry.py.un~ +0 -0
  15. planar/.otel.py.un~ +0 -0
  16. planar/.server.py.un~ +0 -0
  17. planar/.session.py.un~ +0 -0
  18. planar/.sqlalchemy.py.un~ +0 -0
  19. planar/.task_local.py.un~ +0 -0
  20. planar/.test_app.py.un~ +0 -0
  21. planar/.test_config.py.un~ +0 -0
  22. planar/.test_object_config.py.un~ +0 -0
  23. planar/.test_sqlalchemy.py.un~ +0 -0
  24. planar/.test_utils.py.un~ +0 -0
  25. planar/.util.py.un~ +0 -0
  26. planar/.utils.py.un~ +0 -0
  27. planar/__init__.py +26 -0
  28. planar/_version.py +1 -0
  29. planar/ai/.__init__.py.un~ +0 -0
  30. planar/ai/._models.py.un~ +0 -0
  31. planar/ai/.agent.py.un~ +0 -0
  32. planar/ai/.agent_utils.py.un~ +0 -0
  33. planar/ai/.events.py.un~ +0 -0
  34. planar/ai/.files.py.un~ +0 -0
  35. planar/ai/.models.py.un~ +0 -0
  36. planar/ai/.providers.py.un~ +0 -0
  37. planar/ai/.pydantic_ai.py.un~ +0 -0
  38. planar/ai/.pydantic_ai_agent.py.un~ +0 -0
  39. planar/ai/.pydantic_ai_provider.py.un~ +0 -0
  40. planar/ai/.step.py.un~ +0 -0
  41. planar/ai/.test_agent.py.un~ +0 -0
  42. planar/ai/.test_agent_serialization.py.un~ +0 -0
  43. planar/ai/.test_providers.py.un~ +0 -0
  44. planar/ai/.utils.py.un~ +0 -0
  45. planar/ai/__init__.py +15 -0
  46. planar/ai/agent.py +457 -0
  47. planar/ai/agent_utils.py +205 -0
  48. planar/ai/models.py +140 -0
  49. planar/ai/providers.py +1088 -0
  50. planar/ai/test_agent.py +1298 -0
  51. planar/ai/test_agent_serialization.py +229 -0
  52. planar/ai/test_providers.py +463 -0
  53. planar/ai/utils.py +102 -0
  54. planar/app.py +494 -0
  55. planar/cli.py +282 -0
  56. planar/config.py +544 -0
  57. planar/db/.db.py.un~ +0 -0
  58. planar/db/__init__.py +17 -0
  59. planar/db/alembic/env.py +136 -0
  60. planar/db/alembic/script.py.mako +28 -0
  61. planar/db/alembic/versions/3476068c153c_initial_system_tables_migration.py +339 -0
  62. planar/db/alembic.ini +128 -0
  63. planar/db/db.py +318 -0
  64. planar/files/.config.py.un~ +0 -0
  65. planar/files/.local.py.un~ +0 -0
  66. planar/files/.local_filesystem.py.un~ +0 -0
  67. planar/files/.model.py.un~ +0 -0
  68. planar/files/.models.py.un~ +0 -0
  69. planar/files/.s3.py.un~ +0 -0
  70. planar/files/.storage.py.un~ +0 -0
  71. planar/files/.test_files.py.un~ +0 -0
  72. planar/files/__init__.py +2 -0
  73. planar/files/models.py +162 -0
  74. planar/files/storage/.__init__.py.un~ +0 -0
  75. planar/files/storage/.base.py.un~ +0 -0
  76. planar/files/storage/.config.py.un~ +0 -0
  77. planar/files/storage/.context.py.un~ +0 -0
  78. planar/files/storage/.local_directory.py.un~ +0 -0
  79. planar/files/storage/.test_local_directory.py.un~ +0 -0
  80. planar/files/storage/.test_s3.py.un~ +0 -0
  81. planar/files/storage/base.py +61 -0
  82. planar/files/storage/config.py +44 -0
  83. planar/files/storage/context.py +15 -0
  84. planar/files/storage/local_directory.py +188 -0
  85. planar/files/storage/s3.py +220 -0
  86. planar/files/storage/test_local_directory.py +162 -0
  87. planar/files/storage/test_s3.py +299 -0
  88. planar/files/test_files.py +283 -0
  89. planar/human/.human.py.un~ +0 -0
  90. planar/human/.test_human.py.un~ +0 -0
  91. planar/human/__init__.py +2 -0
  92. planar/human/human.py +458 -0
  93. planar/human/models.py +80 -0
  94. planar/human/test_human.py +385 -0
  95. planar/logging/.__init__.py.un~ +0 -0
  96. planar/logging/.attributes.py.un~ +0 -0
  97. planar/logging/.formatter.py.un~ +0 -0
  98. planar/logging/.logger.py.un~ +0 -0
  99. planar/logging/.otel.py.un~ +0 -0
  100. planar/logging/.tracer.py.un~ +0 -0
  101. planar/logging/__init__.py +10 -0
  102. planar/logging/attributes.py +54 -0
  103. planar/logging/context.py +14 -0
  104. planar/logging/formatter.py +113 -0
  105. planar/logging/logger.py +114 -0
  106. planar/logging/otel.py +51 -0
  107. planar/modeling/.mixin.py.un~ +0 -0
  108. planar/modeling/.storage.py.un~ +0 -0
  109. planar/modeling/__init__.py +0 -0
  110. planar/modeling/field_helpers.py +59 -0
  111. planar/modeling/json_schema_generator.py +94 -0
  112. planar/modeling/mixins/__init__.py +10 -0
  113. planar/modeling/mixins/auditable.py +52 -0
  114. planar/modeling/mixins/test_auditable.py +97 -0
  115. planar/modeling/mixins/test_timestamp.py +134 -0
  116. planar/modeling/mixins/test_uuid_primary_key.py +52 -0
  117. planar/modeling/mixins/timestamp.py +53 -0
  118. planar/modeling/mixins/uuid_primary_key.py +19 -0
  119. planar/modeling/orm/.planar_base_model.py.un~ +0 -0
  120. planar/modeling/orm/__init__.py +18 -0
  121. planar/modeling/orm/planar_base_entity.py +29 -0
  122. planar/modeling/orm/query_filter_builder.py +122 -0
  123. planar/modeling/orm/reexports.py +15 -0
  124. planar/object_config/.object_config.py.un~ +0 -0
  125. planar/object_config/__init__.py +11 -0
  126. planar/object_config/models.py +114 -0
  127. planar/object_config/object_config.py +378 -0
  128. planar/object_registry.py +100 -0
  129. planar/registry_items.py +65 -0
  130. planar/routers/.__init__.py.un~ +0 -0
  131. planar/routers/.agents_router.py.un~ +0 -0
  132. planar/routers/.crud.py.un~ +0 -0
  133. planar/routers/.decision.py.un~ +0 -0
  134. planar/routers/.event.py.un~ +0 -0
  135. planar/routers/.file_attachment.py.un~ +0 -0
  136. planar/routers/.files.py.un~ +0 -0
  137. planar/routers/.files_router.py.un~ +0 -0
  138. planar/routers/.human.py.un~ +0 -0
  139. planar/routers/.info.py.un~ +0 -0
  140. planar/routers/.models.py.un~ +0 -0
  141. planar/routers/.object_config_router.py.un~ +0 -0
  142. planar/routers/.rule.py.un~ +0 -0
  143. planar/routers/.test_object_config_router.py.un~ +0 -0
  144. planar/routers/.test_workflow_router.py.un~ +0 -0
  145. planar/routers/.workflow.py.un~ +0 -0
  146. planar/routers/__init__.py +13 -0
  147. planar/routers/agents_router.py +197 -0
  148. planar/routers/entity_router.py +143 -0
  149. planar/routers/event.py +91 -0
  150. planar/routers/files.py +142 -0
  151. planar/routers/human.py +151 -0
  152. planar/routers/info.py +131 -0
  153. planar/routers/models.py +170 -0
  154. planar/routers/object_config_router.py +133 -0
  155. planar/routers/rule.py +108 -0
  156. planar/routers/test_agents_router.py +174 -0
  157. planar/routers/test_object_config_router.py +367 -0
  158. planar/routers/test_routes_security.py +169 -0
  159. planar/routers/test_rule_router.py +470 -0
  160. planar/routers/test_workflow_router.py +274 -0
  161. planar/routers/workflow.py +468 -0
  162. planar/rules/.decorator.py.un~ +0 -0
  163. planar/rules/.runner.py.un~ +0 -0
  164. planar/rules/.test_rules.py.un~ +0 -0
  165. planar/rules/__init__.py +23 -0
  166. planar/rules/decorator.py +184 -0
  167. planar/rules/models.py +355 -0
  168. planar/rules/rule_configuration.py +191 -0
  169. planar/rules/runner.py +64 -0
  170. planar/rules/test_rules.py +750 -0
  171. planar/scaffold_templates/app/__init__.py.j2 +0 -0
  172. planar/scaffold_templates/app/db/entities.py.j2 +11 -0
  173. planar/scaffold_templates/app/flows/process_invoice.py.j2 +67 -0
  174. planar/scaffold_templates/main.py.j2 +13 -0
  175. planar/scaffold_templates/planar.dev.yaml.j2 +34 -0
  176. planar/scaffold_templates/planar.prod.yaml.j2 +28 -0
  177. planar/scaffold_templates/pyproject.toml.j2 +10 -0
  178. planar/security/.jwt_middleware.py.un~ +0 -0
  179. planar/security/auth_context.py +148 -0
  180. planar/security/authorization.py +388 -0
  181. planar/security/default_policies.cedar +77 -0
  182. planar/security/jwt_middleware.py +116 -0
  183. planar/security/security_context.py +18 -0
  184. planar/security/tests/test_authorization_context.py +78 -0
  185. planar/security/tests/test_cedar_basics.py +41 -0
  186. planar/security/tests/test_cedar_policies.py +158 -0
  187. planar/security/tests/test_jwt_principal_context.py +179 -0
  188. planar/session.py +40 -0
  189. planar/sse/.constants.py.un~ +0 -0
  190. planar/sse/.example.html.un~ +0 -0
  191. planar/sse/.hub.py.un~ +0 -0
  192. planar/sse/.model.py.un~ +0 -0
  193. planar/sse/.proxy.py.un~ +0 -0
  194. planar/sse/constants.py +1 -0
  195. planar/sse/example.html +126 -0
  196. planar/sse/hub.py +216 -0
  197. planar/sse/model.py +8 -0
  198. planar/sse/proxy.py +257 -0
  199. planar/task_local.py +37 -0
  200. planar/test_app.py +51 -0
  201. planar/test_cli.py +372 -0
  202. planar/test_config.py +512 -0
  203. planar/test_object_config.py +527 -0
  204. planar/test_object_registry.py +14 -0
  205. planar/test_sqlalchemy.py +158 -0
  206. planar/test_utils.py +105 -0
  207. planar/testing/.client.py.un~ +0 -0
  208. planar/testing/.memory_storage.py.un~ +0 -0
  209. planar/testing/.planar_test_client.py.un~ +0 -0
  210. planar/testing/.predictable_tracer.py.un~ +0 -0
  211. planar/testing/.synchronizable_tracer.py.un~ +0 -0
  212. planar/testing/.test_memory_storage.py.un~ +0 -0
  213. planar/testing/.workflow_observer.py.un~ +0 -0
  214. planar/testing/__init__.py +0 -0
  215. planar/testing/memory_storage.py +78 -0
  216. planar/testing/planar_test_client.py +54 -0
  217. planar/testing/synchronizable_tracer.py +153 -0
  218. planar/testing/test_memory_storage.py +143 -0
  219. planar/testing/workflow_observer.py +73 -0
  220. planar/utils.py +70 -0
  221. planar/workflows/.__init__.py.un~ +0 -0
  222. planar/workflows/.builtin_steps.py.un~ +0 -0
  223. planar/workflows/.concurrency_tracing.py.un~ +0 -0
  224. planar/workflows/.context.py.un~ +0 -0
  225. planar/workflows/.contrib.py.un~ +0 -0
  226. planar/workflows/.decorators.py.un~ +0 -0
  227. planar/workflows/.durable_test.py.un~ +0 -0
  228. planar/workflows/.errors.py.un~ +0 -0
  229. planar/workflows/.events.py.un~ +0 -0
  230. planar/workflows/.exceptions.py.un~ +0 -0
  231. planar/workflows/.execution.py.un~ +0 -0
  232. planar/workflows/.human.py.un~ +0 -0
  233. planar/workflows/.lock.py.un~ +0 -0
  234. planar/workflows/.misc.py.un~ +0 -0
  235. planar/workflows/.model.py.un~ +0 -0
  236. planar/workflows/.models.py.un~ +0 -0
  237. planar/workflows/.notifications.py.un~ +0 -0
  238. planar/workflows/.orchestrator.py.un~ +0 -0
  239. planar/workflows/.runtime.py.un~ +0 -0
  240. planar/workflows/.serialization.py.un~ +0 -0
  241. planar/workflows/.step.py.un~ +0 -0
  242. planar/workflows/.step_core.py.un~ +0 -0
  243. planar/workflows/.sub_workflow_runner.py.un~ +0 -0
  244. planar/workflows/.sub_workflow_scheduler.py.un~ +0 -0
  245. planar/workflows/.test_concurrency.py.un~ +0 -0
  246. planar/workflows/.test_concurrency_detection.py.un~ +0 -0
  247. planar/workflows/.test_human.py.un~ +0 -0
  248. planar/workflows/.test_lock_timeout.py.un~ +0 -0
  249. planar/workflows/.test_orchestrator.py.un~ +0 -0
  250. planar/workflows/.test_race_conditions.py.un~ +0 -0
  251. planar/workflows/.test_serialization.py.un~ +0 -0
  252. planar/workflows/.test_suspend_deserialization.py.un~ +0 -0
  253. planar/workflows/.test_workflow.py.un~ +0 -0
  254. planar/workflows/.tracing.py.un~ +0 -0
  255. planar/workflows/.types.py.un~ +0 -0
  256. planar/workflows/.util.py.un~ +0 -0
  257. planar/workflows/.utils.py.un~ +0 -0
  258. planar/workflows/.workflow.py.un~ +0 -0
  259. planar/workflows/.workflow_wrapper.py.un~ +0 -0
  260. planar/workflows/.wrappers.py.un~ +0 -0
  261. planar/workflows/__init__.py +42 -0
  262. planar/workflows/context.py +44 -0
  263. planar/workflows/contrib.py +190 -0
  264. planar/workflows/decorators.py +217 -0
  265. planar/workflows/events.py +185 -0
  266. planar/workflows/exceptions.py +34 -0
  267. planar/workflows/execution.py +198 -0
  268. planar/workflows/lock.py +229 -0
  269. planar/workflows/misc.py +5 -0
  270. planar/workflows/models.py +154 -0
  271. planar/workflows/notifications.py +96 -0
  272. planar/workflows/orchestrator.py +383 -0
  273. planar/workflows/query.py +256 -0
  274. planar/workflows/serialization.py +409 -0
  275. planar/workflows/step_core.py +373 -0
  276. planar/workflows/step_metadata.py +357 -0
  277. planar/workflows/step_testing_utils.py +86 -0
  278. planar/workflows/sub_workflow_runner.py +191 -0
  279. planar/workflows/test_concurrency_detection.py +120 -0
  280. planar/workflows/test_lock_timeout.py +140 -0
  281. planar/workflows/test_serialization.py +1195 -0
  282. planar/workflows/test_suspend_deserialization.py +231 -0
  283. planar/workflows/test_workflow.py +1967 -0
  284. planar/workflows/tracing.py +106 -0
  285. planar/workflows/wrappers.py +41 -0
  286. planar-0.5.0.dist-info/METADATA +285 -0
  287. planar-0.5.0.dist-info/RECORD +289 -0
  288. planar-0.5.0.dist-info/WHEEL +4 -0
  289. planar-0.5.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,1195 @@
1
+ import uuid
2
+ from dataclasses import dataclass
3
+ from datetime import datetime, timedelta
4
+ from decimal import Decimal
5
+ from typing import Any, List, Optional, Union
6
+
7
+ import pytest
8
+ from pydantic import BaseModel, Field
9
+
10
+ from planar.workflows.serialization import (
11
+ deserialize_args,
12
+ deserialize_primitive,
13
+ deserialize_result,
14
+ deserialize_value,
15
+ is_pydantic_model,
16
+ serialize_args,
17
+ serialize_primitive,
18
+ serialize_result,
19
+ serialize_value,
20
+ )
21
+
22
+
23
+ class SamplePerson(BaseModel):
24
+ name: str
25
+ age: int
26
+ birth_date: datetime
27
+ balance: Decimal
28
+ id: uuid.UUID
29
+ active: bool
30
+ tags: List[str]
31
+
32
+
33
+ class SampleNestedModel(BaseModel):
34
+ person: SamplePerson
35
+ metadata: Optional[dict] = None
36
+
37
+
38
+ @dataclass
39
+ class SampleDataClass:
40
+ name: str
41
+ value: int
42
+
43
+
44
+ def test_is_pydantic_model():
45
+ assert is_pydantic_model(SamplePerson)
46
+ assert is_pydantic_model(SampleNestedModel)
47
+ assert not is_pydantic_model(dict)
48
+ assert not is_pydantic_model(str)
49
+ assert not is_pydantic_model(5)
50
+
51
+
52
+ def test_serialize_primitive():
53
+ # Test basic primitives
54
+ assert serialize_primitive(True) is True
55
+ assert serialize_primitive(42) == 42
56
+ assert serialize_primitive(3.14) == 3.14
57
+
58
+ # Test Decimal
59
+ decimal_value = Decimal("123.456")
60
+ assert serialize_primitive(decimal_value) == "123.456"
61
+
62
+ # Test UUID
63
+ test_uuid = uuid.uuid4()
64
+ assert serialize_primitive(test_uuid) == str(test_uuid)
65
+
66
+ # Test datetime
67
+ test_dt = datetime(2023, 5, 15, 12, 30, 45)
68
+ assert serialize_primitive(test_dt) == test_dt.isoformat()
69
+
70
+ # Test timedelta
71
+ test_td = timedelta(days=1, hours=2, minutes=30)
72
+ serialized_td = serialize_primitive(test_td)
73
+ assert isinstance(serialized_td, dict)
74
+ assert serialized_td["days"] == 1
75
+ assert serialized_td["seconds"] == (2 * 3600) + (30 * 60)
76
+ assert serialized_td["microseconds"] == 0
77
+
78
+
79
+ def test_deserialize_primitive():
80
+ # Test basic primitives
81
+ assert deserialize_primitive(True, bool) is True
82
+ assert deserialize_primitive(42, int) == 42
83
+ assert deserialize_primitive(3.14, float) == 3.14
84
+
85
+ # Test Decimal
86
+ assert deserialize_primitive("123.456", Decimal) == Decimal("123.456")
87
+
88
+ # Test UUID
89
+ test_uuid = uuid.uuid4()
90
+ assert deserialize_primitive(str(test_uuid), uuid.UUID) == test_uuid
91
+
92
+ # Test datetime
93
+ test_dt = datetime(2023, 5, 15, 12, 30, 45)
94
+ assert deserialize_primitive(test_dt.isoformat(), datetime) == test_dt
95
+
96
+ # Test timedelta as dict
97
+ test_td = timedelta(days=1, hours=2, minutes=30)
98
+ serialized_td = {"days": 1, "seconds": (2 * 3600) + (30 * 60), "microseconds": 0}
99
+ assert deserialize_primitive(serialized_td, timedelta) == test_td
100
+
101
+ # Test timedelta as seconds
102
+ assert deserialize_primitive(3600, timedelta) == timedelta(hours=1)
103
+
104
+ # Test invalid timedelta
105
+ with pytest.raises(ValueError):
106
+ deserialize_primitive("invalid", timedelta)
107
+
108
+
109
+ def test_serialize_value():
110
+ # Test None
111
+ assert serialize_value(None) is None
112
+
113
+ # Test Pydantic model
114
+ person = SamplePerson(
115
+ name="John",
116
+ age=30,
117
+ birth_date=datetime(1990, 1, 1),
118
+ balance=Decimal("1000.50"),
119
+ id=uuid.uuid4(),
120
+ active=True,
121
+ tags=["developer", "python"],
122
+ )
123
+ serialized_person = serialize_value(person)
124
+ assert isinstance(serialized_person, dict)
125
+ assert serialized_person["name"] == "John"
126
+ assert serialized_person["age"] == 30
127
+ assert "birth_date" in serialized_person
128
+ # Handle both string representation or Decimal object (implementation might vary)
129
+ assert str(serialized_person["balance"]) == "1000.50"
130
+ assert "id" in serialized_person
131
+ assert serialized_person["active"] is True
132
+ assert serialized_person["tags"] == ["developer", "python"]
133
+
134
+ # Test Pydantic model type
135
+ serialized_model_definition = serialize_value(SamplePerson)
136
+ expected_definition = {
137
+ "title": "SamplePerson",
138
+ "type": "object",
139
+ "properties": {
140
+ "name": {"title": "Name", "type": "string"},
141
+ "age": {"title": "Age", "type": "integer"},
142
+ "birth_date": {
143
+ "title": "Birth Date",
144
+ "type": "string",
145
+ "format": "date-time",
146
+ },
147
+ "balance": {
148
+ "title": "Balance",
149
+ "anyOf": [{"type": "number"}, {"type": "string"}],
150
+ },
151
+ "id": {"format": "uuid", "title": "Id", "type": "string"},
152
+ "active": {"title": "Active", "type": "boolean"},
153
+ "tags": {"title": "Tags", "type": "array", "items": {"type": "string"}},
154
+ },
155
+ "required": ["name", "age", "birth_date", "balance", "id", "active", "tags"],
156
+ }
157
+
158
+ assert serialized_model_definition == expected_definition
159
+
160
+ # Test primitives
161
+ assert serialize_value(42) == 42
162
+ assert serialize_value(Decimal("123.45")) == "123.45"
163
+
164
+ # Test nested model
165
+ nested = SampleNestedModel(person=person, metadata={"source": "test"})
166
+ serialized_nested = serialize_value(nested)
167
+ assert isinstance(serialized_nested, dict)
168
+ assert "person" in serialized_nested
169
+ assert serialized_nested["metadata"] == {"source": "test"}
170
+
171
+
172
+ def test_deserialize_value():
173
+ # Test None
174
+ assert deserialize_value(None) is None
175
+ assert deserialize_value(None, str) is None
176
+
177
+ # Test with no type hint
178
+ assert deserialize_value(42, None) == 42
179
+
180
+ # Test Pydantic model
181
+ test_uuid = uuid.uuid4()
182
+ person_data = {
183
+ "name": "John",
184
+ "age": 30,
185
+ "birth_date": "1990-01-01T00:00:00",
186
+ "balance": "1000.50",
187
+ "id": str(test_uuid),
188
+ "active": True,
189
+ "tags": ["developer", "python"],
190
+ }
191
+ deserialized_person = deserialize_value(person_data, SamplePerson)
192
+ assert isinstance(deserialized_person, SamplePerson)
193
+ assert deserialized_person.name == "John"
194
+ assert deserialized_person.age == 30
195
+ assert deserialized_person.birth_date == datetime(1990, 1, 1)
196
+ assert deserialized_person.balance == Decimal("1000.50")
197
+ assert deserialized_person.id == test_uuid
198
+ assert deserialized_person.active is True
199
+ assert deserialized_person.tags == ["developer", "python"]
200
+
201
+ # Test Pydantic model type
202
+ serialized_model_definition = {
203
+ "title": "SamplePerson",
204
+ "type": "object",
205
+ "properties": {
206
+ "name": {"title": "Name", "type": "string"},
207
+ "age": {"title": "Age", "type": "integer"},
208
+ "birth_date": {
209
+ "title": "Birth Date",
210
+ "type": "string",
211
+ "format": "date-time",
212
+ },
213
+ "balance": {
214
+ "title": "Balance",
215
+ "anyOf": [{"type": "number"}, {"type": "string"}],
216
+ },
217
+ "id": {"format": "uuid", "title": "Id", "type": "string"},
218
+ "active": {"title": "Active", "type": "boolean"},
219
+ "tags": {"title": "Tags", "type": "array", "items": {"type": "string"}},
220
+ },
221
+ "required": ["name", "age", "birth_date", "balance", "id", "active", "tags"],
222
+ }
223
+ deserialized_model_definition = deserialize_value(
224
+ serialized_model_definition, type_hint=type[SamplePerson]
225
+ )
226
+ assert deserialized_model_definition == SamplePerson
227
+
228
+ # Test Union type
229
+ union_type = Union[int, str]
230
+ assert deserialize_value(42, union_type) == 42
231
+ assert deserialize_value("hello", union_type) == "hello"
232
+ union_type_pipe = int | str
233
+ assert deserialize_value(42, union_type_pipe) == 42
234
+ assert deserialize_value("hello", union_type_pipe) == "hello"
235
+
236
+ # Test primitive types
237
+ assert deserialize_value("123.45", Decimal) == Decimal("123.45")
238
+ assert deserialize_value(3600, timedelta) == timedelta(hours=1)
239
+
240
+ # Test an invalid Union type
241
+ with pytest.raises(ValueError):
242
+ deserialize_value("not_a_number", Union[int, float])
243
+
244
+
245
+ def test_deserialize_value_uniontype():
246
+ class FooModel(BaseModel):
247
+ value: int
248
+
249
+ data = {"value": 5}
250
+ deserialized = deserialize_value(data, FooModel | None)
251
+ assert isinstance(deserialized, FooModel)
252
+ assert deserialized.value == 5
253
+
254
+ deserialized = deserialize_value(data, Optional[FooModel])
255
+ assert isinstance(deserialized, FooModel)
256
+ assert deserialized.value == 5
257
+
258
+ deserialized = deserialize_value(data, Union[FooModel, None])
259
+ assert isinstance(deserialized, FooModel)
260
+ assert deserialized.value == 5
261
+
262
+
263
+ def example_function(
264
+ person: SamplePerson, count: int, tags: List[str] = []
265
+ ) -> SampleNestedModel:
266
+ return SampleNestedModel(
267
+ person=person, metadata={"count": count, "tags": tags or []}
268
+ )
269
+
270
+
271
+ def test_serialize_args():
272
+ person = SamplePerson(
273
+ name="Alice",
274
+ age=25,
275
+ birth_date=datetime(1998, 5, 10),
276
+ balance=Decimal("500.25"),
277
+ id=uuid.uuid4(),
278
+ active=True,
279
+ tags=["tester"],
280
+ )
281
+
282
+ args = [person]
283
+ kwargs = {"count": 42, "tags": ["important", "test"]}
284
+
285
+ serialized_args, serialized_kwargs = serialize_args(example_function, args, kwargs)
286
+
287
+ # Check args
288
+ assert isinstance(serialized_args[0], dict)
289
+ assert serialized_args[0]["name"] == "Alice"
290
+
291
+ # Check kwargs
292
+ assert serialized_kwargs["count"] == 42
293
+ assert serialized_kwargs["tags"] == ["important", "test"]
294
+
295
+
296
+ def test_deserialize_args():
297
+ test_uuid = uuid.uuid4()
298
+ person_data = {
299
+ "name": "Alice",
300
+ "age": 25,
301
+ "birth_date": "1998-05-10T00:00:00",
302
+ "balance": "500.25",
303
+ "id": str(test_uuid),
304
+ "active": True,
305
+ "tags": ["tester"],
306
+ }
307
+
308
+ args = [person_data]
309
+ kwargs = {"count": 42, "tags": ["important", "test"]}
310
+
311
+ deserialized_args, deserialized_kwargs = deserialize_args(
312
+ example_function, args, kwargs
313
+ )
314
+
315
+ # Check args
316
+ assert isinstance(deserialized_args[0], SamplePerson)
317
+ assert deserialized_args[0].name == "Alice"
318
+ assert deserialized_args[0].id == test_uuid
319
+
320
+ # Check kwargs
321
+ assert deserialized_kwargs["count"] == 42
322
+ assert deserialized_kwargs["tags"] == ["important", "test"]
323
+
324
+
325
+ def test_serialize_result():
326
+ person = SamplePerson(
327
+ name="Bob",
328
+ age=35,
329
+ birth_date=datetime(1988, 3, 15),
330
+ balance=Decimal("1200.75"),
331
+ id=uuid.uuid4(),
332
+ active=True,
333
+ tags=["developer"],
334
+ )
335
+
336
+ result = SampleNestedModel(person=person, metadata={"source": "result_test"})
337
+
338
+ serialized_result = serialize_result(example_function, result)
339
+
340
+ assert isinstance(serialized_result, dict)
341
+ assert "person" in serialized_result
342
+ assert serialized_result["person"]["name"] == "Bob"
343
+ assert serialized_result["metadata"] == {"source": "result_test"}
344
+
345
+
346
+ def test_deserialize_result():
347
+ test_uuid = uuid.uuid4()
348
+ result_data = {
349
+ "person": {
350
+ "name": "Bob",
351
+ "age": 35,
352
+ "birth_date": "1988-03-15T00:00:00",
353
+ "balance": "1200.75",
354
+ "id": str(test_uuid),
355
+ "active": True,
356
+ "tags": ["developer"],
357
+ },
358
+ "metadata": {"source": "result_test"},
359
+ }
360
+
361
+ deserialized_result = deserialize_result(example_function, result_data)
362
+
363
+ assert isinstance(deserialized_result, SampleNestedModel)
364
+ assert isinstance(deserialized_result.person, SamplePerson)
365
+ assert deserialized_result.person.name == "Bob"
366
+ assert deserialized_result.person.id == test_uuid
367
+ assert deserialized_result.metadata == {"source": "result_test"}
368
+
369
+
370
+ def test_serialize_deserialize_roundtrip():
371
+ # Create test data
372
+ test_uuid = uuid.uuid4()
373
+ person = SamplePerson(
374
+ name="Charlie",
375
+ age=40,
376
+ birth_date=datetime(1983, 7, 22),
377
+ balance=Decimal("2500.10"),
378
+ id=test_uuid,
379
+ active=True,
380
+ tags=["manager", "python"],
381
+ )
382
+
383
+ # Original function call
384
+ args = [person]
385
+ kwargs = {"count": 100, "tags": ["high-priority"]}
386
+
387
+ # Serialize arguments
388
+ serialized_args, serialized_kwargs = serialize_args(example_function, args, kwargs)
389
+
390
+ # Deserialize arguments
391
+ deserialized_args, deserialized_kwargs = deserialize_args(
392
+ example_function, serialized_args, serialized_kwargs
393
+ )
394
+
395
+ # Call function with deserialized arguments
396
+ result = example_function(*deserialized_args, **deserialized_kwargs)
397
+
398
+ # Serialize result
399
+ serialized_result = serialize_result(example_function, result)
400
+
401
+ # Deserialize result
402
+ deserialized_result = deserialize_result(example_function, serialized_result)
403
+
404
+ # Verify everything is intact
405
+ assert isinstance(deserialized_result, SampleNestedModel)
406
+ assert deserialized_result.person.name == "Charlie"
407
+ assert deserialized_result.person.age == 40
408
+ assert deserialized_result.person.tags == ["manager", "python"]
409
+ assert deserialized_result.person.birth_date == datetime(1983, 7, 22)
410
+ assert deserialized_result.person.balance == Decimal("2500.10")
411
+ assert deserialized_result.person.id == test_uuid
412
+ assert deserialized_result.person.active is True
413
+ assert deserialized_result.metadata
414
+ assert deserialized_result.metadata["count"] == 100
415
+ assert deserialized_result.metadata["tags"] == ["high-priority"]
416
+
417
+
418
+ # Test function with Union types for args and kwargs
419
+ class SampleEventModel(BaseModel):
420
+ event_id: uuid.UUID
421
+ timestamp: datetime
422
+ description: str
423
+
424
+
425
+ class ModelWithDatetimeField(BaseModel):
426
+ """Model to test datetime serialization"""
427
+
428
+ timestamp: datetime = Field(...)
429
+ name: str
430
+
431
+
432
+ class ModelWithUUID(BaseModel):
433
+ """Model to test UUID serialization"""
434
+
435
+ id: uuid.UUID = Field(...)
436
+ name: str
437
+
438
+
439
+ def function_with_dicts(data: dict, count: int, metadata: dict[str, Any]) -> dict:
440
+ return {"count": count, "metadata": metadata}
441
+
442
+
443
+ def test_serialize_args_with_dict():
444
+ # Test with SamplePerson
445
+ person = {"name": "David", "age": 28, "id": "1235abc"}
446
+
447
+ args = [person]
448
+ kwargs = {"another_dict_key": {"count": 50, "metadata": {"source": "test"}}}
449
+
450
+ serialized_args, serialized_kwargs = serialize_args(
451
+ function_with_dicts, args, kwargs
452
+ )
453
+
454
+ # Check args - should be serialized to dict
455
+ assert isinstance(serialized_args[0], dict)
456
+ assert serialized_args[0]["name"] == "David"
457
+ assert serialized_args[0]["age"] == 28
458
+ assert serialized_args[0]["id"] == "1235abc"
459
+
460
+ # Check kwargs
461
+ assert serialized_kwargs["another_dict_key"]["count"] == 50
462
+ assert serialized_kwargs["another_dict_key"]["metadata"] == {"source": "test"}
463
+
464
+
465
+ def test_deserialize_args_with_dict():
466
+ # Test with SamplePerson
467
+ person = {"name": "David", "age": 28, "id": "1235abc"}
468
+
469
+ args = [person]
470
+ kwargs = {"another_dict_key": {"count": 50, "metadata": {"source": "test"}}}
471
+
472
+ deserialized_args, deserialized_kwargs = deserialize_args(
473
+ function_with_dicts, args, kwargs
474
+ )
475
+
476
+ assert deserialized_args[0] == person
477
+ assert deserialized_kwargs["another_dict_key"] == {
478
+ "count": 50,
479
+ "metadata": {"source": "test"},
480
+ }
481
+
482
+
483
+ # A function with Union type parameters for testing
484
+ def function_with_unions(
485
+ data: Union[SamplePerson, SampleEventModel],
486
+ count: int,
487
+ metadata: Union[dict, str, None] = None,
488
+ ) -> Union[SampleNestedModel, dict]:
489
+ if isinstance(data, SamplePerson):
490
+ return SampleNestedModel(
491
+ person=data, metadata={"count": count, "extra": metadata}
492
+ )
493
+ else: # SampleEventModel
494
+ return {"event": data.model_dump(), "count": count, "metadata": metadata}
495
+
496
+
497
+ def test_serialize_args_with_unions():
498
+ # Test with SamplePerson
499
+ person = SamplePerson(
500
+ name="David",
501
+ age=28,
502
+ birth_date=datetime(1995, 8, 12),
503
+ balance=Decimal("750.25"),
504
+ id=uuid.uuid4(),
505
+ active=True,
506
+ tags=["engineer"],
507
+ )
508
+
509
+ args = [person]
510
+ kwargs = {"count": 50, "metadata": {"source": "test"}}
511
+
512
+ serialized_args, serialized_kwargs = serialize_args(
513
+ function_with_unions, args, kwargs
514
+ )
515
+
516
+ # Check args - should be serialized to dict
517
+ assert isinstance(serialized_args[0], dict)
518
+ assert serialized_args[0]["name"] == "David"
519
+ assert serialized_args[0]["age"] == 28
520
+ assert serialized_args[0]["id"] == str(person.id)
521
+
522
+ # Check kwargs
523
+ assert serialized_kwargs["count"] == 50
524
+ assert serialized_kwargs["metadata"] == {"source": "test"}
525
+
526
+ # Test with SampleEventModel
527
+ event = SampleEventModel(
528
+ event_id=uuid.uuid4(), timestamp=datetime.now(), description="Test event"
529
+ )
530
+
531
+ args = [event]
532
+ kwargs = {"count": 10, "metadata": "event_metadata"}
533
+
534
+ serialized_args, serialized_kwargs = serialize_args(
535
+ function_with_unions, args, kwargs
536
+ )
537
+
538
+ # Check args
539
+ assert isinstance(serialized_args[0], dict)
540
+ assert "event_id" in serialized_args[0]
541
+ assert "timestamp" in serialized_args[0]
542
+ assert serialized_args[0]["description"] == "Test event"
543
+
544
+ # Check kwargs
545
+ assert serialized_kwargs["count"] == 10
546
+ assert serialized_kwargs["metadata"] == "event_metadata"
547
+
548
+
549
+ def test_deserialize_args_with_unions():
550
+ # Test with SamplePerson data
551
+ test_uuid = uuid.uuid4()
552
+ person_data = {
553
+ "name": "David",
554
+ "age": 28,
555
+ "birth_date": "1995-08-12T00:00:00",
556
+ "balance": "750.25",
557
+ "id": str(test_uuid),
558
+ "active": True,
559
+ "tags": ["engineer"],
560
+ }
561
+
562
+ args = [person_data]
563
+ kwargs = {"count": 50, "metadata": {"source": "test"}}
564
+
565
+ deserialized_args, deserialized_kwargs = deserialize_args(
566
+ function_with_unions, args, kwargs
567
+ )
568
+
569
+ # For Union types, it will try each type and use the first one that works
570
+ # SamplePerson should be successfully deserialized
571
+ assert isinstance(deserialized_args[0], SamplePerson)
572
+ assert deserialized_args[0].name == "David"
573
+ assert deserialized_args[0].age == 28
574
+ assert deserialized_args[0].id == test_uuid
575
+
576
+ # Check kwargs
577
+ assert deserialized_kwargs["count"] == 50
578
+ assert deserialized_kwargs["metadata"] == {"source": "test"}
579
+
580
+ # Test with SampleEventModel data
581
+ event_uuid = uuid.uuid4()
582
+ event_time = datetime.now()
583
+ event_data = {
584
+ "event_id": str(event_uuid),
585
+ "timestamp": event_time.isoformat(),
586
+ "description": "Test event",
587
+ }
588
+
589
+ args = [event_data]
590
+ kwargs = {"count": 10, "metadata": "event_metadata"}
591
+
592
+ deserialized_args, deserialized_kwargs = deserialize_args(
593
+ function_with_unions, args, kwargs
594
+ )
595
+
596
+ # It should deserialize to SampleEventModel (the first compatible type)
597
+ assert isinstance(deserialized_args[0], SampleEventModel)
598
+ assert deserialized_args[0].event_id == event_uuid
599
+ assert deserialized_args[0].description == "Test event"
600
+
601
+ # Check kwargs
602
+ assert deserialized_kwargs["count"] == 10
603
+ assert deserialized_kwargs["metadata"] == "event_metadata"
604
+
605
+
606
+ def test_serialize_result_with_unions():
607
+ # Test with SampleNestedModel result
608
+ person = SamplePerson(
609
+ name="Eve",
610
+ age=32,
611
+ birth_date=datetime(1991, 3, 15),
612
+ balance=Decimal("1500.75"),
613
+ id=uuid.uuid4(),
614
+ active=True,
615
+ tags=["manager"],
616
+ )
617
+
618
+ result = SampleNestedModel(person=person, metadata={"source": "union_test"})
619
+
620
+ serialized_result = serialize_result(function_with_unions, result)
621
+
622
+ assert isinstance(serialized_result, dict)
623
+ assert "person" in serialized_result
624
+ assert serialized_result["person"]["name"] == "Eve"
625
+ assert serialized_result["metadata"] == {"source": "union_test"}
626
+
627
+ # Test with dict result
628
+ event = SampleEventModel(
629
+ event_id=uuid.uuid4(), timestamp=datetime.now(), description="Important event"
630
+ )
631
+
632
+ result = {"event": event.model_dump(), "count": 25, "metadata": "event data"}
633
+
634
+ serialized_result = serialize_result(function_with_unions, result)
635
+
636
+ assert isinstance(serialized_result, dict)
637
+ assert "event" in serialized_result
638
+ assert serialized_result["count"] == 25
639
+ assert serialized_result["metadata"] == "event data"
640
+
641
+
642
+ def test_deserialize_result_with_unions():
643
+ # Test with SampleNestedModel data
644
+ test_uuid = uuid.uuid4()
645
+ result_data = {
646
+ "person": {
647
+ "name": "Eve",
648
+ "age": 32,
649
+ "birth_date": "1991-03-15T00:00:00",
650
+ "balance": "1500.75",
651
+ "id": str(test_uuid),
652
+ "active": True,
653
+ "tags": ["manager"],
654
+ },
655
+ "metadata": {"source": "union_test"},
656
+ }
657
+
658
+ deserialized_result = deserialize_result(function_with_unions, result_data)
659
+
660
+ # For Union types in return type, it tries each type and uses the first one that works
661
+ assert isinstance(deserialized_result, SampleNestedModel)
662
+ assert isinstance(deserialized_result.person, SamplePerson)
663
+ assert deserialized_result.person.name == "Eve"
664
+ assert deserialized_result.person.id == test_uuid
665
+ assert deserialized_result.metadata == {"source": "union_test"}
666
+
667
+ # Test with dict result
668
+ event_uuid = uuid.uuid4()
669
+ event_time = datetime.now().isoformat()
670
+ result_data = {
671
+ "event": {
672
+ "event_id": str(event_uuid),
673
+ "timestamp": event_time,
674
+ "description": "Important event",
675
+ },
676
+ "count": 25,
677
+ "metadata": "event data",
678
+ }
679
+
680
+ deserialized_result = deserialize_result(function_with_unions, result_data)
681
+
682
+ # Since a dict can be parsed as-is without conversion, it should remain a dict
683
+ assert isinstance(deserialized_result, dict)
684
+ assert "event" in deserialized_result
685
+ assert deserialized_result["count"] == 25
686
+ assert deserialized_result["metadata"] == "event data"
687
+
688
+
689
+ def test_serialize_deserialize_roundtrip_with_unions():
690
+ # Create test data - SamplePerson
691
+ person = SamplePerson(
692
+ name="Frank",
693
+ age=45,
694
+ birth_date=datetime(1978, 11, 5),
695
+ balance=Decimal("3200.80"),
696
+ id=uuid.uuid4(),
697
+ active=True,
698
+ tags=["director", "finance"],
699
+ )
700
+
701
+ # Original function call
702
+ args = [person]
703
+ kwargs = {"count": 75, "metadata": {"department": "finance"}}
704
+
705
+ # Serialize arguments
706
+ serialized_args, serialized_kwargs = serialize_args(
707
+ function_with_unions, args, kwargs
708
+ )
709
+
710
+ # Deserialize arguments
711
+ deserialized_args, deserialized_kwargs = deserialize_args(
712
+ function_with_unions, serialized_args, serialized_kwargs
713
+ )
714
+
715
+ # Call function with deserialized arguments
716
+ result = function_with_unions(*deserialized_args, **deserialized_kwargs)
717
+
718
+ # Serialize result
719
+ serialized_result = serialize_result(function_with_unions, result)
720
+
721
+ # Deserialize result
722
+ deserialized_result = deserialize_result(function_with_unions, serialized_result)
723
+
724
+ # Verify everything is intact
725
+ assert isinstance(deserialized_result, SampleNestedModel)
726
+ assert deserialized_result.person.name == "Frank"
727
+ assert deserialized_result.person.age == 45
728
+ assert deserialized_result.person.tags == ["director", "finance"]
729
+ assert deserialized_result.metadata
730
+ assert deserialized_result.metadata["count"] == 75
731
+ assert deserialized_result.metadata["extra"] == {"department": "finance"}
732
+
733
+ # Create test data - SampleEventModel
734
+ event = SampleEventModel(
735
+ event_id=uuid.uuid4(),
736
+ timestamp=datetime(2023, 1, 15, 14, 30),
737
+ description="Quarterly review",
738
+ )
739
+
740
+ # Original function call
741
+ args = [event]
742
+ kwargs = {"count": 120, "metadata": "quarterly"}
743
+
744
+ # Serialize arguments
745
+ serialized_args, serialized_kwargs = serialize_args(
746
+ function_with_unions, args, kwargs
747
+ )
748
+
749
+ # Deserialize arguments
750
+ deserialized_args, deserialized_kwargs = deserialize_args(
751
+ function_with_unions, serialized_args, serialized_kwargs
752
+ )
753
+
754
+ # Call function with deserialized arguments
755
+ result = function_with_unions(*deserialized_args, **deserialized_kwargs)
756
+
757
+ # Serialize result
758
+ serialized_result = serialize_result(function_with_unions, result)
759
+
760
+ # Deserialize result
761
+ deserialized_result = deserialize_result(function_with_unions, serialized_result)
762
+
763
+ # Verify everything is intact
764
+ assert isinstance(deserialized_result, dict)
765
+ assert "event" in deserialized_result
766
+ assert deserialized_result["event"]["description"] == "Quarterly review"
767
+ assert deserialized_result["count"] == 120
768
+ assert deserialized_result["metadata"] == "quarterly"
769
+
770
+
771
+ # This class is used for testing list serialization
772
+ class ListContainer(BaseModel):
773
+ items: List[SamplePerson]
774
+ labels: List[str]
775
+
776
+
777
+ def test_serialize_list_of_primitives():
778
+ # Test serializing lists of primitives
779
+ int_list = [1, 2, 3, 4, 5]
780
+ assert serialize_value(int_list) == int_list
781
+
782
+ # Test with mixed primitive types
783
+ mixed_list = [1, "string", 3.14, True]
784
+ assert serialize_value(mixed_list) == mixed_list
785
+
786
+ # Test with complex primitive types
787
+ now = datetime.now()
788
+ uuid_val = uuid.uuid4()
789
+ decimal_val = Decimal("123.456")
790
+ complex_list = [now, uuid_val, decimal_val]
791
+ serialized = serialize_value(complex_list)
792
+ assert serialized[0] == now.isoformat()
793
+ assert serialized[1] == str(uuid_val)
794
+ assert serialized[2] == str(decimal_val)
795
+
796
+
797
+ def test_deserialize_list_of_primitives():
798
+ # Test deserializing lists of primitives
799
+ serialized_ints = [1, 2, 3, 4, 5]
800
+ assert deserialize_value(serialized_ints, List[int]) == serialized_ints
801
+
802
+ # Test with mixed types that should be cast to a specific type
803
+ serialized_strings = ["1", "2", "3"]
804
+ assert deserialize_value(serialized_strings, List[int]) == [1, 2, 3]
805
+
806
+ # Test with complex types
807
+ uuid_val = uuid.uuid4()
808
+ now = datetime.now()
809
+ serialized_complex = [str(uuid_val), now.isoformat(), "123.456"]
810
+ deserialized = deserialize_value(
811
+ serialized_complex, List[Union[uuid.UUID, datetime, Decimal]]
812
+ )
813
+
814
+ # For Union types, it will use the first type that works
815
+ # In this case, UUID should be the first successful type for str(uuid_val)
816
+ assert deserialized[0] == uuid_val
817
+ assert isinstance(deserialized[1], datetime)
818
+ # The string "123.456" would be deserialized as a UUID which would fail,
819
+ # then datetime which would fail, and finally Decimal which should succeed
820
+ assert deserialized[2] == Decimal("123.456")
821
+
822
+
823
+ def test_serialize_list_of_models():
824
+ # Create sample data
825
+ persons = [
826
+ SamplePerson(
827
+ name="Person 1",
828
+ age=30,
829
+ birth_date=datetime(1993, 5, 10),
830
+ balance=Decimal("500.00"),
831
+ id=uuid.uuid4(),
832
+ active=True,
833
+ tags=["developer"],
834
+ ),
835
+ SamplePerson(
836
+ name="Person 2",
837
+ age=25,
838
+ birth_date=datetime(1998, 8, 15),
839
+ balance=Decimal("750.50"),
840
+ id=uuid.uuid4(),
841
+ active=False,
842
+ tags=["designer", "ui"],
843
+ ),
844
+ ]
845
+
846
+ # Test serializing a list of models
847
+ serialized_persons = serialize_value(persons)
848
+ assert isinstance(serialized_persons, list)
849
+ assert len(serialized_persons) == 2
850
+ assert isinstance(serialized_persons[0], dict)
851
+ assert serialized_persons[0]["name"] == "Person 1"
852
+ assert serialized_persons[1]["name"] == "Person 2"
853
+
854
+ # Check that datetime fields are properly serialized
855
+ assert isinstance(serialized_persons[0]["birth_date"], str)
856
+ assert isinstance(serialized_persons[1]["birth_date"], str)
857
+ # Verify they can be parsed as ISO format
858
+ datetime.fromisoformat(serialized_persons[0]["birth_date"])
859
+ datetime.fromisoformat(serialized_persons[1]["birth_date"])
860
+
861
+ # Test serializing a model with a list field
862
+ container = ListContainer(
863
+ items=persons,
864
+ labels=["employee", "contractor"],
865
+ )
866
+
867
+ serialized_container = serialize_value(container)
868
+ assert isinstance(serialized_container, dict)
869
+ assert isinstance(serialized_container["items"], list)
870
+ assert len(serialized_container["items"]) == 2
871
+ assert serialized_container["items"][0]["name"] == "Person 1"
872
+ assert serialized_container["items"][1]["name"] == "Person 2"
873
+ assert serialized_container["labels"] == ["employee", "contractor"]
874
+
875
+
876
+ def test_deserialize_list_of_models():
877
+ # Create serialized data
878
+ person1_id = uuid.uuid4()
879
+ person2_id = uuid.uuid4()
880
+
881
+ serialized_persons = [
882
+ {
883
+ "name": "Person 1",
884
+ "age": 30,
885
+ "birth_date": "1993-05-10T00:00:00",
886
+ "balance": "500.00",
887
+ "id": str(person1_id),
888
+ "active": True,
889
+ "tags": ["developer"],
890
+ },
891
+ {
892
+ "name": "Person 2",
893
+ "age": 25,
894
+ "birth_date": "1998-08-15T00:00:00",
895
+ "balance": "750.50",
896
+ "id": str(person2_id),
897
+ "active": False,
898
+ "tags": ["designer", "ui"],
899
+ },
900
+ ]
901
+
902
+ # Test deserializing a list of models
903
+ deserialized_persons = deserialize_value(serialized_persons, List[SamplePerson])
904
+ assert isinstance(deserialized_persons, list)
905
+ assert len(deserialized_persons) == 2
906
+ assert isinstance(deserialized_persons[0], SamplePerson)
907
+ assert deserialized_persons[0].id == person1_id
908
+ assert deserialized_persons[0].name == "Person 1"
909
+ assert deserialized_persons[1].id == person2_id
910
+ assert deserialized_persons[1].name == "Person 2"
911
+
912
+ # Test deserializing a model with a list field
913
+ serialized_container = {
914
+ "items": serialized_persons,
915
+ "labels": ["employee", "contractor"],
916
+ }
917
+
918
+ deserialized_container = deserialize_value(serialized_container, ListContainer)
919
+ assert isinstance(deserialized_container, ListContainer)
920
+ assert isinstance(deserialized_container.items, list)
921
+ assert len(deserialized_container.items) == 2
922
+ assert isinstance(deserialized_container.items[0], SamplePerson)
923
+ assert deserialized_container.items[0].name == "Person 1"
924
+ assert deserialized_container.items[1].name == "Person 2"
925
+ assert deserialized_container.labels == ["employee", "contractor"]
926
+
927
+
928
+ # For testing list args and return values
929
+ def function_with_list_args_return(
930
+ persons: List[SamplePerson], tags: List[str]
931
+ ) -> List[ListContainer]:
932
+ """Function that takes and returns lists for testing serialization/deserialization."""
933
+ containers = []
934
+ for i, tag in enumerate(tags):
935
+ # Take a slice of persons for each container
936
+ container_persons = persons[: i + 1]
937
+ containers.append(
938
+ ListContainer(
939
+ items=container_persons,
940
+ labels=[tag] * len(container_persons),
941
+ )
942
+ )
943
+ return containers
944
+
945
+
946
+ def test_timestamp_serialization():
947
+ """Test specifically for datetime serialization in Pydantic models"""
948
+ # Create a model with a timestamp
949
+ model = ModelWithDatetimeField(
950
+ timestamp=datetime(2023, 5, 15, 12, 30, 45), name="Test Model"
951
+ )
952
+
953
+ # Serialize the model
954
+ serialized = serialize_value(model)
955
+
956
+ # Verify the timestamp is properly serialized as a string
957
+ assert isinstance(serialized, dict)
958
+ assert "timestamp" in serialized
959
+ assert isinstance(serialized["timestamp"], str)
960
+
961
+ # Verify it can be parsed back to a datetime
962
+ dt = datetime.fromisoformat(serialized["timestamp"])
963
+ assert dt == model.timestamp
964
+
965
+ # Verify we can deserialize it back to the original model
966
+ deserialized = deserialize_value(serialized, ModelWithDatetimeField)
967
+ assert isinstance(deserialized, ModelWithDatetimeField)
968
+ assert deserialized.timestamp == model.timestamp
969
+ assert deserialized.name == model.name
970
+
971
+
972
+ def test_uuid_serialization():
973
+ """Test specifically for UUID serialization in Pydantic models"""
974
+ # Create a model with a UUID
975
+ test_uuid = uuid.uuid4()
976
+ model = ModelWithUUID(id=test_uuid, name="Test Model")
977
+
978
+ # Serialize the model
979
+ serialized = serialize_value(model)
980
+
981
+ # Verify the UUID is properly serialized as a string
982
+ assert isinstance(serialized, dict)
983
+ assert "id" in serialized
984
+ assert isinstance(serialized["id"], str)
985
+
986
+ # Verify it can be parsed back to a UUID
987
+ parsed_uuid = uuid.UUID(serialized["id"])
988
+ assert parsed_uuid == test_uuid
989
+
990
+ # Verify we can deserialize it back to the original model
991
+ deserialized = deserialize_value(serialized, ModelWithUUID)
992
+ assert isinstance(deserialized, ModelWithUUID)
993
+ assert deserialized.id == test_uuid
994
+ assert deserialized.name == model.name
995
+
996
+
997
+ def test_serialize_deserialize_list_roundtrip():
998
+ # Create test data
999
+ persons = [
1000
+ SamplePerson(
1001
+ name=f"Person {i}",
1002
+ age=30 + i,
1003
+ birth_date=datetime(1990, i, 15),
1004
+ balance=Decimal(f"{i}00.50"),
1005
+ id=uuid.uuid4(),
1006
+ active=i % 2 == 0,
1007
+ tags=[f"tag{i}", f"group{i}"],
1008
+ )
1009
+ for i in range(1, 4)
1010
+ ]
1011
+ tags = ["employee", "contractor"]
1012
+
1013
+ # Serialize arguments
1014
+ serialized_args, serialized_kwargs = serialize_args(
1015
+ function_with_list_args_return, [persons, tags], {}
1016
+ )
1017
+
1018
+ # Check serialized args
1019
+ assert isinstance(serialized_args[0], list)
1020
+ assert len(serialized_args[0]) == 3
1021
+ assert isinstance(serialized_args[0][0], dict)
1022
+ assert serialized_args[0][0]["name"] == "Person 1"
1023
+ assert serialized_args[1] == tags
1024
+
1025
+ # Deserialize arguments
1026
+ deserialized_args, deserialized_kwargs = deserialize_args(
1027
+ function_with_list_args_return, serialized_args, serialized_kwargs
1028
+ )
1029
+
1030
+ # Check deserialized args
1031
+ assert isinstance(deserialized_args[0], list)
1032
+ assert isinstance(deserialized_args[0][0], SamplePerson)
1033
+ assert deserialized_args[0][0].name == "Person 1"
1034
+ assert deserialized_args[1] == tags
1035
+
1036
+ # Call function with deserialized arguments
1037
+ result = function_with_list_args_return(*deserialized_args, **deserialized_kwargs)
1038
+
1039
+ # Serialize result
1040
+ serialized_result = serialize_result(function_with_list_args_return, result)
1041
+
1042
+ # Check serialized result
1043
+ assert isinstance(serialized_result, list)
1044
+ assert len(serialized_result) == 2
1045
+ assert isinstance(serialized_result[0], dict)
1046
+ assert "items" in serialized_result[0]
1047
+ assert isinstance(serialized_result[0]["items"], list)
1048
+
1049
+ # Deserialize result
1050
+ deserialized_result = deserialize_result(
1051
+ function_with_list_args_return,
1052
+ serialized_result,
1053
+ None,
1054
+ deserialized_args,
1055
+ deserialized_kwargs,
1056
+ )
1057
+
1058
+ # Check deserialized result
1059
+ assert isinstance(deserialized_result, list)
1060
+ assert len(deserialized_result) == 2
1061
+ assert isinstance(deserialized_result[0], ListContainer)
1062
+ assert len(deserialized_result[0].items) == 1
1063
+ assert deserialized_result[0].items[0].name == "Person 1"
1064
+ assert deserialized_result[0].labels == ["employee"]
1065
+ assert len(deserialized_result[1].items) == 2
1066
+ assert deserialized_result[1].labels == ["contractor", "contractor"]
1067
+
1068
+
1069
+ class GenericResultModel[T](BaseModel):
1070
+ content: T
1071
+ metadata: str
1072
+
1073
+
1074
+ class SimpleData(BaseModel):
1075
+ value: int
1076
+
1077
+
1078
+ def function_with_generic_return[T](data: T) -> GenericResultModel[T]:
1079
+ return GenericResultModel[T](content=data, metadata="Generic metadata")
1080
+
1081
+
1082
+ def test_deserialize_generic_result():
1083
+ """Test deserialization of a result with a simple generic type."""
1084
+ original_data = SimpleData(value=123)
1085
+ args = [original_data]
1086
+ kwargs = {}
1087
+
1088
+ # Simulate execution and serialization
1089
+ result = function_with_generic_return(*args, **kwargs)
1090
+ serialized_result = serialize_result(function_with_generic_return, result)
1091
+
1092
+ # Deserialize the result, inferring the type from args
1093
+ deserialized = deserialize_result(
1094
+ function_with_generic_return, serialized_result, None, args, kwargs
1095
+ )
1096
+
1097
+ assert isinstance(deserialized, GenericResultModel)
1098
+ assert isinstance(deserialized.content, SimpleData)
1099
+ assert deserialized.content.value == 123
1100
+ assert deserialized.metadata == "Generic metadata"
1101
+
1102
+
1103
+ def function_with_generic_list_return[T](items: list[T]) -> GenericResultModel[list[T]]:
1104
+ return GenericResultModel[list[T]](content=items, metadata="List metadata")
1105
+
1106
+
1107
+ def test_deserialize_generic_list_result():
1108
+ """Test deserialization where the generic type parameter is a List."""
1109
+ original_data = [SimpleData(value=1), SimpleData(value=2)]
1110
+ args = [original_data]
1111
+ kwargs = {}
1112
+
1113
+ result = function_with_generic_list_return(*args, **kwargs)
1114
+ serialized_result = serialize_result(function_with_generic_list_return, result)
1115
+
1116
+ deserialized = deserialize_result(
1117
+ function_with_generic_list_return, serialized_result, None, args, kwargs
1118
+ )
1119
+
1120
+ assert isinstance(deserialized, GenericResultModel)
1121
+ assert isinstance(deserialized.content, list)
1122
+ assert len(deserialized.content) == 2
1123
+ assert isinstance(deserialized.content[0], SimpleData)
1124
+ assert deserialized.content[0].value == 1
1125
+ assert deserialized.content[1].value == 2
1126
+ assert deserialized.metadata == "List metadata"
1127
+
1128
+
1129
+ # Test case where Generic comes before BaseModel (should still work)
1130
+ class AltGenericResultModel[T](BaseModel):
1131
+ content: T
1132
+ metadata: str
1133
+
1134
+
1135
+ def function_with_alt_generic_return[T](data: T) -> AltGenericResultModel[T]:
1136
+ return AltGenericResultModel[T](content=data, metadata="Alt Generic metadata")
1137
+
1138
+
1139
+ def test_deserialize_alt_order_generic_result():
1140
+ """Test deserialization when Generic is inherited before BaseModel."""
1141
+ original_data = SimpleData(value=456)
1142
+ args = [original_data]
1143
+ kwargs = {}
1144
+
1145
+ result = function_with_alt_generic_return(*args, **kwargs)
1146
+ serialized_result = serialize_result(function_with_alt_generic_return, result)
1147
+
1148
+ deserialized = deserialize_result(
1149
+ function_with_alt_generic_return, serialized_result, None, args, kwargs
1150
+ )
1151
+
1152
+ assert isinstance(deserialized, AltGenericResultModel)
1153
+ assert isinstance(deserialized.content, SimpleData)
1154
+ assert deserialized.content.value == 456
1155
+ assert deserialized.metadata == "Alt Generic metadata"
1156
+
1157
+
1158
+ def dataclass_function(data: SampleDataClass) -> SampleDataClass:
1159
+ return SampleDataClass(name=data.name.upper(), value=data.value + 1)
1160
+
1161
+
1162
+ def test_dataclass_serialization_roundtrip():
1163
+ item = SampleDataClass(name="foo", value=1)
1164
+ args = [item]
1165
+ kwargs = {}
1166
+
1167
+ serialized_args, serialized_kwargs = serialize_args(
1168
+ dataclass_function, args, kwargs
1169
+ )
1170
+ assert isinstance(serialized_args[0], dict)
1171
+
1172
+ deserialized_args, deserialized_kwargs = deserialize_args(
1173
+ dataclass_function, serialized_args, serialized_kwargs
1174
+ )
1175
+ assert isinstance(deserialized_args[0], SampleDataClass)
1176
+
1177
+ result = dataclass_function(*deserialized_args, **deserialized_kwargs)
1178
+
1179
+ serialized_result = serialize_result(dataclass_function, result)
1180
+ deserialized_result = deserialize_result(dataclass_function, serialized_result)
1181
+
1182
+ assert isinstance(deserialized_result, SampleDataClass)
1183
+ assert deserialized_result.name == "FOO"
1184
+ assert deserialized_result.value == 2
1185
+
1186
+
1187
+ def test_serialize_deserialize_dataclass_value():
1188
+ item = SampleDataClass(name="bar", value=5)
1189
+ serialized = serialize_value(item)
1190
+ assert serialized == {"name": "bar", "value": 5}
1191
+
1192
+ deserialized = deserialize_value(serialized, SampleDataClass)
1193
+ assert isinstance(deserialized, SampleDataClass)
1194
+ assert deserialized.name == "bar"
1195
+ assert deserialized.value == 5