dbos 1.8.0a3__tar.gz → 1.8.0a5__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 (109) hide show
  1. {dbos-1.8.0a3 → dbos-1.8.0a5}/PKG-INFO +1 -1
  2. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_client.py +15 -4
  3. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_dbos.py +106 -2
  4. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_dbos_config.py +24 -1
  5. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_sys_db.py +3 -10
  6. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/cli/cli.py +2 -2
  7. {dbos-1.8.0a3 → dbos-1.8.0a5}/pyproject.toml +1 -1
  8. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/conftest.py +2 -1
  9. dbos-1.8.0a5/tests/test_async_workflow_management.py +264 -0
  10. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_config.py +16 -0
  11. {dbos-1.8.0a3 → dbos-1.8.0a5}/LICENSE +0 -0
  12. {dbos-1.8.0a3 → dbos-1.8.0a5}/README.md +0 -0
  13. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/__init__.py +0 -0
  14. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/__main__.py +0 -0
  15. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_admin_server.py +0 -0
  16. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_app_db.py +0 -0
  17. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_classproperty.py +0 -0
  18. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_conductor/conductor.py +0 -0
  19. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_conductor/protocol.py +0 -0
  20. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_context.py +0 -0
  21. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_core.py +0 -0
  22. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_croniter.py +0 -0
  23. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_debug.py +0 -0
  24. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_docker_pg_helper.py +0 -0
  25. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_error.py +0 -0
  26. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_event_loop.py +0 -0
  27. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_fastapi.py +0 -0
  28. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_flask.py +0 -0
  29. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_kafka.py +0 -0
  30. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_kafka_message.py +0 -0
  31. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_logger.py +0 -0
  32. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/env.py +0 -0
  33. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/script.py.mako +0 -0
  34. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
  35. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/27ac6900c6ad_add_queue_dedup.py +0 -0
  36. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  37. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  38. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/66478e1b95e5_consolidate_queues.py +0 -0
  39. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/83f3732ae8e7_workflow_timeout.py +0 -0
  40. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/933e86bdac6a_add_queue_priority.py +0 -0
  41. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  42. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  43. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  44. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/d994145b47b6_consolidate_inputs.py +0 -0
  45. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  46. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
  47. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_outcome.py +0 -0
  48. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_queue.py +0 -0
  49. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_recovery.py +0 -0
  50. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_registrations.py +0 -0
  51. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_roles.py +0 -0
  52. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_scheduler.py +0 -0
  53. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_schemas/__init__.py +0 -0
  54. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_schemas/application_database.py +0 -0
  55. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_schemas/system_database.py +0 -0
  56. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_serialization.py +0 -0
  57. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_templates/dbos-db-starter/README.md +0 -0
  58. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
  59. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_templates/dbos-db-starter/__package/main.py.dbos +0 -0
  60. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
  61. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
  62. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
  63. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
  64. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
  65. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
  66. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
  67. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_tracer.py +0 -0
  68. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_utils.py +0 -0
  69. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/_workflow_commands.py +0 -0
  70. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/cli/_github_init.py +0 -0
  71. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/cli/_template_init.py +0 -0
  72. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/dbos-config.schema.json +0 -0
  73. {dbos-1.8.0a3 → dbos-1.8.0a5}/dbos/py.typed +0 -0
  74. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/__init__.py +0 -0
  75. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/atexit_no_ctor.py +0 -0
  76. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/atexit_no_launch.py +0 -0
  77. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/classdefs.py +0 -0
  78. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/client_collateral.py +0 -0
  79. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/client_worker.py +0 -0
  80. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/dupname_classdefs1.py +0 -0
  81. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/dupname_classdefsa.py +0 -0
  82. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/more_classdefs.py +0 -0
  83. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/queuedworkflow.py +0 -0
  84. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_admin_server.py +0 -0
  85. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_async.py +0 -0
  86. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_classdecorators.py +0 -0
  87. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_cli.py +0 -0
  88. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_client.py +0 -0
  89. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_concurrency.py +0 -0
  90. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_croniter.py +0 -0
  91. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_dbos.py +0 -0
  92. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_debug.py +0 -0
  93. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_docker_secrets.py +0 -0
  94. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_failures.py +0 -0
  95. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_fastapi.py +0 -0
  96. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_fastapi_roles.py +0 -0
  97. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_flask.py +0 -0
  98. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_kafka.py +0 -0
  99. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_outcome.py +0 -0
  100. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_package.py +0 -0
  101. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_queue.py +0 -0
  102. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_scheduler.py +0 -0
  103. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_schema_migration.py +0 -0
  104. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_singleton.py +0 -0
  105. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_spans.py +0 -0
  106. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_sqlalchemy.py +0 -0
  107. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_workflow_introspection.py +0 -0
  108. {dbos-1.8.0a3 → dbos-1.8.0a5}/tests/test_workflow_management.py +0 -0
  109. {dbos-1.8.0a3 → dbos-1.8.0a5}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 1.8.0a3
