dbos 0.25.0a7__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 (100) hide show
  1. {dbos-0.25.0a7 → dbos-0.25.0a8}/PKG-INFO +1 -1
  2. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/__init__.py +2 -1
  3. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_conductor/conductor.py +1 -1
  4. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_conductor/protocol.py +13 -7
  5. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_context.py +45 -0
  6. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_core.py +2 -8
  7. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_dbos.py +93 -93
  8. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_outcome.py +6 -2
  9. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_sys_db.py +1 -27
  10. dbos-0.25.0a8/dbos/_workflow_commands.py +175 -0
  11. {dbos-0.25.0a7 → dbos-0.25.0a8}/pyproject.toml +1 -1
  12. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_admin_server.py +3 -3
  13. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_dbos.py +23 -2
  14. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_failures.py +24 -0
  15. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_workflow_cmds.py +49 -73
  16. dbos-0.25.0a7/dbos/_workflow_commands.py +0 -154
  17. {dbos-0.25.0a7 → dbos-0.25.0a8}/LICENSE +0 -0
  18. {dbos-0.25.0a7 → dbos-0.25.0a8}/README.md +0 -0
  19. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/__main__.py +0 -0
  20. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_admin_server.py +0 -0
  21. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_app_db.py +0 -0
  22. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_classproperty.py +0 -0
  23. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_cloudutils/authentication.py +0 -0
  24. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_cloudutils/cloudutils.py +0 -0
  25. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_cloudutils/databases.py +0 -0
  26. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_croniter.py +0 -0
  27. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_db_wizard.py +0 -0
  28. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_dbos_config.py +0 -0
  29. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_debug.py +0 -0
  30. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_error.py +0 -0
  31. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_fastapi.py +0 -0
  32. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_flask.py +0 -0
  33. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_kafka.py +0 -0
  34. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_kafka_message.py +0 -0
  35. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_logger.py +0 -0
  36. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_migrations/env.py +0 -0
  37. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_migrations/script.py.mako +0 -0
  38. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
  39. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  40. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  41. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  42. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  43. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  44. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  45. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
  46. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_queue.py +0 -0
  47. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_recovery.py +0 -0
  48. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_registrations.py +0 -0
  49. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_request.py +0 -0
  50. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_roles.py +0 -0
  51. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_scheduler.py +0 -0
  52. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_schemas/__init__.py +0 -0
  53. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_schemas/application_database.py +0 -0
  54. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_schemas/system_database.py +0 -0
  55. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_serialization.py +0 -0
  56. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_templates/dbos-db-starter/README.md +0 -0
  57. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
  58. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_templates/dbos-db-starter/__package/main.py +0 -0
  59. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
  60. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
  61. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
  62. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
  63. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
  64. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
  65. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
  66. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_tracer.py +0 -0
  67. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/_utils.py +0 -0
  68. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/cli/_github_init.py +0 -0
  69. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/cli/_template_init.py +0 -0
  70. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/cli/cli.py +0 -0
  71. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/dbos-config.schema.json +0 -0
  72. {dbos-0.25.0a7 → dbos-0.25.0a8}/dbos/py.typed +0 -0
  73. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/__init__.py +0 -0
  74. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/atexit_no_ctor.py +0 -0
  75. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/atexit_no_launch.py +0 -0
  76. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/classdefs.py +0 -0
  77. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/conftest.py +0 -0
  78. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/more_classdefs.py +0 -0
  79. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/queuedworkflow.py +0 -0
  80. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_async.py +0 -0
  81. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_classdecorators.py +0 -0
  82. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_concurrency.py +0 -0
  83. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_config.py +0 -0
  84. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_croniter.py +0 -0
  85. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_dbwizard.py +0 -0
  86. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_debug.py +0 -0
  87. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_fastapi.py +0 -0
  88. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_fastapi_roles.py +0 -0
  89. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_flask.py +0 -0
  90. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_kafka.py +0 -0
  91. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_outcome.py +0 -0
  92. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_package.py +0 -0
  93. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_queue.py +0 -0
  94. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_scheduler.py +0 -0
  95. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_schema_migration.py +0 -0
  96. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_singleton.py +0 -0
  97. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_spans.py +0 -0
  98. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_sqlalchemy.py +0 -0
  99. {dbos-0.25.0a7 → dbos-0.25.0a8}/tests/test_workflow_cancel.py +0 -0
  100. {dbos-0.25.0a7 → 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.0a7
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",
@@ -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()
@@ -150,10 +169,12 @@ class DBOSContext:
150
169
  attributes: TracedAttributes,
151
170
  ) -> None:
