dbos 0.25.0a9__py3-none-any.whl → 0.25.0a12__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.
@@ -9,7 +9,12 @@ from websockets.sync.client import connect
9
9
  from websockets.sync.connection import Connection
10
10
 
11
11
  from dbos._utils import GlobalParams
12
- from dbos._workflow_commands import get_workflow, list_queued_workflows, list_workflows
12
+ from dbos._workflow_commands import (
13
+ get_workflow,
14
+ list_queued_workflows,
15
+ list_workflow_steps,
16
+ list_workflows,
17
+ )
13
18
 
14
19
  from . import protocol as p
15
20
 
@@ -243,6 +248,32 @@ class ConductorWebsocket(threading.Thread):
243
248
  )
244
249
  )
245
250
  websocket.send(exist_pending_workflows_response.to_json())
251
+ elif msg_type == p.MessageType.LIST_STEPS:
252
+ list_steps_message = p.ListStepsRequest.from_json(message)
253
+ step_info = None
254
+ try:
255
+ step_info = list_workflow_steps(
256
+ self.dbos._sys_db,
257
+ list_steps_message.workflow_id,
258
+ )
259
+ except Exception as e:
260
+ error_message = f"Exception encountered when getting workflow {list_steps_message.workflow_id}: {traceback.format_exc()}"
261
+ self.dbos.logger.error(error_message)
262
+
263
+ list_steps_response = p.ListStepsResponse(
264
+ type=p.MessageType.LIST_STEPS,
265
+ request_id=base_message.request_id,
266
+ output=(
267
+ [
268
+ p.WorkflowSteps.from_step_info(i)
269
+ for i in step_info
270
+ ]
271
+ if step_info is not None
272
+ else None
273
+ ),
274
+ error_message=error_message,
275
+ )
276
+ websocket.send(list_steps_response.to_json())
246
277
  else:
247
278
  self.dbos.logger.warning(
248
279
  f"Unexpected message type: {msg_type}"
@@ -3,6 +3,7 @@ from dataclasses import asdict, dataclass
3
3
  from enum import Enum
4
4
  from typing import List, Optional, Type, TypedDict, TypeVar
5
5
 
6
+ from dbos._sys_db import StepInfo
6
7
  from dbos._workflow_commands import WorkflowStatus
7
8
 
8
9
 
@@ -16,6 +17,7 @@ class MessageType(str, Enum):
16
17
  RESTART = "restart"
17
18
  GET_WORKFLOW = "get_workflow"
18
19
  EXIST_PENDING_WORKFLOWS = "exist_pending_workflows"
20
+ LIST_STEPS = "list_steps"
19
21
 
20
22
 
21
23
  T = TypeVar("T", bound="BaseMessage")
@@ -176,6 +178,27 @@ class WorkflowsOutput:
176
178
  )
177
179
 
178
180
 
181
+ @dataclass
182
+ class WorkflowSteps:
183
+ function_id: int
184
+ function_name: str
185
+ output: Optional[str]
186
+ error: Optional[str]
187
+ child_workflow_id: Optional[str]
188
+
189
+ @classmethod
190
+ def from_step_info(cls, info: StepInfo) -> "WorkflowSteps":
191
+ output_str = str(info["output"]) if info["output"] is not None else None
192
+ error_str = str(info["error"]) if info["error"] is not None else None
193
+ return cls(
194
+ function_id=info["function_id"],
195
+ function_name=info["function_name"],
196
+ output=output_str,
197
+ error=error_str,
198
+ child_workflow_id=info["child_workflow_id"],
199
+ )
200
+
201
+
179
202
  @dataclass
180
203
  class ListWorkflowsRequest(BaseMessage):
181
204
  body: ListWorkflowsBody
@@ -230,3 +253,14 @@ class ExistPendingWorkflowsRequest(BaseMessage):
230
253
  class ExistPendingWorkflowsResponse(BaseMessage):
231
254
  exist: bool
232
255
  error_message: Optional[str] = None
