dbos 0.26.0a5__tar.gz → 0.26.0a7__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 (104) hide show
  1. {dbos-0.26.0a5 → dbos-0.26.0a7}/PKG-INFO +1 -1
  2. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_app_db.py +61 -2
  3. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_conductor/conductor.py +1 -0
  4. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_core.py +4 -0
  5. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_dbos.py +6 -0
  6. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_schemas/application_database.py +1 -0
  7. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_workflow_commands.py +9 -3
  8. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/cli/cli.py +4 -1
  9. {dbos-0.26.0a5 → dbos-0.26.0a7}/pyproject.toml +1 -1
  10. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/conftest.py +8 -0
  11. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_dbos.py +57 -0
  12. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_workflow_introspection.py +137 -37
  13. {dbos-0.26.0a5 → dbos-0.26.0a7}/LICENSE +0 -0
  14. {dbos-0.26.0a5 → dbos-0.26.0a7}/README.md +0 -0
  15. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/__init__.py +0 -0
  16. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/__main__.py +0 -0
  17. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_admin_server.py +0 -0
  18. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_classproperty.py +0 -0
  19. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_client.py +0 -0
  20. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_cloudutils/authentication.py +0 -0
  21. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_cloudutils/cloudutils.py +0 -0
  22. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_cloudutils/databases.py +0 -0
  23. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_conductor/protocol.py +0 -0
  24. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_context.py +0 -0
  25. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_croniter.py +0 -0
  26. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_db_wizard.py +0 -0
  27. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_dbos_config.py +0 -0
  28. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_debug.py +0 -0
  29. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_error.py +0 -0
  30. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_fastapi.py +0 -0
  31. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_flask.py +0 -0
  32. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_kafka.py +0 -0
  33. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_kafka_message.py +0 -0
  34. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_logger.py +0 -0
  35. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_migrations/env.py +0 -0
  36. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_migrations/script.py.mako +0 -0
  37. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
  38. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  39. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  40. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  41. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  42. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  43. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  44. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
  45. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_outcome.py +0 -0
  46. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_queue.py +0 -0
  47. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_recovery.py +0 -0
  48. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_registrations.py +0 -0
  49. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_request.py +0 -0
  50. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_roles.py +0 -0
  51. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_scheduler.py +0 -0
  52. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_schemas/__init__.py +0 -0
  53. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_schemas/system_database.py +0 -0
  54. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_serialization.py +0 -0
  55. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_sys_db.py +0 -0
  56. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_templates/dbos-db-starter/README.md +0 -0
  57. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
  58. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_templates/dbos-db-starter/__package/main.py +0 -0
  59. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
  60. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
  61. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
  62. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
  63. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
  64. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
  65. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
  66. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_tracer.py +0 -0
  67. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/_utils.py +0 -0
  68. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/cli/_github_init.py +0 -0
  69. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/cli/_template_init.py +0 -0
  70. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/dbos-config.schema.json +0 -0
  71. {dbos-0.26.0a5 → dbos-0.26.0a7}/dbos/py.typed +0 -0
  72. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/__init__.py +0 -0
  73. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/atexit_no_ctor.py +0 -0
  74. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/atexit_no_launch.py +0 -0
  75. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/classdefs.py +0 -0
  76. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/client_collateral.py +0 -0
  77. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/client_worker.py +0 -0
  78. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/more_classdefs.py +0 -0
  79. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/queuedworkflow.py +0 -0
  80. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_admin_server.py +0 -0
  81. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_async.py +0 -0
  82. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_classdecorators.py +0 -0
  83. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_client.py +0 -0
  84. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_concurrency.py +0 -0
  85. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_config.py +0 -0
  86. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_croniter.py +0 -0
  87. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_dbwizard.py +0 -0
  88. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_debug.py +0 -0
  89. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_docker_secrets.py +0 -0
  90. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_failures.py +0 -0
  91. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_fastapi.py +0 -0
  92. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_fastapi_roles.py +0 -0
  93. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_flask.py +0 -0
  94. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_kafka.py +0 -0
  95. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_outcome.py +0 -0
  96. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_package.py +0 -0
  97. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_queue.py +0 -0
  98. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_scheduler.py +0 -0
  99. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_schema_migration.py +0 -0
  100. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_singleton.py +0 -0
  101. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_spans.py +0 -0
  102. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_sqlalchemy.py +0 -0
  103. {dbos-0.26.0a5 → dbos-0.26.0a7}/tests/test_workflow_management.py +0 -0
  104. {dbos-0.26.0a5 → dbos-0.26.0a7}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 0.26.0a5
