dbos 1.7.0a4__tar.gz → 1.8.0__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.
Files changed (110) hide show
  1. {dbos-1.7.0a4 → dbos-1.8.0}/PKG-INFO +1 -1
  2. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_admin_server.py +3 -0
  3. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_client.py +27 -4
  4. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_conductor/conductor.py +6 -0
  5. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_conductor/protocol.py +5 -2
  6. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_context.py +37 -12
  7. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_core.py +4 -1
  8. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_dbos.py +112 -30
  9. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_dbos_config.py +24 -1
  10. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_error.py +5 -5
  11. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_sys_db.py +65 -40
  12. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_workflow_commands.py +9 -2
  13. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/cli/cli.py +4 -4
  14. {dbos-1.7.0a4 → dbos-1.8.0}/pyproject.toml +1 -1
  15. {dbos-1.7.0a4 → dbos-1.8.0}/tests/conftest.py +2 -1
  16. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_admin_server.py +33 -10
  17. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_async.py +6 -2
  18. dbos-1.8.0/tests/test_async_workflow_management.py +264 -0
  19. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_classdecorators.py +27 -0
  20. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_client.py +20 -0
  21. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_config.py +16 -0
  22. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_dbos.py +26 -0
  23. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_failures.py +7 -4
  24. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_queue.py +8 -1
  25. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_scheduler.py +3 -0
  26. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_singleton.py +0 -24
  27. dbos-1.8.0/tests/test_spans.py +272 -0
  28. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_workflow_introspection.py +70 -0
  29. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_workflow_management.py +1 -1
  30. dbos-1.7.0a4/tests/test_spans.py +0 -147
  31. {dbos-1.7.0a4 → dbos-1.8.0}/LICENSE +0 -0
  32. {dbos-1.7.0a4 → dbos-1.8.0}/README.md +0 -0
  33. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/__init__.py +0 -0
  34. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/__main__.py +0 -0
  35. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_app_db.py +0 -0
  36. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_classproperty.py +0 -0
  37. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_croniter.py +0 -0
  38. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_debug.py +0 -0
  39. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_docker_pg_helper.py +0 -0
  40. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_event_loop.py +0 -0
  41. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_fastapi.py +0 -0
  42. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_flask.py +0 -0
  43. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_kafka.py +0 -0
  44. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_kafka_message.py +0 -0
  45. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_logger.py +0 -0
  46. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/env.py +0 -0
  47. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/script.py.mako +0 -0
  48. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
  49. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/27ac6900c6ad_add_queue_dedup.py +0 -0
  50. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  51. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  52. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/66478e1b95e5_consolidate_queues.py +0 -0
  53. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/83f3732ae8e7_workflow_timeout.py +0 -0
  54. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/933e86bdac6a_add_queue_priority.py +0 -0
  55. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  56. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  57. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  58. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/d994145b47b6_consolidate_inputs.py +0 -0
  59. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  60. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
  61. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_outcome.py +0 -0
  62. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_queue.py +0 -0
  63. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_recovery.py +0 -0
  64. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_registrations.py +0 -0
  65. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_roles.py +0 -0
  66. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_scheduler.py +0 -0
  67. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_schemas/__init__.py +0 -0
  68. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_schemas/application_database.py +0 -0
  69. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_schemas/system_database.py +0 -0
  70. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_serialization.py +0 -0
  71. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_templates/dbos-db-starter/README.md +0 -0
  72. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
  73. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_templates/dbos-db-starter/__package/main.py.dbos +0 -0
  74. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
  75. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
  76. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
  77. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
  78. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
  79. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
  80. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
  81. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_tracer.py +0 -0
  82. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_utils.py +0 -0
  83. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/cli/_github_init.py +0 -0
  84. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/cli/_template_init.py +0 -0
  85. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/dbos-config.schema.json +0 -0
  86. {dbos-1.7.0a4 → dbos-1.8.0}/dbos/py.typed +0 -0
  87. {dbos-1.7.0a4 → dbos-1.8.0}/tests/__init__.py +0 -0
  88. {dbos-1.7.0a4 → dbos-1.8.0}/tests/atexit_no_ctor.py +0 -0
  89. {dbos-1.7.0a4 → dbos-1.8.0}/tests/atexit_no_launch.py +0 -0
  90. {dbos-1.7.0a4 → dbos-1.8.0}/tests/classdefs.py +0 -0
  91. {dbos-1.7.0a4 → dbos-1.8.0}/tests/client_collateral.py +0 -0
  92. {dbos-1.7.0a4 → dbos-1.8.0}/tests/client_worker.py +0 -0
  93. {dbos-1.7.0a4 → dbos-1.8.0}/tests/dupname_classdefs1.py +0 -0
  94. {dbos-1.7.0a4 → dbos-1.8.0}/tests/dupname_classdefsa.py +0 -0
  95. {dbos-1.7.0a4 → dbos-1.8.0}/tests/more_classdefs.py +0 -0
  96. {dbos-1.7.0a4 → dbos-1.8.0}/tests/queuedworkflow.py +0 -0
  97. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_cli.py +0 -0
  98. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_concurrency.py +0 -0
  99. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_croniter.py +0 -0
  100. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_debug.py +0 -0
  101. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_docker_secrets.py +0 -0
  102. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_fastapi.py +0 -0
  103. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_fastapi_roles.py +0 -0
  104. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_flask.py +0 -0
  105. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_kafka.py +0 -0
  106. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_outcome.py +0 -0
  107. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_package.py +0 -0
  108. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_schema_migration.py +0 -0
  109. {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_sqlalchemy.py +0 -0
  110. {dbos-1.7.0a4 → dbos-1.8.0}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 1.7.0a4
3
+ Version: 1.8.0
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -343,6 +343,8 @@ class AdminRequestHandler(BaseHTTPRequestHandler):
343
343
  offset=filters.get("offset"),
344
344
  sort_desc=filters.get("sort_desc", False),
345
345
  workflow_id_prefix=filters.get("workflow_id_prefix"),
346
+ load_input=filters.get("load_input", False),
347
+ load_output=filters.get("load_output", False),
346
348
  )