256
+
257
+
258
+ @dataclass
259
+ class ListStepsRequest(BaseMessage):
260
+ workflow_id: str
261
+
262
+
263
+ @dataclass
264
+ class ListStepsResponse(BaseMessage):
265
+ output: Optional[List[WorkflowSteps]]
266
+ error_message: Optional[str] = None
dbos/_core.py CHANGED
@@ -108,7 +108,15 @@ class WorkflowHandleFuture(Generic[R]):
108
108
  return self.workflow_id
109
109
 
110
110
  def get_result(self) -> R:
111
- return self.future.result()
111
+ try:
112
+ r = self.future.result()
113
+ except Exception as e:
114
+ serialized_e = _serialization.serialize_exception(e)
115
+ self.dbos._sys_db.record_get_result(self.workflow_id, None, serialized_e)
116
+ raise
117
+ serialized_r = _serialization.serialize(r)
118
+ self.dbos._sys_db.record_get_result(self.workflow_id, serialized_r, None)
119
+ return r
112
120
 
113
121
  def get_status(self) -> "WorkflowStatus":
114
122
  stat = self.dbos.get_workflow_status(self.workflow_id)
@@ -127,8 +135,15 @@ class WorkflowHandlePolling(Generic[R]):
127
135
  return self.workflow_id
128
136
 
129
137
  def get_result(self) -> R:
130
- res: R = self.dbos._sys_db.await_workflow_result(self.workflow_id)
131
- return res
138
+ try:
139
+ r: R = self.dbos._sys_db.await_workflow_result(self.workflow_id)
140
+ except Exception as e:
141
+ serialized_e = _serialization.serialize_exception(e)
142
+ self.dbos._sys_db.record_get_result(self.workflow_id, None, serialized_e)
143
+ raise
144
+ serialized_r = _serialization.serialize(r)
145
+ self.dbos._sys_db.record_get_result(self.workflow_id, serialized_r, None)
146
+ return r
132
147
 
133
148
  def get_status(self) -> "WorkflowStatus":
134
149
  stat = self.dbos.get_workflow_status(self.workflow_id)
@@ -148,7 +163,22 @@ class WorkflowHandleAsyncTask(Generic[R]):
148
163
  return self.workflow_id
149
164
 
150
165
  async def get_result(self) -> R:
151
- return await self.task
166
+ try:
167
+ r = await self.task
168
+ except Exception as e:
169
+ serialized_e = _serialization.serialize_exception(e)
170
+ await asyncio.to_thread(
171
+ self.dbos._sys_db.record_get_result,
172
+ self.workflow_id,
173
+ None,
174
+ serialized_e,
175
+ )
176
+ raise
177
+ serialized_r = _serialization.serialize(r)
178
+ await asyncio.to_thread(
179
+ self.dbos._sys_db.record_get_result, self.workflow_id, serialized_r, None
180
+ )
181
+ return r
152
182
 
153
183
  async def get_status(self) -> "WorkflowStatus":
154
184
  stat = await asyncio.to_thread(self.dbos.get_workflow_status, self.workflow_id)
@@ -167,10 +197,24 @@ class WorkflowHandleAsyncPolling(Generic[R]):
167
197
  return self.workflow_id
168
198
 
169
199
  async def get_result(self) -> R:
170
- res: R = await asyncio.to_thread(
171
- self.dbos._sys_db.await_workflow_result, self.workflow_id
200
+ try:
201
+ r: R = await asyncio.to_thread(
202
+ self.dbos._sys_db.await_workflow_result, self.workflow_id
203
+ )
204
+ except Exception as e:
205
+ serialized_e = _serialization.serialize_exception(e)
206
+ await asyncio.to_thread(
207
+ self.dbos._sys_db.record_get_result,
208
+ self.workflow_id,
209
+ None,
210
+ serialized_e,
211
+ )
212
+ raise
213
+ serialized_r = _serialization.serialize(r)
214
+ await asyncio.to_thread(
215
+ self.dbos._sys_db.record_get_result, self.workflow_id, serialized_r, None
172
216
  )
173
- return res
217
+ return r
174
218
 
175
219
  async def get_status(self) -> "WorkflowStatus":
176
220
  stat = await asyncio.to_thread(self.dbos.get_workflow_status, self.workflow_id)
@@ -278,13 +322,9 @@ def _get_wf_invoke_func(
278
322
  dbos._sys_db.buffer_workflow_status(status)
279
323
  return output
280
324
  except DBOSWorkflowConflictIDError:
281
- # Retrieve the workflow handle and wait for the result.
282
- # Must use existing_workflow=False because workflow status might not be set yet for single transaction workflows.
283
- wf_handle: "WorkflowHandle[R]" = dbos.retrieve_workflow(
284
- status["workflow_uuid"], existing_workflow=False
285
- )
286
- output = wf_handle.get_result()
287
- return output
325
+ # Await the workflow result
326
+ r: R = dbos._sys_db.await_workflow_result(status["workflow_uuid"])
327
+ return r
288
328
  except DBOSWorkflowCancelledError as error:
289
329
  raise
290
330
  except Exception as error:
@@ -504,7 +544,7 @@ def start_workflow(
504
544
  ctx.parent_workflow_id,
505
545
  new_child_workflow_id,
506
546
  ctx.parent_workflow_fid,
507
- func.__name__,
547
+ get_dbos_func_name(func),
508
548
  )
509
549
 
510
550
  if not execute_workflow or (
@@ -589,7 +629,7 @@ async def start_workflow_async(
589
629
  ctx.parent_workflow_id,
590
630
  new_child_workflow_id,
591
631
  ctx.parent_workflow_fid,
592
- func.__name__,
632
+ get_dbos_func_name(func),
593
633
  )
594
634
 
595
635
  wf_status = status["status"]
@@ -631,8 +671,6 @@ def workflow_wrapper(
631
671
  ) -> Callable[P, R]:
632
672
  func.__orig_func = func # type: ignore
633
673
 
634
- funcName = func.__name__
635
-
636
674
  fi = get_or_create_func_info(func)
637
675
  fi.max_recovery_attempts = max_recovery_attempts
638
676
 
@@ -662,17 +700,22 @@ def workflow_wrapper(
662
700
 
663
701
  wfOutcome = Outcome[R].make(functools.partial(func, *args, **kwargs))
664
702
 
703
+ workflow_id = None
704
+
665
705
  def init_wf() -> Callable[[Callable[[], R]], R]:
666
706
 
667
707
  def recorded_result(
668
708
  c_wfid: str, dbos: "DBOS"
669
709
  ) -> Callable[[Callable[[], R]], R]:
670
710
  def recorded_result_inner(func: Callable[[], R]) -> R:
671
- return WorkflowHandlePolling(c_wfid, dbos).get_result()
711
+ r: R = dbos._sys_db.await_workflow_result(c_wfid)
712
+ return r
672
713
 
673
714
  return recorded_result_inner
674
715
 
675
716
  ctx = assert_current_dbos_context() # Now the child ctx
717
+ nonlocal workflow_id
718
+ workflow_id = ctx.workflow_id
676
719
 
677
720
  if ctx.has_parent():
678
721
  child_workflow_id = dbos._sys_db.check_child_workflow(
@@ -702,15 +745,33 @@ def workflow_wrapper(
702
745
  ctx.parent_workflow_id,
703
746
  ctx.workflow_id,
704
747
  ctx.parent_workflow_fid,
705
- funcName,
748
+ get_dbos_func_name(func),
706
749
  )
707
750
 
708
751
  return _get_wf_invoke_func(dbos, status)
709
752
 
753
+ def record_get_result(func: Callable[[], R]) -> R:
754
+ """
755
+ If a child workflow is invoked synchronously, this records the implicit "getResult" where the
756
+ parent retrieves the child's output. It executes in the CALLER'S context, not the workflow's.
757
+ """
758
+ try:
759
+ r = func()
760
+ except Exception as e:
761
+ serialized_e = _serialization.serialize_exception(e)
762
+ assert workflow_id is not None
763
+ dbos._sys_db.record_get_result(workflow_id, None, serialized_e)
764
+ raise
765
+ serialized_r = _serialization.serialize(r)
766
+ assert workflow_id is not None
767
+ dbos._sys_db.record_get_result(workflow_id, serialized_r, None)
768
+ return r
769
+
710
770
  outcome = (
711
771
  wfOutcome.wrap(init_wf)
712
772
  .also(DBOSAssumeRole(rr))
713
773
  .also(enterWorkflowCtxMgr(attributes))
774
+ .then(record_get_result)
714
775
  )
715
776
  return outcome() # type: ignore
716
777
 
@@ -913,7 +974,7 @@ def decorate_step(
913
974
  ) -> Callable[[Callable[P, R]], Callable[P, R]]:
914
975
  def decorator(func: Callable[P, R]) -> Callable[P, R]:
915
976
 
916
- stepName = func.__name__
977
+ stepName = func.__qualname__
917
978
 
918
979
  def invoke_step(*args: Any, **kwargs: Any) -> Any:
919
980
  if dbosreg.dbos is None:
dbos/_dbos.py CHANGED
@@ -548,6 +548,8 @@ class DBOS:
548
548
  """
549
549
  if _dbos_global_instance is not None:
550
550
  _dbos_global_instance._reset_system_database()
551
+ else:
552
+ dbos_logger.warning("reset_system_database has no effect because global DBOS object does not exist")
551
553
 
552
554
  def _reset_system_database(self) -> None:
553
555
  assert (
dbos/_sys_db.py CHANGED
@@ -33,7 +33,6 @@ from ._dbos_config import ConfigFile
33
33
  from ._error import (
34
34
  DBOSConflictingWorkflowError,
35
35
  DBOSDeadLetterQueueError,
36
- DBOSException,
37
36
  DBOSNonExistentWorkflowError,
38
37
  DBOSWorkflowConflictIDError,
39
38
  )
@@ -154,10 +153,15 @@ class GetPendingWorkflowsOutput:
154
153
 
155
154
 
156
155
  class StepInfo(TypedDict):
156
+ # The unique ID of the step in the workflow
157
157
  function_id: int
158
+ # The (fully qualified) name of the step
158
159
  function_name: str
159
- output: Optional[str] # JSON (jsonpickle)
160
- error: Optional[str] # JSON (jsonpickle)
160
+ # The step's output, if any
161
+ output: Optional[Any]
162
+ # The error the step threw, if any
163
+ error: Optional[Exception]
164
+ # If the step starts or retrieves the result of a workflow, its ID
161
165
  child_workflow_id: Optional[str]
162
166
 
163
167
 
@@ -771,8 +775,16 @@ class SystemDatabase:
771
775
  StepInfo(
772
776
  function_id=row[0],
773
777
  function_name=row[1],
774
- output=row[2], # Preserve JSON data
775
- error=row[3],
778
+ output=(
779
+ _serialization.deserialize(row[2])
780
+ if row[2] is not None
781
+ else row[2]
782
+ ),
783
+ error=(
784
+ _serialization.deserialize_exception(row[3])
785
+ if row[3] is not None
786
+ else row[3]
787
+ ),
776
788
  child_workflow_id=row[4],
777
789
  )
778
790
  for row in rows
@@ -804,6 +816,31 @@ class SystemDatabase:
804
816
  raise DBOSWorkflowConflictIDError(result["workflow_uuid"])
805
817
  raise
806
818
 
819
+ def record_get_result(
820
+ self, result_workflow_id: str, output: Optional[str], error: Optional[str]
821
+ ) -> None:
822
+ ctx = get_local_dbos_context()
823
+ # Only record get_result called in workflow functions
824
+ if ctx is None or not ctx.is_workflow():
825
+ return
826
+ ctx.function_id += 1 # Record the get_result as a step
827
+ # Because there's no corresponding check, we do nothing on conflict
828
+ # and do not raise a DBOSWorkflowConflictIDError
829
+ sql = (
830
+ pg.insert(SystemSchema.operation_outputs)
831
+ .values(
832
+ workflow_uuid=ctx.workflow_id,
833
+ function_id=ctx.function_id,
834
+ function_name="DBOS.getResult",
835
+ output=output,
836
+ error=error,
837
+ child_workflow_id=result_workflow_id,
838
+ )
839
+ .on_conflict_do_nothing()
840
+ )
841
+ with self.engine.begin() as c:
842
+ c.execute(sql)
843
+
807
844
  def record_child_workflow(
808
845
  self,
809
846
  parentUUID: str,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 0.25.0a9
3
+ Version: 0.25.0a12
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -1,7 +1,7 @@
1
- dbos-0.25.0a9.dist-info/METADATA,sha256=zE1UQqo38SY2JASBzAeKriGW_47W7Y3Z0atOx1frcVk,5553
2
- dbos-0.25.0a9.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
3
- dbos-0.25.0a9.dist-info/entry_points.txt,sha256=_QOQ3tVfEjtjBlr1jS4sHqHya9lI2aIEIWkz8dqYp14,58
4
- dbos-0.25.0a9.dist-info/licenses/LICENSE,sha256=VGZit_a5-kdw9WT6fY5jxAWVwGQzgLFyPWrcVVUhVNU,1067
1
+ dbos-0.25.0a12.dist-info/METADATA,sha256=_7a_sxE2zkGk8RlELQfYhjJ9az9uFS4FUNHA2haL3No,5554
2
+ dbos-0.25.0a12.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
3
+ dbos-0.25.0a12.dist-info/entry_points.txt,sha256=_QOQ3tVfEjtjBlr1jS4sHqHya9lI2aIEIWkz8dqYp14,58
4
+ dbos-0.25.0a12.dist-info/licenses/LICENSE,sha256=VGZit_a5-kdw9WT6fY5jxAWVwGQzgLFyPWrcVVUhVNU,1067
5
5
  dbos/__init__.py,sha256=2Ur8QyNElSVn7CeL9Ovek2Zsye8A_ZCyjb9djF-N4A4,785
6
6
  dbos/__main__.py,sha256=G7Exn-MhGrVJVDbgNlpzhfh8WMX_72t3_oJaFT9Lmt8,653
7
7
  dbos/_admin_server.py,sha256=7kguOf9jEt4vg9LO-QJdh4jYddp6Uqtrt14gh7mKA2Y,6387
@@ -10,13 +10,13 @@ dbos/_classproperty.py,sha256=f0X-_BySzn3yFDRKB2JpCbLYQ9tLwt1XftfshvY7CBs,626
10
10
  dbos/_cloudutils/authentication.py,sha256=V0fCWQN9stCkhbuuxgPTGpvuQcDqfU3KAxPAh01vKW4,5007
11
11
  dbos/_cloudutils/cloudutils.py,sha256=YC7jGsIopT0KveLsqbRpQk2KlRBk-nIRC_UCgep4f3o,7797
12
12
  dbos/_cloudutils/databases.py,sha256=_shqaqSvhY4n2ScgQ8IP5PDZvzvcx3YBKV8fj-cxhSY,8543
13
- dbos/_conductor/conductor.py,sha256=oDlRpGxLT-uLDjEX1JwTwcJiH2FzDsrOTtnrkt_X_1U,15253
14
- dbos/_conductor/protocol.py,sha256=Bj4dhbAhAfj4IrMs_8OYJda2SdjAv1lcePXXG1MejPM,5800
13
+ dbos/_conductor/conductor.py,sha256=7elKINsgl4s1Tg5DwrU-K7xQ5vQvmDAIfAvUgfwpGN0,16784
14
+ dbos/_conductor/protocol.py,sha256=xN7pmooyF1pqbH1b6WhllU5718P7zSb_b0KCwA6bzcs,6716
15
15
  dbos/_context.py,sha256=3He4w46OTFbR7h8U1MLcdaU10wNyIPBSRqzLkdggv7U,19368
16
- dbos/_core.py,sha256=b7ndBxicB66j2LW61pUWbFVAQR0RO9fAefqn9vqSh40,43303
16
+ dbos/_core.py,sha256=llBq1lW2spbRDa0ICgVdtgAxVwmGjYNtdXoOVut6xKQ,45732
17
17
  dbos/_croniter.py,sha256=XHAyUyibs_59sJQfSNWkP7rqQY6_XrlfuuCxk4jYqek,47559
18
18
  dbos/_db_wizard.py,sha256=VnMa6OL87Lc-XPDD1RnXp8NjsJE8YgiQLj3wtWAXp-8,8252
19
- dbos/_dbos.py,sha256=UP5Dl8Tp0xMMv63E7-9r6qnnQxbVKk2nMNad4XbNi10,45331
19
+ dbos/_dbos.py,sha256=YXQjNLR9SgOr2Y9R3tMBA_DkXBJPECzYRTaz-i9GNWA,45458
20
20
  dbos/_dbos_config.py,sha256=7Qm3FARP3lTKZS0gSxDHLbpaDCT30GzfyERxfCde4bc,21566
21
21
  dbos/_debug.py,sha256=mmgvLkqlrljMBBow9wk01PPur9kUf2rI_11dTJXY4gw,1822
22
22
  dbos/_error.py,sha256=B6Y9XLS1f6yrawxB2uAEYFMxFwk9BHhdxPNddKco-Fw,5399
@@ -46,7 +46,7 @@ dbos/_schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
46
  dbos/_schemas/application_database.py,sha256=KeyoPrF7hy_ODXV7QNike_VFSD74QBRfQ76D7QyE9HI,966
47
47
  dbos/_schemas/system_database.py,sha256=W9eSpL7SZzQkxcEZ4W07BOcwkkDr35b9oCjUOgfHWek,5336
48
48
  dbos/_serialization.py,sha256=YCYv0qKAwAZ1djZisBC7khvKqG-5OcIv9t9EC5PFIog,1743
49
- dbos/_sys_db.py,sha256=vA8G_r2OpX3lQr0JbxRkpIe6A5Cs7ED1wqVV1BosSf0,66759
49
+ dbos/_sys_db.py,sha256=k9fMfg6daAQB012uaPwgkiBhA0wPON8PIwY5Ki9_7Io,68208
50
50
  dbos/_templates/dbos-db-starter/README.md,sha256=GhxhBj42wjTt1fWEtwNriHbJuKb66Vzu89G4pxNHw2g,930
51
51
  dbos/_templates/dbos-db-starter/__package/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
52
  dbos/_templates/dbos-db-starter/__package/main.py,sha256=nJMN3ZD2lmwg4Dcgmiwqc-tQGuCJuJal2Xl85iA277U,2453
@@ -66,4 +66,4 @@ dbos/cli/cli.py,sha256=ut47q-R6A423je0zvBTEgwdxENagaKKoyIvyTeACFIU,15977
66
66
  dbos/dbos-config.schema.json,sha256=HtF_njVTGHLdzBGZ4OrGQz3qbPPT0Go-iwd1PgFVTNg,5847
67
67
  dbos/py.typed,sha256=QfzXT1Ktfk3Rj84akygc7_42z0lRpCq0Ilh8OXI6Zas,44
68
68
  version/__init__.py,sha256=L4sNxecRuqdtSFdpUGX3TtBi9KL3k7YsZVIvv-fv9-A,1678
69
- dbos-0.25.0a9.dist-info/RECORD,,
69
+ dbos-0.25.0a12.dist-info/RECORD,,