dbos 0.26.0a11__py3-none-any.whl → 0.26.0a14__py3-none-any.whl

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.
dbos/_core.py CHANGED
@@ -365,7 +365,9 @@ def _execute_workflow_wthread(
365
365
  if isinstance(result, Immediate):
366
366
  return cast(Immediate[R], result)()
367
367
  else:
368
- return asyncio.run(cast(Pending[R], result)())
368
+ return dbos._background_event_loop.submit_coroutine(
369
+ cast(Pending[R], result)()
370
+ )
369
371
  except Exception:
370
372
  dbos.logger.error(
371
373
  f"Exception encountered in asynchronous workflow: {traceback.format_exc()}"
dbos/_dbos.py CHANGED
@@ -4,7 +4,6 @@ import asyncio
4
4
  import atexit
5
5
  import hashlib
6
6
  import inspect
7
- import json
8
7
  import os
9
8
  import sys
10
9
  import threading
@@ -31,7 +30,6 @@ from typing import (
31
30
 
32
31
  from opentelemetry.trace import Span
33
32
 
34
- from dbos import _serialization
35
33
  from dbos._conductor.conductor import ConductorWebsocket
36
34
  from dbos._utils import INTERNAL_QUEUE_NAME, GlobalParams
37
35
  from dbos._workflow_commands import (
@@ -62,6 +60,7 @@ from ._recovery import recover_pending_workflows, startup_recovery_thread
62
60
  from ._registrations import (
63
61
  DEFAULT_MAX_RECOVERY_ATTEMPTS,
64
62
  DBOSClassInfo,
63
+ _class_fqn,
65
64
  get_or_create_class_info,
66
65
  set_dbos_func_name,
67
66
  set_temp_workflow_type,
@@ -111,6 +110,7 @@ from ._error import (
111
110
  DBOSException,
112
111
  DBOSNonExistentWorkflowError,
113
112
  )
113
+ from ._event_loop import BackgroundEventLoop
114
114
  from ._logger import add_otlp_to_all_loggers, config_logger, dbos_logger, init_logger
115
115
  from ._sys_db import SystemDatabase
116
116
  from ._workflow_commands import WorkflowStatus, get_workflow
@@ -181,15 +181,17 @@ class DBOSRegistry:
181
181
  self.workflow_info_map[name] = wrapped_func
182
182
 
183
183
  def register_class(self, cls: type, ci: DBOSClassInfo) -> None:
184
- class_name = cls.__name__
184
+ class_name = _class_fqn(cls)
185
185
  if class_name in self.class_info_map:
186
186
  if self.class_info_map[class_name] is not cls:
187
187
  raise Exception(f"Duplicate type registration for class '{class_name}'")
188
188
  else:
189
189
  self.class_info_map[class_name] = cls
190
190
 
191
- def create_class_info(self, cls: Type[T]) -> Type[T]:
192
- ci = get_or_create_class_info(cls)
191
+ def create_class_info(
192
+ self, cls: Type[T], class_name: Optional[str] = None
193
+ ) -> Type[T]:
194
+ ci = get_or_create_class_info(cls, class_name)
193
195
  self.register_class(cls, ci)
194
196
  return cls
195
197
 
@@ -204,7 +206,7 @@ class DBOSRegistry:
204
206
 
205
207
  def register_instance(self, inst: object) -> None:
206
208
  config_name = getattr(inst, "config_name")
207
- class_name = inst.__class__.__name__
209
+ class_name = _class_fqn(inst.__class__)
208
210
  fn = f"{class_name}/{config_name}"
209
211
  if fn in self.instance_info_map:
210
212
  if self.instance_info_map[fn] is not inst:
@@ -338,6 +340,7 @@ class DBOS:
338
340
  self.conductor_url: Optional[str] = conductor_url
339
341
  self.conductor_key: Optional[str] = conductor_key
340
342
  self.conductor_websocket: Optional[ConductorWebsocket] = None
343
+ self._background_event_loop: BackgroundEventLoop = BackgroundEventLoop()
341
344
 
342
345
  init_logger()
343
346
 
@@ -448,6 +451,7 @@ class DBOS:
448
451
  dbos_logger.info(f"Executor ID: {GlobalParams.executor_id}")
449
452
  dbos_logger.info(f"Application version: {GlobalParams.app_version}")
450
453
  self._executor_field = ThreadPoolExecutor(max_workers=64)
454
+ self._background_event_loop.start()
451
455
  self._sys_db_field = SystemDatabase(
452
456
  self.config["database"], debug_mode=debug_mode
453
457
  )
@@ -565,6 +569,7 @@ class DBOS:
565
569
  self._initialized = False
566
570
  for event in self.stop_events:
567
571
  event.set()
572
+ self._background_event_loop.stop()
568
573
  if self._sys_db_field is not None:
569
574
  self._sys_db_field.destroy()
570
575
  self._sys_db_field = None
@@ -641,15 +646,22 @@ class DBOS:
641
646
  )
642
647
 
643
648
  @classmethod
644
- def dbos_class(cls) -> Callable[[Type[T]], Type[T]]:
649
+ def dbos_class(
650
+ cls, class_name: Optional[str] = None
651
+ ) -> Callable[[Type[T]], Type[T]]:
645
652
  """
646
653
  Decorate a class that contains DBOS member functions.
647
654
 
648
655
  All DBOS classes must be decorated, as this associates the class with
649
- its member functions.
656
+ its member functions. Class names must be globally unique. By default, the class name is class.__qualname__ but you can optionally provide a class name that is different from the default name.
650
657
  """
651
658
 
652
- return _get_or_create_dbos_registry().create_class_info
659
+ def register_class(cls: Type[T]) -> Type[T]:
660
+ # Register the class with the DBOS registry
661
+ _get_or_create_dbos_registry().create_class_info(cls, class_name)
662
+ return cls
663
+
664
+ return register_class
653
665
 
654
666
  @classmethod
655
667
  def default_required_roles(cls, roles: List[str]) -> Callable[[Type[T]], Type[T]]:
dbos/_docker_pg_helper.py CHANGED
@@ -46,7 +46,7 @@ def start_docker_pg() -> None:
46
46
  if has_docker:
47
47
  start_docker_postgres(pool_config)
48
48
  logging.info(
49
- f"Postgres available at postgres://postgres:{pool_config['password']}@{pool_config['host']}:{pool_config['port']}"
49
+ f"Postgres available at postgresql://postgres:{pool_config['password']}@{pool_config['host']}:{pool_config['port']}"
50
50
  )
51
51
  else:
52
52
  logging.warning("Docker not detected locally")
dbos/_error.py CHANGED
@@ -26,6 +26,29 @@ class DBOSException(Exception):
26
26
  return f"DBOS Error: {self.message}"
27
27
 
28
28
 
29
+ class DBOSBaseException(BaseException):
30
+ """
31
+ This class is for DBOS exceptions that should not be caught by user code.
32
+ It inherits from BaseException instead of Exception so it cannot be caught
33
+ except by code specifically trying to catch it.
34
+
35
+ Attributes:
36
+ message(str): The error message string
37
+ dbos_error_code(DBOSErrorCode): The error code, from the `DBOSErrorCode` enum
38
+ """
39
+
40
+ def __init__(self, message: str, dbos_error_code: Optional[int] = None):
41
+ self.message = message
42
+ self.dbos_error_code = dbos_error_code
43
+ self.status_code: Optional[int] = None
44
+ super().__init__(self.message)
45
+
46
+ def __str__(self) -> str:
47
+ if self.dbos_error_code:
48
+ return f"DBOS Error {self.dbos_error_code}: {self.message}"
49
+ return f"DBOS Error: {self.message}"
50
+
51
+
29
52
  class DBOSErrorCode(Enum):
30
53
  ConflictingIDError = 1
31
54
  RecoveryError = 2
@@ -41,14 +64,9 @@ class DBOSErrorCode(Enum):
41
64
  ConflictingRegistrationError = 25
42
65
 
43
66
 
44
- class DBOSWorkflowConflictIDError(DBOSException):
45
- """Exception raised when a workflow database record already exists."""
46
-
47
- def __init__(self, workflow_id: str):
48
- super().__init__(
49
- f"Conflicting workflow ID {workflow_id}",
50
- dbos_error_code=DBOSErrorCode.ConflictingIDError.value,
51
- )
67
+ #######################################
68
+ ## Exception
69
+ #######################################
52
70
 
53
71
 
54
72
  class DBOSConflictingWorkflowError(DBOSException):
@@ -138,16 +156,6 @@ class DBOSMaxStepRetriesExceeded(DBOSException):
138
156
  return (self.__class__, (self.step_name, self.max_retries))
139
157
 
140
158
 
141
- class DBOSWorkflowCancelledError(DBOSException):
142
- """Exception raised when the workflow has already been cancelled."""
143
-
144
- def __init__(self, msg: str) -> None:
145
- super().__init__(
146
- msg,
147
- dbos_error_code=DBOSErrorCode.WorkflowCancelled.value,
148
- )
149
-
150
-
151
159
  class DBOSConflictingRegistrationError(DBOSException):
152
160
  """Exception raised when conflicting decorators are applied to the same function."""
153
161
 
@@ -168,3 +176,28 @@ class DBOSUnexpectedStepError(DBOSException):
168
176
  f"During execution of workflow {workflow_id} step {step_id}, function {recorded_name} was recorded when {expected_name} was expected. Check that your workflow is deterministic.",
169
177
  dbos_error_code=DBOSErrorCode.UnexpectedStep.value,
170
178
  )
179
+
180
+
181
+ #######################################
182
+ ## BaseException
183
+ #######################################
184
+
185
+
186
+ class DBOSWorkflowCancelledError(DBOSBaseException):
187
+ """BaseException raised when the workflow has already been cancelled."""
188
+
189
+ def __init__(self, msg: str) -> None:
190
+ super().__init__(
191
+ msg,
192
+ dbos_error_code=DBOSErrorCode.WorkflowCancelled.value,
193
+ )
194
+
195
+
196
+ class DBOSWorkflowConflictIDError(DBOSBaseException):
197
+ """BaseException raised when a workflow database record already exists."""
198
+
199
+ def __init__(self, workflow_id: str):
200
+ super().__init__(
201
+ f"Conflicting workflow ID {workflow_id}",
202
+ dbos_error_code=DBOSErrorCode.ConflictingIDError.value,
203
+ )
dbos/_event_loop.py ADDED
@@ -0,0 +1,67 @@
1
+ import asyncio
2
+ import threading
3
+ from typing import Any, Coroutine, Optional, TypeVar
4
+
5
+
6
+ class BackgroundEventLoop:
7
+ """
8
+ This is the event loop to which DBOS submits any coroutines that are not started from within an event loop.
9
+ In particular, coroutines submitted to queues (such as from scheduled workflows) run on this event loop.
10
+ """
11
+
12
+ def __init__(self) -> None:
13
+ self._loop: Optional[asyncio.AbstractEventLoop] = None
14
+ self._thread: Optional[threading.Thread] = None
15
+ self._running = False
16
+ self._ready = threading.Event()
17
+
18
+ def start(self) -> None:
19
+ if self._running:
20
+ return
21
+
22
+ self._thread = threading.Thread(target=self._run_event_loop, daemon=True)
23
+ self._thread.start()
24
+ self._ready.wait() # Wait until the loop is running
25
+
26
+ def stop(self) -> None:
27
+ if not self._running or self._loop is None or self._thread is None:
28
+ return
29
+
30
+ asyncio.run_coroutine_threadsafe(self._shutdown(), self._loop)
31
+ self._thread.join()
32
+ self._running = False
33
+
34
+ def _run_event_loop(self) -> None:
35
+ self._loop = asyncio.new_event_loop()
36
+ asyncio.set_event_loop(self._loop)
37
+
38
+ self._running = True
39
+ self._ready.set() # Signal that the loop is ready
40
+
41
+ try:
42
+ self._loop.run_forever()
43
+ finally:
44
+ self._loop.close()
45
+
46
+ async def _shutdown(self) -> None:
47
+ if self._loop is None:
48
+ raise RuntimeError("Event loop not started")
49
+ tasks = [
50
+ task
51
+ for task in asyncio.all_tasks(self._loop)
52
+ if task is not asyncio.current_task(self._loop)
53
+ ]
54
+
55
+ for task in tasks:
56
+ task.cancel()
57
+
58
+ await asyncio.gather(*tasks, return_exceptions=True)
59
+ self._loop.stop()
60
+
61
+ T = TypeVar("T")
62
+
63
+ def submit_coroutine(self, coro: Coroutine[Any, Any, T]) -> T:
64
+ """Submit a coroutine to the background event loop"""
65
+ if self._loop is None:
66
+ raise RuntimeError("Event loop not started")
67
+ return asyncio.run_coroutine_threadsafe(coro, self._loop).result()
dbos/_registrations.py CHANGED
@@ -34,6 +34,7 @@ def set_temp_workflow_type(f: Any, name: TempWorkflowType) -> None:
34
34
 
35
35
  @dataclass
36
36
  class DBOSClassInfo:
37
+ registered_name: str
37
38
  def_required_roles: Optional[List[str]] = None
38
39
 
39
40
 
@@ -53,11 +54,17 @@ class DBOSFuncInfo:
53
54
  max_recovery_attempts: int = DEFAULT_MAX_RECOVERY_ATTEMPTS
54
55
 
55
56
 
56
- def get_or_create_class_info(cls: Type[Any]) -> DBOSClassInfo:
57
+ def get_or_create_class_info(
58
+ cls: Type[Any], provided_name: Optional[str] = None
59
+ ) -> DBOSClassInfo:
57
60
  if hasattr(cls, "dbos_class_decorator_info"):
58
61
  ci: DBOSClassInfo = getattr(cls, "dbos_class_decorator_info")
59
62
  return ci
60
- ci = DBOSClassInfo()
63
+ class_name = _class_fqn(cls)
64
+ # Use the provided name instead of the class name if it is not None
65
+ if provided_name is not None:
66
+ class_name = provided_name
67
+ ci = DBOSClassInfo(registered_name=class_name)
61
68
  setattr(cls, "dbos_class_decorator_info", ci)
62
69
 
63
70
  # Tell all DBOS functions about this
@@ -166,16 +173,24 @@ def get_config_name(
166
173
  return None
167
174
 
168
175
 
176
+ def _class_fqn(cls: type) -> str:
177
+ """Returns the registered name of the given class. If the class name was not overridden at registration time, it returns the qualified name of the class."""
178
+ ci = get_class_info(cls)
179
+ if ci is not None:
180
+ return ci.registered_name
181
+ return cls.__qualname__
182
+
183
+
169
184
  def get_dbos_class_name(
170
185
  fi: Optional[DBOSFuncInfo], func: Callable[..., Any], args: Tuple[Any, ...]
171
186
  ) -> Optional[str]:
172
187
  if fi and fi.func_type != DBOSFuncType.Unknown and len(args) > 0:
173
188
  if fi.func_type == DBOSFuncType.Instance:
174
189
  first_arg = args[0]
175
- return str(first_arg.__class__.__name__)
190
+ return _class_fqn(first_arg.__class__)
176
191
  if fi.func_type == DBOSFuncType.Class:
177
192
  first_arg = args[0]
178
- return str(first_arg.__name__)
193
+ return _class_fqn(first_arg)
179
194
  return None
180
195
 
181
196
  # Check for improperly-registered functions
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 0.26.0a11
3
+ Version: 0.26.0a14
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -1,7 +1,7 @@
1
- dbos-0.26.0a11.dist-info/METADATA,sha256=sw5u9uXooyqFBMqX-mEFYFXehYWd-ism0o9xcqokW4g,5554
2
- dbos-0.26.0a11.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
3
- dbos-0.26.0a11.dist-info/entry_points.txt,sha256=_QOQ3tVfEjtjBlr1jS4sHqHya9lI2aIEIWkz8dqYp14,58
4
- dbos-0.26.0a11.dist-info/licenses/LICENSE,sha256=VGZit_a5-kdw9WT6fY5jxAWVwGQzgLFyPWrcVVUhVNU,1067
1
+ dbos-0.26.0a14.dist-info/METADATA,sha256=KmqNCgW2bcxs1qddgonsP0MDPe1tt5tlUvs07bN-XDY,5554
2
+ dbos-0.26.0a14.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
3
+ dbos-0.26.0a14.dist-info/entry_points.txt,sha256=_QOQ3tVfEjtjBlr1jS4sHqHya9lI2aIEIWkz8dqYp14,58
4
+ dbos-0.26.0a14.dist-info/licenses/LICENSE,sha256=VGZit_a5-kdw9WT6fY5jxAWVwGQzgLFyPWrcVVUhVNU,1067
5
5
  dbos/__init__.py,sha256=3NQfGlBiiUSM_v88STdVP3rNZvGkUL_9WbSotKb8Voo,873
6
6
  dbos/__main__.py,sha256=G7Exn-MhGrVJVDbgNlpzhfh8WMX_72t3_oJaFT9Lmt8,653
7
7
  dbos/_admin_server.py,sha256=vxPG_YJ6lYrkfPCSp42FiATVLBOij7Fm52Yngg5Z_tE,7027
@@ -11,13 +11,14 @@ dbos/_client.py,sha256=5iaoFsu5wAqwjjj3EWusZ1eDbBAW8FwYazhokdCJ9h4,10964
11
11
  dbos/_conductor/conductor.py,sha256=HYzVL29IMMrs2Mnms_7cHJynCnmmEN5SDQOMjzn3UoU,16840
12
12
  dbos/_conductor/protocol.py,sha256=xN7pmooyF1pqbH1b6WhllU5718P7zSb_b0KCwA6bzcs,6716
13
13
  dbos/_context.py,sha256=I8sLkdKTTkZEz7wG-MjynaQB6XEF2bLXuwNksiauP7w,19430
14
- dbos/_core.py,sha256=tjBGVbSgOn59lR29gcYi5f6fcKNKQM5EP1QXrQGUkXA,45426
14
+ dbos/_core.py,sha256=de8GecFmW5DNf5dYfnpSX3IDO24Wc6pBpCC1VZ1iVyI,45505
15
15
  dbos/_croniter.py,sha256=XHAyUyibs_59sJQfSNWkP7rqQY6_XrlfuuCxk4jYqek,47559
16
- dbos/_dbos.py,sha256=rc5MLk6k-B0BlDpsQ9mYW509M1HUYa9SZvdWPpZS35o,45587
16
+ dbos/_dbos.py,sha256=byXhhiG14nS3iU85NphvQ26vvnJ-gu1tMwTIoUc3dYc,46239
17
17
  dbos/_dbos_config.py,sha256=m05IFjM0jSwZBsnFMF_4qP2JkjVFc0gqyM2tnotXq20,20636
18
18
  dbos/_debug.py,sha256=MNlQVZ6TscGCRQeEEL0VE8Uignvr6dPeDDDefS3xgIE,1823
19
- dbos/_docker_pg_helper.py,sha256=9OGbuavRA_cwE-uPiLZJSdpbQu-6PPgl9clQZB2zT_U,5852
20
- dbos/_error.py,sha256=HtdV6Qy7qRyGD57wxLwE7YT0WdYtlx5ZLEe_Kv_gC-U,5953
19
+ dbos/_docker_pg_helper.py,sha256=NmcgqmR5rQA_4igfeqh8ugNT2z3YmoOvuep_MEtxTiY,5854
20
+ dbos/_error.py,sha256=9ITvFsN_Udpx0xXtYQHXXXb6PjPr3TmMondGmprV-L0,7003
21
+ dbos/_event_loop.py,sha256=NmaLbEQFfEK36S_0KhVD39YdYrGce3qSKCTJ-5RqKQ0,2136
21
22
  dbos/_fastapi.py,sha256=PhaKftbApHnjtYEOw0EYna_3K0cmz__J9of7mRJWzu4,3704
22
23
  dbos/_flask.py,sha256=DZKUZR5-xOzPI7tYZ53r2PvvHVoAb8SYwLzMVFsVfjI,2608
23
24
  dbos/_kafka.py,sha256=pz0xZ9F3X9Ky1k-VSbeF3tfPhP3UPr3lUUhUfE41__U,4198
@@ -36,7 +37,7 @@ dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py,sha256
36
37
  dbos/_outcome.py,sha256=EXxBg4jXCVJsByDQ1VOCIedmbeq_03S6d-p1vqQrLFU,6810
37
38
  dbos/_queue.py,sha256=l0g_CXJbxEmftCA9yhy-cyaR_sddfQSCfm-5XgIWzqU,3397
38
39
  dbos/_recovery.py,sha256=98Py7icfytyIELJ54gIsdvmURBvTb0HmWaxEAuYL0dc,2546
39
- dbos/_registrations.py,sha256=_zy6k944Ll8QwqU12Kr3OP23ukVtm8axPNN1TS_kJRc,6717
40
+ dbos/_registrations.py,sha256=ZDC5lghy_1ZMdMGsSBrXSyS96DH3baA4nyCkFdUmIlc,7292
40
41
  dbos/_request.py,sha256=cX1B3Atlh160phgS35gF1VEEV4pD126c9F3BDgBmxZU,929
41
42
  dbos/_roles.py,sha256=iOsgmIAf1XVzxs3gYWdGRe1B880YfOw5fpU7Jwx8_A8,2271
42
43
  dbos/_scheduler.py,sha256=SR1oRZRcVzYsj-JauV2LA8JtwTkt8mru7qf6H1AzQ1U,2027
@@ -64,4 +65,4 @@ dbos/cli/cli.py,sha256=Lb_RYmXoT5KH0xDbwaYpROE4c-svZ0eCq2Kxg7cAxTw,16537
64
65
  dbos/dbos-config.schema.json,sha256=i7jcxXqByKq0Jzv3nAUavONtj03vTwj6vWP4ylmBr8o,5694
65
66
  dbos/py.typed,sha256=QfzXT1Ktfk3Rj84akygc7_42z0lRpCq0Ilh8OXI6Zas,44
66
67
  version/__init__.py,sha256=L4sNxecRuqdtSFdpUGX3TtBi9KL3k7YsZVIvv-fv9-A,1678
67
- dbos-0.26.0a11.dist-info/RECORD,,
68
+ dbos-0.26.0a14.dist-info/RECORD,,