347
349
  workflows_output = [
348
350
  conductor_protocol.WorkflowsOutput.from_workflow_information(i)
@@ -367,6 +369,7 @@ class AdminRequestHandler(BaseHTTPRequestHandler):
367
369
  offset=filters.get("offset"),
368
370
  queue_name=filters.get("queue_name"),
369
371
  sort_desc=filters.get("sort_desc", False),
372
+ load_input=filters.get("load_input", False),
370
373
  )
371
374
  workflows_output = [
372
375
  conductor_protocol.WorkflowsOutput.from_workflow_information(i)
@@ -13,7 +13,7 @@ else:
13
13
 
14
14
  from dbos import _serialization
15
15
  from dbos._dbos import WorkflowHandle, WorkflowHandleAsync
16
- from dbos._dbos_config import is_valid_database_url
16
+ from dbos._dbos_config import get_system_database_url, is_valid_database_url
17
17
  from dbos._error import DBOSException, DBOSNonExistentWorkflowError
18
18
  from dbos._registrations import DEFAULT_MAX_RECOVERY_ATTEMPTS
19
19
  from dbos._serialization import WorkflowInputs
@@ -97,17 +97,28 @@ class WorkflowHandleClientAsyncPolling(Generic[R]):
97
97
 
98
98
 
99
99
  class DBOSClient:
100
- def __init__(self, database_url: str, *, system_database: Optional[str] = None):
100
+ def __init__(
101
+ self,
102
+ database_url: str,
103
+ *,
104
+ system_database_url: Optional[str] = None,
105
+ system_database: Optional[str] = None,
106
+ ):
101
107
  assert is_valid_database_url(database_url)
102
108
  # We only create database connections but do not run migrations
103
109
  self._sys_db = SystemDatabase(
104
- database_url=database_url,
110
+ system_database_url=get_system_database_url(
111
+ {
112
+ "system_database_url": system_database_url,
113
+ "database_url": database_url,
114
+ "database": {"sys_db_name": system_database},
115
+ }
116
+ ),
105
117
  engine_kwargs={
106
118
  "pool_timeout": 30,
107
119
  "max_overflow": 0,
108
120
  "pool_size": 2,
109
121
  },
110
- sys_db_name=system_database,
111
122
  )
112
123
  self._sys_db.check_connection()
113
124
  self._app_db = ApplicationDatabase(
@@ -294,6 +305,8 @@ class DBOSClient:
294
305
  offset: Optional[int] = None,
295
306
  sort_desc: bool = False,
296
307
  workflow_id_prefix: Optional[str] = None,
308
+ load_input: bool = True,
309
+ load_output: bool = True,
297
310
  ) -> List[WorkflowStatus]:
298
311
  return list_workflows(
299
312
  self._sys_db,
@@ -308,6 +321,8 @@ class DBOSClient:
308
321
  offset=offset,
309
322
  sort_desc=sort_desc,
310
323
  workflow_id_prefix=workflow_id_prefix,
324
+ load_input=load_input,
325
+ load_output=load_output,
311
326
  )
312
327
 
313
328
  async def list_workflows_async(
@@ -324,6 +339,8 @@ class DBOSClient:
324
339
  offset: Optional[int] = None,
325
340
  sort_desc: bool = False,
326
341
  workflow_id_prefix: Optional[str] = None,
342
+ load_input: bool = True,
343
+ load_output: bool = True,
327
344
  ) -> List[WorkflowStatus]:
328
345
  return await asyncio.to_thread(
329
346
  self.list_workflows,
@@ -338,6 +355,8 @@ class DBOSClient:
338
355
  offset=offset,
339
356
  sort_desc=sort_desc,
340
357
  workflow_id_prefix=workflow_id_prefix,
358
+ load_input=load_input,
359
+ load_output=load_output,
341
360
  )
342
361
 
343
362
  def list_queued_workflows(
@@ -351,6 +370,7 @@ class DBOSClient:
351
370
  limit: Optional[int] = None,
352
371
  offset: Optional[int] = None,
353
372
  sort_desc: bool = False,
373
+ load_input: bool = True,
354
374
  ) -> List[WorkflowStatus]:
355
375
  return list_queued_workflows(
356
376
  self._sys_db,
@@ -362,6 +382,7 @@ class DBOSClient:
362
382
  limit=limit,
363
383
  offset=offset,
364
384
  sort_desc=sort_desc,
385
+ load_input=load_input,
365
386
  )
366
387
 
367
388
  async def list_queued_workflows_async(
@@ -375,6 +396,7 @@ class DBOSClient:
375
396
  limit: Optional[int] = None,
376
397
  offset: Optional[int] = None,
377
398
  sort_desc: bool = False,
399
+ load_input: bool = True,
378
400
  ) -> List[WorkflowStatus]:
379
401
  return await asyncio.to_thread(
380
402
  self.list_queued_workflows,
@@ -386,6 +408,7 @@ class DBOSClient:
386
408
  limit=limit,
387
409
  offset=offset,
388
410
  sort_desc=sort_desc,
411
+ load_input=load_input,
389
412
  )
390
413
 
391
414
  def list_workflow_steps(self, workflow_id: str) -> List[StepInfo]:
@@ -223,6 +223,8 @@ class ConductorWebsocket(threading.Thread):
223
223
  body = list_workflows_message.body
224
224
  infos = []
225
225
  try:
226
+ load_input = body.get("load_input", False)
227
+ load_output = body.get("load_output", False)
226
228
  infos = list_workflows(
227
229
  self.dbos._sys_db,
228
230
  workflow_ids=body["workflow_uuids"],
@@ -235,6 +237,8 @@ class ConductorWebsocket(threading.Thread):
235
237
  limit=body["limit"],
236
238
  offset=body["offset"],
237
239
  sort_desc=body["sort_desc"],
240
+ load_input=load_input,
241
+ load_output=load_output,
238
242
  )
239
243
  except Exception as e:
240
244
  error_message = f"Exception encountered when listing workflows: {traceback.format_exc()}"
@@ -257,6 +261,7 @@ class ConductorWebsocket(threading.Thread):
257
261
  q_body = list_queued_workflows_message.body
258
262
  infos = []
259
263
  try:
264
+ q_load_input = q_body.get("load_input", False)
260
265
  infos = list_queued_workflows(
261
266
  self.dbos._sys_db,
262
267
  start_time=q_body["start_time"],
@@ -267,6 +272,7 @@ class ConductorWebsocket(threading.Thread):
267
272
  offset=q_body["offset"],
268
273
  queue_name=q_body["queue_name"],
269
274
  sort_desc=q_body["sort_desc"],
275
+ load_input=q_load_input,
270
276
  )
271
277
  except Exception as e:
272
278
  error_message = f"Exception encountered when listing queued workflows: {traceback.format_exc()}"
@@ -110,7 +110,7 @@ class RestartResponse(BaseMessage):
110
110
  error_message: Optional[str] = None
111
111
 
112
112
 
113
- class ListWorkflowsBody(TypedDict):
113
+ class ListWorkflowsBody(TypedDict, total=False):
114
114
  workflow_uuids: List[str]
115
115
  workflow_name: Optional[str]
116
116
  authenticated_user: Optional[str]
@@ -121,6 +121,8 @@ class ListWorkflowsBody(TypedDict):
121
121
  limit: Optional[int]
122
122
  offset: Optional[int]
123
123
  sort_desc: bool
124
+ load_input: bool
125
+ load_output: bool
124
126
 
125
127
 
126
128
  @dataclass
@@ -209,7 +211,7 @@ class ListWorkflowsResponse(BaseMessage):
209
211
  error_message: Optional[str] = None
210
212
 
211
213
 
212
- class ListQueuedWorkflowsBody(TypedDict):
214
+ class ListQueuedWorkflowsBody(TypedDict, total=False):
213
215
  workflow_name: Optional[str]
214
216
  start_time: Optional[str]
215
217
  end_time: Optional[str]
@@ -218,6 +220,7 @@ class ListQueuedWorkflowsBody(TypedDict):
218
220
  limit: Optional[int]
219
221
  offset: Optional[int]
220
222
  sort_desc: bool
223
+ load_input: bool
221
224
 
222
225
 
223
226
  @dataclass
@@ -10,7 +10,7 @@ from enum import Enum
10
10
  from types import TracebackType
11
11
  from typing import List, Literal, Optional, Type, TypedDict
12
12
 
13
- from opentelemetry.trace import Span, Status, StatusCode
13
+ from opentelemetry.trace import Span, Status, StatusCode, use_span
14
14
  from sqlalchemy.orm import Session
15
15
 
16
16
  from dbos._utils import GlobalParams
@@ -68,6 +68,20 @@ class StepStatus:
68
68
  max_attempts: Optional[int]
69
69
 
70
70
 
71
+ @dataclass
72
+ class ContextSpan:
73
+ """
74
+ A span that is used to track the context of a workflow or step execution.
75
+
76
+ Attributes:
77
+ span: The OpenTelemetry span object.
78
+ context_manager: The context manager that is used to manage the span's lifecycle.
79
+ """
80
+
81
+ span: Span
82
+ context_manager: AbstractContextManager[Span]
83
+
84
+
71
85
  class DBOSContext:
72
86
  def __init__(self) -> None:
73
87
  self.executor_id = GlobalParams.executor_id
@@ -86,7 +100,7 @@ class DBOSContext:
86
100
  self.curr_step_function_id: int = -1
87
101
  self.curr_tx_function_id: int = -1
88
102
  self.sql_session: Optional[Session] = None
89
- self.spans: list[Span] = []
103
+ self.context_spans: list[ContextSpan] = []
90
104
 
91
105
  self.authenticated_user: Optional[str] = None
92
106
  self.authenticated_roles: Optional[List[str]] = None
@@ -202,8 +216,8 @@ class DBOSContext:
202
216
  self._end_span(exc_value)
203
217
 
204
218
  def get_current_span(self) -> Optional[Span]:
205
- if len(self.spans):
206
- return self.spans[-1]
219
+ if len(self.context_spans) > 0:
220
+ return self.context_spans[-1].span
207
221
  return None
208
222
 
209
223
  def _start_span(self, attributes: TracedAttributes) -> None:
@@ -218,27 +232,38 @@ class DBOSContext:
218
232
  )