3
+ Version: 1.8.0a5
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -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(
@@ -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,
@@ -1057,6 +1092,42 @@ class DBOS:
1057
1092
  fn, "DBOS.listWorkflows"
1058
1093
  )
1059
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
+
1060
1131
  @classmethod
1061
1132
  def list_queued_workflows(
1062
1133
  cls,
@@ -1089,6 +1160,34 @@ class DBOS:
1089
1160
  fn, "DBOS.listQueuedWorkflows"
1090
1161
  )
1091
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
+
1092
1191
  @classmethod
1093
1192
  def list_workflow_steps(cls, workflow_id: str) -> List[StepInfo]:
1094
1193
  def fn() -> List[StepInfo]:
@@ -1100,6 +1199,11 @@ class DBOS:
1100
1199
  fn, "DBOS.listWorkflowSteps"
1101
1200
  )
1102
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
+
1103
1207
  @classproperty
1104
1208
  def logger(cls) -> Logger:
1105
1209
  """Return the DBOS `Logger` for the current context."""
@@ -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
+ )
@@ -331,22 +331,15 @@ class SystemDatabase:
331
331
  def __init__(
332
332
  self,
333
333
  *,
334
- database_url: str,
334
+ system_database_url: str,
335
335
  engine_kwargs: Dict[str, Any],
336
- sys_db_name: Optional[str] = None,
337
336
  debug_mode: bool = False,
338
337
  ):
339
338
  # Set driver
340
- system_db_url = sa.make_url(database_url).set(drivername="postgresql+psycopg")
341
- # Resolve system database name
342
- sysdb_name = sys_db_name
343
- if not sysdb_name:
344
- assert system_db_url.database is not None
345
- sysdb_name = system_db_url.database + SystemSchema.sysdb_suffix
346
- system_db_url = system_db_url.set(database=sysdb_name)
339
+ url = sa.make_url(system_database_url).set(drivername="postgresql+psycopg")
347
340
 
348
341
  self.engine = sa.create_engine(
349
- system_db_url,
342
+ url,
350
343
  **engine_kwargs,
351
344
  )
352
345
  self._engine_kwargs = engine_kwargs
@@ -21,6 +21,7 @@ from .._client import DBOSClient
21
21
  from .._dbos_config import (
22
22
  _app_name_to_db_name,
23
23
  _is_valid_app_name,
24
+ get_system_database_url,
24
25
  is_valid_database_url,
25
26
  load_config,
26
27
  )