152
171
  self.curr_step_function_id = fid
172
+ self.step_status = StepStatus(fid, None, None)
153
173
  self._start_span(attributes)
154
174
 
155
175
  def end_step(self, exc_value: Optional[BaseException]) -> None:
156
176
  self.curr_step_function_id = -1
177
+ self.step_status = None
157
178
  self._end_span(exc_value)
158
179
 
159
180
  def start_transaction(
@@ -432,6 +453,30 @@ class EnterDBOSStep:
432
453
  return False # Did not handle
433
454
 
434
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
+
435
480
  class EnterDBOSTransaction:
436
481
  def __init__(self, sqls: Session, attributes: TracedAttributes) -> None:
437
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
 
@@ -515,9 +515,6 @@ def start_workflow(
515
515
  or wf_status == WorkflowStatusString.SUCCESS.value
516
516
  )
517
517
  ):
518
- dbos.logger.debug(
519
- f"Workflow {new_wf_id} already completed with status {wf_status}. Directly returning a workflow handle."
520
- )
521
518
  return WorkflowHandlePolling(new_wf_id, dbos)
522
519
 
523
520
  future = dbos._executor.submit(
@@ -605,9 +602,6 @@ async def start_workflow_async(
605
602
  or wf_status == WorkflowStatusString.SUCCESS.value
606
603
  )
607
604
  ):
608
- dbos.logger.debug(
609
- f"Workflow {new_wf_id} already completed with status {wf_status}. Directly returning a workflow handle."
610
- )
611
605
  return WorkflowHandleAsyncPolling(new_wf_id, dbos)
612
606
 
613
607
  coro = _execute_workflow_async(dbos, status, func, new_wf_ctx, *args, **kwargs)
@@ -946,7 +940,7 @@ def decorate_step(
946
940
 
947
941
  def on_exception(attempt: int, error: BaseException) -> float:
948
942
  dbos.logger.warning(
949
- 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()}"
950
944
  )
951
945
  ctx = assert_current_dbos_context()