3
+ Version: 0.26.0a7
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -1,13 +1,16 @@
1
- from typing import Optional, TypedDict
1
+ from typing import List, Optional, TypedDict
2
2
 
3
3
  import sqlalchemy as sa
4
4
  import sqlalchemy.dialects.postgresql as pg
5
+ from sqlalchemy import inspect, text
5
6
  from sqlalchemy.exc import DBAPIError
6
7
  from sqlalchemy.orm import Session, sessionmaker
7
8
 
9
+ from . import _serialization
8
10
  from ._dbos_config import ConfigFile, DatabaseConfig
9
11
  from ._error import DBOSWorkflowConflictIDError
10
12
  from ._schemas.application_database import ApplicationSchema
13
+ from ._sys_db import StepInfo
11
14
 
12
15
 
13
16
  class TransactionResultInternal(TypedDict):
@@ -18,6 +21,7 @@ class TransactionResultInternal(TypedDict):
18
21
  txn_id: Optional[str]
19
22
  txn_snapshot: str
20
23
  executor_id: Optional[str]
24
+ function_name: Optional[str]
21
25
 
22
26
 
23
27
  class RecordedResult(TypedDict):
@@ -87,7 +91,30 @@ class ApplicationDatabase:
87
91
  f"CREATE SCHEMA IF NOT EXISTS {ApplicationSchema.schema}"
88
92
  )
89
93
  conn.execute(schema_creation_query)
90
- ApplicationSchema.metadata_obj.create_all(self.engine)
94
+
95
+ inspector = inspect(self.engine)
96
+ if not inspector.has_table(
97
+ "transaction_outputs", schema=ApplicationSchema.schema
98
+ ):
99
+ ApplicationSchema.metadata_obj.create_all(self.engine)
100
+ else:
101
+ columns = inspector.get_columns(
102
+ "transaction_outputs", schema=ApplicationSchema.schema
103
+ )
104
+ column_names = [col["name"] for col in columns]
105
+
106
+ if "function_name" not in column_names:
107
+ # Column missing, alter table to add it
108
+ with self.engine.connect() as conn:
109
+ conn.execute(
110
+ text(
111
+ f"""
112
+ ALTER TABLE {ApplicationSchema.schema}.transaction_outputs
113
+ ADD COLUMN function_name TEXT NOT NULL DEFAULT '';
114
+ """
115
+ )
116
+ )
117
+ conn.commit()
91
118
 
92
119
  def destroy(self) -> None:
93
120
  self.engine.dispose()
@@ -108,6 +135,7 @@ class ApplicationDatabase:
108
135
  executor_id=(
109
136
  output["executor_id"] if output["executor_id"] else None
110
137
  ),
138
+ function_name=output["function_name"],
111
139
  )
112
140
  )
113
141
  except DBAPIError as dbapi_error:
@@ -133,6 +161,7 @@ class ApplicationDatabase:
133
161
  executor_id=(
134
162
  output["executor_id"] if output["executor_id"] else None
135
163
  ),
164
+ function_name=output["function_name"],
136
165
  )
137
166
  )
138
167
  except DBAPIError as dbapi_error:
@@ -160,3 +189,33 @@ class ApplicationDatabase:
160
189
  "error": rows[0][1],
161
190
  }
162
191
  return result
192
+
193
+ def get_transactions(self, workflow_uuid: str) -> List[StepInfo]:
194
+ with self.engine.begin() as conn:
195
+ rows = conn.execute(
196
+ sa.select(
197
+ ApplicationSchema.transaction_outputs.c.function_id,
198
+ ApplicationSchema.transaction_outputs.c.function_name,
199
+ ApplicationSchema.transaction_outputs.c.output,
200
+ ApplicationSchema.transaction_outputs.c.error,
201
+ ).where(
202
+ ApplicationSchema.transaction_outputs.c.workflow_uuid
203
+ == workflow_uuid,
204
+ )
205
+ ).all()
206
+ return [
207
+ StepInfo(
208
+ function_id=row[0],
209
+ function_name=row[1],
210
+ output=(
211
+ _serialization.deserialize(row[2]) if row[2] is not None else row[2]
212
+ ),
213
+ error=(
214
+ _serialization.deserialize_exception(row[3])
215
+ if row[3] is not None
216
+ else row[3]
217
+ ),
218
+ child_workflow_id=None,
219
+ )
220
+ for row in rows
221
+ ]
@@ -254,6 +254,7 @@ class ConductorWebsocket(threading.Thread):
254
254
  try:
