dbos 0.26.0a18__py3-none-any.whl → 0.26.0a21__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.
dbos/__init__.py CHANGED
@@ -5,8 +5,7 @@ from ._dbos import DBOS, DBOSConfiguredInstance, WorkflowHandle
5
5
  from ._dbos_config import ConfigFile, DBOSConfig, get_dbos_database_url, load_config
6
6
  from ._kafka_message import KafkaMessage
7
7
  from ._queue import Queue
8
- from ._sys_db import GetWorkflowsInput, WorkflowStatusString
9
- from ._workflow_commands import WorkflowStatus
8
+ from ._sys_db import GetWorkflowsInput, WorkflowStatus, WorkflowStatusString
10
9
 
11
10
  __all__ = [
12
11
  "ConfigFile",
dbos/_app_db.py CHANGED
@@ -74,9 +74,12 @@ class ApplicationDatabase:
74
74
  database["connectionTimeoutMillis"] / 1000
75
75
  )
76
76
 
77
+ pool_size = database.get("app_db_pool_size")
78
+ if pool_size is None:
79
+ pool_size = 20
77
80
  self.engine = sa.create_engine(
78
81
  app_db_url,
79
- pool_size=database["app_db_pool_size"],
82
+ pool_size=pool_size,
80
83
  max_overflow=0,
81
84
  pool_timeout=30,
82
85
  connect_args=connect_args,
dbos/_client.py CHANGED
@@ -3,6 +3,8 @@ import sys
3
3
  import uuid
4
4
  from typing import Any, Generic, List, Optional, TypedDict, TypeVar
5
5
 
6
+ from dbos._app_db import ApplicationDatabase
7
+
6
8
  if sys.version_info < (3, 11):
7
9
  from typing_extensions import NotRequired
8
10
  else:
@@ -14,11 +16,18 @@ from dbos._dbos_config import parse_database_url_to_dbconfig
14
16
  from dbos._error import DBOSNonExistentWorkflowError
15
17
  from dbos._registrations import DEFAULT_MAX_RECOVERY_ATTEMPTS
16
18
  from dbos._serialization import WorkflowInputs
17
- from dbos._sys_db import SystemDatabase, WorkflowStatusInternal, WorkflowStatusString
18
- from dbos._workflow_commands import (
19
+ from dbos._sys_db import (
20
+ StepInfo,
21
+ SystemDatabase,
19
22
  WorkflowStatus,
23
+ WorkflowStatusInternal,
24
+ WorkflowStatusString,
25
+ )
26
+ from dbos._workflow_commands import (
27
+ fork_workflow,
20
28
  get_workflow,
21
29
  list_queued_workflows,
30
+ list_workflow_steps,
22
31
  list_workflows,
23
32
  )
24
33
 
@@ -45,7 +54,7 @@ class WorkflowHandleClientPolling(Generic[R]):
45
54
  res: R = self._sys_db.await_workflow_result(self.workflow_id)
46
55
  return res
47
56
 
48
- def get_status(self) -> "WorkflowStatus":
57
+ def get_status(self) -> WorkflowStatus:
49
58
  status = get_workflow(self._sys_db, self.workflow_id, True)
50
59
  if status is None:
51
60
  raise DBOSNonExistentWorkflowError(self.workflow_id)
@@ -67,7 +76,7 @@ class WorkflowHandleClientAsyncPolling(Generic[R]):
67
76
  )
68
77
  return res
69
78
 
70
- async def get_status(self) -> "WorkflowStatus":
79
+ async def get_status(self) -> WorkflowStatus:
71
80
  status = await asyncio.to_thread(
72
81
  get_workflow, self._sys_db, self.workflow_id, True
73
82
  )
@@ -82,6 +91,7 @@ class DBOSClient:
82
91
  if system_database is not None:
83
92
  db_config["sys_db_name"] = system_database
84
93
  self._sys_db = SystemDatabase(db_config)
94
+ self._app_db = ApplicationDatabase(db_config)
85
95
 
86
96
  def destroy(self) -> None:
87
97
  self._sys_db.destroy()
@@ -124,7 +134,9 @@ class DBOSClient:
124
134
  "kwargs": kwargs,
125
135
  }
126
136
 
127
- self._sys_db.init_workflow(status, _serialization.serialize_args(inputs))
137
+ self._sys_db.init_workflow(
138
+ status, _serialization.serialize_args(inputs), max_recovery_attempts=None
139
+ )
128
140
  return workflow_id
129
141
 
130
142
  def enqueue(
@@ -180,7 +192,9 @@ class DBOSClient:
180
192
  "app_version": None,
181
193
  }
182
194
  with self._sys_db.engine.begin() as conn:
183
- self._sys_db.insert_workflow_status(status, conn)
195
+ self._sys_db.insert_workflow_status(
196
+ status, conn, max_recovery_attempts=None
197
+ )
184
198
  self._sys_db.send(status["workflow_uuid"], 0, destination_id, message, topic)
185
199
 
186
200
  async def send_async(
@@ -321,3 +335,23 @@ class DBOSClient:
321
335
  offset=offset,
322
336
  sort_desc=sort_desc,
323
337
  )
338
+
339
+ def list_workflow_steps(self, workflow_id: str) -> List[StepInfo]:
340
+ return list_workflow_steps(self._sys_db, self._app_db, workflow_id)
341
+
342
+ async def list_workflow_steps_async(self, workflow_id: str) -> List[StepInfo]:
343
+ return await asyncio.to_thread(self.list_workflow_steps, workflow_id)
344
+
345
+ def fork_workflow(self, workflow_id: str, start_step: int) -> WorkflowHandle[R]:
346
+ forked_workflow_id = fork_workflow(
347
+ self._sys_db, self._app_db, workflow_id, start_step
348
+ )
349
+ return WorkflowHandleClientPolling[R](forked_workflow_id, self._sys_db)
350
+
351
+ async def fork_workflow_async(
352
+ self, workflow_id: str, start_step: int
353
+ ) -> WorkflowHandleAsync[R]:
354
+ forked_workflow_id = await asyncio.to_thread(
355
+ fork_workflow, self._sys_db, self._app_db, workflow_id, start_step
356
+ )
357
+ return WorkflowHandleClientAsyncPolling[R](forked_workflow_id, self._sys_db)
@@ -3,8 +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
7
- from dbos._workflow_commands import WorkflowStatus
6
+ from dbos._sys_db import StepInfo, WorkflowStatus
8
7
 