219
233
  attributes["authenticatedUserAssumedRole"] = self.assumed_role
220
234
  span = dbos_tracer.start_span(
221
- attributes, parent=self.spans[-1] if len(self.spans) > 0 else None
235
+ attributes,
236
+ parent=self.context_spans[-1].span if len(self.context_spans) > 0 else None,
237
+ )
238
+ # Activate the current span
239
+ cm = use_span(
240
+ span,
241
+ end_on_exit=False,
242
+ record_exception=False,
243
+ set_status_on_exception=False,
222
244
  )
223
- self.spans.append(span)
245
+ self.context_spans.append(ContextSpan(span, cm))
246
+ cm.__enter__()
224
247
 
225
248
  def _end_span(self, exc_value: Optional[BaseException]) -> None:
249
+ context_span = self.context_spans.pop()
226
250
  if exc_value is None:
227
- self.spans[-1].set_status(Status(StatusCode.OK))
251
+ context_span.span.set_status(Status(StatusCode.OK))
228
252
  else:
229
- self.spans[-1].set_status(
253
+ context_span.span.set_status(
230
254
  Status(StatusCode.ERROR, description=str(exc_value))
231
255
  )
232
- dbos_tracer.end_span(self.spans.pop())
256
+ dbos_tracer.end_span(context_span.span)
257
+ context_span.context_manager.__exit__(None, None, None)
233
258
 
234
259
  def set_authentication(
235
260
  self, user: Optional[str], roles: Optional[List[str]]
236
261
  ) -> None:
237
262
  self.authenticated_user = user
238
263
  self.authenticated_roles = roles
239
- if user is not None and len(self.spans) > 0:
240
- self.spans[-1].set_attribute("authenticatedUser", user)
241
- self.spans[-1].set_attribute(
264
+ if user is not None and len(self.context_spans) > 0:
265
+ self.context_spans[-1].span.set_attribute("authenticatedUser", user)
266
+ self.context_spans[-1].span.set_attribute(
242
267
  "authenticatedUserRoles", json.dumps(roles) if roles is not None else ""
243
268
  )
244
269
 
@@ -1157,13 +1157,16 @@ def decorate_step(
1157
1157
  def wrapper(*args: Any, **kwargs: Any) -> Any:
1158
1158
  rr: Optional[str] = check_required_roles(func, fi)
1159
1159
  # Entering step is allowed:
1160
+ # No DBOS, just call the original function directly
1160
1161
  # In a step already, just call the original function directly.
1161
1162
  # In a workflow (that is not in a step already)
1162
1163
  # Not in a workflow (we will start the single op workflow)
1164
+ if not dbosreg.dbos or not dbosreg.dbos._launched:
1165
+ # Call the original function directly
1166
+ return func(*args, **kwargs)
1163
1167
  ctx = get_local_dbos_context()
1164
1168
  if ctx and ctx.is_step():
1165
1169
  # Call the original function directly
1166
-
1167
1170
  return func(*args, **kwargs)
1168
1171
  if ctx and ctx.is_within_workflow():
1169
1172
  assert ctx.is_workflow(), "Steps must be called from within workflows"
@@ -91,6 +91,7 @@ from ._context import (
91
91
  from ._dbos_config import (
92
92
  ConfigFile,
93
93
  DBOSConfig,
94
+ get_system_database_url,
94
95
  overwrite_config,
95
96
  process_config,
96
97
  translate_dbos_config_to_config_file,
@@ -424,9 +425,8 @@ class DBOS:
424
425
  assert self._config["database_url"] is not None
425
426
  assert self._config["database"]["sys_db_engine_kwargs"] is not None
426
427
  self._sys_db_field = SystemDatabase(
427
- database_url=self._config["database_url"],
428
+ system_database_url=get_system_database_url(self._config),
428
429
  engine_kwargs=self._config["database"]["sys_db_engine_kwargs"],
429
- sys_db_name=self._config["database"]["sys_db_name"],
430
430
  debug_mode=debug_mode,
431
431
  )
432
432
  assert self._config["database"]["db_engine_kwargs"] is not None
@@ -966,6 +966,12 @@ class DBOS:
966
966
  fn, "DBOS.cancelWorkflow"
967
967
  )
968
968
 
969
+ @classmethod
970
+ async def cancel_workflow_async(cls, workflow_id: str) -> None:
971
+ """Cancel a workflow by ID."""
972
+ await cls._configure_asyncio_thread_pool()
973
+ await asyncio.to_thread(cls.cancel_workflow, workflow_id)
974
+
969
975
  @classmethod
970
976
  async def _configure_asyncio_thread_pool(cls) -> None:
971
977
  """
@@ -987,11 +993,23 @@ class DBOS:
987
993
  _get_dbos_instance()._sys_db.call_function_as_step(fn, "DBOS.resumeWorkflow")
988
994
  return cls.retrieve_workflow(workflow_id)
989
995
 
996
+ @classmethod
997
+ async def resume_workflow_async(cls, workflow_id: str) -> WorkflowHandleAsync[Any]:
998
+ """Resume a workflow by ID."""
999
+ await cls._configure_asyncio_thread_pool()
1000
+ await asyncio.to_thread(cls.resume_workflow, workflow_id)
1001
+ return await cls.retrieve_workflow_async(workflow_id)
1002
+
990
1003
  @classmethod
991
1004
  def restart_workflow(cls, workflow_id: str) -> WorkflowHandle[Any]:
992
1005
  """Restart a workflow with a new workflow ID"""
993
1006
  return cls.fork_workflow(workflow_id, 1)
994
1007
 
1008
+ @classmethod
1009
+ async def restart_workflow_async(cls, workflow_id: str) -> WorkflowHandleAsync[Any]:
1010
+ """Restart a workflow with a new workflow ID"""
1011
+ return await cls.fork_workflow_async(workflow_id, 1)
1012
+
995
1013
  @classmethod
996
1014
  def fork_workflow(
997
1015
  cls,
@@ -1017,6 +1035,23 @@ class DBOS:
1017
1035
  )
1018
1036
  return cls.retrieve_workflow(new_id)
1019
1037
 
1038
+ @classmethod
1039
+ async def fork_workflow_async(
1040
+ cls,
1041
+ workflow_id: str,
1042
+ start_step: int,
1043
+ *,
1044
+ application_version: Optional[str] = None,
1045
+ ) -> WorkflowHandleAsync[Any]:
1046
+ """Restart a workflow with a new workflow ID from a specific step"""
1047
+ await cls._configure_asyncio_thread_pool()
1048
+ new_id = await asyncio.to_thread(
1049
+ lambda: cls.fork_workflow(
1050
+ workflow_id, start_step, application_version=application_version
1051
+ ).get_workflow_id()
1052
+ )
1053
+ return await cls.retrieve_workflow_async(new_id)
1054
+
1020
1055
  @classmethod
1021
1056
  def list_workflows(
1022
1057
  cls,
@@ -1032,6 +1067,8 @@ class DBOS:
1032
1067
  offset: Optional[int] = None,
1033
1068
  sort_desc: bool = False,
1034
1069
  workflow_id_prefix: Optional[str] = None,
1070
+ load_input: bool = True,
1071
+ load_output: bool = True,
1035
1072
  ) -> List[WorkflowStatus]:
1036
1073
  def fn() -> List[WorkflowStatus]:
1037
1074
  return list_workflows(
@@ -1047,12 +1084,50 @@ class DBOS:
1047
1084
  offset=offset,
1048
1085
  sort_desc=sort_desc,
1049
1086
  workflow_id_prefix=workflow_id_prefix,
1087
+ load_input=load_input,
1088
+ load_output=load_output,
1050
1089
  )
1051
1090
 
1052
1091
  return _get_dbos_instance()._sys_db.call_function_as_step(
1053
1092
  fn, "DBOS.listWorkflows"
1054
1093
  )
1055
1094
 
1095
+ @classmethod
1096
+ async def list_workflows_async(
1097
+ cls,
1098
+ *,
1099
+ workflow_ids: Optional[List[str]] = None,
1100
+ status: Optional[Union[str, List[str]]] = None,
1101
+ start_time: Optional[str] = None,
1102
+ end_time: Optional[str] = None,
1103
+ name: Optional[str] = None,
1104
+ app_version: Optional[str] = None,
1105
+ user: Optional[str] = None,
1106
+ limit: Optional[int] = None,
1107
+ offset: Optional[int] = None,
1108
+ sort_desc: bool = False,
1109
+ workflow_id_prefix: Optional[str] = None,
1110
+ load_input: bool = True,
1111
+ load_output: bool = True,
1112
+ ) -> List[WorkflowStatus]:
1113
+ await cls._configure_asyncio_thread_pool()
1114
+ return await asyncio.to_thread(
1115
+ cls.list_workflows,
1116
+ workflow_ids=workflow_ids,
1117
+ status=status,
1118
+ start_time=start_time,
1119
+ end_time=end_time,
1120
+ name=name,
1121
+ app_version=app_version,
1122
+ user=user,
1123
+ limit=limit,
1124
+ offset=offset,
1125
+ sort_desc=sort_desc,
1126
+ workflow_id_prefix=workflow_id_prefix,
1127
+ load_input=load_input,
1128
+ load_output=load_output,
1129
+ )
1130
+
1056
1131
  @classmethod
1057
1132
  def list_queued_workflows(
1058
1133
  cls,
@@ -1065,6 +1140,7 @@ class DBOS:
1065
1140
  limit: Optional[int] = None,
1066
1141
  offset: Optional[int] = None,
1067
1142
  sort_desc: bool = False,
1143
+ load_input: bool = True,
1068
1144
  ) -> List[WorkflowStatus]:
1069
1145
  def fn() -> List[WorkflowStatus]:
1070
1146
  return list_queued_workflows(
@@ -1077,12 +1153,41 @@ class DBOS:
1077
1153
  limit=limit,
1078
1154
  offset=offset,
1079
1155
  sort_desc=sort_desc,
1156
+ load_input=load_input,
1080
1157
  )
1081
1158
 
1082
1159
  return _get_dbos_instance()._sys_db.call_function_as_step(
1083
1160
  fn, "DBOS.listQueuedWorkflows"
1084
1161
  )
1085
1162
 
1163
+ @classmethod
1164
+ async def list_queued_workflows_async(
1165
+ cls,
1166
+ *,
1167
+ queue_name: Optional[str] = None,
1168
+ status: Optional[Union[str, List[str]]] = None,
1169
+ start_time: Optional[str] = None,
1170
+ end_time: Optional[str] = None,
1171
+ name: Optional[str] = None,
1172
+ limit: Optional[int] = None,
1173
+ offset: Optional[int] = None,
1174
+ sort_desc: bool = False,
1175
+ load_input: bool = True,
1176
+ ) -> List[WorkflowStatus]:
1177
+ await cls._configure_asyncio_thread_pool()
1178
+ return await asyncio.to_thread(
1179
+ cls.list_queued_workflows,
1180
+ queue_name=queue_name,
1181
+ status=status,
1182
+ start_time=start_time,
1183
+ end_time=end_time,
1184
+ name=name,
1185
+ limit=limit,
1186
+ offset=offset,
1187
+ sort_desc=sort_desc,
1188
+ load_input=load_input,
1189
+ )
1190
+
1086
1191
  @classmethod
1087
1192
  def list_workflow_steps(cls, workflow_id: str) -> List[StepInfo]:
1088
1193
  def fn() -> List[StepInfo]:
@@ -1094,6 +1199,11 @@ class DBOS:
1094
1199
  fn, "DBOS.listWorkflowSteps"
1095
1200
  )
1096
1201
 
1202
+ @classmethod
1203
+ async def list_workflow_steps_async(cls, workflow_id: str) -> List[StepInfo]:
1204
+ await cls._configure_asyncio_thread_pool()
1205
+ return await asyncio.to_thread(cls.list_workflow_steps, workflow_id)
1206
+
1097
1207
  @classproperty
1098
1208
  def logger(cls) -> Logger:
1099
1209
  """Return the DBOS `Logger` for the current context."""
@@ -1266,31 +1376,3 @@ class DBOSConfiguredInstance:
1266
1376
  def __init__(self, config_name: str) -> None:
1267
1377
  self.config_name = config_name
1268
1378
  DBOS.register_instance(self)
1269
-
1270
-
1271
- # Apps that import DBOS probably don't exit. If they do, let's see if
1272
- # it looks like startup was abandoned or a call was forgotten...
1273
- def _dbos_exit_hook() -> None:
1274
- if _dbos_global_registry is None:
1275
- # Probably used as or for a support module
1276
- return
1277
- if _dbos_global_instance is None:
1278
- print("DBOS exiting; functions were registered but DBOS() was not called")
1279
- dbos_logger.warning(
1280
- "DBOS exiting; functions were registered but DBOS() was not called"
1281
- )
1282
- return
1283
- if not _dbos_global_instance._launched:
1284
- if _dbos_global_instance.fastapi is not None:
1285
- # FastAPI lifespan middleware will call launch/destroy, so we can ignore this.
1286
- # This is likely to happen during fastapi dev runs, where the reloader loads the module multiple times.
1287
- return
1288
- print("DBOS exiting; DBOS exists but launch() was not called")
1289
- dbos_logger.warning("DBOS exiting; DBOS exists but launch() was not called")
1290
- return
1291
- # If we get here, we're exiting normally
1292
- _dbos_global_instance.destroy()
1293
-
1294
-
1295
- # Register the exit hook
1296
- atexit.register(_dbos_exit_hook)
@@ -23,7 +23,8 @@ class DBOSConfig(TypedDict, total=False):
23
23
  Attributes:
24
24
  name (str): Application name
25
25
  database_url (str): Database connection string
26
- sys_db_name (str): System database name
26
+ system_database_url (str): Connection string for the system database (if different from the application database)
27
+ sys_db_name (str): System database name (deprecated)
27
28
  sys_db_pool_size (int): System database pool size
28
29
  db_engine_kwargs (Dict[str, Any]): SQLAlchemy engine kwargs (See https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine)
29
30
  log_level (str): Log level
@@ -36,6 +37,7 @@ class DBOSConfig(TypedDict, total=False):
36
37
 
37
38
  name: str
38
39
  database_url: Optional[str]
40
+ system_database_url: Optional[str]
39
41
  sys_db_name: Optional[str]
40
42
  sys_db_pool_size: Optional[int]
41
43
  db_engine_kwargs: Optional[Dict[str, Any]]
@@ -111,6 +113,7 @@ class ConfigFile(TypedDict, total=False):
111
113
  runtimeConfig: RuntimeConfig
112
114
  database: DatabaseConfig
113
115
  database_url: Optional[str]
116
+ system_database_url: Optional[str]
114
117
  telemetry: Optional[TelemetryConfig]
115
118
  env: Dict[str, str]
116
119
 
@@ -136,6 +139,8 @@ def translate_dbos_config_to_config_file(config: DBOSConfig) -> ConfigFile:
136
139
 
137
140
  if "database_url" in config:
138
141
  translated_config["database_url"] = config.get("database_url")
142
+ if "system_database_url" in config:
143
+ translated_config["system_database_url"] = config.get("system_database_url")
139
144
 
140
145
  # Runtime config
141
146
  translated_config["runtimeConfig"] = {"run_admin_server": True}
@@ -488,6 +493,8 @@ def overwrite_config(provided_config: ConfigFile) -> ConfigFile:
488
493
  "DBOS_DATABASE_URL environment variable is not set. This is required to connect to the database."
489
494
  )
490
495
  provided_config["database_url"] = db_url
496
+ if "system_database_url" in provided_config:
497
+ del provided_config["system_database_url"]
491
498
 
492
499
  # Telemetry config
493
500
  if "telemetry" not in provided_config or provided_config["telemetry"] is None:
@@ -537,3 +544,19 @@ def overwrite_config(provided_config: ConfigFile) -> ConfigFile:
537
544
  del provided_config["env"]
538
545
 
539
546
  return provided_config
547
+
548
+
549
+ def get_system_database_url(config: ConfigFile) -> str:
550
+ if "system_database_url" in config and config["system_database_url"] is not None:
551
+ return config["system_database_url"]
552
+ else:
553
+ assert config["database_url"] is not None
554
+ app_db_url = make_url(config["database_url"])
555
+ if config["database"].get("sys_db_name") is not None:
556
+ sys_db_name = config["database"]["sys_db_name"]
557
+ else:
558
+ assert app_db_url.database is not None
559
+ sys_db_name = app_db_url.database + SystemSchema.sysdb_suffix
560
+ return app_db_url.set(database=sys_db_name).render_as_string(
561
+ hide_password=False
562
+ )
@@ -55,7 +55,7 @@ class DBOSErrorCode(Enum):
55
55
  InitializationError = 3
56
56
  WorkflowFunctionNotFound = 4
57
57
  NonExistentWorkflowError = 5
58
- DeadLetterQueueError = 6
58
+ MaxRecoveryAttemptsExceeded = 6
59
59
  MaxStepRetriesExceeded = 7
60
60
  NotAuthorized = 8
61
61
  ConflictingWorkflowError = 9
@@ -121,13 +121,13 @@ class DBOSNonExistentWorkflowError(DBOSException):
121
121
  )
122
122
 
123
123
 
124
- class DBOSDeadLetterQueueError(DBOSException):
125
- """Exception raised when a workflow database record does not exist for a given ID."""
124
+ class MaxRecoveryAttemptsExceededError(DBOSException):
125
+ """Exception raised when a workflow exceeds its max recovery attempts."""
126
126
 
127
127
  def __init__(self, wf_id: str, max_retries: int):
128
128
  super().__init__(
129
- f"Workflow {wf_id} has been moved to the dead-letter queue after exceeding the maximum of ${max_retries} retries",
130
- dbos_error_code=DBOSErrorCode.DeadLetterQueueError.value,
129
+ f"Workflow {wf_id} has exceeded its maximum of {max_retries} execution or recovery attempts. Further attempts to execute or recover it will fail. See documentation for details: https://docs.dbos.dev/python/reference/decorators",
130
+ dbos_error_code=DBOSErrorCode.MaxRecoveryAttemptsExceeded.value,
131
131
  )
132
132
 
133
133