255
255
  step_info = list_workflow_steps(
256
256
  self.dbos._sys_db,
257
+ self.dbos._app_db,
257
258
  list_steps_message.workflow_id,
258
259
  )
259
260
  except Exception as e:
@@ -782,6 +782,9 @@ def decorate_transaction(
782
782
  dbosreg: "DBOSRegistry", isolation_level: "IsolationLevel" = "SERIALIZABLE"
783
783
  ) -> Callable[[F], F]:
784
784
  def decorator(func: F) -> F:
785
+
786
+ transactionName = func.__qualname__
787
+
785
788
  def invoke_tx(*args: Any, **kwargs: Any) -> Any:
786
789
  if dbosreg.dbos is None:
787
790
  raise DBOSException(
@@ -810,6 +813,7 @@ def decorate_transaction(
810
813
  "txn_snapshot": "", # TODO: add actual snapshot
811
814
  "executor_id": None,
812
815
  "txn_id": None,
816
+ "function_name": transactionName,
813
817
  }
814
818
  retry_wait_seconds = 0.001
815
819
  backoff_factor = 1.5
@@ -172,6 +172,12 @@ class DBOSRegistry:
172
172
  if name in self.function_type_map:
173
173
  if self.function_type_map[name] != functype:
174
174
  raise DBOSConflictingRegistrationError(name)
175
+ if name != TEMP_SEND_WF_NAME:
176
+ # Remove the `<temp>` prefix from the function name to avoid confusion
177
+ truncated_name = name.replace("<temp>.", "")
178
+ dbos_logger.warning(
179
+ f"Duplicate registration of function '{truncated_name}'. A function named '{truncated_name}' has already been registered with DBOS. All functions registered with DBOS must have unique names."
180
+ )
175
181
  self.function_type_map[name] = functype
176
182
  self.workflow_info_map[name] = wrapped_func
177
183
 
@@ -25,6 +25,7 @@ class ApplicationSchema:
25
25
  Column("txn_id", Text, nullable=True),
26
26
  Column("txn_snapshot", Text),
27
27
  Column("executor_id", Text, nullable=True),
28
+ Column("function_name", Text, nullable=False, server_default=""),
28
29
  Column(
29
30
  "created_at",
30
31
  BigInteger,
@@ -2,6 +2,7 @@ import json
2
2
  from typing import Any, List, Optional
3
3
 
4
4
  from . import _serialization
5
+ from ._app_db import ApplicationDatabase
5
6
  from ._sys_db import (
6
7
  GetQueuedWorkflowsInput,
7
8
  GetWorkflowsInput,
@@ -170,6 +171,11 @@ def get_workflow(
170
171
  return info
171
172
 
172
173
 
173
- def list_workflow_steps(sys_db: SystemDatabase, workflow_id: str) -> List[StepInfo]:
174
- output = sys_db.get_workflow_steps(workflow_id)
175
- return output
174
+ def list_workflow_steps(
175
+ sys_db: SystemDatabase, app_db: ApplicationDatabase, workflow_id: str
176
+ ) -> List[StepInfo]:
177
+ steps = sys_db.get_workflow_steps(workflow_id)
178
+ transactions = app_db.get_transactions(workflow_id)
179
+ merged_steps = steps + transactions
180
+ merged_steps.sort(key=lambda step: step["function_id"])
181
+ return merged_steps
@@ -350,8 +350,11 @@ def steps(
350
350
  ) -> None:
351
351
  config = load_config(silent=True)
352
352
  sys_db = SystemDatabase(config["database"])
353
+ app_db = ApplicationDatabase(config["database"])
353
354
  print(
354
- jsonpickle.encode(list_workflow_steps(sys_db, workflow_id), unpicklable=False)
355
+ jsonpickle.encode(
356
+ list_workflow_steps(sys_db, app_db, workflow_id), unpicklable=False
357
+ )
355
358
  )
356
359
 
357
360
 
@@ -28,7 +28,7 @@ dependencies = [
28
28
  ]
29
29
  requires-python = ">=3.9"
30
30
  readme = "README.md"
31
- version = "0.26.0a5"
31
+ version = "0.26.0a7"
32
32
 
33
33
  [project.license]
34
34
  text = "MIT"
@@ -11,6 +11,7 @@ from fastapi import FastAPI
11
11
  from flask import Flask
12
12
 
13
13
  from dbos import DBOS, ConfigFile, DBOSClient
14
+ from dbos._app_db import ApplicationDatabase
14
15
  from dbos._schemas.system_database import SystemSchema
15
16
  from dbos._sys_db import SystemDatabase
16
17
 
@@ -48,6 +49,13 @@ def sys_db(config: ConfigFile) -> Generator[SystemDatabase, Any, None]:
48
49
  sys_db.destroy()
49
50
 
50
51
 
52
+ @pytest.fixture()
53
+ def app_db(config: ConfigFile) -> Generator[ApplicationDatabase, Any, None]:
54
+ app_db = ApplicationDatabase(config["database"])
55
+ yield app_db
56
+ app_db.destroy()
57
+
58
+
51
59
  @pytest.fixture(scope="session")
52
60
  def postgres_db_engine() -> sa.Engine:
53
61
  cfg = default_config()
@@ -1257,6 +1257,63 @@ def test_double_decoration(dbos: DBOS) -> None:
1257
1257
  my_function()
1258
1258
 
1259
1259
 
1260
+ def test_duplicate_registration(
1261
+ dbos: DBOS, caplog: pytest.LogCaptureFixture, config: ConfigFile
1262
+ ) -> None:
1263
+ original_propagate = logging.getLogger("dbos").propagate
1264
+ caplog.set_level(logging.WARNING, "dbos")
1265
+ logging.getLogger("dbos").propagate = True
1266
+
1267
+ @DBOS.transaction()
1268
+ def my_transaction() -> None:
1269
+ pass
1270
+
1271
+ @DBOS.transaction()
1272
+ def my_transaction() -> None:
1273
+ pass
1274
+
1275
+ assert (
1276
+ "Duplicate registration of function 'test_duplicate_registration.<locals>.my_transaction'"
1277
+ in caplog.text
1278
+ )
1279
+
1280
+ @DBOS.step()
1281
+ def my_step() -> None:
1282
+ pass
1283
+
1284
+ @DBOS.step()
1285
+ def my_step() -> None:
1286
+ pass
1287
+
1288
+ assert (
1289
+ "Duplicate registration of function 'test_duplicate_registration.<locals>.my_step'"
1290
+ in caplog.text
1291
+ )
1292
+
1293
+ @DBOS.workflow()
1294
+ def my_workflow() -> None:
1295
+ my_step()
1296
+ my_transaction()
1297
+
1298
+ @DBOS.workflow()
1299
+ def my_workflow() -> None:
1300
+ my_step()
1301
+ my_transaction()
1302
+
1303
+ assert (
1304
+ "Duplicate registration of function 'test_duplicate_registration.<locals>.my_workflow'"
1305
+ in caplog.text
1306
+ )
1307
+
1308
+ DBOS.destroy()
1309
+ DBOS(config=config)
1310
+ DBOS.launch()
1311
+ assert "Duplicate registration of function 'temp_send_workflow'" not in caplog.text
1312
+
1313
+ # Reset logging
1314
+ logging.getLogger("dbos").propagate = original_propagate
1315
+
1316
+
1260
1317
  def test_app_version(config: ConfigFile) -> None:
1261
1318
  def is_hex(s: str) -> bool:
1262
1319
  return all(c in "0123456789abcdefABCDEF" for c in s)
@@ -14,6 +14,7 @@ from dbos import (
14
14
  WorkflowStatusString,
15
15
  _workflow_commands,
16
16
  )
17
+ from dbos._app_db import ApplicationDatabase
17
18
  from dbos._sys_db import SystemDatabase
18
19
  from dbos._utils import GlobalParams
19
20
 
@@ -187,7 +188,9 @@ def test_get_workflow(dbos: DBOS, config: ConfigFile, sys_db: SystemDatabase) ->
187
188
  assert info.workflow_id == wfUuid, f"Expected workflow_uuid to be {wfUuid}"
188
189
 
189
190
 
190
- def test_queued_workflows(dbos: DBOS, sys_db: SystemDatabase) -> None:
191
+ def test_queued_workflows(
192
+ dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
193
+ ) -> None:
191
194
  queued_steps = 5
192
195
  step_events = [threading.Event() for _ in range(queued_steps)]
193
196
  event = threading.Event()
@@ -292,7 +295,7 @@ def test_queued_workflows(dbos: DBOS, sys_db: SystemDatabase) -> None:
292
295
  assert len(workflows) == 0
293
296
 
294
297
  # Test the steps are listed properly
295
- steps = _workflow_commands.list_workflow_steps(sys_db, handle.workflow_id)
298
+ steps = _workflow_commands.list_workflow_steps(sys_db, app_db, handle.workflow_id)
296
299
  assert len(steps) == queued_steps * 2
297
300
  for i in range(queued_steps):
298
301
  # Check the enqueues
@@ -311,7 +314,7 @@ def test_queued_workflows(dbos: DBOS, sys_db: SystemDatabase) -> None:
311
314
  child_workflows = DBOS.list_workflows(name=f"<temp>.{blocking_step.__qualname__}")
312
315
  assert (len(child_workflows)) == queued_steps
313
316
  for i, c in enumerate(child_workflows):
314
- steps = _workflow_commands.list_workflow_steps(sys_db, c.workflow_id)
317
+ steps = _workflow_commands.list_workflow_steps(sys_db, app_db, c.workflow_id)
315
318
  assert len(steps) == 1
316
319
  assert steps[0]["function_id"] == 1
317
320
  assert steps[0]["function_name"] == blocking_step.__qualname__
@@ -320,7 +323,9 @@ def test_queued_workflows(dbos: DBOS, sys_db: SystemDatabase) -> None:
320
323
  assert steps[0]["error"] is None
321
324
 
322
325
 
323
- def test_list_2steps_sleep(dbos: DBOS, sys_db: SystemDatabase) -> None:
326
+ def test_list_2steps_sleep(
327
+ dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
328
+ ) -> None:
324
329
 
325
330
  @DBOS.workflow()
326
331
  def simple_workflow() -> None:
@@ -341,14 +346,16 @@ def test_list_2steps_sleep(dbos: DBOS, sys_db: SystemDatabase) -> None:
341
346
  with SetWorkflowID(wfid):
342
347
  simple_workflow()
343
348
 
344
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, wfid)
349
+ wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
345
350
  assert len(wfsteps) == 3
346
351
  assert wfsteps[0]["function_name"] == stepOne.__qualname__
347
352
  assert wfsteps[1]["function_name"] == stepTwo.__qualname__
348
353
  assert wfsteps[2]["function_name"] == "DBOS.sleep"
349
354
 
350
355
 
351
- def test_send_recv(dbos: DBOS, sys_db: SystemDatabase) -> None:
356
+ def test_send_recv(
357
+ dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
358
+ ) -> None:
352
359
 
353
360
  @DBOS.workflow()
354
361
  def send_workflow(target: str) -> None:
@@ -367,17 +374,19 @@ def test_send_recv(dbos: DBOS, sys_db: SystemDatabase) -> None:
367
374
  with SetWorkflowID(wfid_s):
368
375
  send_workflow(wfid_r)
369
376
 
370
- wfsteps_send = _workflow_commands.list_workflow_steps(sys_db, wfid_s)
377
+ wfsteps_send = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid_s)
371
378
  assert len(wfsteps_send) == 1
372
379
  assert wfsteps_send[0]["function_name"] == "DBOS.send"
373
380
 
374
- wfsteps_recv = _workflow_commands.list_workflow_steps(sys_db, wfid_r)
381
+ wfsteps_recv = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid_r)
375
382
  assert len(wfsteps_recv) == 2
376
- assert wfsteps_recv[0]["function_name"] == "DBOS.sleep"
377
- assert wfsteps_recv[1]["function_name"] == "DBOS.recv"
383
+ assert wfsteps_recv[1]["function_name"] == "DBOS.sleep"
384
+ assert wfsteps_recv[0]["function_name"] == "DBOS.recv"
378
385
 
379
386
 
380
- def test_set_get_event(dbos: DBOS, sys_db: SystemDatabase) -> None:
387
+ def test_set_get_event(
388
+ dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
389
+ ) -> None:
381
390
  value = "Hello, World!"
382
391
 
383
392
  @DBOS.workflow()
@@ -395,22 +404,24 @@ def test_set_get_event(dbos: DBOS, sys_db: SystemDatabase) -> None:
395
404
  with SetWorkflowID(wfid):
396
405
  assert set_get_workflow() == value
397
406
 
398
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, wfid)
407
+ wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
399
408
  assert len(wfsteps) == 5