@@ -294,13 +295,12 @@ def migrate(
294
295
  sys_db = None
295
296
  try:
296
297
  sys_db = SystemDatabase(
297
- database_url=connection_string,
298
+ system_database_url=get_system_database_url(config),
298
299
  engine_kwargs={
299
300
  "pool_timeout": 30,
300
301
  "max_overflow": 0,
301
302
  "pool_size": 2,
302
303
  },
303
- sys_db_name=sys_db_name,
304
304
  )
305
305
  app_db = ApplicationDatabase(
306
306
  database_url=connection_string,
@@ -27,7 +27,7 @@ dependencies = [
27
27
  ]
28
28
  requires-python = ">=3.9"
29
29
  readme = "README.md"
30
- version = "1.8.0a3"
30
+ version = "1.8.0a5"
31
31
 
32
32
  [project.license]
33
33
  text = "MIT"
@@ -12,6 +12,7 @@ from flask import Flask
12
12
 
13
13
  from dbos import DBOS, DBOSClient, DBOSConfig
14
14
  from dbos._app_db import ApplicationDatabase
15
+ from dbos._dbos_config import get_system_database_url
15
16
  from dbos._schemas.system_database import SystemSchema
16
17
  from dbos._sys_db import SystemDatabase
17
18
 
@@ -40,7 +41,7 @@ def config() -> DBOSConfig:
40
41
  def sys_db(config: DBOSConfig) -> Generator[SystemDatabase, Any, None]:
41
42
  assert config["database_url"] is not None
42
43
  sys_db = SystemDatabase(
43
- database_url=config["database_url"],
44
+ system_database_url=f"{config['database_url']}_dbos_sys",
44
45
  engine_kwargs={
45
46
  "pool_timeout": 30,
46
47
  "max_overflow": 0,
@@ -0,0 +1,264 @@
1
+ import asyncio
2
+ import threading
3
+ import time
4
+ import uuid
5
+ from typing import List
6
+
7
+ import pytest
8
+
9
+ from dbos import DBOS, Queue, SetWorkflowID
10
+ from dbos._error import DBOSWorkflowCancelledError
11
+ from dbos._sys_db import StepInfo, WorkflowStatus
12
+ from tests.conftest import queue_entries_are_cleaned_up
13
+
14
+
15
+ @pytest.mark.asyncio
16
+ async def test_cancel_workflow_async(dbos: DBOS) -> None:
17
+ """Test async cancel_workflow method."""
18
+ steps_completed = 0
19
+ workflow_event = threading.Event()
20
+ main_thread_event = threading.Event()
21
+ input_val = 5
22
+
23
+ @DBOS.step()
24
+ def step_one() -> None:
25
+ nonlocal steps_completed
26
+ steps_completed += 1
27
+
28
+ @DBOS.step()
29
+ def step_two() -> None:
30
+ nonlocal steps_completed
31
+ steps_completed += 1
32
+
33
+ @DBOS.workflow()
34
+ def simple_workflow(x: int) -> int:
35
+ step_one()
36
+ main_thread_event.set()
37
+ workflow_event.wait()
38
+ step_two()
39
+ return x
40
+
41
+ # Start the workflow and cancel it async
42
+ wfid = str(uuid.uuid4())
43
+ with SetWorkflowID(wfid):
44
+ handle = DBOS.start_workflow(simple_workflow, input_val)
45
+ main_thread_event.wait()
46
+ await DBOS.cancel_workflow_async(wfid)
47
+ workflow_event.set()
48
+
49
+ with pytest.raises(DBOSWorkflowCancelledError):
50
+ handle.get_result()
51
+ assert steps_completed == 1
52
+
53
+
54
+ @pytest.mark.asyncio
55
+ async def test_resume_workflow_async(dbos: DBOS) -> None:
56
+ """Test async resume_workflow method."""
57
+ steps_completed = 0
58
+ workflow_event = threading.Event()
59
+ main_thread_event = threading.Event()
60
+ input_val = 5
61
+
62
+ @DBOS.step()
63
+ def step_one() -> None:
64
+ nonlocal steps_completed
65
+ steps_completed += 1
66
+
67
+ @DBOS.step()
68
+ def step_two() -> None:
69
+ nonlocal steps_completed
70
+ steps_completed += 1
71
+
72
+ @DBOS.workflow()
73
+ def simple_workflow(x: int) -> int:
74
+ step_one()
75
+ main_thread_event.set()
76
+ workflow_event.wait()
77
+ step_two()
78
+ return x
79
+
80
+ # Start the workflow and cancel it
81
+ wfid = str(uuid.uuid4())
82
+ with SetWorkflowID(wfid):
83
+ handle = DBOS.start_workflow(simple_workflow, input_val)
84
+ main_thread_event.wait()
85
+ DBOS.cancel_workflow(wfid)
86
+ workflow_event.set()
87
+
88
+ with pytest.raises(DBOSWorkflowCancelledError):
89
+ handle.get_result()
90
+ assert steps_completed == 1
91
+
92
+ # Resume the workflow async
93
+ async_handle = await DBOS.resume_workflow_async(wfid)
94
+ assert (await async_handle.get_result()) == input_val
95
+ assert steps_completed == 2
96
+
97
+
98
+ @pytest.mark.asyncio
99
+ async def test_restart_workflow_async(dbos: DBOS) -> None:
100
+ """Test async restart_workflow method."""
101
+ input_val = 2
102
+ multiplier = 5
103
+
104
+ @DBOS.workflow()
105
+ def simple_workflow(x: int) -> int:
106
+ return x * multiplier
107
+
108
+ # Start the workflow, let it finish, restart it async
109
+ handle = DBOS.start_workflow(simple_workflow, input_val)
110
+ assert handle.get_result() == input_val * multiplier
111
+
112
+ forked_handle = await DBOS.restart_workflow_async(handle.workflow_id)
113
+ assert forked_handle.workflow_id != handle.workflow_id
114
+ assert (await forked_handle.get_result()) == input_val * multiplier
115
+
116
+
117
+ @pytest.mark.asyncio
118
+ async def test_fork_workflow_async(dbos: DBOS) -> None:
119
+ """Test async fork_workflow method."""
120
+ step_one_count = 0
121
+ step_two_count = 0
122
+ step_three_count = 0
123
+
124
+ @DBOS.workflow()
125
+ def simple_workflow(x: int) -> int:
126
+ return step_one(x) + step_two(x) + step_three(x)
127
+
128
+ @DBOS.step()
129
+ def step_one(x: int) -> int:
130
+ nonlocal step_one_count
131
+ step_one_count += 1
132
+ return x + 1
133
+
134
+ @DBOS.step()
135
+ def step_two(x: int) -> int:
136
+ nonlocal step_two_count
137
+ step_two_count += 1
138
+ return x + 2
139
+
140
+ @DBOS.step()
141
+ def step_three(x: int) -> int:
142
+ nonlocal step_three_count
143
+ step_three_count += 1
144
+ return x + 3
145
+
146
+ input_val = 1
147
+ output = 3 * input_val + 6
148
+
149
+ wfid = str(uuid.uuid4())
150
+ with SetWorkflowID(wfid):
151
+ assert simple_workflow(input_val) == output
152
+
153
+ assert step_one_count == 1
154
+ assert step_two_count == 1
155
+ assert step_three_count == 1
156
+
157
+ # Fork from step 2 async
158
+ forked_handle = await DBOS.fork_workflow_async(wfid, 2)
159
+ assert forked_handle.workflow_id != wfid
160
+ assert (await forked_handle.get_result()) == output
161
+
162
+ # Verify step 1 didn't run again, but steps 2 and 3 did
163
+ assert step_one_count == 1
164
+ assert step_two_count == 2
165
+ assert step_three_count == 2
166
+
167
+
168
+ @pytest.mark.asyncio
169
+ async def test_list_workflows_async(dbos: DBOS) -> None:
170
+ """Test async list_workflows method."""
171
+ workflow_ids = []
172
+
173
+ @DBOS.workflow()
174
+ def simple_workflow(x: int) -> int:
175
+ return x
176
+
177
+ # Create a few workflows
178
+ for i in range(3):
179
+ wfid = str(uuid.uuid4())
180
+ workflow_ids.append(wfid)
181
+ with SetWorkflowID(wfid):
182
+ handle = DBOS.start_workflow(simple_workflow, i)
183
+ handle.get_result()
184
+
185
+ # List workflows async
186
+ workflows: List[WorkflowStatus] = await DBOS.list_workflows_async()
187
+
188
+ # Verify we have at least our workflows
189
+ assert len(workflows) >= 3
190
+
191
+ # Verify our workflow IDs are present
192
+ found_ids = {wf.workflow_id for wf in workflows}
193
+ for wfid in workflow_ids:
194
+ assert wfid in found_ids
195
+
196
+ # Test filtering by workflow_ids
197
+ filtered_workflows = await DBOS.list_workflows_async(workflow_ids=workflow_ids[:2])
198
+ assert len(filtered_workflows) == 2
199
+ assert all(wf.workflow_id in workflow_ids[:2] for wf in filtered_workflows)
200
+
201
+
202
+ @pytest.mark.asyncio
203
+ async def test_list_queued_workflows_async(dbos: DBOS) -> None:
204
+ """Test async list_queued_workflows method."""
205
+ queue = Queue("test_queue_async")
206
+ workflow_event = threading.Event()
207
+
208
+ @DBOS.workflow()
209
+ def blocking_workflow(x: int) -> int:
210
+ workflow_event.wait()
211
+ return x
212
+
213
+ # Enqueue a workflow but don't let it complete yet
214
+ wfid = str(uuid.uuid4())
215
+ with SetWorkflowID(wfid):
216
+ handle = queue.enqueue(blocking_workflow, 42)
217
+
218
+ # List queued workflows async while workflow is still running
219
+ queued_workflows = await DBOS.list_queued_workflows_async(
220
+ queue_name="test_queue_async"
221
+ )
222
+
223
+ # Verify our workflow is in the list
224
+ assert len(queued_workflows) >= 1
225
+ assert any(wf.workflow_id == wfid for wf in queued_workflows)
226
+
227
+ # Let the workflow complete
228
+ workflow_event.set()
229
+ handle.get_result()
230
+
231
+ assert queue_entries_are_cleaned_up(dbos)
232
+
233
+
234
+ @pytest.mark.asyncio
235
+ async def test_list_workflow_steps_async(dbos: DBOS) -> None:
236
+ """Test async list_workflow_steps method."""
237
+
238
+ @DBOS.workflow()
239
+ def simple_workflow(x: int) -> int:
240
+ step_one(x)
241
+ step_two(x)
242
+ return x
243
+
244
+ @DBOS.step()
245
+ def step_one(x: int) -> int:
246
+ return x + 1
247
+
248
+ @DBOS.step()
249
+ def step_two(x: int) -> int:
250
+ return x + 2
251
+
252
+ wfid = str(uuid.uuid4())
253
+ with SetWorkflowID(wfid):
254
+ handle = DBOS.start_workflow(simple_workflow, 1)
255
+ handle.get_result()
256
+
257
+ # List workflow steps async
258
+ steps: List[StepInfo] = await DBOS.list_workflow_steps_async(wfid)
259
+
260
+ # Verify we have the expected steps (steps are returned as dictionaries)
261
+ assert len(steps) >= 2
262
+ step_names = {step["function_name"] for step in steps}
263
+ assert any("step_one" in name for name in step_names)
264
+ assert any("step_two" in name for name in step_names)
@@ -1135,6 +1135,22 @@ def test_configured_pool_default():
1135
1135
  dbos.destroy()
1136
1136
 
1137
1137
 
1138
+ def test_configured_pool_custom_url():
1139
+ DBOS.destroy()
1140
+ config: DBOSConfig = {
1141
+ "name": "test-app",
1142
+ "database_url": f"postgres://postgres:{quote(os.environ.get('PGPASSWORD', 'dbos'))}@localhost:5432/postgres",
1143
+ "system_database_url": f"postgres://postgres:{quote(os.environ.get('PGPASSWORD', 'dbos'))}@localhost:5432/dbostesturl",
1144
+ }
1145
+
1146
+ dbos = DBOS(config=config)
1147
+ dbos.launch()
1148
+ assert "postgres" in dbos._app_db.engine.url
1149
+ assert "dbostesturl" in dbos._sys_db.engine.url
1150
+
1151
+ dbos.destroy()
1152
+
1153
+
1138
1154
  def test_configured_pool_user_provided():
1139
1155
  DBOS.destroy()
1140
1156
  config: DBOSConfig = {
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes