dbos 1.14.0a2__tar.gz → 1.14.0a4__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 (116) hide show
  1. {dbos-1.14.0a2 → dbos-1.14.0a4}/PKG-INFO +1 -1
  2. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_core.py +25 -5
  3. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_outcome.py +67 -13
  4. {dbos-1.14.0a2 → dbos-1.14.0a4}/pyproject.toml +1 -1
  5. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_async.py +90 -4
  6. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_async_workflow_management.py +1 -1
  7. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_dbos.py +35 -3
  8. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_workflow_introspection.py +4 -4
  9. {dbos-1.14.0a2 → dbos-1.14.0a4}/LICENSE +0 -0
  10. {dbos-1.14.0a2 → dbos-1.14.0a4}/README.md +0 -0
  11. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/__init__.py +0 -0
  12. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/__main__.py +0 -0
  13. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_admin_server.py +0 -0
  14. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_alembic_migrations/env.py +0 -0
  15. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_alembic_migrations/script.py.mako +0 -0
  16. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_alembic_migrations/versions/01ce9f07bd10_streaming.py +0 -0
  17. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_alembic_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
  18. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_alembic_migrations/versions/27ac6900c6ad_add_queue_dedup.py +0 -0
  19. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_alembic_migrations/versions/471b60d64126_dbos_migrations.py +0 -0
  20. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_alembic_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  21. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_alembic_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  22. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_alembic_migrations/versions/66478e1b95e5_consolidate_queues.py +0 -0
  23. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_alembic_migrations/versions/83f3732ae8e7_workflow_timeout.py +0 -0
  24. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_alembic_migrations/versions/933e86bdac6a_add_queue_priority.py +0 -0
  25. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_alembic_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  26. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_alembic_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  27. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_alembic_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  28. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_alembic_migrations/versions/d994145b47b6_consolidate_inputs.py +0 -0
  29. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_alembic_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  30. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_alembic_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
  31. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_app_db.py +0 -0
  32. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_classproperty.py +0 -0
  33. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_client.py +0 -0
  34. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_conductor/conductor.py +0 -0
  35. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_conductor/protocol.py +0 -0
  36. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_context.py +0 -0
  37. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_croniter.py +0 -0
  38. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_dbos.py +0 -0
  39. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_dbos_config.py +0 -0
  40. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_debug.py +0 -0
  41. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_docker_pg_helper.py +0 -0
  42. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_error.py +0 -0
  43. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_event_loop.py +0 -0
  44. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_fastapi.py +0 -0
  45. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_flask.py +0 -0
  46. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_kafka.py +0 -0
  47. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_kafka_message.py +0 -0
  48. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_logger.py +0 -0
  49. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_migration.py +0 -0
  50. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_queue.py +0 -0
  51. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_recovery.py +0 -0
  52. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_registrations.py +0 -0
  53. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_roles.py +0 -0
  54. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_scheduler.py +0 -0
  55. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_schemas/__init__.py +0 -0
  56. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_schemas/application_database.py +0 -0
  57. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_schemas/system_database.py +0 -0
  58. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_serialization.py +0 -0
  59. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_sys_db.py +0 -0
  60. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_sys_db_postgres.py +0 -0
  61. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_sys_db_sqlite.py +0 -0
  62. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_templates/dbos-db-starter/README.md +0 -0
  63. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
  64. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_templates/dbos-db-starter/__package/main.py.dbos +0 -0
  65. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
  66. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
  67. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
  68. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
  69. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
  70. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
  71. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
  72. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_tracer.py +0 -0
  73. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_utils.py +0 -0
  74. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/_workflow_commands.py +0 -0
  75. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/cli/_github_init.py +0 -0
  76. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/cli/_template_init.py +0 -0
  77. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/cli/cli.py +0 -0
  78. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/cli/migration.py +0 -0
  79. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/dbos-config.schema.json +0 -0
  80. {dbos-1.14.0a2 → dbos-1.14.0a4}/dbos/py.typed +0 -0
  81. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/__init__.py +0 -0
  82. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/atexit_no_ctor.py +0 -0
  83. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/atexit_no_launch.py +0 -0
  84. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/classdefs.py +0 -0
  85. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/client_collateral.py +0 -0
  86. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/client_worker.py +0 -0
  87. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/conftest.py +0 -0
  88. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/dupname_classdefs1.py +0 -0
  89. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/dupname_classdefsa.py +0 -0
  90. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/more_classdefs.py +0 -0
  91. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/queuedworkflow.py +0 -0
  92. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_admin_server.py +0 -0
  93. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_classdecorators.py +0 -0
  94. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_cli.py +0 -0
  95. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_client.py +0 -0
  96. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_concurrency.py +0 -0
  97. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_config.py +0 -0
  98. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_croniter.py +0 -0
  99. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_debug.py +0 -0
  100. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_docker_secrets.py +0 -0
  101. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_failures.py +0 -0
  102. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_fastapi.py +0 -0
  103. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_fastapi_roles.py +0 -0
  104. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_flask.py +0 -0
  105. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_kafka.py +0 -0
  106. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_outcome.py +0 -0
  107. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_package.py +0 -0
  108. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_queue.py +0 -0
  109. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_scheduler.py +0 -0
  110. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_schema_migration.py +0 -0
  111. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_singleton.py +0 -0
  112. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_spans.py +0 -0
  113. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_sqlalchemy.py +0 -0
  114. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_streaming.py +0 -0
  115. {dbos-1.14.0a2 → dbos-1.14.0a4}/tests/test_workflow_management.py +0 -0
  116. {dbos-1.14.0a2 → dbos-1.14.0a4}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 1.14.0a2
3
+ Version: 1.14.0a4
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -19,8 +19,6 @@ from typing import (
19
19
  cast,
20
20
  )
21
21
 
22
- import psycopg
23
-
24
22
  from dbos._outcome import Immediate, NoResult, Outcome, Pending
25
23
  from dbos._utils import GlobalParams, retriable_postgres_exception
26
24
 
@@ -58,6 +56,7 @@ from ._error import (
58
56
  DBOSWorkflowConflictIDError,
59
57
  DBOSWorkflowFunctionNotFoundError,
60
58
  )
59
+ from ._logger import dbos_logger
61
60
  from ._registrations import (
62
61
  DEFAULT_MAX_RECOVERY_ATTEMPTS,
63
62
  get_config_name,
@@ -98,6 +97,14 @@ F = TypeVar("F", bound=Callable[..., Any])
98
97
  TEMP_SEND_WF_NAME = "<temp>.temp_send_workflow"
99
98
 
100
99
 
100
+ def check_is_in_coroutine() -> bool:
101
+ try:
102
+ asyncio.get_running_loop()
103
+ return True
104
+ except RuntimeError:
105
+ return False
106
+
107
+
101
108
  class WorkflowHandleFuture(Generic[R]):
102
109
 
103
110
  def __init__(self, workflow_id: str, future: Future[R], dbos: "DBOS"):
@@ -830,11 +837,16 @@ def workflow_wrapper(
830
837
  dbos._sys_db.record_get_result(workflow_id, serialized_r, None)
831
838
  return r
832
839
 
840
+ if check_is_in_coroutine() and not inspect.iscoroutinefunction(func):
841
+ dbos_logger.warning(
842
+ f"Sync workflow ({get_dbos_func_name(func)}) shouldn't be invoked from within another async function. Define it as async or use asyncio.to_thread instead."
843
+ )
844
+
833
845
  outcome = (
834
- wfOutcome.wrap(init_wf)
846
+ wfOutcome.wrap(init_wf, dbos=dbos)
835
847
  .also(DBOSAssumeRole(rr))
836
848
  .also(enterWorkflowCtxMgr(attributes))
837
- .then(record_get_result)
849
+ .then(record_get_result, dbos=dbos)
838
850
  )
839
851
  return outcome() # type: ignore
840
852
 
@@ -1011,6 +1023,10 @@ def decorate_transaction(
1011
1023
  assert (
1012
1024
  ctx.is_workflow()
1013
1025
  ), "Transactions must be called from within workflows"
1026
+ if check_is_in_coroutine():
1027
+ dbos_logger.warning(
1028
+ f"Transaction function ({get_dbos_func_name(func)}) shouldn't be invoked from within another async function. Use asyncio.to_thread instead."
1029
+ )
1014
1030
  with DBOSAssumeRole(rr):
1015
1031
  return invoke_tx(*args, **kwargs)
1016
1032
  else:
@@ -1146,7 +1162,7 @@ def decorate_step(
1146
1162
 
1147
1163
  outcome = (
1148
1164
  stepOutcome.then(record_step_result)
1149
- .intercept(check_existing_result)
1165
+ .intercept(check_existing_result, dbos=dbos)
1150
1166
  .also(EnterDBOSStep(attributes))
1151
1167
  )
1152
1168
  return outcome()
@@ -1155,6 +1171,10 @@ def decorate_step(
1155
1171
 
1156
1172
  @wraps(func)
1157
1173
  def wrapper(*args: Any, **kwargs: Any) -> Any:
1174
+ if check_is_in_coroutine() and not inspect.iscoroutinefunction(func):
1175
+ dbos_logger.warning(
1176
+ f"Sync step ({get_dbos_func_name(func)}) shouldn't be invoked from within another async function. Define it as async or use asyncio.to_thread instead."
1177
+ )
1158
1178
  # If the step is called from a workflow, run it as a step.
1159
1179
  # Otherwise, run it as a normal function.
1160
1180
  ctx = get_local_dbos_context()
@@ -2,9 +2,24 @@ import asyncio
2
2
  import contextlib
3
3
  import inspect
4
4
  import time
5
- from typing import Any, Callable, Coroutine, Optional, Protocol, TypeVar, Union, cast
5
+ from typing import (
6
+ TYPE_CHECKING,
7
+ Any,
8
+ Callable,
9
+ Coroutine,
10
+ Optional,
11
+ Protocol,
12
+ TypeVar,
13
+ Union,
14
+ cast,
15
+ )
6
16
 
7
17
  from dbos._context import EnterDBOSStepRetry
18
+ from dbos._error import DBOSException
19
+ from dbos._registrations import get_dbos_func_name
20
+
21
+ if TYPE_CHECKING:
22
+ from ._dbos import DBOS
8
23
 
9
24
  T = TypeVar("T")
10
25
  R = TypeVar("R")
@@ -24,10 +39,15 @@ class NoResult:
24
39
  class Outcome(Protocol[T]):
25
40
 
26
41
  def wrap(
27
- self, before: Callable[[], Callable[[Callable[[], T]], R]]
42
+ self,
43
+ before: Callable[[], Callable[[Callable[[], T]], R]],
44
+ *,
45
+ dbos: Optional["DBOS"] = None,
28
46
  ) -> "Outcome[R]": ...
29
47
 
30
- def then(self, next: Callable[[Callable[[], T]], R]) -> "Outcome[R]": ...
48
+ def then(
49
+ self, next: Callable[[Callable[[], T]], R], *, dbos: Optional["DBOS"] = None
50
+ ) -> "Outcome[R]": ...
31
51
 
32
52
  def also(
33
53
  self, cm: contextlib.AbstractContextManager[Any, bool]
@@ -41,7 +61,10 @@ class Outcome(Protocol[T]):
41
61
  ) -> "Outcome[T]": ...
42
62
 
43
63
  def intercept(
44
- self, interceptor: Callable[[], Union[NoResult, T]]
64
+ self,
65
+ interceptor: Callable[[], Union[NoResult, T]],
66
+ *,
67
+ dbos: Optional["DBOS"] = None,
45
68
  ) -> "Outcome[T]": ...
46
69
 
47
70
  def __call__(self) -> Union[T, Coroutine[Any, Any, T]]: ...
@@ -63,11 +86,17 @@ class Immediate(Outcome[T]):
63
86
  def __init__(self, func: Callable[[], T]):
64
87
  self._func = func
65
88
 
66
- def then(self, next: Callable[[Callable[[], T]], R]) -> "Immediate[R]":
89
+ def then(
90
+ self,
91
+ next: Callable[[Callable[[], T]], R],
92
+ dbos: Optional["DBOS"] = None,
93
+ ) -> "Immediate[R]":
67
94
  return Immediate(lambda: next(self._func))
68
95
 
69
96
  def wrap(
70
- self, before: Callable[[], Callable[[Callable[[], T]], R]]
97
+ self,
98
+ before: Callable[[], Callable[[Callable[[], T]], R]],
99
+ dbos: Optional["DBOS"] = None,
71
100
  ) -> "Immediate[R]":
72
101
  return Immediate(lambda: before()(self._func))
73
102
 
@@ -79,7 +108,10 @@ class Immediate(Outcome[T]):
79
108
  return intercepted if not isinstance(intercepted, NoResult) else func()
80
109
 
81
110
  def intercept(
82
- self, interceptor: Callable[[], Union[NoResult, T]]
111
+ self,
112
+ interceptor: Callable[[], Union[NoResult, T]],
113
+ *,
114
+ dbos: Optional["DBOS"] = None,
83
115
  ) -> "Immediate[T]":
84
116
  return Immediate[T](lambda: Immediate._intercept(self._func, interceptor))
85
117
 
@@ -142,7 +174,12 @@ class Pending(Outcome[T]):
142
174
  async def _wrap(
143
175
  func: Callable[[], Coroutine[Any, Any, T]],
144
176
  before: Callable[[], Callable[[Callable[[], T]], R]],
177
+ *,
178
+ dbos: Optional["DBOS"] = None,
145
179
  ) -> R:
180
+ # Make sure the executor pool is configured correctly
181
+ if dbos is not None:
182
+ await dbos._configure_asyncio_thread_pool()
146
183
  after = await asyncio.to_thread(before)
147
184
  try:
148
185
  value = await func()
@@ -151,12 +188,17 @@ class Pending(Outcome[T]):
151
188
  return await asyncio.to_thread(after, lambda: Pending._raise(exp))
152
189
 
153
190
  def wrap(
154
- self, before: Callable[[], Callable[[Callable[[], T]], R]]
191
+ self,
192
+ before: Callable[[], Callable[[Callable[[], T]], R]],
193
+ *,
194
+ dbos: Optional["DBOS"] = None,
155
195
  ) -> "Pending[R]":
156
- return Pending[R](lambda: Pending._wrap(self._func, before))
196
+ return Pending[R](lambda: Pending._wrap(self._func, before, dbos=dbos))
157
197
 
158
- def then(self, next: Callable[[Callable[[], T]], R]) -> "Pending[R]":
159
- return Pending[R](lambda: Pending._wrap(self._func, lambda: next))
198
+ def then(
199
+ self, next: Callable[[Callable[[], T]], R], *, dbos: Optional["DBOS"] = None
200
+ ) -> "Pending[R]":
201
+ return Pending[R](lambda: Pending._wrap(self._func, lambda: next, dbos=dbos))
160
202
 
161
203
  @staticmethod
162
204
  async def _also( # type: ignore
@@ -173,12 +215,24 @@ class Pending(Outcome[T]):
173
215
  async def _intercept(
174
216
  func: Callable[[], Coroutine[Any, Any, T]],
175
217
  interceptor: Callable[[], Union[NoResult, T]],
218
+ *,
219
+ dbos: Optional["DBOS"] = None,
176
220
  ) -> T:
221
+ # Make sure the executor pool is configured correctly
222
+ if dbos is not None:
223
+ await dbos._configure_asyncio_thread_pool()
177
224
  intercepted = await asyncio.to_thread(interceptor)
178
225
  return intercepted if not isinstance(intercepted, NoResult) else await func()
179
226
 
180
- def intercept(self, interceptor: Callable[[], Union[NoResult, T]]) -> "Pending[T]":
181
- return Pending[T](lambda: Pending._intercept(self._func, interceptor))
227
+ def intercept(
228
+ self,
229
+ interceptor: Callable[[], Union[NoResult, T]],
230
+ *,
231
+ dbos: Optional["DBOS"] = None,
232
+ ) -> "Pending[T]":
233
+ return Pending[T](
234
+ lambda: Pending._intercept(self._func, interceptor, dbos=dbos)
235
+ )
182
236
 
183
237
  @staticmethod
184
238
  async def _retry(
@@ -27,7 +27,7 @@ dependencies = [
27
27
  ]
28
28
  requires-python = ">=3.9"
29
29
  readme = "README.md"
30
- version = "1.14.0a2"
30
+ version = "1.14.0a4"
31
31
 
32
32
  [project.license]
33
33
  text = "MIT"
@@ -5,6 +5,9 @@ from typing import List, Optional
5
5
 
6
6
  import pytest
7
7
  import sqlalchemy as sa
8
+ from opentelemetry._logs import set_logger_provider
9
+ from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
10
+ from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, InMemoryLogExporter
8
11
 
9
12
  # Public API
10
13
  from dbos import (
@@ -19,6 +22,8 @@ from dbos._context import assert_current_dbos_context
19
22
  from dbos._dbos import WorkflowHandle
20
23
  from dbos._dbos_config import ConfigFile
21
24
  from dbos._error import DBOSAwaitedWorkflowCancelledError, DBOSException
25
+ from dbos._logger import dbos_logger
26
+ from dbos._registrations import get_dbos_func_name
22
27
 
23
28
 
24
29
  @pytest.mark.asyncio
@@ -31,7 +36,7 @@ async def test_async_workflow(dbos: DBOS) -> None:
31
36
  async def test_workflow(var1: str, var2: str) -> str:
32
37
  nonlocal wf_counter
33
38
  wf_counter += 1
34
- res1 = test_transaction(var1)
39
+ res1 = await asyncio.to_thread(test_transaction, var1)
35
40
  res2 = await test_step(var2)
36
41
  DBOS.logger.info("I'm test_workflow")
37
42
  return res1 + res2
@@ -88,7 +93,7 @@ async def test_async_step(dbos: DBOS) -> None:
88
93
  async def test_workflow(var1: str, var2: str) -> str:
89
94
  nonlocal wf_counter
90
95
  wf_counter += 1
91
- res1 = test_transaction(var1)
96
+ res1 = await asyncio.to_thread(test_transaction, var1)
92
97
  res2 = await test_step(var2)
93
98
  DBOS.logger.info("I'm test_workflow")
94
99
  return res1 + res2
@@ -325,6 +330,7 @@ def test_async_tx_raises(config: ConfigFile) -> None:
325
330
  async def test_async_tx() -> None:
326
331
  pass
327
332
 
333
+ assert "is a coroutine function" in str(exc_info.value)
328
334
  # destroy call needed to avoid "functions were registered but DBOS() was not called" warning
329
335
  DBOS.destroy(destroy_registry=True)
330
336
 
@@ -343,12 +349,12 @@ async def test_start_workflow_async(dbos: DBOS) -> None:
343
349
  wf_el_id = id(asyncio.get_running_loop())
344
350
  nonlocal wf_counter
345
351
  wf_counter += 1
346
- res2 = test_step(var2)
352
+ res2 = await test_step(var2)
347
353
  DBOS.logger.info("I'm test_workflow")
348
354
  return var1 + res2
349
355
 
350
356
  @DBOS.step()
351
- def test_step(var: str) -> str:
357
+ async def test_step(var: str) -> str:
352
358
  nonlocal step_el_id
353
359
  step_el_id = id(asyncio.get_running_loop())
354
360
  nonlocal step_counter
@@ -605,3 +611,83 @@ async def test_workflow_with_task_cancellation(dbos: DBOS) -> None:
605
611
  # Verify the workflow completes despite the task cancellation
606
612
  handle: WorkflowHandleAsync[str] = await DBOS.retrieve_workflow_async(wfid)
607
613
  assert await handle.get_result() == "completed"
614
+
615
+
616
+ @pytest.mark.asyncio
617
+ async def test_check_async_violation(dbos: DBOS) -> None:
618
+ # Set up in-memory log exporter
619
+ log_exporter = InMemoryLogExporter() # type: ignore
620
+ log_processor = BatchLogRecordProcessor(log_exporter)
621
+ log_provider = LoggerProvider()
622
+ log_provider.add_log_record_processor(log_processor)
623
+ set_logger_provider(log_provider)
624
+ dbos_logger.addHandler(LoggingHandler(logger_provider=log_provider))
625
+
626
+ @DBOS.workflow()
627
+ def sync_workflow() -> str:
628
+ return "sync"
629
+
630
+ @DBOS.step()
631
+ def sync_step() -> str:
632
+ return "step"
633
+
634
+ @DBOS.workflow()
635
+ async def async_workflow_sync_step() -> str:
636
+ return sync_step()
637
+
638
+ @DBOS.transaction()
639
+ def sync_transaction() -> str:
640
+ return "txn"
641
+
642
+ @DBOS.workflow()
643
+ async def async_workflow_sync_txn() -> str:
644
+ return sync_transaction()
645
+
646
+ # Call a sync workflow should log a warning
647
+ sync_workflow()
648
+
649
+ log_processor.force_flush(timeout_millis=5000)
650
+ logs = log_exporter.get_finished_logs()
651
+ assert len(logs) == 1
652
+ assert (
653
+ logs[0].log_record.body is not None
654
+ and f"Sync workflow ({get_dbos_func_name(sync_workflow)}) shouldn't be invoked from within another async function."
655
+ in logs[0].log_record.body
656
+ )
657
+ log_exporter.clear()
658
+
659
+ # Call a sync step from within an async workflow should log a warning
660
+ await async_workflow_sync_step()
661
+ log_processor.force_flush(timeout_millis=5000)
662
+ logs = log_exporter.get_finished_logs()
663
+ assert len(logs) == 1
664
+ assert (
665
+ logs[0].log_record.body is not None
666
+ and f"Sync step ({get_dbos_func_name(sync_step)}) shouldn't be invoked from within another async function."
667
+ in logs[0].log_record.body
668
+ )
669
+ log_exporter.clear()
670
+
671
+ # Directly call a sync step should log a warning
672
+ sync_step()
673
+ log_processor.force_flush(timeout_millis=5000)
674
+ logs = log_exporter.get_finished_logs()
675
+ assert len(logs) == 1
676
+ assert (
677
+ logs[0].log_record.body is not None
678
+ and f"Sync step ({get_dbos_func_name(sync_step)}) shouldn't be invoked from within another async function."
679
+ in logs[0].log_record.body
680
+ )
681
+ log_exporter.clear()
682
+
683
+ # Call a sync transaction from within an async workflow should log a warning
684
+ await async_workflow_sync_txn()
685
+ log_processor.force_flush(timeout_millis=5000)
686
+ logs = log_exporter.get_finished_logs()
687
+ assert len(logs) == 1
688
+ assert (
689
+ logs[0].log_record.body is not None
690
+ and f"Transaction function ({get_dbos_func_name(sync_transaction)}) shouldn't be invoked from within another async function."
691
+ in logs[0].log_record.body
692
+ )
693
+ log_exporter.clear()
@@ -148,7 +148,7 @@ async def test_fork_workflow_async(dbos: DBOS) -> None:
148
148
 
149
149
  wfid = str(uuid.uuid4())
150
150
  with SetWorkflowID(wfid):
151
- assert simple_workflow(input_val) == output
151
+ assert await asyncio.to_thread(simple_workflow, input_val) == output
152
152
 
153
153
  assert step_one_count == 1
154
154
  assert step_two_count == 1
@@ -1,5 +1,6 @@
1
1
  # mypy: disable-error-code="no-redef"
2
2
 
3
+ import asyncio
3
4
  import datetime
4
5
  import logging
5
6
  import os
@@ -1250,12 +1251,43 @@ def test_destroy_semantics(dbos: DBOS, config: DBOSConfig) -> None:
1250
1251
  var = "test"
1251
1252
  assert test_workflow(var) == var
1252
1253
 
1254
+ # Start the workflow asynchornously
1255
+ wf = dbos.start_workflow(test_workflow, var)
1256
+ assert wf.get_result() == var
1257
+
1253
1258
  DBOS.destroy()
1254
1259
  DBOS(config=config)
1255
1260
  DBOS.launch()
1256
1261
 
1257
1262
  assert test_workflow(var) == var
1258
1263
 
1264
+ wf = dbos.start_workflow(test_workflow, var)
1265
+ assert wf.get_result() == var
1266
+
1267
+
1268
+ @pytest.mark.asyncio
1269
+ async def test_destroy_semantics_async(dbos: DBOS, config: DBOSConfig) -> None:
1270
+
1271
+ @DBOS.workflow()
1272
+ async def test_workflow(var: str) -> str:
1273
+ return var
1274
+
1275
+ var = "test"
1276
+ assert await test_workflow(var) == var
1277
+
1278
+ # Start the workflow asynchornously
1279
+ wf = await dbos.start_workflow_async(test_workflow, var)
1280
+ assert await wf.get_result() == var
1281
+
1282
+ DBOS.destroy()
1283
+ DBOS(config=config)
1284
+ DBOS.launch()
1285
+
1286
+ assert await test_workflow(var) == var
1287
+
1288
+ wf = await dbos.start_workflow_async(test_workflow, var)
1289
+ assert await wf.get_result() == var
1290
+
1259
1291
 
1260
1292
  def test_double_decoration(dbos: DBOS) -> None:
1261
1293
  with pytest.raises(
@@ -1637,17 +1669,17 @@ async def test_step_without_dbos(dbos: DBOS, config: DBOSConfig) -> None:
1637
1669
  assert DBOS.workflow_id is None
1638
1670
  return x
1639
1671
 
1640
- assert step(5) == 5
1672
+ assert await asyncio.to_thread(step, 5) == 5
1641
1673
  assert await async_step(5) == 5
1642
1674
 
1643
1675
  DBOS(config=config)
1644
1676
 
1645
- assert step(5) == 5
1677
+ assert await asyncio.to_thread(step, 5) == 5
1646
1678
  assert await async_step(5) == 5
1647
1679
 
1648
1680
  DBOS.launch()
1649
1681
 
1650
- assert step(5) == 5
1682
+ assert await asyncio.to_thread(step, 5) == 5
1651
1683
  assert await async_step(5) == 5
1652
1684
 
1653
1685
  assert len(DBOS.list_workflows()) == 0
@@ -828,18 +828,18 @@ async def test_callchild_first_asyncio(dbos: DBOS) -> None:
828
828
  async def parentWorkflow() -> str:
829
829
  handle = await dbos.start_workflow_async(child_workflow)
830
830
  child_id = await handle.get_result()
831
- stepOne()
832
- stepTwo()
831
+ await stepOne()
832
+ await stepTwo()
833
833
  return child_id
834
834
 
835
835
  @DBOS.step()
836
- def stepOne() -> str:
836
+ async def stepOne() -> str:
837
837
  workflow_id = DBOS.workflow_id
838
838
  assert workflow_id is not None
839
839
  return workflow_id
840
840
 
841
841
  @DBOS.step()
842
- def stepTwo() -> None:
842
+ async def stepTwo() -> None:
843
843
  return
844
844
 
845
845
  @DBOS.workflow()
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