400
409
  assert wfsteps[0]["function_name"] == "DBOS.setEvent"
401
410
  assert wfsteps[1]["function_name"] == stepOne.__qualname__
402
- assert wfsteps[2]["function_name"] == "DBOS.sleep"
403
- assert wfsteps[3]["function_name"] == "DBOS.getEvent"
404
- assert wfsteps[3]["child_workflow_id"] == None
405
- assert wfsteps[3]["output"] == None
406
- assert wfsteps[3]["error"] == None
411
+ assert wfsteps[3]["function_name"] == "DBOS.sleep"
412
+ assert wfsteps[2]["function_name"] == "DBOS.getEvent"
413
+ assert wfsteps[2]["child_workflow_id"] == None
414
+ assert wfsteps[2]["output"] == None
415
+ assert wfsteps[2]["error"] == None
407
416
  assert wfsteps[4]["function_name"] == "DBOS.getEvent"
408
417
  assert wfsteps[4]["child_workflow_id"] == None
409
418
  assert wfsteps[4]["output"] == value
410
419
  assert wfsteps[4]["error"] == None
411
420
 
412
421
 
413
- def test_callchild_first_sync(dbos: DBOS, sys_db: SystemDatabase) -> None:
422
+ def test_callchild_first_sync(
423
+ dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
424
+ ) -> None:
414
425
 
