dbos 0.26.0a18__tar.gz → 0.26.0a19__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 (103) hide show
  1. {dbos-0.26.0a18 → dbos-0.26.0a19}/PKG-INFO +1 -1
  2. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_app_db.py +4 -1
  3. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_client.py +31 -1
  4. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_dbos.py +21 -30
  5. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_workflow_commands.py +24 -0
  6. {dbos-0.26.0a18 → dbos-0.26.0a19}/pyproject.toml +1 -1
  7. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/client_collateral.py +15 -0
  8. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_client.py +53 -1
  9. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_workflow_introspection.py +36 -66
  10. {dbos-0.26.0a18 → dbos-0.26.0a19}/LICENSE +0 -0
  11. {dbos-0.26.0a18 → dbos-0.26.0a19}/README.md +0 -0
  12. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/__init__.py +0 -0
  13. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/__main__.py +0 -0
  14. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_admin_server.py +0 -0
  15. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_classproperty.py +0 -0
  16. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_conductor/conductor.py +0 -0
  17. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_conductor/protocol.py +0 -0
  18. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_context.py +0 -0
  19. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_core.py +0 -0
  20. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_croniter.py +0 -0
  21. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_dbos_config.py +0 -0
  22. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_debug.py +0 -0
  23. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_docker_pg_helper.py +0 -0
  24. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_error.py +0 -0
  25. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_event_loop.py +0 -0
  26. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_fastapi.py +0 -0
  27. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_flask.py +0 -0
  28. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_kafka.py +0 -0
  29. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_kafka_message.py +0 -0
  30. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_logger.py +0 -0
  31. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_migrations/env.py +0 -0
  32. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_migrations/script.py.mako +0 -0
  33. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
  34. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  35. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  36. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  37. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  38. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  39. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  40. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
  41. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_outcome.py +0 -0
  42. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_queue.py +0 -0
  43. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_recovery.py +0 -0
  44. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_registrations.py +0 -0
  45. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_request.py +0 -0
  46. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_roles.py +0 -0
  47. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_scheduler.py +0 -0
  48. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_schemas/__init__.py +0 -0
  49. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_schemas/application_database.py +0 -0
  50. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_schemas/system_database.py +0 -0
  51. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_serialization.py +0 -0
  52. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_sys_db.py +0 -0
  53. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_templates/dbos-db-starter/README.md +0 -0
  54. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
  55. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_templates/dbos-db-starter/__package/main.py +0 -0
  56. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
  57. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
  58. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
  59. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
  60. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
  61. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
  62. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
  63. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_tracer.py +0 -0
  64. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/_utils.py +0 -0
  65. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/cli/_github_init.py +0 -0
  66. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/cli/_template_init.py +0 -0
  67. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/cli/cli.py +0 -0
  68. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/dbos-config.schema.json +0 -0
  69. {dbos-0.26.0a18 → dbos-0.26.0a19}/dbos/py.typed +0 -0
  70. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/__init__.py +0 -0
  71. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/atexit_no_ctor.py +0 -0
  72. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/atexit_no_launch.py +0 -0
  73. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/classdefs.py +0 -0
  74. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/client_worker.py +0 -0
  75. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/conftest.py +0 -0
  76. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/dupname_classdefs1.py +0 -0
  77. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/dupname_classdefsa.py +0 -0
  78. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/more_classdefs.py +0 -0
  79. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/queuedworkflow.py +0 -0
  80. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_admin_server.py +0 -0
  81. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_async.py +0 -0
  82. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_classdecorators.py +0 -0
  83. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_concurrency.py +0 -0
  84. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_config.py +0 -0
  85. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_croniter.py +0 -0
  86. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_dbos.py +0 -0
  87. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_debug.py +0 -0
  88. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_docker_secrets.py +0 -0
  89. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_failures.py +0 -0
  90. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_fastapi.py +0 -0
  91. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_fastapi_roles.py +0 -0
  92. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_flask.py +0 -0
  93. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_kafka.py +0 -0
  94. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_outcome.py +0 -0
  95. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_package.py +0 -0
  96. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_queue.py +0 -0
  97. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_scheduler.py +0 -0
  98. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_schema_migration.py +0 -0
  99. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_singleton.py +0 -0
  100. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_spans.py +0 -0
  101. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_sqlalchemy.py +0 -0
  102. {dbos-0.26.0a18 → dbos-0.26.0a19}/tests/test_workflow_management.py +0 -0
  103. {dbos-0.26.0a18 → dbos-0.26.0a19}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 0.26.0a18
3
+ Version: 0.26.0a19
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -74,9 +74,12 @@ class ApplicationDatabase:
74
74
  database["connectionTimeoutMillis"] / 1000
75
75
  )
76
76
 
77
+ pool_size = database.get("app_db_pool_size")
78
+ if pool_size is None:
79
+ pool_size = 20
77
80
  self.engine = sa.create_engine(
78
81
  app_db_url,
79
- pool_size=database["app_db_pool_size"],
82
+ pool_size=pool_size,
80
83
  max_overflow=0,
81
84
  pool_timeout=30,
82
85
  connect_args=connect_args,
@@ -3,6 +3,8 @@ import sys
3
3
  import uuid
4
4
  from typing import Any, Generic, List, Optional, TypedDict, TypeVar
5
5
 
6
+ from dbos._app_db import ApplicationDatabase
7
+
6
8
  if sys.version_info < (3, 11):
7
9
  from typing_extensions import NotRequired
8
10
  else:
@@ -14,11 +16,18 @@ from dbos._dbos_config import parse_database_url_to_dbconfig
14
16
  from dbos._error import DBOSNonExistentWorkflowError
15
17
  from dbos._registrations import DEFAULT_MAX_RECOVERY_ATTEMPTS
16
18
  from dbos._serialization import WorkflowInputs
17
- from dbos._sys_db import SystemDatabase, WorkflowStatusInternal, WorkflowStatusString
19
+ from dbos._sys_db import (
20
+ StepInfo,
21
+ SystemDatabase,
22
+ WorkflowStatusInternal,
23
+ WorkflowStatusString,
24
+ )
18
25
  from dbos._workflow_commands import (
19
26
  WorkflowStatus,
27
+ fork_workflow,
20
28
  get_workflow,
21
29
  list_queued_workflows,
30
+ list_workflow_steps,
22
31
  list_workflows,
23
32
  )
24
33
 
@@ -82,6 +91,7 @@ class DBOSClient:
82
91
  if system_database is not None:
83
92
  db_config["sys_db_name"] = system_database
84
93
  self._sys_db = SystemDatabase(db_config)
94
+ self._app_db = ApplicationDatabase(db_config)
85
95
 
86
96
  def destroy(self) -> None:
87
97
  self._sys_db.destroy()
@@ -321,3 +331,23 @@ class DBOSClient:
321
331
  offset=offset,
322
332
  sort_desc=sort_desc,
323
333
  )
334
+
335
+ def list_workflow_steps(self, workflow_id: str) -> List[StepInfo]:
336
+ return list_workflow_steps(self._sys_db, self._app_db, workflow_id)
337
+
338
+ async def list_workflow_steps_async(self, workflow_id: str) -> List[StepInfo]:
339
+ return await asyncio.to_thread(self.list_workflow_steps, workflow_id)
340
+
341
+ def fork_workflow(self, workflow_id: str, start_step: int) -> WorkflowHandle[R]:
342
+ forked_workflow_id = fork_workflow(
343
+ self._sys_db, self._app_db, workflow_id, start_step
344
+ )
345
+ return WorkflowHandleClientPolling[R](forked_workflow_id, self._sys_db)
346
+
347
+ async def fork_workflow_async(
348
+ self, workflow_id: str, start_step: int
349
+ ) -> WorkflowHandleAsync[R]:
350
+ forked_workflow_id = await asyncio.to_thread(
351
+ fork_workflow, self._sys_db, self._app_db, workflow_id, start_step
352
+ )
353
+ return WorkflowHandleClientAsyncPolling[R](forked_workflow_id, self._sys_db)
@@ -34,6 +34,7 @@ from dbos._conductor.conductor import ConductorWebsocket
34
34
  from dbos._utils import INTERNAL_QUEUE_NAME, GlobalParams
35
35
  from dbos._workflow_commands import (
36
36
  WorkflowStatus,
37
+ fork_workflow,
37
38
  list_queued_workflows,
38
39
  list_workflows,
39
40
  )
@@ -67,7 +68,7 @@ from ._registrations import (
67
68
  )
68
69
  from ._roles import default_required_roles, required_roles
69
70
  from ._scheduler import ScheduledWorkflow, scheduled