952
946
  ctx.get_current_span().add_event(
@@ -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,13 +27,18 @@ 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 (
@@ -86,6 +90,7 @@ from ._admin_server import AdminServer
86
90
  from ._app_db import ApplicationDatabase
87
91
  from ._context import (
88
92
  EnterDBOSStep,
93
+ StepStatus,
89
94
  TracedAttributes,
90
95
  assert_current_dbos_context,
91
96
  get_local_dbos_context,
@@ -108,6 +113,7 @@ from ._error import (
108
113
  )
109
114
  from ._logger import add_otlp_to_all_loggers, config_logger, dbos_logger, init_logger
110
115
  from ._sys_db import SystemDatabase
116
+ from ._workflow_commands import WorkflowStatus, get_workflow
111
117
 
112
118
  # Most DBOS functions are just any callable F, so decorators / wrappers work on F
113
119
  # There are cases where the parameters P and return value R should be separate
@@ -729,72 +735,39 @@ class DBOS:
729
735
  @classmethod
730
736
  def get_workflow_status(cls, workflow_id: str) -> Optional[WorkflowStatus]:
731
737
  """Return the status of a workflow execution."""
738
+ sys_db = _get_dbos_instance()._sys_db
732
739
  ctx = get_local_dbos_context()
733
740
  if ctx and ctx.is_within_workflow():
734
741
  ctx.function_id += 1
735
- stat = _get_dbos_instance()._sys_db.get_workflow_status_within_wf(
736
- 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
+ }
737
762
  )
738
- else:
739
- stat = _get_dbos_instance()._sys_db.get_workflow_status(workflow_id)
740
- if stat is None:
741
- return None
742
-
743
- return WorkflowStatus(
744
- workflow_id=workflow_id,
745
- status=stat["status"],
746
- name=stat["name"],
747
- executor_id=stat["executor_id"],
748
- recovery_attempts=stat["recovery_attempts"],
749
- class_name=stat["class_name"],
750
- config_name=stat["config_name"],
751
- queue_name=stat["queue_name"],
752
- authenticated_user=stat["authenticated_user"],
753
- assumed_role=stat["assumed_role"],
754
- authenticated_roles=(
755
- json.loads(stat["authenticated_roles"])
756
- if stat["authenticated_roles"] is not None
757
- else None
758
- ),
759
- )
763
+ return stat
760
764
 
761
765
  @classmethod
762
766
  async def get_workflow_status_async(
763
767
  cls, workflow_id: str
764
768
  ) -> Optional[WorkflowStatus]:
765
769
  """Return the status of a workflow execution."""
766
- ctx = get_local_dbos_context()
767
- if ctx and ctx.is_within_workflow():
768
- ctx.function_id += 1
769
- stat = await asyncio.to_thread(
770
- lambda: _get_dbos_instance()._sys_db.get_workflow_status_within_wf(
771
- workflow_id, ctx.workflow_id, ctx.function_id
772
- )
773
- )
774
- else:
775
- stat = await asyncio.to_thread(
776
- lambda: _get_dbos_instance()._sys_db.get_workflow_status(workflow_id)
777
- )
778
- if stat is None:
779
- return None
780
-
781
- return WorkflowStatus(
782
- workflow_id=workflow_id,
783
- status=stat["status"],
784
- name=stat["name"],
785
- executor_id=stat["executor_id"],
786
- recovery_attempts=stat["recovery_attempts"],
787
- class_name=stat["class_name"],
788
- config_name=stat["config_name"],
789
- queue_name=stat["queue_name"],
790
- authenticated_user=stat["authenticated_user"],
791
- assumed_role=stat["assumed_role"],
792
- authenticated_roles=(
793
- json.loads(stat["authenticated_roles"])
794
- if stat["authenticated_roles"] is not None
795
- else None
796
- ),
797
- )
770
+ return await asyncio.to_thread(cls.get_workflow_status, workflow_id)
798
771
 
799
772
  @classmethod
800
773
  def retrieve_workflow(
@@ -994,6 +967,60 @@ class DBOS:
994
967
  _get_or_create_dbos_registry().clear_workflow_cancelled(workflow_id)
995
968
  return execute_workflow_by_id(_get_dbos_instance(), workflow_id, False)
996
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
+
997
1024
  @classproperty
998
1025
  def logger(cls) -> Logger:
999
1026
  """Return the DBOS `Logger` for the current context."""
@@ -1041,6 +1068,14 @@ class DBOS:
1041
1068
  ), "step_id is only available within a DBOS workflow."
1042
1069
  return ctx.function_id
1043
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
+
1044
1079
  @classproperty
1045
1080
  def parent_workflow_id(cls) -> str:
1046
1081
  """
@@ -1095,41 +1130,6 @@ class DBOS:
1095
1130
  ctx.authenticated_roles = authenticated_roles
1096
1131
 
1097
1132
 
1098
- @dataclass
1099
- class WorkflowStatus:
1100
- """
1101
- Status of workflow execution.
1102
-
1103
- This captures the state of a workflow execution at a point in time.
1104
-
1105
- Attributes:
1106
- workflow_id(str): The ID of the workflow execution
1107
- status(str): The status of the execution, from `WorkflowStatusString`
1108
- name(str): The workflow function name
1109
- executor_id(str): The ID of the executor running the workflow
1110
- class_name(str): For member functions, the name of the class containing the workflow function
1111
- config_name(str): For instance member functions, the name of the class instance for the execution
1112
- queue_name(str): For workflows that are or were queued, the queue name
1113
- authenticated_user(str): The user who invoked the workflow
1114
- assumed_role(str): The access role used by the user to allow access to the workflow function
1115
- authenticated_roles(List[str]): List of all access roles available to the authenticated user
1116
- recovery_attempts(int): Number of times the workflow has been restarted (usually by recovery)
1117
-
1118
- """
1119
-
1120
- workflow_id: str
1121
- status: str
1122
- name: str
1123
- executor_id: Optional[str]
1124
- class_name: Optional[str]
1125
- config_name: Optional[str]
1126
- queue_name: Optional[str]
1127
- authenticated_user: Optional[str]
1128
- assumed_role: Optional[str]
1129
- authenticated_roles: Optional[List[str]]
1130
- recovery_attempts: Optional[int]
1131
-
1132
-
1133
1133
  class WorkflowHandle(Generic[R], Protocol):
1134
1134
  """
1135
1135
  Handle to a workflow function.
@@ -4,6 +4,8 @@ import inspect
4
4
  import time
5
5
  from typing import Any, Callable, Coroutine, Optional, Protocol, TypeVar, Union, cast
6
6
 
7
+ from dbos._context import EnterDBOSStepRetry
8
+
7
9
  T = TypeVar("T")
8
10
  R = TypeVar("R")
9
11
 
@@ -98,7 +100,8 @@ class Immediate(Outcome[T]):
98
100
  ) -> T:
99
101
  for i in range(attempts):
100
102
  try:
101
- return func()
103
+ with EnterDBOSStepRetry(i, attempts):
104
+ return func()
102
105
  except Exception as exp:
103
106
  wait_time = on_exception(i, exp)
104
107
  time.sleep(wait_time)
@@ -184,7 +187,8 @@ class Pending(Outcome[T]):
184
187
  ) -> T:
185
188
  for i in range(attempts):
186
189
  try:
187
- return await func()
190
+ with EnterDBOSStepRetry(i, attempts):
191
+ return await func()
188
192
  except Exception as exp:
189
193
  wait_time = on_exception(i, exp)
190
194
  await asyncio.sleep(wait_time)
@@ -116,7 +116,7 @@ class GetWorkflowsInput:
116
116
  self.authenticated_user: Optional[str] = None # The user who ran the workflow.
117
117
  self.start_time: Optional[str] = None # Timestamp in ISO 8601 format
118
118
  self.end_time: Optional[str] = None # Timestamp in ISO 8601 format
119
- self.status: Optional[WorkflowStatuses] = None
119
+ self.status: Optional[str] = None
120
120
  self.application_version: Optional[str] = (
121
121
  None # The application version that ran this workflow. = None
122
122
  )
@@ -541,32 +541,6 @@ class SystemDatabase:
541
541
  }
542
542
  return status
543
543
 
544
- def get_workflow_status_within_wf(
545
- self, workflow_uuid: str, calling_wf: str, calling_wf_fn: int
546
- ) -> Optional[WorkflowStatusInternal]:
547
- res = self.check_operation_execution(calling_wf, calling_wf_fn)
548
- if res is not None:
549
- if res["output"]:
550
- resstat: WorkflowStatusInternal = _serialization.deserialize(
551
- res["output"]
552
- )
553
- return resstat
554
- else:
555
- raise DBOSException(
556
- "Workflow status record not found. This should not happen! \033[1m Hint: Check if your workflow is deterministic.\033[0m"
557
- )
558
- stat = self.get_workflow_status(workflow_uuid)
559
- self.record_operation_result(
560
- {
561
- "workflow_uuid": calling_wf,
562
- "function_id": calling_wf_fn,
563
- "function_name": "DBOS.getStatus",
564
- "output": _serialization.serialize(stat),
565
- "error": None,
566
- }
567
- )
568
- return stat
569
-
570
544
  def await_workflow_result_internal(self, workflow_uuid: str) -> dict[str, Any]:
571
545
  polling_interval_secs: float = 1.000
572
546