415
426
  @DBOS.workflow()
416
427
  def parentWorkflow() -> str:
@@ -435,7 +446,7 @@ def test_callchild_first_sync(dbos: DBOS, sys_db: SystemDatabase) -> None:
435
446
  with SetWorkflowID(wfid):
436
447
  child_id = parentWorkflow()
437
448
 
438
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, wfid)
449
+ wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
439
450
  assert len(wfsteps) == 4
440
451
  assert wfsteps[0]["function_name"] == child_workflow.__qualname__
441
452
  assert wfsteps[0]["child_workflow_id"] == child_id
@@ -450,7 +461,9 @@ def test_callchild_first_sync(dbos: DBOS, sys_db: SystemDatabase) -> None:
450
461
 
451
462
 
452
463
  @pytest.mark.asyncio
453
- async def test_callchild_direct_asyncio(dbos: DBOS, sys_db: SystemDatabase) -> None:
464
+ async def test_callchild_direct_asyncio(
465
+ dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
466
+ ) -> None:
454
467
 
455
468
  @DBOS.workflow()
456
469
  async def parentWorkflow() -> str:
@@ -475,7 +488,7 @@ async def test_callchild_direct_asyncio(dbos: DBOS, sys_db: SystemDatabase) -> N
475
488
  with SetWorkflowID(wfid):
