dbos 0.25.0a3__tar.gz → 0.25.0a8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of dbos might be problematic. Click here for more details.

Files changed (101) hide show
  1. {dbos-0.25.0a3 → dbos-0.25.0a8}/PKG-INFO +1 -1
  2. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/__init__.py +2 -1
  3. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/__main__.py +3 -0
  4. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_admin_server.py +20 -2
  5. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_conductor/conductor.py +1 -1
  6. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_conductor/protocol.py +13 -7
  7. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_context.py +48 -0
  8. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_core.py +76 -12
  9. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_dbos.py +112 -61
  10. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_fastapi.py +4 -1
  11. dbos-0.25.0a8/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +46 -0
  12. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_outcome.py +6 -2
  13. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_schemas/system_database.py +2 -0
  14. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_sys_db.py +80 -26
  15. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_templates/dbos-db-starter/__package/main.py +6 -11
  16. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +2 -4
  17. dbos-0.25.0a8/dbos/_workflow_commands.py +175 -0
  18. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/cli/_template_init.py +8 -3
  19. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/cli/cli.py +17 -1
  20. {dbos-0.25.0a3 → dbos-0.25.0a8}/pyproject.toml +1 -1
  21. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/test_admin_server.py +3 -3
  22. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/test_async.py +27 -0
  23. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/test_dbos.py +29 -4
  24. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/test_failures.py +78 -0
  25. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/test_package.py +1 -2
  26. dbos-0.25.0a8/tests/test_workflow_cmds.py +615 -0
  27. dbos-0.25.0a3/dbos/_workflow_commands.py +0 -148
  28. dbos-0.25.0a3/tests/test_workflow_cmds.py +0 -314
  29. {dbos-0.25.0a3 → dbos-0.25.0a8}/LICENSE +0 -0
  30. {dbos-0.25.0a3 → dbos-0.25.0a8}/README.md +0 -0
  31. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_app_db.py +0 -0
  32. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_classproperty.py +0 -0
  33. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_cloudutils/authentication.py +0 -0
  34. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_cloudutils/cloudutils.py +0 -0
  35. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_cloudutils/databases.py +0 -0
  36. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_croniter.py +0 -0
  37. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_db_wizard.py +0 -0
  38. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_dbos_config.py +0 -0
  39. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_debug.py +0 -0
  40. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_error.py +0 -0
  41. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_flask.py +0 -0
  42. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_kafka.py +0 -0
  43. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_kafka_message.py +0 -0
  44. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_logger.py +0 -0
  45. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_migrations/env.py +0 -0
  46. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_migrations/script.py.mako +0 -0
  47. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
  48. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  49. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  50. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  51. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  52. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  53. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  54. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_queue.py +0 -0
  55. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_recovery.py +0 -0
  56. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_registrations.py +0 -0
  57. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_request.py +0 -0
  58. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_roles.py +0 -0
  59. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_scheduler.py +0 -0
  60. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_schemas/__init__.py +0 -0
  61. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_schemas/application_database.py +0 -0
  62. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_serialization.py +0 -0
  63. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_templates/dbos-db-starter/README.md +0 -0
  64. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
  65. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
  66. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
  67. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
  68. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
  69. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
  70. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
  71. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_tracer.py +0 -0
  72. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/_utils.py +0 -0
  73. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/cli/_github_init.py +0 -0
  74. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/dbos-config.schema.json +0 -0
  75. {dbos-0.25.0a3 → dbos-0.25.0a8}/dbos/py.typed +0 -0
  76. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/__init__.py +0 -0
  77. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/atexit_no_ctor.py +0 -0
  78. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/atexit_no_launch.py +0 -0
  79. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/classdefs.py +0 -0
  80. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/conftest.py +0 -0
  81. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/more_classdefs.py +0 -0
  82. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/queuedworkflow.py +0 -0
  83. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/test_classdecorators.py +0 -0
  84. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/test_concurrency.py +0 -0
  85. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/test_config.py +0 -0
  86. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/test_croniter.py +0 -0
  87. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/test_dbwizard.py +0 -0
  88. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/test_debug.py +0 -0
  89. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/test_fastapi.py +0 -0
  90. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/test_fastapi_roles.py +0 -0
  91. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/test_flask.py +0 -0
  92. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/test_kafka.py +0 -0
  93. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/test_outcome.py +0 -0
  94. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/test_queue.py +0 -0
  95. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/test_scheduler.py +0 -0
  96. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/test_schema_migration.py +0 -0
  97. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/test_singleton.py +0 -0
  98. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/test_spans.py +0 -0
  99. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/test_sqlalchemy.py +0 -0
  100. {dbos-0.25.0a3 → dbos-0.25.0a8}/tests/test_workflow_cancel.py +0 -0
  101. {dbos-0.25.0a3 → dbos-0.25.0a8}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 0.25.0a3
3
+ Version: 0.25.0a8
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -1,10 +1,11 @@
1
1
  from . import _error as error
2
2
  from ._context import DBOSContextEnsure, DBOSContextSetAuth, SetWorkflowID
3
- from ._dbos import DBOS, DBOSConfiguredInstance, WorkflowHandle, WorkflowStatus
3
+ from ._dbos import DBOS, DBOSConfiguredInstance, WorkflowHandle
4
4
  from ._dbos_config import ConfigFile, DBOSConfig, get_dbos_database_url, load_config
5
5
  from ._kafka_message import KafkaMessage
6
6
  from ._queue import Queue
7
7
  from ._sys_db import GetWorkflowsInput, WorkflowStatusString
8
+ from ._workflow_commands import WorkflowStatus
8
9
 
9
10
  __all__ = [
10
11
  "ConfigFile",
@@ -4,6 +4,9 @@ from typing import NoReturn, Optional, Union
4
4
 
5
5
  from dbos.cli.cli import app
6
6
 
7
+ # This is used by the debugger to execute DBOS as a module.
8
+ # Never used otherwise.
9
+
7
10
 
8
11
  def main() -> NoReturn:
9
12
  # Modify sys.argv[0] to remove script or executable extensions
@@ -7,6 +7,8 @@ from functools import partial
7
7
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
8
8
  from typing import TYPE_CHECKING, Any, List, TypedDict
9
9
 
10
+ import jsonpickle # type: ignore
11
+
10
12
  from ._logger import dbos_logger
11
13
  from ._recovery import recover_pending_workflows
12
14
 
@@ -20,6 +22,7 @@ _workflow_queues_metadata_path = "/dbos-workflow-queues-metadata"
20
22
  # /workflows/:workflow_id/cancel
21
23
  # /workflows/:workflow_id/resume
22
24
  # /workflows/:workflow_id/restart
25
+ # /workflows/:workflow_id/steps
23
26
 
24
27
 
25
28
  class AdminServer:
@@ -86,8 +89,16 @@ class AdminRequestHandler(BaseHTTPRequestHandler):
86
89
  self._end_headers()
87
90
  self.wfile.write(json.dumps(queue_metadata_array).encode("utf-8"))
88
91
  else:
89
- self.send_response(404)
90
- self._end_headers()
92
+ steps_match = re.match(
93
+ r"^/workflows/(?P<workflow_id>[^/]+)/steps$", self.path
94
+ )
95
+
96
+ if steps_match:
97
+ workflow_id = steps_match.group("workflow_id")
98
+ self._handle_steps(workflow_id)
99
+ else:
100
+ self.send_response(404)
101
+ self._end_headers()
91
102
 
92
103
  def do_POST(self) -> None:
93
104
  content_length = int(
@@ -149,6 +160,13 @@ class AdminRequestHandler(BaseHTTPRequestHandler):
149
160
  self.send_response(204)
150
161
  self._end_headers()
151
162
 
163
+ def _handle_steps(self, workflow_id: str) -> None:
164
+ steps = self.dbos._sys_db.get_workflow_steps(workflow_id)
165
+ json_steps = jsonpickle.encode(steps, unpicklable=False).encode("utf-8")
166
+ self.send_response(200)
167
+ self._end_headers()
168
+ self.wfile.write(json_steps)
169
+
152
170
 
153
171
  # Be consistent with DBOS-TS response.
154
172
  class PerfUtilization(TypedDict):
@@ -203,7 +203,7 @@ class ConductorWebsocket(threading.Thread):
203
203
  info = get_workflow(
204
204
  self.dbos._sys_db,
205
205
  get_workflow_message.workflow_id,
206
- getRequest=False,
206
+ get_request=False,
207
207
  )
208
208
  except Exception as e:
209
209
  error_message = f"Exception encountered when getting workflow {get_workflow_message.workflow_id}: {traceback.format_exc()}"
@@ -3,7 +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._workflow_commands import WorkflowInformation
6
+ from dbos._workflow_commands import WorkflowStatus
7
7
 
8
8
 
9
9
  class MessageType(str, Enum):
@@ -141,27 +141,33 @@ class WorkflowsOutput:
141
141
  ExecutorID: Optional[str]
142
142
 
143
143
  @classmethod
144
- def from_workflow_information(cls, info: WorkflowInformation) -> "WorkflowsOutput":
144
+ def from_workflow_information(cls, info: WorkflowStatus) -> "WorkflowsOutput":
145
145
  # Convert fields to strings as needed
146
146
  created_at_str = str(info.created_at) if info.created_at is not None else None
147
147
  updated_at_str = str(info.updated_at) if info.updated_at is not None else None
148
148
  inputs_str = str(info.input) if info.input is not None else None
149
149
  outputs_str = str(info.output) if info.output is not None else None
150
+ error_str = str(info.error) if info.error is not None else None
150
151
  request_str = str(info.request) if info.request is not None else None
152
+ roles_str = (
153
+ str(info.authenticated_roles)
154
+ if info.authenticated_roles is not None
155
+ else None
156
+ )
151
157
 
152
158
  return cls(
153
159
  WorkflowUUID=info.workflow_id,
154
160
  Status=info.status,
155
- WorkflowName=info.workflow_name,
156
- WorkflowClassName=info.workflow_class_name,
157
- WorkflowConfigName=info.workflow_config_name,
161
+ WorkflowName=info.name,
162
+ WorkflowClassName=info.class_name,
163
+ WorkflowConfigName=info.config_name,
158
164
  AuthenticatedUser=info.authenticated_user,
159
165
  AssumedRole=info.assumed_role,
160
- AuthenticatedRoles=info.authenticated_roles,
166
+ AuthenticatedRoles=roles_str,
161
167
  Input=inputs_str,
162
168
  Output=outputs_str,
163
169
  Request=request_str,
164
- Error=info.error,
170
+ Error=error_str,
165
171
  CreatedAt=created_at_str,
166
172
  UpdatedAt=updated_at_str,
167
173
  QueueName=info.queue_name,
@@ -5,6 +5,7 @@ import os
5
5
  import uuid
6
6
  from contextlib import AbstractContextManager
7
7
  from contextvars import ContextVar
8
+ from dataclasses import dataclass
8
9
  from enum import Enum
9
10
  from types import TracebackType
10
11
  from typing import List, Literal, Optional, Type, TypedDict
@@ -48,6 +49,23 @@ class TracedAttributes(TypedDict, total=False):
48
49
  authenticatedUserAssumedRole: Optional[str]
49
50
 
50
51
 
52
+ @dataclass
53
+ class StepStatus:
54
+ """
55
+ Status of a step execution.
56
+
57
+ Attributes:
58
+ step_id: The unique ID of this step in its workflow.
59
+ current_attempt: For steps with automatic retries, which attempt number (zero-indexed) is currently executing.
60
+ max_attempts: For steps with automatic retries, the maximum number of attempts that will be made before the step fails.
61
+
62
+ """
63
+
64
+ step_id: int
65
+ current_attempt: Optional[int]
66
+ max_attempts: Optional[int]
67
+
68
+
51
69
  class DBOSContext:
52
70
  def __init__(self) -> None:
53
71
  self.executor_id = GlobalParams.executor_id
@@ -73,6 +91,7 @@ class DBOSContext:
73
91
  self.authenticated_user: Optional[str] = None
74
92
  self.authenticated_roles: Optional[List[str]] = None
75
93
  self.assumed_role: Optional[str] = None
94
+ self.step_status: Optional[StepStatus] = None
76
95
 
77
96
  def create_child(self) -> DBOSContext:
78
97
  rv = DBOSContext()
@@ -92,6 +111,9 @@ class DBOSContext:
92
111
  rv.assumed_role = self.assumed_role
93
112
  return rv
94
113
 
114
+ def has_parent(self) -> bool:
115
+ return len(self.parent_workflow_id) > 0
116
+
95
117
  def assign_workflow_id(self) -> str:
96
118
  if len(self.id_assigned_for_next_workflow) > 0:
97
119
  wfid = self.id_assigned_for_next_workflow
@@ -147,10 +169,12 @@ class DBOSContext:
147
169
  attributes: TracedAttributes,
148
170
  ) -> None:
149
171
  self.curr_step_function_id = fid
172
+ self.step_status = StepStatus(fid, None, None)
150
173
  self._start_span(attributes)
151
174
 
152
175
  def end_step(self, exc_value: Optional[BaseException]) -> None:
153
176
  self.curr_step_function_id = -1
177
+ self.step_status = None
154
178
  self._end_span(exc_value)
155
179
 
156
180
  def start_transaction(
@@ -429,6 +453,30 @@ class EnterDBOSStep:
429
453
  return False # Did not handle
430
454
 
431
455
 
456
+ class EnterDBOSStepRetry:
457
+ def __init__(self, current_attempt: int, max_attempts: int) -> None:
458
+ self.current_attempt = current_attempt
459
+ self.max_attempts = max_attempts
460
+
461
+ def __enter__(self) -> None:
462
+ ctx = get_local_dbos_context()
463
+ if ctx is not None and ctx.step_status is not None:
464
+ ctx.step_status.current_attempt = self.current_attempt
465
+ ctx.step_status.max_attempts = self.max_attempts
466
+
467
+ def __exit__(
468
+ self,
469
+ exc_type: Optional[Type[BaseException]],
470
+ exc_value: Optional[BaseException],
471
+ traceback: Optional[TracebackType],
472
+ ) -> Literal[False]:
473
+ ctx = get_local_dbos_context()
474
+ if ctx is not None and ctx.step_status is not None:
475
+ ctx.step_status.current_attempt = None
476
+ ctx.step_status.max_attempts = None
477
+ return False # Did not handle
478
+
479
+
432
480
  class EnterDBOSTransaction:
433
481
  def __init__(self, sqls: Session, attributes: TracedAttributes) -> None:
434
482
  self.sqls = sqls
@@ -84,10 +84,10 @@ if TYPE_CHECKING:
84
84
  Workflow,
85
85
  WorkflowHandle,
86
86
  WorkflowHandleAsync,
87
- WorkflowStatus,
88
87
  DBOSRegistry,
89
88
  IsolationLevel,
90
89
  )
90
+ from ._workflow_commands import WorkflowStatus
91
91
 
92
92
  from sqlalchemy.exc import DBAPIError, InvalidRequestError
93
93
 
@@ -243,12 +243,15 @@ def _init_workflow(
243
243
  wf_status = dbos._sys_db.insert_workflow_status(
244
244
  status, max_recovery_attempts=max_recovery_attempts
245
245
  )
246
+
246
247
  # TODO: Modify the inputs if they were changed by `update_workflow_inputs`
247
248
  dbos._sys_db.update_workflow_inputs(
248
249
  wfid, _serialization.serialize_args(inputs)
249
250
  )
251
+
250
252
  else:
251
253
  # Buffer the inputs for single-transaction workflows, but don't buffer the status
254
+
252
255
  dbos._sys_db.buffer_workflow_inputs(
253
256
  wfid, _serialization.serialize_args(inputs)
254
257
  )
@@ -475,6 +478,15 @@ def start_workflow(
475
478
 
476
479
  new_wf_id, new_wf_ctx = _get_new_wf()
477
480
 
481
+ ctx = new_wf_ctx
482
+ new_child_workflow_id = ctx.id_assigned_for_next_workflow
483
+ if ctx.has_parent():
484
+ child_workflow_id = dbos._sys_db.check_child_workflow(
485
+ ctx.parent_workflow_id, ctx.parent_workflow_fid
486
+ )
487
+ if child_workflow_id is not None:
488
+ return WorkflowHandlePolling(child_workflow_id, dbos)
489
+
478
490
  status = _init_workflow(
479
491
  dbos,
480
492
  new_wf_ctx,
@@ -488,6 +500,13 @@ def start_workflow(
488
500
  )
489
501
 
490
502
  wf_status = status["status"]
503
+ if ctx.has_parent():
504
+ dbos._sys_db.record_child_workflow(
505
+ ctx.parent_workflow_id,
506
+ new_child_workflow_id,
507
+ ctx.parent_workflow_fid,
508
+ func.__name__,
509
+ )
491
510
 
492
511
  if not execute_workflow or (
493
512
  not dbos.debug_mode
@@ -496,9 +515,6 @@ def start_workflow(
496
515
  or wf_status == WorkflowStatusString.SUCCESS.value
497
516
  )
498
517
  ):
499
- dbos.logger.debug(
500
- f"Workflow {new_wf_id} already completed with status {wf_status}. Directly returning a workflow handle."
501
- )
502
518
  return WorkflowHandlePolling(new_wf_id, dbos)
503
519
 
504
520
  future = dbos._executor.submit(
@@ -544,6 +560,17 @@ async def start_workflow_async(
544
560
 
545
561
  new_wf_id, new_wf_ctx = _get_new_wf()
546
562
 
563
+ ctx = new_wf_ctx
564
+ new_child_workflow_id = ctx.id_assigned_for_next_workflow
565
+ if ctx.has_parent():
566
+ child_workflow_id = await asyncio.to_thread(
567
+ dbos._sys_db.check_child_workflow,
568
+ ctx.parent_workflow_id,
569
+ ctx.parent_workflow_fid,
570
+ )
571
+ if child_workflow_id is not None:
572
+ return WorkflowHandleAsyncPolling(child_workflow_id, dbos)
573
+
547
574
  status = await asyncio.to_thread(
548
575
  _init_workflow,
549
576
  dbos,
@@ -557,6 +584,15 @@ async def start_workflow_async(
557
584
  max_recovery_attempts=fi.max_recovery_attempts,
558
585
  )
559
586
 
587
+ if ctx.has_parent():
588
+ await asyncio.to_thread(
589
+ dbos._sys_db.record_child_workflow,
590
+ ctx.parent_workflow_id,
591
+ new_child_workflow_id,
592
+ ctx.parent_workflow_fid,
593
+ func.__name__,
594
+ )
595
+
560
596
  wf_status = status["status"]
561
597
 
562
598
  if not execute_workflow or (
@@ -566,9 +602,6 @@ async def start_workflow_async(
566
602
  or wf_status == WorkflowStatusString.SUCCESS.value
567
603
  )
568
604
  ):
569
- dbos.logger.debug(
570
- f"Workflow {new_wf_id} already completed with status {wf_status}. Directly returning a workflow handle."
571
- )
572
605
  return WorkflowHandleAsyncPolling(new_wf_id, dbos)
573
606
 
574
607
  coro = _execute_workflow_async(dbos, status, func, new_wf_ctx, *args, **kwargs)
@@ -599,6 +632,8 @@ def workflow_wrapper(
599
632
  ) -> Callable[P, R]:
600
633
  func.__orig_func = func # type: ignore
601
634
 
635
+ funcName = func.__name__
636
+
602
637
  fi = get_or_create_func_info(func)
603
638
  fi.max_recovery_attempts = max_recovery_attempts
604
639
 
@@ -629,7 +664,24 @@ def workflow_wrapper(
629
664
  wfOutcome = Outcome[R].make(functools.partial(func, *args, **kwargs))
630
665
 
631
666
  def init_wf() -> Callable[[Callable[[], R]], R]:
667
+
668
+ def recorded_result(
669
+ c_wfid: str, dbos: "DBOS"
670
+ ) -> Callable[[Callable[[], R]], R]:
671
+ def recorded_result_inner(func: Callable[[], R]) -> R:
672
+ return WorkflowHandlePolling(c_wfid, dbos).get_result()
673
+
674
+ return recorded_result_inner
675
+
632
676
  ctx = assert_current_dbos_context() # Now the child ctx
677
+
678
+ if ctx.has_parent():
679
+ child_workflow_id = dbos._sys_db.check_child_workflow(
680
+ ctx.parent_workflow_id, ctx.parent_workflow_fid
681
+ )
682
+ if child_workflow_id is not None:
683
+ return recorded_result(child_workflow_id, dbos)
684
+
633
685
  status = _init_workflow(
634
686
  dbos,
635
687
  ctx,
@@ -640,11 +692,20 @@ def workflow_wrapper(
640
692
  temp_wf_type=get_temp_workflow_type(func),
641
693
  max_recovery_attempts=max_recovery_attempts,
642
694
  )
695
+
643
696
  # TODO: maybe modify the parameters if they've been changed by `_init_workflow`
644
697
  dbos.logger.debug(
645
698
  f"Running workflow, id: {ctx.workflow_id}, name: {get_dbos_func_name(func)}"
646
699
  )
647
700
 
701
+ if ctx.has_parent():
702
+ dbos._sys_db.record_child_workflow(
703
+ ctx.parent_workflow_id,
704
+ ctx.workflow_id,
705
+ ctx.parent_workflow_fid,
706
+ funcName,
707
+ )
708
+
648
709
  return _get_wf_invoke_func(dbos, status)
649
710
 
650
711
  outcome = (
@@ -853,6 +914,8 @@ def decorate_step(
853
914
  ) -> Callable[[Callable[P, R]], Callable[P, R]]:
854
915
  def decorator(func: Callable[P, R]) -> Callable[P, R]:
855
916
 
917
+ stepName = func.__name__
918
+
856
919
  def invoke_step(*args: Any, **kwargs: Any) -> Any:
857
920
  if dbosreg.dbos is None:
858
921
  raise DBOSException(
@@ -877,7 +940,7 @@ def decorate_step(
877
940
 
878
941
  def on_exception(attempt: int, error: BaseException) -> float:
879
942
  dbos.logger.warning(
880
- f"Step being automatically retried. (attempt {attempt} of {attempts}). {traceback.format_exc()}"
943
+ f"Step being automatically retried. (attempt {attempt + 1} of {attempts}). {traceback.format_exc()}"
881
944
  )
882
945
  ctx = assert_current_dbos_context()
883
946
  ctx.get_current_span().add_event(
@@ -897,19 +960,20 @@ def decorate_step(
897
960
  step_output: OperationResultInternal = {
898
961
  "workflow_uuid": ctx.workflow_id,
899
962
  "function_id": ctx.function_id,
963
+ "function_name": stepName,
900
964
  "output": None,
901
965
  "error": None,
902
966
  }
903
967
 
904
968
  try:
905
969
  output = func()
906
- step_output["output"] = _serialization.serialize(output)
907
- return output
908
970
  except Exception as error:
909
971
  step_output["error"] = _serialization.serialize_exception(error)
910
- raise
911
- finally:
912
972
  dbos._sys_db.record_operation_result(step_output)
973
+ raise
974
+ step_output["output"] = _serialization.serialize(output)
975
+ dbos._sys_db.record_operation_result(step_output)
976
+ return output
913
977
 
914
978
  def check_existing_result() -> Union[NoResult, R]:
915
979
  ctx = assert_current_dbos_context()
@@ -11,7 +11,6 @@ import threading
11
11
  import traceback
12
12
  import uuid
13
13
  from concurrent.futures import ThreadPoolExecutor
14
- from dataclasses import dataclass
15
14
  from logging import Logger
16
15
  from typing import (
17
16
  TYPE_CHECKING,
@@ -28,17 +27,23 @@ from typing import (
28
27
  TypeVar,
29
28
  Union,
30
29
  cast,
31
- overload,
32
30
  )
33
31
 
34
32
  from opentelemetry.trace import Span
35
33
 
34
+ from dbos import _serialization
36
35
  from dbos._conductor.conductor import ConductorWebsocket
37
36
  from dbos._utils import GlobalParams
37
+ from dbos._workflow_commands import (
38
+ WorkflowStatus,
39
+ list_queued_workflows,
40
+ list_workflows,
41
+ )
38
42
 
39
43
  from ._classproperty import classproperty
40
44
  from ._core import (
41
45
  TEMP_SEND_WF_NAME,
46
+ WorkflowHandleAsyncPolling,
42
47
  WorkflowHandlePolling,
43
48
  decorate_step,
44
49
  decorate_transaction,
@@ -85,6 +90,7 @@ from ._admin_server import AdminServer
85
90
  from ._app_db import ApplicationDatabase
86
91
  from ._context import (
87
92
  EnterDBOSStep,
93
+ StepStatus,
88
94
  TracedAttributes,
89
95
  assert_current_dbos_context,
90
96
  get_local_dbos_context,
@@ -107,6 +113,7 @@ from ._error import (
107
113
  )
108
114
  from ._logger import add_otlp_to_all_loggers, config_logger, dbos_logger, init_logger
109
115
  from ._sys_db import SystemDatabase
116
+ from ._workflow_commands import WorkflowStatus, get_workflow
110
117
 
111
118
  # Most DBOS functions are just any callable F, so decorators / wrappers work on F
112
119
  # There are cases where the parameters P and return value R should be separate
@@ -728,34 +735,39 @@ class DBOS:
728
735
  @classmethod
729
736
  def get_workflow_status(cls, workflow_id: str) -> Optional[WorkflowStatus]:
730
737
  """Return the status of a workflow execution."""
738
+ sys_db = _get_dbos_instance()._sys_db
731
739
  ctx = get_local_dbos_context()
732
740
  if ctx and ctx.is_within_workflow():
733
741
  ctx.function_id += 1
734
- stat = _get_dbos_instance()._sys_db.get_workflow_status_within_wf(
735
- workflow_id, ctx.workflow_id, ctx.function_id
742
+ res = sys_db.check_operation_execution(ctx.workflow_id, ctx.function_id)
743
+ if res is not None:
744
+ if res["output"]:
745
+ resstat: WorkflowStatus = _serialization.deserialize(res["output"])
746
+ return resstat
747
+ else:
748
+ raise DBOSException(
749
+ "Workflow status record not found. This should not happen! \033[1m Hint: Check if your workflow is deterministic.\033[0m"
750
+ )
751
+ stat = get_workflow(_get_dbos_instance()._sys_db, workflow_id, True)
752
+
753
+ if ctx and ctx.is_within_workflow():
754
+ sys_db.record_operation_result(
755
+ {
756
+ "workflow_uuid": ctx.workflow_id,
757
+ "function_id": ctx.function_id,
758
+ "function_name": "DBOS.getStatus",
759
+ "output": _serialization.serialize(stat),
760
+ "error": None,
761
+ }
736
762
  )
737
- else:
738
- stat = _get_dbos_instance()._sys_db.get_workflow_status(workflow_id)
739
- if stat is None:
740
- return None
741
-
742
- return WorkflowStatus(
743
- workflow_id=workflow_id,
744
- status=stat["status"],
745
- name=stat["name"],
746
- executor_id=stat["executor_id"],
747
- recovery_attempts=stat["recovery_attempts"],
748
- class_name=stat["class_name"],
749
- config_name=stat["config_name"],
750
- queue_name=stat["queue_name"],
751
- authenticated_user=stat["authenticated_user"],
752
- assumed_role=stat["assumed_role"],
753
- authenticated_roles=(
754
- json.loads(stat["authenticated_roles"])
755
- if stat["authenticated_roles"] is not None
756
- else None
757
- ),
758
- )
763
+ return stat
764
+
765
+ @classmethod
766
+ async def get_workflow_status_async(
767
+ cls, workflow_id: str
768
+ ) -> Optional[WorkflowStatus]:
769
+ """Return the status of a workflow execution."""
770
+ return await asyncio.to_thread(cls.get_workflow_status, workflow_id)
759
771
 
760
772
  @classmethod
761
773
  def retrieve_workflow(
@@ -769,6 +781,18 @@ class DBOS:
769
781
  raise DBOSNonExistentWorkflowError(workflow_id)
770
782
  return WorkflowHandlePolling(workflow_id, dbos)
771
783
 
784
+ @classmethod
785
+ async def retrieve_workflow_async(
786
+ cls, workflow_id: str, existing_workflow: bool = True
787
+ ) -> WorkflowHandleAsync[R]:
788
+ """Return a `WorkflowHandle` for a workflow execution."""
789
+ dbos = _get_dbos_instance()
790
+ if existing_workflow:
791
+ stat = await dbos.get_workflow_status_async(workflow_id)
792
+ if stat is None:
793
+ raise DBOSNonExistentWorkflowError(workflow_id)
794
+ return WorkflowHandleAsyncPolling(workflow_id, dbos)
795
+
772
796
  @classmethod
773
797
  def send(
774
798
  cls, destination_id: str, message: Any, topic: Optional[str] = None
@@ -943,6 +967,60 @@ class DBOS:
943
967
  _get_or_create_dbos_registry().clear_workflow_cancelled(workflow_id)
944
968
  return execute_workflow_by_id(_get_dbos_instance(), workflow_id, False)
945
969
 
970
+ @classmethod
971
+ def list_workflows(
972
+ cls,
973
+ *,
974
+ workflow_ids: Optional[List[str]] = None,
975
+ status: Optional[str] = None,
976
+ start_time: Optional[str] = None,
977
+ end_time: Optional[str] = None,
978
+ name: Optional[str] = None,
979
+ app_version: Optional[str] = None,
980
+ user: Optional[str] = None,
981
+ limit: Optional[int] = None,
982
+ offset: Optional[int] = None,
983
+ sort_desc: bool = False,
984
+ ) -> List[WorkflowStatus]:
985
+ return list_workflows(
986
+ _get_dbos_instance()._sys_db,
987
+ workflow_ids=workflow_ids,
988
+ status=status,
989
+ start_time=start_time,
990
+ end_time=end_time,
991
+ name=name,
992
+ app_version=app_version,
993
+ user=user,
994
+ limit=limit,
995
+ offset=offset,
996
+ sort_desc=sort_desc,
997
+ )
998
+
999
+ @classmethod
1000
+ def list_queued_workflows(
1001
+ cls,
1002
+ *,
1003
+ queue_name: Optional[str] = None,
1004
+ status: Optional[str] = None,
1005
+ start_time: Optional[str] = None,
1006
+ end_time: Optional[str] = None,
1007
+ name: Optional[str] = None,
1008
+ limit: Optional[int] = None,
1009
+ offset: Optional[int] = None,
1010
+ sort_desc: bool = False,
1011
+ ) -> List[WorkflowStatus]:
1012
+ return list_queued_workflows(
1013
+ _get_dbos_instance()._sys_db,
1014
+ queue_name=queue_name,
1015
+ status=status,
1016
+ start_time=start_time,
1017
+ end_time=end_time,
1018
+ name=name,
1019
+ limit=limit,
1020
+ offset=offset,
1021
+ sort_desc=sort_desc,
1022
+ )
1023
+
946
1024
  @classproperty
947
1025
  def logger(cls) -> Logger:
948
1026
  """Return the DBOS `Logger` for the current context."""
@@ -990,6 +1068,14 @@ class DBOS:
990
1068
  ), "step_id is only available within a DBOS workflow."
991
1069
  return ctx.function_id
992
1070
 
1071
+ @classproperty
1072
+ def step_status(cls) -> StepStatus:
1073
+ """Return the status of the currently executing step."""
1074
+ ctx = assert_current_dbos_context()
1075
+ assert ctx.is_step(), "step_status is only available within a DBOS step."
1076
+ assert ctx.step_status is not None
1077
+ return ctx.step_status
1078
+
993
1079
  @classproperty
994
1080
  def parent_workflow_id(cls) -> str:
995
1081
  """
@@ -1044,41 +1130,6 @@ class DBOS:
1044
1130
  ctx.authenticated_roles = authenticated_roles
1045
1131
 
1046
1132
 
1047
- @dataclass
1048
- class WorkflowStatus:
1049
- """
1050
- Status of workflow execution.
1051
-
1052
- This captures the state of a workflow execution at a point in time.
1053
-
1054
- Attributes:
1055
- workflow_id(str): The ID of the workflow execution
1056
- status(str): The status of the execution, from `WorkflowStatusString`
1057
- name(str): The workflow function name
1058
- executor_id(str): The ID of the executor running the workflow
1059
- class_name(str): For member functions, the name of the class containing the workflow function
1060
- config_name(str): For instance member functions, the name of the class instance for the execution
1061
- queue_name(str): For workflows that are or were queued, the queue name
1062
- authenticated_user(str): The user who invoked the workflow
1063
- assumed_role(str): The access role used by the user to allow access to the workflow function
1064
- authenticated_roles(List[str]): List of all access roles available to the authenticated user
1065
- recovery_attempts(int): Number of times the workflow has been restarted (usually by recovery)
1066
-
1067
- """
1068
-
1069
- workflow_id: str
1070
- status: str
1071
- name: str
1072
- executor_id: Optional[str]
1073
- class_name: Optional[str]
1074
- config_name: Optional[str]
1075
- queue_name: Optional[str]
1076
- authenticated_user: Optional[str]
1077
- assumed_role: Optional[str]
1078
- authenticated_roles: Optional[List[str]]
1079
- recovery_attempts: Optional[int]
1080
-
1081
-
1082
1133
  class WorkflowHandle(Generic[R], Protocol):
1083
1134
  """
1084
1135
  Handle to a workflow function.