70
- from ._sys_db import reset_system_database
71
+ from ._sys_db import StepInfo, reset_system_database
71
72
  from ._tracer import dbos_tracer
72
73
 
73
74
  if TYPE_CHECKING:
@@ -113,7 +114,7 @@ from ._error import (
113
114
  from ._event_loop import BackgroundEventLoop
114
115
  from ._logger import add_otlp_to_all_loggers, config_logger, dbos_logger, init_logger
115
116
  from ._sys_db import SystemDatabase
116
- from ._workflow_commands import WorkflowStatus, get_workflow
117
+ from ._workflow_commands import WorkflowStatus, get_workflow, list_workflow_steps
117
118
 
118
119
  # Most DBOS functions are just any callable F, so decorators / wrappers work on F
119
120
  # There are cases where the parameters P and return value R should be separate
@@ -959,40 +960,19 @@ class DBOS:
959
960
  @classmethod
960
961
  def restart_workflow(cls, workflow_id: str) -> WorkflowHandle[Any]:
961
962
  """Restart a workflow with a new workflow ID"""
962
-
963
963
  return cls.fork_workflow(workflow_id, 1)
964
964
 
965
965
  @classmethod
966
- def fork_workflow(
967
- cls, workflow_id: str, start_step: int = 1
968
- ) -> WorkflowHandle[Any]:
969
- """Restart a workflow with a new workflow ID"""
970
-
971
- def get_max_function_id(workflow_uuid: str) -> int:
972
- max_transactions = (
973
- _get_dbos_instance()._app_db.get_max_function_id(workflow_uuid) or 0
974
- )
975
- max_operations = (
976
- _get_dbos_instance()._sys_db.get_max_function_id(workflow_uuid) or 0
977
- )
978
- return max(max_transactions, max_operations)
979
-
980
- max_function_id = get_max_function_id(workflow_id)
981
- if max_function_id > 0 and start_step > max_function_id:
982
- raise DBOSException(
983
- f"Cannot fork workflow {workflow_id} at step {start_step}. The workflow has {max_function_id} steps."
984
- )
966
+ def fork_workflow(cls, workflow_id: str, start_step: int) -> WorkflowHandle[Any]:
967
+ """Restart a workflow with a new workflow ID from a specific step"""
985
968
 
986
969
  def fn() -> str:
987
- forked_workflow_id = str(uuid.uuid4())
988
970
  dbos_logger.info(f"Forking workflow: {workflow_id} from step {start_step}")
989
-
990
- _get_dbos_instance()._app_db.clone_workflow_transactions(
991
- workflow_id, forked_workflow_id, start_step
992
- )
993
-
994
- return _get_dbos_instance()._sys_db.fork_workflow(
995
- workflow_id, forked_workflow_id, start_step
971
+ return fork_workflow(
972
+ _get_dbos_instance()._sys_db,
973
+ _get_dbos_instance()._app_db,
974
+ workflow_id,
975
+ start_step,
996
976
  )
997
977
 
998
978
  new_id = _get_dbos_instance()._sys_db.call_function_as_step(
@@ -1066,6 +1046,17 @@ class DBOS:
1066
1046
  fn, "DBOS.listQueuedWorkflows"
1067
1047
  )
1068
1048
 
1049
+ @classmethod
1050
+ def list_workflow_steps(cls, workflow_id: str) -> List[StepInfo]:
1051
+ def fn() -> List[StepInfo]:
1052
+ return list_workflow_steps(
1053
+ _get_dbos_instance()._sys_db, _get_dbos_instance()._app_db, workflow_id
1054
+ )
1055
+
1056
+ return _get_dbos_instance()._sys_db.call_function_as_step(
1057
+ fn, "DBOS.listWorkflowSteps"
1058
+ )
1059
+
1069
1060
  @classproperty
1070
1061
  def logger(cls) -> Logger:
1071
1062
  """Return the DBOS `Logger` for the current context."""
@@ -2,6 +2,8 @@ import json
2
2
  import uuid
3
3
  from typing import Any, List, Optional
4
4
 
5
+ from dbos._error import DBOSException
6
+
5
7
  from . import _serialization
6
8
  from ._app_db import ApplicationDatabase
7
9
  from ._sys_db import (
@@ -185,3 +187,25 @@ def list_workflow_steps(
185
187
  merged_steps = steps + transactions
186
188
  merged_steps.sort(key=lambda step: step["function_id"])
187
189
  return merged_steps
190
+
191
+
192
+ def fork_workflow(
193
+ sys_db: SystemDatabase,
194
+ app_db: ApplicationDatabase,
195
+ workflow_id: str,
196
+ start_step: int,
197
+ ) -> str:
198
+ def get_max_function_id(workflow_uuid: str) -> int:
199
+ max_transactions = app_db.get_max_function_id(workflow_uuid) or 0
200
+ max_operations = sys_db.get_max_function_id(workflow_uuid) or 0
201
+ return max(max_transactions, max_operations)
202
+
203
+ max_function_id = get_max_function_id(workflow_id)
204
+ if max_function_id > 0 and start_step > max_function_id:
205
+ raise DBOSException(
206
+ f"Cannot fork workflow {workflow_id} from step {start_step}. The workflow has {max_function_id} steps."
207
+ )
208
+ forked_workflow_id = str(uuid.uuid4())
209
+ app_db.clone_workflow_transactions(workflow_id, forked_workflow_id, start_step)
210
+ sys_db.fork_workflow(workflow_id, forked_workflow_id, start_step)
211
+ return forked_workflow_id
@@ -28,7 +28,7 @@ dependencies = [
28
28
  ]
29
29
  requires-python = ">=3.9"
30
30
  readme = "README.md"
31
- version = "0.26.0a18"
31
+ version = "0.26.0a19"
32
32
 
33
33
  [project.license]
34
34
  text = "MIT"
@@ -36,3 +36,18 @@ def event_test(key: str, value: str, update: Optional[int] = None) -> str:
36
36
  DBOS.sleep(update)
37
37
  DBOS.set_event(key, f"updated-{value}")
38
38
  return f"{key}-{value}"
39
+
40
+
41
+ @DBOS.transaction()
42
+ def test_txn(x: int) -> int:
43
+ return x
44
+
45
+
46
+ @DBOS.step()
47
+ def test_step(x: int) -> int:
48
+ return x
49
+
50
+
51
+ @DBOS.workflow()
52
+ def fork_test(x: int) -> int:
53
+ return test_txn(x) + test_step(x)
@@ -8,10 +8,11 @@ import time
8
8
  import uuid
9
9
  from typing import Any, Optional, TypedDict, cast
10
10
 
11
+ import pytest
11
12
  import sqlalchemy as sa
12
13
 
13
14
  from dbos import DBOS, ConfigFile, DBOSClient, EnqueueOptions, Queue, SetWorkflowID
14
- from dbos._dbos import WorkflowHandle
15
+ from dbos._dbos import WorkflowHandle, WorkflowHandleAsync
15
16
  from dbos._schemas.system_database import SystemSchema
16
17
  from dbos._sys_db import SystemDatabase
17
18
  from dbos._utils import GlobalParams
@@ -397,3 +398,54 @@ def test_client_retrieve_wf_done(client: DBOSClient, dbos: DBOS) -> None:
397
398
  assert handle1.get_workflow_id() == handle2.get_workflow_id()
398
399
  result2 = handle2.get_result()
399
400
  assert result2 == message
401
+
402
+
403
+ def test_client_fork(dbos: DBOS, client: DBOSClient) -> None:
404
+ run_client_collateral()
405
+
406
+ options: EnqueueOptions = {
407
+ "queue_name": "test_queue",
408
+ "workflow_name": "fork_test",
409
+ }
410
+
411
+ input = 5
412
+ handle: WorkflowHandle[int] = client.enqueue(options, input)
413
+ assert handle.get_result() == input * 2
414
+ assert len(client.list_workflow_steps(handle.workflow_id)) == 2
415
+
416
+ forked_handle: WorkflowHandle[int] = client.fork_workflow(handle.workflow_id, 1)
417
+ assert forked_handle.workflow_id != handle.workflow_id
418
+ assert forked_handle.get_result() == input * 2
419
+
420
+ forked_handle = client.fork_workflow(handle.workflow_id, 2)
421
+ assert forked_handle.workflow_id != handle.workflow_id
422
+ assert forked_handle.get_result() == input * 2
423
+
424
+ assert len(client.list_workflows()) == 3
425
+
426
+
427
+ @pytest.mark.asyncio
428
+ async def test_client_fork_async(dbos: DBOS, client: DBOSClient) -> None:
429
+ run_client_collateral()
430
+
431
+ options: EnqueueOptions = {
432
+ "queue_name": "test_queue",
433
+ "workflow_name": "fork_test",
434
+ }
435
+
436
+ input = 5
437
+ handle: WorkflowHandleAsync[int] = await client.enqueue_async(options, input)
438
+ assert await handle.get_result() == input * 2
439
+ assert len(await client.list_workflow_steps_async(handle.workflow_id)) == 2
440
+
441
+ forked_handle: WorkflowHandleAsync[int] = await client.fork_workflow_async(
442
+ handle.workflow_id, 1
443
+ )
444
+ assert forked_handle.workflow_id != handle.workflow_id
445
+ assert await forked_handle.get_result() == input * 2
446
+
447
+ forked_handle = await client.fork_workflow_async(handle.workflow_id, 2)
448
+ assert forked_handle.workflow_id != handle.workflow_id
449
+ assert await forked_handle.get_result() == input * 2
450
+
451
+ assert len(await client.list_workflows_async()) == 3
@@ -188,9 +188,7 @@ def test_get_workflow(dbos: DBOS, config: ConfigFile, sys_db: SystemDatabase) ->
188
188
  assert info.workflow_id == wfUuid, f"Expected workflow_uuid to be {wfUuid}"
189
189
 
190
190
 
191
- def test_queued_workflows(
192
- dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
193
- ) -> None:
191
+ def test_queued_workflows(dbos: DBOS) -> None:
194
192
  queued_steps = 5
195
193
  step_events = [threading.Event() for _ in range(queued_steps)]
196
194
  event = threading.Event()
@@ -295,7 +293,7 @@ def test_queued_workflows(
295
293
  assert len(workflows) == 0
296
294
 
297
295
  # Test the steps are listed properly
298
- steps = _workflow_commands.list_workflow_steps(sys_db, app_db, handle.workflow_id)
296
+ steps = DBOS.list_workflow_steps(handle.workflow_id)
299
297
  assert len(steps) == queued_steps * 2
300
298
  for i in range(queued_steps):
301
299
  # Check the enqueues
@@ -314,7 +312,7 @@ def test_queued_workflows(
314
312
  child_workflows = DBOS.list_workflows(name=f"<temp>.{blocking_step.__qualname__}")
315
313
  assert (len(child_workflows)) == queued_steps
316
314
  for i, c in enumerate(child_workflows):
317
- steps = _workflow_commands.list_workflow_steps(sys_db, app_db, c.workflow_id)
315
+ steps = DBOS.list_workflow_steps(c.workflow_id)
318
316
  assert len(steps) == 1
319
317
  assert steps[0]["function_id"] == 1
320
318
  assert steps[0]["function_name"] == blocking_step.__qualname__
@@ -323,9 +321,7 @@ def test_queued_workflows(
323
321
  assert steps[0]["error"] is None
324
322
 
325
323
 
326
- def test_list_2steps_sleep(
327
- dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
328
- ) -> None:
324
+ def test_list_2steps_sleep(dbos: DBOS) -> None:
329
325
 
330
326
  @DBOS.workflow()
331
327
  def simple_workflow() -> None:
@@ -346,16 +342,14 @@ def test_list_2steps_sleep(
346
342
  with SetWorkflowID(wfid):
347
343
  simple_workflow()
348
344
 
349
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
345
+ wfsteps = DBOS.list_workflow_steps(wfid)
350
346
  assert len(wfsteps) == 3
351
347
  assert wfsteps[0]["function_name"] == stepOne.__qualname__
352
348
  assert wfsteps[1]["function_name"] == stepTwo.__qualname__
353
349
  assert wfsteps[2]["function_name"] == "DBOS.sleep"
354
350
 
355
351
 
356
- def test_send_recv(
357
- dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
358
- ) -> None:
352
+ def test_send_recv(dbos: DBOS) -> None:
359
353
 
360
354
  @DBOS.workflow()
361
355
  def send_workflow(target: str) -> None:
@@ -374,19 +368,17 @@ def test_send_recv(
374
368
  with SetWorkflowID(wfid_s):
375
369
  send_workflow(wfid_r)
376
370
 
377
- wfsteps_send = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid_s)
371
+ wfsteps_send = DBOS.list_workflow_steps(wfid_s)
378
372
  assert len(wfsteps_send) == 1
379
373
  assert wfsteps_send[0]["function_name"] == "DBOS.send"
380
374
 
381
- wfsteps_recv = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid_r)
375
+ wfsteps_recv = DBOS.list_workflow_steps(wfid_r)
382
376
  assert len(wfsteps_recv) == 2
383
377
  assert wfsteps_recv[1]["function_name"] == "DBOS.sleep"
384
378
  assert wfsteps_recv[0]["function_name"] == "DBOS.recv"
385
379
 
386
380
 
387
- def test_set_get_event(
388
- dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
389
- ) -> None:
381
+ def test_set_get_event(dbos: DBOS) -> None:
390
382
  value = "Hello, World!"
391
383
 
392
384
  @DBOS.workflow()
@@ -404,7 +396,7 @@ def test_set_get_event(
404
396
  with SetWorkflowID(wfid):
405
397
  assert set_get_workflow() == value
406
398
 
407
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
399
+ wfsteps = DBOS.list_workflow_steps(wfid)
408
400
  assert len(wfsteps) == 5
409
401
  assert wfsteps[0]["function_name"] == "DBOS.setEvent"
410
402
  assert wfsteps[1]["function_name"] == stepOne.__qualname__
@@ -419,9 +411,7 @@ def test_set_get_event(
419
411
  assert wfsteps[4]["error"] == None
420
412
 
421
413
 
422
- def test_callchild_first_sync(
423
- dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
424
- ) -> None:
414
+ def test_callchild_first_sync(dbos: DBOS) -> None:
425
415
 
426
416
  @DBOS.workflow()
427
417
  def parentWorkflow() -> str:
@@ -446,7 +436,7 @@ def test_callchild_first_sync(
446
436
  with SetWorkflowID(wfid):
447
437
  child_id = parentWorkflow()
448
438
 
449
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
439
+ wfsteps = DBOS.list_workflow_steps(wfid)
450
440
  assert len(wfsteps) == 4
451
441
  assert wfsteps[0]["function_name"] == child_workflow.__qualname__
452
442
  assert wfsteps[0]["child_workflow_id"] == child_id
@@ -461,9 +451,7 @@ def test_callchild_first_sync(
461
451
 
462
452
 
463
453
  @pytest.mark.asyncio
464
- async def test_callchild_direct_asyncio(
465
- dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
466
- ) -> None:
454
+ async def test_callchild_direct_asyncio(dbos: DBOS) -> None:
467
455
 
468
456
  @DBOS.workflow()
469
457
  async def parentWorkflow() -> str:
@@ -488,7 +476,7 @@ async def test_callchild_direct_asyncio(
488
476
  with SetWorkflowID(wfid):
489
477
  child_id = await parentWorkflow()
490
478
 
491
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
479
+ wfsteps = DBOS.list_workflow_steps(wfid)
492
480
  assert len(wfsteps) == 4
493
481
  assert wfsteps[0]["function_name"] == child_workflow.__qualname__
494
482
  assert wfsteps[0]["child_workflow_id"] == child_id
@@ -502,9 +490,7 @@ async def test_callchild_direct_asyncio(
502
490
  assert wfsteps[3]["function_name"] == stepTwo.__qualname__
503
491
 
504
492
 
505
- def test_callchild_last_sync(
506
- dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
507
- ) -> None:
493
+ def test_callchild_last_sync(dbos: DBOS) -> None:
508
494
 
509
495
  @DBOS.workflow()
510
496
  def parentWorkflow() -> None:
@@ -529,7 +515,7 @@ def test_callchild_last_sync(
529
515
  with SetWorkflowID(wfid):
530
516
  parentWorkflow()
531
517
 
532
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
518
+ wfsteps = DBOS.list_workflow_steps(wfid)
533
519
  assert len(wfsteps) == 4
534
520
  assert wfsteps[0]["function_name"] == stepOne.__qualname__
535
521
  assert wfsteps[1]["function_name"] == stepTwo.__qualname__
@@ -537,9 +523,7 @@ def test_callchild_last_sync(
537
523
  assert wfsteps[3]["function_name"] == "DBOS.getResult"
538
524
 
539
525
 
540
- def test_callchild_first_async_thread(
541
- dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
542
- ) -> None:
526
+ def test_callchild_first_async_thread(dbos: DBOS) -> None:
543
527
 
544
528
  @DBOS.workflow()
545
529
  def parentWorkflow() -> None:
@@ -565,7 +549,7 @@ def test_callchild_first_async_thread(
565
549
  with SetWorkflowID(wfid):
566
550
  parentWorkflow()
567
551
 
568
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
552
+ wfsteps = DBOS.list_workflow_steps(wfid)
569
553
  assert len(wfsteps) == 4
570
554
  assert wfsteps[0]["function_name"] == child_workflow.__qualname__
571
555
  assert wfsteps[1]["function_name"] == "DBOS.getStatus"
@@ -573,9 +557,7 @@ def test_callchild_first_async_thread(
573
557
  assert wfsteps[3]["function_name"] == stepTwo.__qualname__
574
558
 
575
559
 
576
- def test_list_steps_errors(
577
- dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
578
- ) -> None:
560
+ def test_list_steps_errors(dbos: DBOS) -> None:
579
561
  queue = Queue("test-queue")
580
562
 
581
563
  @DBOS.step()
@@ -601,7 +583,7 @@ def test_list_steps_errors(
601
583
  with SetWorkflowID(wfid):
602
584
  with pytest.raises(Exception):
603
585
  call_step()
604
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
586
+ wfsteps = DBOS.list_workflow_steps(wfid)
605
587
  assert len(wfsteps) == 1
606
588
  assert wfsteps[0]["function_name"] == failing_step.__qualname__
607
589
  assert wfsteps[0]["child_workflow_id"] == None
@@ -613,7 +595,7 @@ def test_list_steps_errors(
613
595
  with SetWorkflowID(wfid):
614
596
  with pytest.raises(Exception):
615
597
  start_step()
616
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
598
+ wfsteps = DBOS.list_workflow_steps(wfid)
617
599
  assert len(wfsteps) == 2
618
600
  assert wfsteps[0]["function_name"] == f"<temp>.{failing_step.__qualname__}"
619
601
  assert wfsteps[0]["child_workflow_id"] == f"{wfid}-1"
@@ -629,7 +611,7 @@ def test_list_steps_errors(
629
611
  with SetWorkflowID(wfid):
630
612
  with pytest.raises(Exception):
631
613
  enqueue_step()
632
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
614
+ wfsteps = DBOS.list_workflow_steps(wfid)
633
615
  assert len(wfsteps) == 2
634
616
  assert wfsteps[0]["function_name"] == f"<temp>.{failing_step.__qualname__}"
635
617
  assert wfsteps[0]["child_workflow_id"] == f"{wfid}-1"
@@ -642,9 +624,7 @@ def test_list_steps_errors(
642
624
 
643
625
 
644
626
  @pytest.mark.asyncio
645
- async def test_list_steps_errors_async(
646
- dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
647
- ) -> None:
627
+ async def test_list_steps_errors_async(dbos: DBOS) -> None:
648
628
  queue = Queue("test-queue")
649
629
 
650
630
  @DBOS.step()
@@ -670,7 +650,7 @@ async def test_list_steps_errors_async(
670
650
  with SetWorkflowID(wfid):
671
651
  with pytest.raises(Exception):
672
652
  await call_step()
673
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
653
+ wfsteps = DBOS.list_workflow_steps(wfid)
674
654
  assert len(wfsteps) == 1
675
655
  assert wfsteps[0]["function_name"] == failing_step.__qualname__
676
656
  assert wfsteps[0]["child_workflow_id"] == None
@@ -682,7 +662,7 @@ async def test_list_steps_errors_async(
682
662
  with SetWorkflowID(wfid):
683
663
  with pytest.raises(Exception):
684
664
  await start_step()
685
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
665
+ wfsteps = DBOS.list_workflow_steps(wfid)
686
666
  assert len(wfsteps) == 2
687
667
  assert wfsteps[0]["function_name"] == f"<temp>.{failing_step.__qualname__}"
688
668
  assert wfsteps[0]["child_workflow_id"] == f"{wfid}-1"
@@ -698,7 +678,7 @@ async def test_list_steps_errors_async(
698
678
  with SetWorkflowID(wfid):
699
679
  with pytest.raises(Exception):
700
680
  await enqueue_step()
701
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
681
+ wfsteps = DBOS.list_workflow_steps(wfid)
702
682
  assert len(wfsteps) == 2
703
683
  assert wfsteps[0]["function_name"] == f"<temp>.{failing_step.__qualname__}"
704
684
  assert wfsteps[0]["child_workflow_id"] == f"{wfid}-1"
@@ -710,9 +690,7 @@ async def test_list_steps_errors_async(
710
690
  assert isinstance(wfsteps[1]["error"], Exception)
711
691
 
712
692
 
713
- def test_callchild_middle_async_thread(
714
- dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
715
- ) -> None:
693
+ def test_callchild_middle_async_thread(dbos: DBOS) -> None:
716
694
 
717
695
  @DBOS.workflow()
718
696
  def parentWorkflow() -> str:
@@ -739,7 +717,7 @@ def test_callchild_middle_async_thread(
739
717
  with SetWorkflowID(wfid):
740
718
  child_id = parentWorkflow()
741
719
 
742
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
720
+ wfsteps = DBOS.list_workflow_steps(wfid)
743
721
  assert len(wfsteps) == 5
744
722
  assert wfsteps[0]["function_name"] == stepOne.__qualname__
745
723
  assert wfsteps[0]["child_workflow_id"] == None
@@ -761,9 +739,7 @@ def test_callchild_middle_async_thread(
761
739
 
762
740
 
763
741
  @pytest.mark.asyncio
764
- async def test_callchild_first_asyncio(
765
- dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
766
- ) -> None:
742
+ async def test_callchild_first_asyncio(dbos: DBOS) -> None:
767
743
 
768
744
  @DBOS.workflow()
769
745
  async def parentWorkflow() -> str:
@@ -790,7 +766,7 @@ async def test_callchild_first_asyncio(
790
766
  handle = await dbos.start_workflow_async(parentWorkflow)
791
767
  child_id = await handle.get_result()
792
768
 
793
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
769
+ wfsteps = DBOS.list_workflow_steps(wfid)
794
770
  assert len(wfsteps) == 4
795
771
  assert wfsteps[0]["function_name"] == child_workflow.__qualname__
796
772
  assert wfsteps[0]["child_workflow_id"] == child_id
@@ -883,9 +859,7 @@ async def test_callchild_rerun_asyncio(dbos: DBOS) -> None:
883
859
  assert res1 == res2
884
860
 
885
861
 
886
- def test_list_transaction(
887
- dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
888
- ) -> None:
862
+ def test_list_transaction(dbos: DBOS) -> None:
889
863
 
890
864
  @DBOS.workflow()
891
865
  def simple_workflow() -> None:
@@ -906,7 +880,7 @@ def test_list_transaction(
906
880
  with SetWorkflowID(wfid):
907
881
  simple_workflow()
908
882
 
909
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
883
+ wfsteps = DBOS.list_workflow_steps(wfid)
910
884
  assert len(wfsteps) == 3
911
885
  assert wfsteps[0]["function_name"] == transactionOne.__qualname__
912
886
  assert wfsteps[0]["output"] == "a test transaction"
@@ -915,9 +889,7 @@ def test_list_transaction(
915
889
  assert wfsteps[2]["function_name"] == "DBOS.sleep"
916
890
 
917
891
 
918
- def test_list_transaction_error(
919
- dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
920
- ) -> None:
892
+ def test_list_transaction_error(dbos: DBOS) -> None:
921
893
 
922
894
  @DBOS.workflow()
923
895
  def simple_workflow() -> None:
@@ -946,7 +918,7 @@ def test_list_transaction_error(
946
918
  with SetWorkflowID(wfid):
947
919
  simple_workflow()
948
920
 
949
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
921
+ wfsteps = DBOS.list_workflow_steps(wfid)
950
922
  assert len(wfsteps) == 4
951
923
  assert wfsteps[0]["function_name"] == transactionOne.__qualname__
952
924
  assert wfsteps[0]["output"] == "a test transaction"
@@ -992,9 +964,7 @@ def test_list_workflows_as_step(dbos: DBOS) -> None:
992
964
  assert handle_two.get_result() == 1
993
965
 
994
966
 
995
- def test_call_as_step_within_step(
996
- dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
997
- ) -> None:
967
+ def test_call_as_step_within_step(dbos: DBOS) -> None:
998
968
  # If we call any util functions within a step, it should be called directly without checkpointing
999
969
 
1000
970
  @DBOS.step()
@@ -1016,7 +986,7 @@ def test_call_as_step_within_step(
1016
986
  status = getStatusWorkflow()
1017
987
  assert status == WorkflowStatusString.PENDING.value
1018
988
 
1019
- steps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
989
+ steps = DBOS.list_workflow_steps(wfid)
1020
990
 
1021
991
  assert len(steps) == 1
1022
992
  assert steps[0]["function_name"] == getStatus.__qualname__
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