476
489
  child_id = await parentWorkflow()
477
490
 
478
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, wfid)
491
+ wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
479
492
  assert len(wfsteps) == 4
480
493
  assert wfsteps[0]["function_name"] == child_workflow.__qualname__
481
494
  assert wfsteps[0]["child_workflow_id"] == child_id
@@ -489,7 +502,9 @@ async def test_callchild_direct_asyncio(dbos: DBOS, sys_db: SystemDatabase) -> N
489
502
  assert wfsteps[3]["function_name"] == stepTwo.__qualname__
490
503
 
491
504
 
492
- def test_callchild_last_sync(dbos: DBOS, sys_db: SystemDatabase) -> None:
505
+ def test_callchild_last_sync(
506
+ dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
507
+ ) -> None:
493
508
 
494
509
  @DBOS.workflow()
495
510
  def parentWorkflow() -> None:
@@ -514,7 +529,7 @@ def test_callchild_last_sync(dbos: DBOS, sys_db: SystemDatabase) -> None:
514
529
  with SetWorkflowID(wfid):
515
530
  parentWorkflow()
516
531
 
517
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, wfid)
532
+ wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
518
533
  assert len(wfsteps) == 4
519
534
  assert wfsteps[0]["function_name"] == stepOne.__qualname__
520
535
  assert wfsteps[1]["function_name"] == stepTwo.__qualname__
@@ -522,7 +537,9 @@ def test_callchild_last_sync(dbos: DBOS, sys_db: SystemDatabase) -> None:
522
537
  assert wfsteps[3]["function_name"] == "DBOS.getResult"
523
538
 
524
539
 
525
- def test_callchild_first_async_thread(dbos: DBOS, sys_db: SystemDatabase) -> None:
540
+ def test_callchild_first_async_thread(
541
+ dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
542
+ ) -> None:
526
543
 
527
544
  @DBOS.workflow()
528
545
  def parentWorkflow() -> None:
@@ -548,7 +565,7 @@ def test_callchild_first_async_thread(dbos: DBOS, sys_db: SystemDatabase) -> Non
548
565
  with SetWorkflowID(wfid):
549
566
  parentWorkflow()
550
567
 
551
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, wfid)
568
+ wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
552
569
  assert len(wfsteps) == 4
553
570
  assert wfsteps[0]["function_name"] == child_workflow.__qualname__
554
571
  assert wfsteps[1]["function_name"] == "DBOS.getStatus"
@@ -556,7 +573,9 @@ def test_callchild_first_async_thread(dbos: DBOS, sys_db: SystemDatabase) -> Non
556
573
  assert wfsteps[3]["function_name"] == stepTwo.__qualname__
557
574
 
558
575
 