9
8
 
10
9
  class MessageType(str, Enum):
dbos/_core.py CHANGED
@@ -75,6 +75,7 @@ from ._serialization import WorkflowInputs
75
75
  from ._sys_db import (
76
76
  GetEventWorkflowContext,
77
77
  OperationResultInternal,
78
+ WorkflowStatus,
78
79
  WorkflowStatusInternal,
79
80
  WorkflowStatusString,
80
81
  )
@@ -87,7 +88,6 @@ if TYPE_CHECKING:
87
88
  DBOSRegistry,
88
89
  IsolationLevel,
89
90
  )
90
- from ._workflow_commands import WorkflowStatus
91
91
 
92
92
  from sqlalchemy.exc import DBAPIError, InvalidRequestError
93
93
 
@@ -119,7 +119,7 @@ class WorkflowHandleFuture(Generic[R]):
119
119
  self.dbos._sys_db.record_get_result(self.workflow_id, serialized_r, None)
120
120
  return r
121
121
 
122
- def get_status(self) -> "WorkflowStatus":
122
+ def get_status(self) -> WorkflowStatus:
123
123
  stat = self.dbos.get_workflow_status(self.workflow_id)
124
124
  if stat is None:
125
125
  raise DBOSNonExistentWorkflowError(self.workflow_id)
@@ -146,7 +146,7 @@ class WorkflowHandlePolling(Generic[R]):
146
146
  self.dbos._sys_db.record_get_result(self.workflow_id, serialized_r, None)
147
147
  return r
148
148
 
149
- def get_status(self) -> "WorkflowStatus":
149
+ def get_status(self) -> WorkflowStatus:
150
150
  stat = self.dbos.get_workflow_status(self.workflow_id)
151
151
  if stat is None:
152
152
  raise DBOSNonExistentWorkflowError(self.workflow_id)
@@ -181,7 +181,7 @@ class WorkflowHandleAsyncTask(Generic[R]):
181
181
  )
182
182
  return r
183
183
 
184
- async def get_status(self) -> "WorkflowStatus":
184
+ async def get_status(self) -> WorkflowStatus:
185
185
  stat = await asyncio.to_thread(self.dbos.get_workflow_status, self.workflow_id)
186
186
  if stat is None:
187
187
  raise DBOSNonExistentWorkflowError(self.workflow_id)
@@ -217,7 +217,7 @@ class WorkflowHandleAsyncPolling(Generic[R]):
217
217
  )
218
218
  return r
219
219
 
220
- async def get_status(self) -> "WorkflowStatus":
220
+ async def get_status(self) -> WorkflowStatus:
221
221
  stat = await asyncio.to_thread(self.dbos.get_workflow_status, self.workflow_id)
222
222
  if stat is None:
223
223
  raise DBOSNonExistentWorkflowError(self.workflow_id)
@@ -232,8 +232,8 @@ def _init_workflow(
232
232
  class_name: Optional[str],
233
233
  config_name: Optional[str],
234
234
  temp_wf_type: Optional[str],
235
- queue: Optional[str] = None,
236
- max_recovery_attempts: int = DEFAULT_MAX_RECOVERY_ATTEMPTS,
235
+ queue: Optional[str],
236
+ max_recovery_attempts: Optional[int],
237
237
  ) -> WorkflowStatusInternal:
238
238
  wfid = (
239
239
  ctx.workflow_id
@@ -653,7 +653,7 @@ else:
653
653
  def workflow_wrapper(
654
654
  dbosreg: "DBOSRegistry",
655
655
  func: Callable[P, R],
656
- max_recovery_attempts: int = DEFAULT_MAX_RECOVERY_ATTEMPTS,
656
+ max_recovery_attempts: Optional[int] = DEFAULT_MAX_RECOVERY_ATTEMPTS,
657
657
  ) -> Callable[P, R]:
658
658
  func.__orig_func = func # type: ignore
659
659
 
@@ -718,6 +718,7 @@ def workflow_wrapper(
718
718
  class_name=get_dbos_class_name(fi, func, args),
719
719
  config_name=get_config_name(fi, func, args),
720
720
  temp_wf_type=get_temp_workflow_type(func),
721
+ queue=None,
721
722
  max_recovery_attempts=max_recovery_attempts,
722
723
  )
723
724
 
@@ -765,7 +766,7 @@ def workflow_wrapper(
765
766
 
766
767
 
767
768
  def decorate_workflow(
768
- reg: "DBOSRegistry", max_recovery_attempts: int
769
+ reg: "DBOSRegistry", max_recovery_attempts: Optional[int]
769
770
  ) -> Callable[[Callable[P, R]], Callable[P, R]]:
770
771
  def _workflow_decorator(func: Callable[P, R]) -> Callable[P, R]:
771
772
  wrapped_func = workflow_wrapper(reg, func, max_recovery_attempts)
dbos/_dbos.py CHANGED
@@ -31,9 +31,10 @@ from typing import (
31
31
  from opentelemetry.trace import Span
32
32
 
33
33
  from dbos._conductor.conductor import ConductorWebsocket
34
+ from dbos._sys_db import WorkflowStatus
34
35
  from dbos._utils import INTERNAL_QUEUE_NAME, GlobalParams
35
36
  from dbos._workflow_commands import (
36
- WorkflowStatus,
37
+ fork_workflow,
37
38
  list_queued_workflows,
38
39
  list_workflows,
39
40
  )
@@ -67,7 +68,7 @@ from ._registrations import (
67
68
  )
68
69
  from ._roles import default_required_roles, required_roles
69
70
  from ._scheduler import ScheduledWorkflow, scheduled
70
- from ._sys_db import reset_system_database
71
+ from ._sys_db import StepInfo, WorkflowStatus, reset_system_database
71
72
  from ._tracer import dbos_tracer
72
73
 
73
74
  if TYPE_CHECKING:
@@ -113,7 +114,7 @@ from ._error import (
113
114
  from ._event_loop import BackgroundEventLoop
114
115
  from ._logger import add_otlp_to_all_loggers, config_logger, dbos_logger, init_logger
115
116
  from ._sys_db import SystemDatabase
116
- from ._workflow_commands import WorkflowStatus, get_workflow
117
+ from ._workflow_commands import get_workflow, list_workflow_steps
117
118
 
118
119
  # Most DBOS functions are just any callable F, so decorators / wrappers work on F
119
120
  # There are cases where the parameters P and return value R should be separate
@@ -599,7 +600,7 @@ class DBOS:
599
600
  # Decorators for DBOS functionality
600
601
  @classmethod
601
602
  def workflow(
602
- cls, *, max_recovery_attempts: int = DEFAULT_MAX_RECOVERY_ATTEMPTS
603
+ cls, *, max_recovery_attempts: Optional[int] = DEFAULT_MAX_RECOVERY_ATTEMPTS
603
604
  ) -> Callable[[Callable[P, R]], Callable[P, R]]:
604
605
  """Decorate a function for use as a DBOS workflow."""
605
606
  return decorate_workflow(_get_or_create_dbos_registry(), max_recovery_attempts)
@@ -959,40 +960,19 @@ class DBOS:
959
960
  @classmethod
960
961
  def restart_workflow(cls, workflow_id: str) -> WorkflowHandle[Any]:
961
962
  """Restart a workflow with a new workflow ID"""
962
-
963
963
  return cls.fork_workflow(workflow_id, 1)
964
964
 
965
965
  @classmethod
966
- def fork_workflow(
967
- cls, workflow_id: str, start_step: int = 1
968
- ) -> WorkflowHandle[Any]:
969
- """Restart a workflow with a new workflow ID"""
970
-
971
- def get_max_function_id(workflow_uuid: str) -> int:
972
- max_transactions = (
973
- _get_dbos_instance()._app_db.get_max_function_id(workflow_uuid) or 0
974
- )
975
- max_operations = (
976
- _get_dbos_instance()._sys_db.get_max_function_id(workflow_uuid) or 0
977
- )
978
- return max(max_transactions, max_operations)
979
-
980
- max_function_id = get_max_function_id(workflow_id)
981
- if max_function_id > 0 and start_step > max_function_id:
982
- raise DBOSException(
983
- f"Cannot fork workflow {workflow_id} at step {start_step}. The workflow has {max_function_id} steps."
984
- )
966
+ def fork_workflow(cls, workflow_id: str, start_step: int) -> WorkflowHandle[Any]:
967
+ """Restart a workflow with a new workflow ID from a specific step"""
985
968
 
986
969
  def fn() -> str:
987
- forked_workflow_id = str(uuid.uuid4())
988
970
  dbos_logger.info(f"Forking workflow: {workflow_id} from step {start_step}")
989
-
990
- _get_dbos_instance()._app_db.clone_workflow_transactions(
991
- workflow_id, forked_workflow_id, start_step
992
- )
993
-
994
- return _get_dbos_instance()._sys_db.fork_workflow(
995
- workflow_id, forked_workflow_id, start_step
971
+ return fork_workflow(
972
+ _get_dbos_instance()._sys_db,
973
+ _get_dbos_instance()._app_db,
974
+ workflow_id,
975
+ start_step,
996
976
  )
997
977
 
998
978
  new_id = _get_dbos_instance()._sys_db.call_function_as_step(
@@ -1066,6 +1046,17 @@ class DBOS:
1066
1046
  fn, "DBOS.listQueuedWorkflows"
1067
1047
  )
1068
1048
 
1049
+ @classmethod
1050
+ def list_workflow_steps(cls, workflow_id: str) -> List[StepInfo]:
1051
+ def fn() -> List[StepInfo]:
1052
+ return list_workflow_steps(
1053
+ _get_dbos_instance()._sys_db, _get_dbos_instance()._app_db, workflow_id
1054
+ )
1055
+
1056
+ return _get_dbos_instance()._sys_db.call_function_as_step(
1057
+ fn, "DBOS.listWorkflowSteps"
1058
+ )
1059
+
1069
1060
  @classproperty
1070
1061
  def logger(cls) -> Logger:
1071
1062
  """Return the DBOS `Logger` for the current context."""
dbos/_registrations.py CHANGED
@@ -51,7 +51,7 @@ class DBOSFuncInfo:
51
51
  class_info: Optional[DBOSClassInfo] = None
52
52
  func_type: DBOSFuncType = DBOSFuncType.Unknown
53
53
  required_roles: Optional[List[str]] = None
54
- max_recovery_attempts: int = DEFAULT_MAX_RECOVERY_ATTEMPTS
54
+ max_recovery_attempts: Optional[int] = DEFAULT_MAX_RECOVERY_ATTEMPTS
55
55
 
56
56
 
57
57
  def get_or_create_class_info(
dbos/_sys_db.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import datetime
2
+ import json
2
3
  import logging
3
4
  import os
4
5
  import re
@@ -64,6 +65,50 @@ WorkflowStatuses = Literal[
64
65
  ]
65
66
 
66
67
 
68
+ class WorkflowStatus:
69
+ # The workflow ID
70
+ workflow_id: str
71
+ # The workflow status. Must be one of ENQUEUED, PENDING, SUCCESS, ERROR, CANCELLED, or RETRIES_EXCEEDED
72
+ status: str
73
+ # The name of the workflow function
74
+ name: str
75
+ # The name of the workflow's class, if any
76
+ class_name: Optional[str]
77
+ # The name with which the workflow's class instance was configured, if any
78
+ config_name: Optional[str]
79
+ # The user who ran the workflow, if specified
80
+ authenticated_user: Optional[str]
81
+ # The role with which the workflow ran, if specified
82
+ assumed_role: Optional[str]
83
+ # All roles which the authenticated user could assume
84
+ authenticated_roles: Optional[list[str]]
85
+ # The deserialized workflow input object
86
+ input: Optional[_serialization.WorkflowInputs]
87
+ # The workflow's output, if any
88
+ output: Optional[Any] = None
89
+ # The error the workflow threw, if any
90
+ error: Optional[Exception] = None
91
+ # Workflow start time, as a Unix epoch timestamp in ms
92
+ created_at: Optional[int]
93
+ # Last time the workflow status was updated, as a Unix epoch timestamp in ms
94
+ updated_at: Optional[int]
95
+ # If this workflow was enqueued, on which queue
96
+ queue_name: Optional[str]
97
+ # The executor to most recently executed this workflow
98
+ executor_id: Optional[str]
99
+ # The application version on which this workflow was started
100
+ app_version: Optional[str]
101
+
102
+ # INTERNAL FIELDS
103
+
104
+ # The ID of the application executing this workflow
105
+ app_id: Optional[str]
106
+ # The number of times this workflow's execution has been attempted
107
+ recovery_attempts: Optional[int]
108
+ # The HTTP request that triggered the workflow, if known
109
+ request: Optional[str]
110
+
111
+
67
112
  class WorkflowStatusInternal(TypedDict):
68
113
  workflow_uuid: str
69
114
  status: WorkflowStatuses
@@ -148,11 +193,6 @@ class GetQueuedWorkflowsInput(TypedDict):
148
193
  sort_desc: Optional[bool] # Sort by created_at in DESC or ASC order
149
194
 
150
195
 
151
- class GetWorkflowsOutput:
152
- def __init__(self, workflow_uuids: List[str]):
153
- self.workflow_uuids = workflow_uuids
154
-
155
-
156
196
  class GetPendingWorkflowsOutput:
157
197
  def __init__(self, *, workflow_uuid: str, queue_name: Optional[str] = None):
158
198
  self.workflow_uuid: str = workflow_uuid
@@ -287,7 +327,7 @@ class SystemDatabase:
287
327
  status: WorkflowStatusInternal,
288
328
  conn: sa.Connection,
289
329
  *,
290
- max_recovery_attempts: int = DEFAULT_MAX_RECOVERY_ATTEMPTS,
330
+ max_recovery_attempts: Optional[int],
291
331
  ) -> WorkflowStatuses:
292
332
  if self._debug_mode:
293
333
  raise Exception("called insert_workflow_status in debug mode")
@@ -354,7 +394,11 @@ class SystemDatabase:
354
394
 
355
395
  # Every time we start executing a workflow (and thus attempt to insert its status), we increment `recovery_attempts` by 1.
356
396
  # When this number becomes equal to `maxRetries + 1`, we mark the workflow as `RETRIES_EXCEEDED`.
357
- if recovery_attempts > max_recovery_attempts + 1:
397
+ if (
398
+ (wf_status != "SUCCESS" and wf_status != "ERROR")
399
+ and max_recovery_attempts is not None
400
+ and recovery_attempts > max_recovery_attempts + 1
401
+ ):
358
402
  delete_cmd = sa.delete(SystemSchema.workflow_queue).where(
359
403
  SystemSchema.workflow_queue.c.workflow_uuid
360
404
  == status["workflow_uuid"]
@@ -702,8 +746,37 @@ class SystemDatabase:
702
746
  )
703
747
  return inputs
704
748
 
705
- def get_workflows(self, input: GetWorkflowsInput) -> GetWorkflowsOutput:
706
- query = sa.select(SystemSchema.workflow_status.c.workflow_uuid)
749
+ def get_workflows(
750
+ self, input: GetWorkflowsInput, get_request: bool = False
751
+ ) -> List[WorkflowStatus]:
752
+ """
753
+ Retrieve a list of workflows result and inputs based on the input criteria. The result is a list of external-facing workflow status objects.
754
+ """
755
+ query = sa.select(
756
+ SystemSchema.workflow_status.c.workflow_uuid,
757
+ SystemSchema.workflow_status.c.status,
758
+ SystemSchema.workflow_status.c.name,
759
+ SystemSchema.workflow_status.c.request,
760
+ SystemSchema.workflow_status.c.recovery_attempts,
761
+ SystemSchema.workflow_status.c.config_name,
762
+ SystemSchema.workflow_status.c.class_name,
763
+ SystemSchema.workflow_status.c.authenticated_user,
764
+ SystemSchema.workflow_status.c.authenticated_roles,
765
+ SystemSchema.workflow_status.c.assumed_role,
766
+ SystemSchema.workflow_status.c.queue_name,
767
+ SystemSchema.workflow_status.c.executor_id,
768
+ SystemSchema.workflow_status.c.created_at,
769
+ SystemSchema.workflow_status.c.updated_at,
770
+ SystemSchema.workflow_status.c.application_version,
771
+ SystemSchema.workflow_status.c.application_id,
772
+ SystemSchema.workflow_inputs.c.inputs,
773
+ SystemSchema.workflow_status.c.output,
774
+ SystemSchema.workflow_status.c.error,
775
+ ).join(
776
+ SystemSchema.workflow_inputs,
777
+ SystemSchema.workflow_status.c.workflow_uuid
778
+ == SystemSchema.workflow_inputs.c.workflow_uuid,
779
+ )
707
780
  if input.sort_desc:
708
781
  query = query.order_by(SystemSchema.workflow_status.c.created_at.desc())
709
782
  else:
@@ -749,18 +822,76 @@ class SystemDatabase:
749
822
 
750
823
  with self.engine.begin() as c:
751
824
  rows = c.execute(query)
752
- workflow_ids = [row[0] for row in rows]
753
825
 
754
- return GetWorkflowsOutput(workflow_ids)
826
+ infos: List[WorkflowStatus] = []
827
+ for row in rows:
828
+ info = WorkflowStatus()
829
+ info.workflow_id = row[0]
830
+ info.status = row[1]
831
+ info.name = row[2]
832
+ info.request = row[3] if get_request else None
833
+ info.recovery_attempts = row[4]
834
+ info.config_name = row[5]
835
+ info.class_name = row[6]
836
+ info.authenticated_user = row[7]
837
+ info.authenticated_roles = (
838
+ json.loads(row[8]) if row[8] is not None else None
839
+ )
840
+ info.assumed_role = row[9]
841
+ info.queue_name = row[10]
842
+ info.executor_id = row[11]
843
+ info.created_at = row[12]
844
+ info.updated_at = row[13]
845
+ info.app_version = row[14]
846
+ info.app_id = row[15]
847
+
848
+ inputs = _serialization.deserialize_args(row[16])
849
+ if inputs is not None:
850
+ info.input = inputs
851
+ if info.status == WorkflowStatusString.SUCCESS.value:
852
+ info.output = _serialization.deserialize(row[17])
853
+ elif info.status == WorkflowStatusString.ERROR.value:
854
+ info.error = _serialization.deserialize_exception(row[18])
855
+
856
+ infos.append(info)
857
+ return infos
755
858
 
756
859
  def get_queued_workflows(
757
- self, input: GetQueuedWorkflowsInput
758
- ) -> GetWorkflowsOutput:
759
-
760
- query = sa.select(SystemSchema.workflow_queue.c.workflow_uuid).join(
761
- SystemSchema.workflow_status,
762
- SystemSchema.workflow_queue.c.workflow_uuid
763
- == SystemSchema.workflow_status.c.workflow_uuid,
860
+ self, input: GetQueuedWorkflowsInput, get_request: bool = False
861
+ ) -> List[WorkflowStatus]:
862
+ """
863
+ Retrieve a list of queued workflows result and inputs based on the input criteria. The result is a list of external-facing workflow status objects.
864
+ """
865
+ query = sa.select(
866
+ SystemSchema.workflow_status.c.workflow_uuid,
867
+ SystemSchema.workflow_status.c.status,
868
+ SystemSchema.workflow_status.c.name,
869
+ SystemSchema.workflow_status.c.request,
870
+ SystemSchema.workflow_status.c.recovery_attempts,
871
+ SystemSchema.workflow_status.c.config_name,
872
+ SystemSchema.workflow_status.c.class_name,
873
+ SystemSchema.workflow_status.c.authenticated_user,
874
+ SystemSchema.workflow_status.c.authenticated_roles,
875
+ SystemSchema.workflow_status.c.assumed_role,
876
+ SystemSchema.workflow_status.c.queue_name,
877
+ SystemSchema.workflow_status.c.executor_id,
878
+ SystemSchema.workflow_status.c.created_at,
879
+ SystemSchema.workflow_status.c.updated_at,
880
+ SystemSchema.workflow_status.c.application_version,
881
+ SystemSchema.workflow_status.c.application_id,
882
+ SystemSchema.workflow_inputs.c.inputs,
883
+ SystemSchema.workflow_status.c.output,
884
+ SystemSchema.workflow_status.c.error,
885
+ ).select_from(
886
+ SystemSchema.workflow_queue.join(
887
+ SystemSchema.workflow_status,
888
+ SystemSchema.workflow_queue.c.workflow_uuid
889
+ == SystemSchema.workflow_status.c.workflow_uuid,
890
+ ).join(
891
+ SystemSchema.workflow_inputs,
892
+ SystemSchema.workflow_queue.c.workflow_uuid
893
+ == SystemSchema.workflow_inputs.c.workflow_uuid,
894
+ )
764
895
  )
765
896
  if input["sort_desc"]:
766
897
  query = query.order_by(SystemSchema.workflow_status.c.created_at.desc())
@@ -797,9 +928,40 @@ class SystemDatabase:
797
928
 
798
929
  with self.engine.begin() as c:
799
930
  rows = c.execute(query)
800
- workflow_uuids = [row[0] for row in rows]
801
931
 
802
- return GetWorkflowsOutput(workflow_uuids)
932
+ infos: List[WorkflowStatus] = []
933
+ for row in rows:
934
+ info = WorkflowStatus()
935
+ info.workflow_id = row[0]
936
+ info.status = row[1]
937
+ info.name = row[2]
938
+ info.request = row[3] if get_request else None
939
+ info.recovery_attempts = row[4]
940
+ info.config_name = row[5]
941
+ info.class_name = row[6]
942
+ info.authenticated_user = row[7]
943
+ info.authenticated_roles = (
944
+ json.loads(row[8]) if row[8] is not None else None
945
+ )
946
+ info.assumed_role = row[9]
947
+ info.queue_name = row[10]
948
+ info.executor_id = row[11]
949
+ info.created_at = row[12]
950
+ info.updated_at = row[13]
951
+ info.app_version = row[14]
952
+ info.app_id = row[15]
953
+
954
+ inputs = _serialization.deserialize_args(row[16])
955
+ if inputs is not None:
956
+ info.input = inputs
957
+ if info.status == WorkflowStatusString.SUCCESS.value:
958
+ info.output = _serialization.deserialize(row[17])
959
+ elif info.status == WorkflowStatusString.ERROR.value:
960
+ info.error = _serialization.deserialize_exception(row[18])
961
+
962
+ infos.append(info)
963
+
964
+ return infos
803
965
 
804
966
  def get_pending_workflows(
805
967
  self, executor_id: str, app_version: str
@@ -1658,7 +1820,7 @@ class SystemDatabase:
1658
1820
  status: WorkflowStatusInternal,
1659
1821
  inputs: str,
1660
1822
  *,
1661
- max_recovery_attempts: int = DEFAULT_MAX_RECOVERY_ATTEMPTS,
1823
+ max_recovery_attempts: Optional[int],
1662
1824
  ) -> WorkflowStatuses:
1663
1825
  """
1664
1826
  Synchronously record the status and inputs for workflows in a single transaction
@@ -1,62 +1,18 @@
1
- import json
2
1
  import uuid
3
- from typing import Any, List, Optional
2
+ from typing import List, Optional
3
+
4
+ from dbos._error import DBOSException
4
5
 
5
- from . import _serialization
6
6
  from ._app_db import ApplicationDatabase
7
7
  from ._sys_db import (
8
8
  GetQueuedWorkflowsInput,
9
9
  GetWorkflowsInput,
10
- GetWorkflowsOutput,
11
10
  StepInfo,
12
11
  SystemDatabase,
12
+ WorkflowStatus,
13
13
  )
14
14
 
15
15
 
16
- class WorkflowStatus:
17
- # The workflow ID
18
- workflow_id: str
19
- # The workflow status. Must be one of ENQUEUED, PENDING, SUCCESS, ERROR, CANCELLED, or RETRIES_EXCEEDED
20
- status: str
21
- # The name of the workflow function
22
- name: str
23
- # The name of the workflow's class, if any
24
- class_name: Optional[str]
25
- # The name with which the workflow's class instance was configured, if any
26
- config_name: Optional[str]
27
- # The user who ran the workflow, if specified
28
- authenticated_user: Optional[str]
29
- # The role with which the workflow ran, if specified
30
- assumed_role: Optional[str]
31
- # All roles which the authenticated user could assume
32
- authenticated_roles: Optional[list[str]]
33
- # The deserialized workflow input object
34
- input: Optional[_serialization.WorkflowInputs]
35
- # The workflow's output, if any
36
- output: Optional[Any] = None
37
- # The error the workflow threw, if any
38
- error: Optional[Exception] = None
39
- # Workflow start time, as a Unix epoch timestamp in ms
40
- created_at: Optional[int]
41
- # Last time the workflow status was updated, as a Unix epoch timestamp in ms
42
- updated_at: Optional[int]
43
- # If this workflow was enqueued, on which queue
44
- queue_name: Optional[str]
45
- # The executor to most recently executed this workflow
46
- executor_id: Optional[str]
47
- # The application version on which this workflow was started
48
- app_version: Optional[str]
49
-
50
- # INTERNAL FIELDS
51
-
52
- # The ID of the application executing this workflow
53
- app_id: Optional[str]
54
- # The number of times this workflow's execution has been attempted
55
- recovery_attempts: Optional[int]
56
- # The HTTP request that triggered the workflow, if known
57
- request: Optional[str]
58
-
59
-
60
16
  def list_workflows(
61
17
  sys_db: SystemDatabase,
62
18
  *,
@@ -86,12 +42,8 @@ def list_workflows(
86
42
  input.sort_desc = sort_desc
87
43
  input.workflow_id_prefix = workflow_id_prefix
88
44
 
89
- output: GetWorkflowsOutput = sys_db.get_workflows(input)
90
- infos: List[WorkflowStatus] = []
91
- for workflow_id in output.workflow_uuids:
92
- info = get_workflow(sys_db, workflow_id, request) # Call the method for each ID
93
- if info is not None:
94
- infos.append(info)
45
+ infos: List[WorkflowStatus] = sys_db.get_workflows(input, request)
46
+
95
47
  return infos
96
48
 
97
49
 
@@ -118,63 +70,22 @@ def list_queued_workflows(
118
70
  "offset": offset,
119
71
  "sort_desc": sort_desc,
120
72
  }
121
- output: GetWorkflowsOutput = sys_db.get_queued_workflows(input)
122
- infos: List[WorkflowStatus] = []
123
- for workflow_id in output.workflow_uuids:
124
- info = get_workflow(sys_db, workflow_id, request) # Call the method for each ID
125
- if info is not None:
126
- infos.append(info)
73
+
74
+ infos: List[WorkflowStatus] = sys_db.get_queued_workflows(input, request)
127
75
  return infos
128
76
 
129
77
 
130
78
  def get_workflow(
131
79
  sys_db: SystemDatabase, workflow_id: str, get_request: bool
132
80
  ) -> Optional[WorkflowStatus]:
81
+ input = GetWorkflowsInput()
82
+ input.workflow_ids = [workflow_id]
133
83
 
134
- internal_status = sys_db.get_workflow_status(workflow_id)
135
- if internal_status is None:
84
+ infos: List[WorkflowStatus] = sys_db.get_workflows(input, get_request)
85
+ if not infos:
136
86
  return None
137
87
 
138
- info = WorkflowStatus()
139
-
140
- info.workflow_id = workflow_id
141
- info.status = internal_status["status"]
142
- info.name = internal_status["name"]
143
- info.class_name = internal_status["class_name"]
144
- info.config_name = internal_status["config_name"]
145
- info.authenticated_user = internal_status["authenticated_user"]
146
- info.assumed_role = internal_status["assumed_role"]
147
- info.authenticated_roles = (
148
- json.loads(internal_status["authenticated_roles"])
149
- if internal_status["authenticated_roles"] is not None
150
- else None
151
- )
152
- info.request = internal_status["request"]
153
- info.created_at = internal_status["created_at"]
154
- info.updated_at = internal_status["updated_at"]
155
- info.queue_name = internal_status["queue_name"]
156
- info.executor_id = internal_status["executor_id"]
157
- info.app_version = internal_status["app_version"]
158
- info.app_id = internal_status["app_id"]
159
- info.recovery_attempts = internal_status["recovery_attempts"]
160
-
161
- input_data = sys_db.get_workflow_inputs(workflow_id)
162
- if input_data is not None:
163
- info.input = input_data
164
-
165
- if internal_status.get("status") == "SUCCESS":
166
- result = sys_db.await_workflow_result(workflow_id)
167
- info.output = result
168
- elif internal_status.get("status") == "ERROR":
169
- try:
170
- sys_db.await_workflow_result(workflow_id)
171
- except Exception as e:
172
- info.error = e
173
-
174
- if not get_request:
175
- info.request = None
176
-
177
- return info
88
+ return infos[0]
178
89
 
179
90
 
180
91
  def list_workflow_steps(
@@ -185,3 +96,25 @@ def list_workflow_steps(
185
96
  merged_steps = steps + transactions
186
97
  merged_steps.sort(key=lambda step: step["function_id"])
187
98
  return merged_steps
99
+
100
+
101
+ def fork_workflow(
102
+ sys_db: SystemDatabase,
103
+ app_db: ApplicationDatabase,
104
+ workflow_id: str,
105
+ start_step: int,
106
+ ) -> str:
107
+ def get_max_function_id(workflow_uuid: str) -> int:
108
+ max_transactions = app_db.get_max_function_id(workflow_uuid) or 0
109
+ max_operations = sys_db.get_max_function_id(workflow_uuid) or 0
110
+ return max(max_transactions, max_operations)
111
+
112
+ max_function_id = get_max_function_id(workflow_id)
113
+ if max_function_id > 0 and start_step > max_function_id:
114
+ raise DBOSException(
115
+ f"Cannot fork workflow {workflow_id} from step {start_step}. The workflow has {max_function_id} steps."
116
+ )
117
+ forked_workflow_id = str(uuid.uuid4())
118
+ app_db.clone_workflow_transactions(workflow_id, forked_workflow_id, start_step)
119
+ sys_db.fork_workflow(workflow_id, forked_workflow_id, start_step)
120
+ return forked_workflow_id
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 0.26.0a18
3
+ Version: 0.26.0a21
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -1,19 +1,19 @@
1
- dbos-0.26.0a18.dist-info/METADATA,sha256=QG3XkqovR0FvEIL1_sHK6K80-comGqhpyC1LCWUDEzA,5554
2
- dbos-0.26.0a18.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
3
- dbos-0.26.0a18.dist-info/entry_points.txt,sha256=_QOQ3tVfEjtjBlr1jS4sHqHya9lI2aIEIWkz8dqYp14,58
4
- dbos-0.26.0a18.dist-info/licenses/LICENSE,sha256=VGZit_a5-kdw9WT6fY5jxAWVwGQzgLFyPWrcVVUhVNU,1067
5
- dbos/__init__.py,sha256=3NQfGlBiiUSM_v88STdVP3rNZvGkUL_9WbSotKb8Voo,873
1
+ dbos-0.26.0a21.dist-info/METADATA,sha256=6JPLTUn5uCaKpHI_sEis2zJWOrNqsRUIQ2_4h7ZzWfw,5554
2
+ dbos-0.26.0a21.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
3
+ dbos-0.26.0a21.dist-info/entry_points.txt,sha256=_QOQ3tVfEjtjBlr1jS4sHqHya9lI2aIEIWkz8dqYp14,58
4
+ dbos-0.26.0a21.dist-info/licenses/LICENSE,sha256=VGZit_a5-kdw9WT6fY5jxAWVwGQzgLFyPWrcVVUhVNU,1067
5
+ dbos/__init__.py,sha256=VoGS7H9GVtNAnD2S4zseIEioS1dNIJXRovQ4oHlg8og,842
6
6
  dbos/__main__.py,sha256=G7Exn-MhGrVJVDbgNlpzhfh8WMX_72t3_oJaFT9Lmt8,653
7
7
  dbos/_admin_server.py,sha256=RrbABfR1D3p9c_QLrCSrgFuYce6FKi0fjMRIYLjO_Y8,9038
8
- dbos/_app_db.py,sha256=Q9lEyCJFoZMTlnjMO8Pj8bczVmVWyDOP8qPQ6l5PpEU,11241
8
+ dbos/_app_db.py,sha256=obNlgC9IZ20y8tqQeA1q4TjceG3jBFalxz70ieDOWCA,11332
9
9
  dbos/_classproperty.py,sha256=f0X-_BySzn3yFDRKB2JpCbLYQ9tLwt1XftfshvY7CBs,626
10
- dbos/_client.py,sha256=QiIR-mwRYb1ffgwGR96ICQgFORki2QpR5najtVJ2WsA,10906
10
+ dbos/_client.py,sha256=S3tejQ7xAJ9wjo1PhQ0P3UYuloDOdZqXsQwE4YjAQ8s,12124
11
11
  dbos/_conductor/conductor.py,sha256=HYzVL29IMMrs2Mnms_7cHJynCnmmEN5SDQOMjzn3UoU,16840
12
- dbos/_conductor/protocol.py,sha256=xN7pmooyF1pqbH1b6WhllU5718P7zSb_b0KCwA6bzcs,6716
12
+ dbos/_conductor/protocol.py,sha256=zEKIuOQdIaSduNqfZKpo8PSD9_1oNpKIPnBNCu3RUyE,6681
13
13
  dbos/_context.py,sha256=I8sLkdKTTkZEz7wG-MjynaQB6XEF2bLXuwNksiauP7w,19430
14
- dbos/_core.py,sha256=uxDIJui4WS_2V1k2np0Ifue_IRzLTyq-c52bgZSQYn4,45118
14
+ dbos/_core.py,sha256=SecObOKLjNinNAXDcYVMVUURHcoaPe0js-axLMMNwqY,45098
15
15
  dbos/_croniter.py,sha256=XHAyUyibs_59sJQfSNWkP7rqQY6_XrlfuuCxk4jYqek,47559
16
- dbos/_dbos.py,sha256=jtvBQOvwdXFfknx9pDHgKC4DuiH58ICAs_0NoJQMI4w,47526
16
+ dbos/_dbos.py,sha256=bbio_FjBfU__Zk1BFegfS16IrPPejFxOKm5rUg5nW1o,47185
17
17
  dbos/_dbos_config.py,sha256=m05IFjM0jSwZBsnFMF_4qP2JkjVFc0gqyM2tnotXq20,20636
18
18
  dbos/_debug.py,sha256=MNlQVZ6TscGCRQeEEL0VE8Uignvr6dPeDDDefS3xgIE,1823
19
19
  dbos/_docker_pg_helper.py,sha256=NmcgqmR5rQA_4igfeqh8ugNT2z3YmoOvuep_MEtxTiY,5854
@@ -37,7 +37,7 @@ dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py,sha256
37
37
  dbos/_outcome.py,sha256=EXxBg4jXCVJsByDQ1VOCIedmbeq_03S6d-p1vqQrLFU,6810
38
38
  dbos/_queue.py,sha256=l0g_CXJbxEmftCA9yhy-cyaR_sddfQSCfm-5XgIWzqU,3397
39
39
  dbos/_recovery.py,sha256=98Py7icfytyIELJ54gIsdvmURBvTb0HmWaxEAuYL0dc,2546
40
- dbos/_registrations.py,sha256=ZDC5lghy_1ZMdMGsSBrXSyS96DH3baA4nyCkFdUmIlc,7292
40
+ dbos/_registrations.py,sha256=EZzG3ZfYmWA2bHX2hpnSIQ3PTi3-cXsvbcmXjyOusMk,7302
41
41
  dbos/_request.py,sha256=cX1B3Atlh160phgS35gF1VEEV4pD126c9F3BDgBmxZU,929
42
42
  dbos/_roles.py,sha256=iOsgmIAf1XVzxs3gYWdGRe1B880YfOw5fpU7Jwx8_A8,2271
43
43
  dbos/_scheduler.py,sha256=SR1oRZRcVzYsj-JauV2LA8JtwTkt8mru7qf6H1AzQ1U,2027
@@ -45,7 +45,7 @@ dbos/_schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
45
  dbos/_schemas/application_database.py,sha256=SypAS9l9EsaBHFn9FR8jmnqt01M74d9AF1AMa4m2hhI,1040
46
46
  dbos/_schemas/system_database.py,sha256=W9eSpL7SZzQkxcEZ4W07BOcwkkDr35b9oCjUOgfHWek,5336
47
47
  dbos/_serialization.py,sha256=YCYv0qKAwAZ1djZisBC7khvKqG-5OcIv9t9EC5PFIog,1743
48
- dbos/_sys_db.py,sha256=kfNR9R7rQ6MTqBuPt4OI5nZElIJNXlGuUjG_ypGKHWI,71195
48
+ dbos/_sys_db.py,sha256=M3BVJVhG0YXkLhw5axSrKjBN1AOS3KmvgWEYn2l94pw,78203
49
49
  dbos/_templates/dbos-db-starter/README.md,sha256=GhxhBj42wjTt1fWEtwNriHbJuKb66Vzu89G4pxNHw2g,930
50
50
  dbos/_templates/dbos-db-starter/__package/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
51
  dbos/_templates/dbos-db-starter/__package/main.py,sha256=nJMN3ZD2lmwg4Dcgmiwqc-tQGuCJuJal2Xl85iA277U,2453
@@ -58,11 +58,11 @@ dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py,sh
58
58
  dbos/_templates/dbos-db-starter/start_postgres_docker.py,sha256=lQVLlYO5YkhGPEgPqwGc7Y8uDKse9HsWv5fynJEFJHM,1681
59
59
  dbos/_tracer.py,sha256=dFDSFlta-rfA3-ahIRLYwnnoAOmlavdxAGllqwFgnCA,2440
60
60
  dbos/_utils.py,sha256=nFRUHzVjXG5AusF85AlYHikj63Tzi-kQm992ihsrAxA,201
61
- dbos/_workflow_commands.py,sha256=hHNcW4zopgxVXWfg3flHwqZEFGYpYp8ZAfUXmqiULUk,6261
61
+ dbos/_workflow_commands.py,sha256=7wyxTfIyh2IVIqlkaTr8CMBq8yxWP3Hhddyv1YJY8zE,3576
62
62
  dbos/cli/_github_init.py,sha256=Y_bDF9gfO2jB1id4FV5h1oIxEJRWyqVjhb7bNEa5nQ0,3224
63
63
  dbos/cli/_template_init.py,sha256=-WW3kbq0W_Tq4WbMqb1UGJG3xvJb3woEY5VspG95Srk,2857
64
64
  dbos/cli/cli.py,sha256=1qCTs__A9LOEfU44XZ6TufwmRwe68ZEwbWEPli3vnVM,17873
65
65
  dbos/dbos-config.schema.json,sha256=i7jcxXqByKq0Jzv3nAUavONtj03vTwj6vWP4ylmBr8o,5694
66
66
  dbos/py.typed,sha256=QfzXT1Ktfk3Rj84akygc7_42z0lRpCq0Ilh8OXI6Zas,44
67
67
  version/__init__.py,sha256=L4sNxecRuqdtSFdpUGX3TtBi9KL3k7YsZVIvv-fv9-A,1678
68
- dbos-0.26.0a18.dist-info/RECORD,,
68
+ dbos-0.26.0a21.dist-info/RECORD,,