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,231 @@
1
+ from datetime import datetime, timedelta
2
+
3
+ from pydantic import BaseModel
4
+ from sqlmodel.ext.asyncio.session import AsyncSession
5
+
6
+ from planar.workflows.decorators import step, workflow
7
+ from planar.workflows.execution import execute
8
+ from planar.workflows.models import Workflow, WorkflowStatus
9
+ from planar.workflows.step_core import Suspend, suspend
10
+
11
+
12
+ class ModelData(BaseModel):
13
+ name: str
14
+ value: int
15
+ created_at: datetime
16
+
17
+
18
+ async def test_suspend_with_pydantic_return_value(session: AsyncSession):
19
+ """Test that a step returning a pydantic model properly deserializes after suspension."""
20
+
21
+ @step()
22
+ async def data_step() -> ModelData:
23
+ """Step that returns a pydantic model."""
24
+ return ModelData(name="test-data", value=42, created_at=datetime.now())
25
+
26
+ @workflow()
27
+ async def suspend_workflow():
28
+ # First get the pydantic model from the step
29
+ data = await data_step()
30
+
31
+ await suspend(interval=timedelta(seconds=0.1))
32
+
33
+ # After resume, verify we can still access the pydantic model's properties
34
+ # This verifies the model was properly deserialized on re-execution
35
+ return ModelData(name=data.name, value=data.value, created_at=data.created_at)
36
+
37
+ # Start the workflow
38
+ wf = await suspend_workflow.start()
39
+
40
+ # First execution should suspend
41
+ result = await execute(wf)
42
+ assert result is not None
43
+ assert isinstance(result, Suspend)
44
+
45
+ # Resume workflow after suspend
46
+ result = await execute(wf)
47
+ assert result is not None
48
+ assert isinstance(result, ModelData)
49
+ # Check that the result contains our expected data with proper types
50
+ assert result.name == "test-data"
51
+ assert result.value == 42
52
+ assert isinstance(result.created_at, datetime)
53
+
54
+ # Verify workflow completed successfully
55
+ updated_wf = await session.get(Workflow, wf.id)
56
+ assert updated_wf is not None
57
+ assert updated_wf.status == WorkflowStatus.SUCCEEDED
58
+
59
+
60
+ async def test_suspend_with_generic_model_deserialization(session: AsyncSession):
61
+ """Test deserialization of a generic model (like Agent's CompletionResponse) after workflow suspension."""
62
+
63
+ class ToolCall(BaseModel):
64
+ """Simplified version of a tool call."""
65
+
66
+ name: str
67
+ arguments: dict[str, str]
68
+
69
+ # Define a model similar to what an Agent might return
70
+ class GenericResponse[DataT](BaseModel):
71
+ """Similar structure to CompletionResponse used by Agent."""
72
+
73
+ content: DataT | None = None
74
+ tool_calls: list[ToolCall] | None = None
75
+
76
+ class ResultData(BaseModel):
77
+ """Example result data model."""
78
+
79
+ title: str
80
+ score: int
81
+ timestamp: datetime
82
+
83
+ @step()
84
+ async def generic_data_step[DataT](payload: DataT) -> GenericResponse[DataT]:
85
+ """Step that returns a generic response with a pydantic model."""
86
+ return GenericResponse[DataT](
87
+ content=payload,
88
+ tool_calls=[ToolCall(name="test_tool", arguments={"param": "value"})],
89
+ )
90
+
91
+ @workflow()
92
+ async def generic_suspend_workflow():
93
+ # Get the generic response from the step
94
+ result_data = ResultData(
95
+ title="Test Generic Response",
96
+ score=95,
97
+ timestamp=datetime(2021, 1, 1, 12, 0, 0),
98
+ )
99
+ response = await generic_data_step(result_data)
100
+
101
+ # Suspend the workflow
102
+ await suspend(interval=timedelta(seconds=0.1))
103
+
104
+ # After resuming, verify we can still access properties of the generic response
105
+ # This confirms proper deserialization of the generic model
106
+ return response
107
+
108
+ # Start the workflow
109
+ wf = await generic_suspend_workflow.start()
110
+
111
+ # First execution should suspend
112
+ result = await execute(wf)
113
+ assert result is not None
114
+ assert isinstance(result, Suspend)
115
+
116
+ # Resume workflow after suspend
117
+ final_result = await execute(wf)
118
+ assert final_result is not None
119
+ # Check against the origin type first
120
+ assert isinstance(final_result, GenericResponse)
121
+ assert final_result.content is not None
122
+ assert isinstance(final_result.content, ResultData)
123
+ assert final_result.content.title == "Test Generic Response"
124
+ assert final_result.content.score == 95
125
+ assert isinstance(final_result.content.timestamp, datetime)
126
+ assert final_result.tool_calls is not None
127
+ assert len(final_result.tool_calls) == 1
128
+ assert final_result.tool_calls[0].name == "test_tool"
129
+
130
+ # Verify workflow completed successfully
131
+ updated_wf = await session.get(Workflow, wf.id)
132
+ assert updated_wf is not None
133
+ assert updated_wf.status == WorkflowStatus.SUCCEEDED
134
+
135
+
136
+ async def test_suspend_with_generic_list_deserialization(session: AsyncSession):
137
+ """Test deserialization of a generic model containing a list after suspension."""
138
+
139
+ # Define a model similar to what an Agent might return
140
+ class GenericListResponse[DataT](BaseModel):
141
+ """Generic response containing a list."""
142
+
143
+ items: list[DataT] | None = None
144
+ description: str = ""
145
+
146
+ @step()
147
+ async def generic_list_step[DataT](
148
+ payload: list[DataT],
149
+ ) -> GenericListResponse[DataT]:
150
+ """Step that returns a generic response with a list."""
151
+ return GenericListResponse[DataT](items=payload, description="List of items")
152
+
153
+ @workflow()
154
+ async def generic_list_suspend_workflow():
155
+ # Example list data
156
+ str_list = ["apple", "banana", "cherry"]
157
+ response = await generic_list_step(str_list)
158
+
159
+ # Suspend the workflow
160
+ await suspend(interval=timedelta(seconds=0.1))
161
+
162
+ # After resuming, verify we can still access properties
163
+ return response
164
+
165
+ # Start the workflow
166
+ wf = await generic_list_suspend_workflow.start()
167
+
168
+ # First execution should suspend
169
+ result = await execute(wf)
170
+ assert result is not None
171
+ assert isinstance(result, Suspend)
172
+
173
+ # Resume workflow after suspend
174
+ final_result = await execute(wf)
175
+ assert final_result is not None
176
+ assert isinstance(final_result, GenericListResponse)
177
+ assert final_result.items is not None
178
+ assert isinstance(final_result.items, list)
179
+ assert final_result.items == ["apple", "banana", "cherry"]
180
+ assert final_result.description == "List of items"
181
+
182
+ # Verify workflow completed successfully
183
+ updated_wf = await session.get(Workflow, wf.id)
184
+ assert updated_wf is not None
185
+ assert updated_wf.status == WorkflowStatus.SUCCEEDED
186
+
187
+
188
+ async def test_suspend_with_dict_deserialization(session: AsyncSession):
189
+ """Test deserialization of a dict after suspension."""
190
+
191
+ dict_payload = {
192
+ "a": 1,
193
+ "b": "string",
194
+ "c": {"d": "nested", "e": [1, 2, 3]},
195
+ "f": [{"g": "nested_list"}],
196
+ # "h": datetime(2021, 1, 1, 12, 0, 0), # Not supported in dict
197
+ # "i": uuid.uuid4(), # Not supported in dict
198
+ "j": True,
199
+ "k": False,
200
+ "l": None,
201
+ # "m": Decimal(100), # Not supported in dict
202
+ "n": [1, 2, 3],
203
+ "o": 1.1,
204
+ }
205
+
206
+ @step()
207
+ async def dict_step(
208
+ payload: dict,
209
+ ) -> dict:
210
+ """Step that returns a generic response with a list."""
211
+ return payload
212
+
213
+ @workflow()
214
+ async def dict_suspend_workflow(dict_payload: dict):
215
+ response = await dict_step(dict_payload)
216
+
217
+ # Suspend the workflow
218
+ await suspend(interval=timedelta(seconds=0.1))
219
+
220
+ # After resuming, verify we can still access properties
221
+ return response
222
+
223
+ wf = await dict_suspend_workflow.start(dict_payload)
224
+
225
+ result = await execute(wf)
226
+ assert result is not None
227
+ assert isinstance(result, Suspend)
228
+
229
+ final_result = await execute(wf)
230
+ assert final_result is not None
231
+ assert final_result == dict_payload