559
- def test_list_steps_errors(dbos: DBOS, sys_db: SystemDatabase) -> None:
576
+ def test_list_steps_errors(
577
+ dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
578
+ ) -> None:
560
579
  queue = Queue("test-queue")
561
580
 
562
581
  @DBOS.step()
@@ -582,7 +601,7 @@ def test_list_steps_errors(dbos: DBOS, sys_db: SystemDatabase) -> None:
582
601
  with SetWorkflowID(wfid):
583
602
  with pytest.raises(Exception):
584
603
  call_step()
585
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, wfid)
604
+ wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
586
605
  assert len(wfsteps) == 1
587
606
  assert wfsteps[0]["function_name"] == failing_step.__qualname__
588
607
  assert wfsteps[0]["child_workflow_id"] == None
@@ -594,7 +613,7 @@ def test_list_steps_errors(dbos: DBOS, sys_db: SystemDatabase) -> None:
594
613
  with SetWorkflowID(wfid):
595
614
  with pytest.raises(Exception):
596
615
  start_step()
597
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, wfid)
616
+ wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
598
617
  assert len(wfsteps) == 2
599
618
  assert wfsteps[0]["function_name"] == f"<temp>.{failing_step.__qualname__}"
600
619
  assert wfsteps[0]["child_workflow_id"] == f"{wfid}-1"
@@ -610,7 +629,7 @@ def test_list_steps_errors(dbos: DBOS, sys_db: SystemDatabase) -> None:
610
629
  with SetWorkflowID(wfid):
611
630
  with pytest.raises(Exception):
612
631
  enqueue_step()
613
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, wfid)
632
+ wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
614
633
  assert len(wfsteps) == 2
615
634
  assert wfsteps[0]["function_name"] == f"<temp>.{failing_step.__qualname__}"
616
635
  assert wfsteps[0]["child_workflow_id"] == f"{wfid}-1"
@@ -623,7 +642,9 @@ def test_list_steps_errors(dbos: DBOS, sys_db: SystemDatabase) -> None:
623
642
 
624
643
 
625
644
  @pytest.mark.asyncio
626
- async def test_list_steps_errors_async(dbos: DBOS, sys_db: SystemDatabase) -> None:
645
+ async def test_list_steps_errors_async(
646
+ dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
647
+ ) -> None:
627
648
  queue = Queue("test-queue")
628
649
 
629
650
  @DBOS.step()
@@ -649,7 +670,7 @@ async def test_list_steps_errors_async(dbos: DBOS, sys_db: SystemDatabase) -> No
649
670
  with SetWorkflowID(wfid):
650
671
  with pytest.raises(Exception):
651
672
  await call_step()
652
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, wfid)
673
+ wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
653
674
  assert len(wfsteps) == 1
654
675
  assert wfsteps[0]["function_name"] == failing_step.__qualname__
655
676
  assert wfsteps[0]["child_workflow_id"] == None
@@ -661,7 +682,7 @@ async def test_list_steps_errors_async(dbos: DBOS, sys_db: SystemDatabase) -> No
661
682
  with SetWorkflowID(wfid):
662
683
  with pytest.raises(Exception):
663
684
  await start_step()
664
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, wfid)
685
+ wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
665
686
  assert len(wfsteps) == 2
666
687
  assert wfsteps[0]["function_name"] == f"<temp>.{failing_step.__qualname__}"
667
688
  assert wfsteps[0]["child_workflow_id"] == f"{wfid}-1"
@@ -677,7 +698,7 @@ async def test_list_steps_errors_async(dbos: DBOS, sys_db: SystemDatabase) -> No
677
698
  with SetWorkflowID(wfid):
678
699
  with pytest.raises(Exception):
679
700
  await enqueue_step()
680
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, wfid)
701
+ wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
681
702
  assert len(wfsteps) == 2
682
703
  assert wfsteps[0]["function_name"] == f"<temp>.{failing_step.__qualname__}"
683
704
  assert wfsteps[0]["child_workflow_id"] == f"{wfid}-1"
@@ -689,7 +710,9 @@ async def test_list_steps_errors_async(dbos: DBOS, sys_db: SystemDatabase) -> No
689
710
  assert isinstance(wfsteps[1]["error"], Exception)
690
711
 
691
712
 
692
- def test_callchild_middle_async_thread(dbos: DBOS, sys_db: SystemDatabase) -> None:
713
+ def test_callchild_middle_async_thread(
714
+ dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
715
+ ) -> None:
693
716
 
694
717
  @DBOS.workflow()
695
718
  def parentWorkflow() -> str:
@@ -716,7 +739,7 @@ def test_callchild_middle_async_thread(dbos: DBOS, sys_db: SystemDatabase) -> No
716
739
  with SetWorkflowID(wfid):
717
740
  child_id = parentWorkflow()
718
741
 
719
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, wfid)
742
+ wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
720
743
  assert len(wfsteps) == 5
721
744
  assert wfsteps[0]["function_name"] == stepOne.__qualname__
722
745
  assert wfsteps[0]["child_workflow_id"] == None
@@ -738,7 +761,9 @@ def test_callchild_middle_async_thread(dbos: DBOS, sys_db: SystemDatabase) -> No
738
761
 
739
762
 
740
763
  @pytest.mark.asyncio
741
- async def test_callchild_first_asyncio(dbos: DBOS, sys_db: SystemDatabase) -> None:
764
+ async def test_callchild_first_asyncio(
765
+ dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
766
+ ) -> None:
742
767
 
743
768
  @DBOS.workflow()
744
769
  async def parentWorkflow() -> str:
@@ -765,7 +790,7 @@ async def test_callchild_first_asyncio(dbos: DBOS, sys_db: SystemDatabase) -> No
765
790
  handle = await dbos.start_workflow_async(parentWorkflow)
766
791
  child_id = await handle.get_result()
767
792
 
768
- wfsteps = _workflow_commands.list_workflow_steps(sys_db, wfid)
793
+ wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
769
794
  assert len(wfsteps) == 4
770
795
  assert wfsteps[0]["function_name"] == child_workflow.__qualname__
771
796
  assert wfsteps[0]["child_workflow_id"] == child_id
@@ -856,3 +881,78 @@ async def test_callchild_rerun_asyncio(dbos: DBOS) -> None:
856
881
  res2 = await handle.get_result()
857
882
 
858
883
  assert res1 == res2
884
+
885
+
886
+ def test_list_transaction(
887
+ dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
888
+ ) -> None:
889
+
890
+ @DBOS.workflow()
891
+ def simple_workflow() -> None:
892
+ transactionOne()
893
+ stepTwo()
894
+ DBOS.sleep(1)
895
+ return
896
+
897
+ @DBOS.transaction()
898
+ def transactionOne() -> str:
899
+ return "a test transaction"
900
+
901
+ @DBOS.step()
902
+ def stepTwo() -> None:
903
+ return
904
+
905
+ wfid = str(uuid.uuid4())
906
+ with SetWorkflowID(wfid):
907
+ simple_workflow()
908
+
909
+ wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
910
+ assert len(wfsteps) == 3
911
+ assert wfsteps[0]["function_name"] == transactionOne.__qualname__
912
+ assert wfsteps[0]["output"] == "a test transaction"
913
+ assert wfsteps[0]["error"] == None
914
+ assert wfsteps[1]["function_name"] == stepTwo.__qualname__
915
+ assert wfsteps[2]["function_name"] == "DBOS.sleep"
916
+
917
+
918
+ def test_list_transaction_error(
919
+ dbos: DBOS, sys_db: SystemDatabase, app_db: ApplicationDatabase
920
+ ) -> None:
921
+
922
+ @DBOS.workflow()
923
+ def simple_workflow() -> None:
924
+ transactionOne()
925
+ stepTwo()
926
+ try:
927
+ transactionErr()
928
+ except Exception as e:
929
+ print(f"Error: {e}")
930
+ DBOS.sleep(1)
931
+ return
932
+
933
+ @DBOS.transaction()
934
+ def transactionOne() -> str:
935
+ return "a test transaction"
936
+
937
+ @DBOS.transaction()
938
+ def transactionErr() -> None:
939
+ raise Exception("a test transaction error")
940
+
941
+ @DBOS.step()
942
+ def stepTwo() -> None:
943
+ return
944
+
945
+ wfid = str(uuid.uuid4())
946
+ with SetWorkflowID(wfid):
947
+ simple_workflow()
948
+
949
+ wfsteps = _workflow_commands.list_workflow_steps(sys_db, app_db, wfid)
950
+ assert len(wfsteps) == 4
951
+ assert wfsteps[0]["function_name"] == transactionOne.__qualname__
952
+ assert wfsteps[0]["output"] == "a test transaction"
953
+ assert wfsteps[0]["error"] == None
954
+ assert wfsteps[1]["function_name"] == stepTwo.__qualname__
955
+ assert wfsteps[2]["function_name"] == transactionErr.__qualname__
956
+ assert wfsteps[2]["output"] == None
957
+ assert isinstance(wfsteps[2]["error"], Exception)
958
+ assert wfsteps[3]["function_name"] == "DBOS.sleep"
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