dbos 0.23.0a13__py3-none-any.whl → 0.24.0__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.
Potentially problematic release.
This version of dbos might be problematic. Click here for more details.
- dbos/__init__.py +2 -1
- dbos/_app_db.py +16 -2
- dbos/_conductor/conductor.py +122 -56
- dbos/_conductor/protocol.py +29 -0
- dbos/_core.py +159 -39
- dbos/_db_wizard.py +18 -14
- dbos/_dbos.py +96 -31
- dbos/_dbos_config.py +352 -58
- dbos/_debug.py +7 -1
- dbos/_error.py +9 -3
- dbos/_logger.py +8 -7
- dbos/_queue.py +14 -3
- dbos/_scheduler.py +5 -2
- dbos/_schemas/system_database.py +1 -1
- dbos/_sys_db.py +35 -26
- dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +2 -4
- dbos/_tracer.py +5 -6
- dbos/cli/cli.py +1 -1
- dbos/dbos-config.schema.json +4 -1
- {dbos-0.23.0a13.dist-info → dbos-0.24.0.dist-info}/METADATA +1 -2
- {dbos-0.23.0a13.dist-info → dbos-0.24.0.dist-info}/RECORD +24 -24
- {dbos-0.23.0a13.dist-info → dbos-0.24.0.dist-info}/WHEEL +0 -0
- {dbos-0.23.0a13.dist-info → dbos-0.24.0.dist-info}/entry_points.txt +0 -0
- {dbos-0.23.0a13.dist-info → dbos-0.24.0.dist-info}/licenses/LICENSE +0 -0
dbos/_core.py
CHANGED
|
@@ -58,6 +58,7 @@ from ._error import (
|
|
|
58
58
|
)
|
|
59
59
|
from ._registrations import (
|
|
60
60
|
DEFAULT_MAX_RECOVERY_ATTEMPTS,
|
|
61
|
+
DBOSFuncInfo,
|
|
61
62
|
get_config_name,
|
|
62
63
|
get_dbos_class_name,
|
|
63
64
|
get_dbos_func_name,
|
|
@@ -82,6 +83,7 @@ if TYPE_CHECKING:
|
|
|
82
83
|
DBOS,
|
|
83
84
|
Workflow,
|
|
84
85
|
WorkflowHandle,
|
|
86
|
+
WorkflowHandleAsync,
|
|
85
87
|
WorkflowStatus,
|
|
86
88
|
DBOSRegistry,
|
|
87
89
|
IsolationLevel,
|
|
@@ -136,6 +138,48 @@ class WorkflowHandlePolling(Generic[R]):
|
|
|
136
138
|
return stat
|
|
137
139
|
|
|
138
140
|
|
|
141
|
+
class WorkflowHandleAsyncTask(Generic[R]):
|
|
142
|
+
|
|
143
|
+
def __init__(self, workflow_id: str, task: asyncio.Task[R], dbos: "DBOS"):
|
|
144
|
+
self.workflow_id = workflow_id
|
|
145
|
+
self.task = task
|
|
146
|
+
self.dbos = dbos
|
|
147
|
+
|
|
148
|
+
def get_workflow_id(self) -> str:
|
|
149
|
+
return self.workflow_id
|
|
150
|
+
|
|
151
|
+
async def get_result(self) -> R:
|
|
152
|
+
return await self.task
|
|
153
|
+
|
|
154
|
+
async def get_status(self) -> "WorkflowStatus":
|
|
155
|
+
stat = await asyncio.to_thread(self.dbos.get_workflow_status, self.workflow_id)
|
|
156
|
+
if stat is None:
|
|
157
|
+
raise DBOSNonExistentWorkflowError(self.workflow_id)
|
|
158
|
+
return stat
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class WorkflowHandleAsyncPolling(Generic[R]):
|
|
162
|
+
|
|
163
|
+
def __init__(self, workflow_id: str, dbos: "DBOS"):
|
|
164
|
+
self.workflow_id = workflow_id
|
|
165
|
+
self.dbos = dbos
|
|
166
|
+
|
|
167
|
+
def get_workflow_id(self) -> str:
|
|
168
|
+
return self.workflow_id
|
|
169
|
+
|
|
170
|
+
async def get_result(self) -> R:
|
|
171
|
+
res: R = await asyncio.to_thread(
|
|
172
|
+
self.dbos._sys_db.await_workflow_result, self.workflow_id
|
|
173
|
+
)
|
|
174
|
+
return res
|
|
175
|
+
|
|
176
|
+
async def get_status(self) -> "WorkflowStatus":
|
|
177
|
+
stat = await asyncio.to_thread(self.dbos.get_workflow_status, self.workflow_id)
|
|
178
|
+
if stat is None:
|
|
179
|
+
raise DBOSNonExistentWorkflowError(self.workflow_id)
|
|
180
|
+
return stat
|
|
181
|
+
|
|
182
|
+
|
|
139
183
|
def _init_workflow(
|
|
140
184
|
dbos: "DBOS",
|
|
141
185
|
ctx: DBOSContext,
|
|
@@ -285,6 +329,32 @@ def _execute_workflow_wthread(
|
|
|
285
329
|
raise
|
|
286
330
|
|
|
287
331
|
|
|
332
|
+
async def _execute_workflow_async(
|
|
333
|
+
dbos: "DBOS",
|
|
334
|
+
status: WorkflowStatusInternal,
|
|
335
|
+
func: "Workflow[P, Coroutine[Any, Any, R]]",
|
|
336
|
+
ctx: DBOSContext,
|
|
337
|
+
*args: Any,
|
|
338
|
+
**kwargs: Any,
|
|
339
|
+
) -> R:
|
|
340
|
+
attributes: TracedAttributes = {
|
|
341
|
+
"name": func.__name__,
|
|
342
|
+
"operationType": OperationType.WORKFLOW.value,
|
|
343
|
+
}
|
|
344
|
+
with DBOSContextSwap(ctx):
|
|
345
|
+
with EnterDBOSWorkflow(attributes):
|
|
346
|
+
try:
|
|
347
|
+
result = Pending[R](functools.partial(func, *args, **kwargs)).then(
|
|
348
|
+
_get_wf_invoke_func(dbos, status)
|
|
349
|
+
)
|
|
350
|
+
return await result()
|
|
351
|
+
except Exception:
|
|
352
|
+
dbos.logger.error(
|
|
353
|
+
f"Exception encountered in asynchronous workflow: {traceback.format_exc()}"
|
|
354
|
+
)
|
|
355
|
+
raise
|
|
356
|
+
|
|
357
|
+
|
|
288
358
|
def execute_workflow_by_id(
|
|
289
359
|
dbos: "DBOS", workflow_id: str, startNew: bool = False
|
|
290
360
|
) -> "WorkflowHandle[Any]":
|
|
@@ -349,26 +419,29 @@ def execute_workflow_by_id(
|
|
|
349
419
|
)
|
|
350
420
|
|
|
351
421
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
422
|
+
def _get_new_wf() -> tuple[str, DBOSContext]:
|
|
423
|
+
# Sequence of events for starting a workflow:
|
|
424
|
+
# First - is there a WF already running?
|
|
425
|
+
# (and not in step as that is an error)
|
|
426
|
+
# Assign an ID to the workflow, if it doesn't have an app-assigned one
|
|
427
|
+
# If this is a root workflow, assign a new ID
|
|
428
|
+
# If this is a child workflow, assign parent wf id with call# suffix
|
|
429
|
+
# Make a (system) DB record for the workflow
|
|
430
|
+
# Pass the new context to a worker thread that will run the wf function
|
|
431
|
+
cur_ctx = get_local_dbos_context()
|
|
432
|
+
if cur_ctx is not None and cur_ctx.is_within_workflow():
|
|
433
|
+
assert cur_ctx.is_workflow() # Not in a step
|
|
434
|
+
cur_ctx.function_id += 1
|
|
435
|
+
if len(cur_ctx.id_assigned_for_next_workflow) == 0:
|
|
436
|
+
cur_ctx.id_assigned_for_next_workflow = (
|
|
437
|
+
cur_ctx.workflow_id + "-" + str(cur_ctx.function_id)
|
|
438
|
+
)
|
|
361
439
|
|
|
440
|
+
new_wf_ctx = DBOSContext() if cur_ctx is None else cur_ctx.create_child()
|
|
441
|
+
new_wf_ctx.id_assigned_for_next_workflow = new_wf_ctx.assign_workflow_id()
|
|
442
|
+
new_wf_id = new_wf_ctx.id_assigned_for_next_workflow
|
|
362
443
|
|
|
363
|
-
|
|
364
|
-
def start_workflow(
|
|
365
|
-
dbos: "DBOS",
|
|
366
|
-
func: "Workflow[P, R]",
|
|
367
|
-
queue_name: Optional[str],
|
|
368
|
-
execute_workflow: bool,
|
|
369
|
-
*args: P.args,
|
|
370
|
-
**kwargs: P.kwargs,
|
|
371
|
-
) -> "WorkflowHandle[R]": ...
|
|
444
|
+
return (new_wf_id, new_wf_ctx)
|
|
372
445
|
|
|
373
446
|
|
|
374
447
|
def start_workflow(
|
|
@@ -379,6 +452,7 @@ def start_workflow(
|
|
|
379
452
|
*args: P.args,
|
|
380
453
|
**kwargs: P.kwargs,
|
|
381
454
|
) -> "WorkflowHandle[R]":
|
|
455
|
+
|
|
382
456
|
# If the function has a class, add the class object as its first argument
|
|
383
457
|
fself: Optional[object] = None
|
|
384
458
|
if hasattr(func, "__self__"):
|
|
@@ -399,26 +473,7 @@ def start_workflow(
|
|
|
399
473
|
"kwargs": kwargs,
|
|
400
474
|
}
|
|
401
475
|
|
|
402
|
-
|
|
403
|
-
# First - is there a WF already running?
|
|
404
|
-
# (and not in step as that is an error)
|
|
405
|
-
# Assign an ID to the workflow, if it doesn't have an app-assigned one
|
|
406
|
-
# If this is a root workflow, assign a new ID
|
|
407
|
-
# If this is a child workflow, assign parent wf id with call# suffix
|
|
408
|
-
# Make a (system) DB record for the workflow
|
|
409
|
-
# Pass the new context to a worker thread that will run the wf function
|
|
410
|
-
cur_ctx = get_local_dbos_context()
|
|
411
|
-
if cur_ctx is not None and cur_ctx.is_within_workflow():
|
|
412
|
-
assert cur_ctx.is_workflow() # Not in a step
|
|
413
|
-
cur_ctx.function_id += 1
|
|
414
|
-
if len(cur_ctx.id_assigned_for_next_workflow) == 0:
|
|
415
|
-
cur_ctx.id_assigned_for_next_workflow = (
|
|
416
|
-
cur_ctx.workflow_id + "-" + str(cur_ctx.function_id)
|
|
417
|
-
)
|
|
418
|
-
|
|
419
|
-
new_wf_ctx = DBOSContext() if cur_ctx is None else cur_ctx.create_child()
|
|
420
|
-
new_wf_ctx.id_assigned_for_next_workflow = new_wf_ctx.assign_workflow_id()
|
|
421
|
-
new_wf_id = new_wf_ctx.id_assigned_for_next_workflow
|
|
476
|
+
new_wf_id, new_wf_ctx = _get_new_wf()
|
|
422
477
|
|
|
423
478
|
status = _init_workflow(
|
|
424
479
|
dbos,
|
|
@@ -458,6 +513,69 @@ def start_workflow(
|
|
|
458
513
|
return WorkflowHandleFuture(new_wf_id, future, dbos)
|
|
459
514
|
|
|
460
515
|
|
|
516
|
+
async def start_workflow_async(
|
|
517
|
+
dbos: "DBOS",
|
|
518
|
+
func: "Workflow[P, Coroutine[Any, Any, R]]",
|
|
519
|
+
queue_name: Optional[str],
|
|
520
|
+
execute_workflow: bool,
|
|
521
|
+
*args: P.args,
|
|
522
|
+
**kwargs: P.kwargs,
|
|
523
|
+
) -> "WorkflowHandleAsync[R]":
|
|
524
|
+
|
|
525
|
+
# If the function has a class, add the class object as its first argument
|
|
526
|
+
fself: Optional[object] = None
|
|
527
|
+
if hasattr(func, "__self__"):
|
|
528
|
+
fself = func.__self__
|
|
529
|
+
if fself is not None:
|
|
530
|
+
args = (fself,) + args # type: ignore
|
|
531
|
+
|
|
532
|
+
fi = get_func_info(func)
|
|
533
|
+
if fi is None:
|
|
534
|
+
raise DBOSWorkflowFunctionNotFoundError(
|
|
535
|
+
"<NONE>", f"start_workflow: function {func.__name__} is not registered"
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
func = cast("Workflow[P, R]", func.__orig_func) # type: ignore
|
|
539
|
+
|
|
540
|
+
inputs: WorkflowInputs = {
|
|
541
|
+
"args": args,
|
|
542
|
+
"kwargs": kwargs,
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
new_wf_id, new_wf_ctx = _get_new_wf()
|
|
546
|
+
|
|
547
|
+
status = await asyncio.to_thread(
|
|
548
|
+
_init_workflow,
|
|
549
|
+
dbos,
|
|
550
|
+
new_wf_ctx,
|
|
551
|
+
inputs=inputs,
|
|
552
|
+
wf_name=get_dbos_func_name(func),
|
|
553
|
+
class_name=get_dbos_class_name(fi, func, args),
|
|
554
|
+
config_name=get_config_name(fi, func, args),
|
|
555
|
+
temp_wf_type=get_temp_workflow_type(func),
|
|
556
|
+
queue=queue_name,
|
|
557
|
+
max_recovery_attempts=fi.max_recovery_attempts,
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
wf_status = status["status"]
|
|
561
|
+
|
|
562
|
+
if not execute_workflow or (
|
|
563
|
+
not dbos.debug_mode
|
|
564
|
+
and (
|
|
565
|
+
wf_status == WorkflowStatusString.ERROR.value
|
|
566
|
+
or wf_status == WorkflowStatusString.SUCCESS.value
|
|
567
|
+
)
|
|
568
|
+
):
|
|
569
|
+
dbos.logger.debug(
|
|
570
|
+
f"Workflow {new_wf_id} already completed with status {wf_status}. Directly returning a workflow handle."
|
|
571
|
+
)
|
|
572
|
+
return WorkflowHandleAsyncPolling(new_wf_id, dbos)
|
|
573
|
+
|
|
574
|
+
coro = _execute_workflow_async(dbos, status, func, new_wf_ctx, *args, **kwargs)
|
|
575
|
+
task = asyncio.create_task(coro)
|
|
576
|
+
return WorkflowHandleAsyncTask(new_wf_id, task, dbos)
|
|
577
|
+
|
|
578
|
+
|
|
461
579
|
if sys.version_info < (3, 12):
|
|
462
580
|
|
|
463
581
|
def _mark_coroutine(func: Callable[P, R]) -> Callable[P, R]:
|
|
@@ -824,7 +942,9 @@ def decorate_step(
|
|
|
824
942
|
stepOutcome = Outcome[R].make(functools.partial(func, *args, **kwargs))
|
|
825
943
|
if retries_allowed:
|
|
826
944
|
stepOutcome = stepOutcome.retry(
|
|
827
|
-
max_attempts,
|
|
945
|
+
max_attempts,
|
|
946
|
+
on_exception,
|
|
947
|
+
lambda i: DBOSMaxStepRetriesExceeded(func.__name__, i),
|
|
828
948
|
)
|
|
829
949
|
|
|
830
950
|
outcome = (
|
dbos/_db_wizard.py
CHANGED
|
@@ -28,7 +28,20 @@ class DatabaseConnection(TypedDict):
|
|
|
28
28
|
local_suffix: Optional[bool]
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def db_wizard(config: "ConfigFile"
|
|
31
|
+
def db_wizard(config: "ConfigFile") -> "ConfigFile":
|
|
32
|
+
"""Checks database connectivity and helps the user start a database if needed
|
|
33
|
+
|
|
34
|
+
First, check connectivity to the database configured in the provided `config` object.
|
|
35
|
+
If it fails:
|
|
36
|
+
- Return an error if the connection failed due to incorrect credentials.
|
|
37
|
+
- Return an error if it detects a non-default configuration.
|
|
38
|
+
- Otherwise assume the configured database is not running and guide the user through setting it up.
|
|
39
|
+
|
|
40
|
+
The wizard will first attempt to start a local Postgres instance using Docker.
|
|
41
|
+
If Docker is not available, it will prompt the user to connect to a DBOS Cloud database.
|
|
42
|
+
|
|
43
|
+
Finally, if a database was configured, its connection details will be saved in the local `.dbos/db_connection` file.
|
|
44
|
+
"""
|
|
32
45
|
# 1. Check the connectivity to the database. Return if successful. If cannot connect, continue to the following steps.
|
|
33
46
|
db_connection_error = _check_db_connectivity(config)
|
|
34
47
|
if db_connection_error is None:
|
|
@@ -44,27 +57,18 @@ def db_wizard(config: "ConfigFile", config_file_path: str) -> "ConfigFile":
|
|
|
44
57
|
raise DBOSInitializationError(
|
|
45
58
|
f"Could not connect to Postgres: password authentication failed: {db_connection_error}"
|
|
46
59
|
)
|
|
47
|
-
db_config = config["database"]
|
|
48
|
-
|
|
49
|
-
# Read the config file and check if the database hostname/port/username are set. If so, skip the wizard.
|
|
50
|
-
with open(config_file_path, "r") as file:
|
|
51
|
-
content = file.read()
|
|
52
|
-
local_config = yaml.safe_load(content)
|
|
53
|
-
if "database" not in local_config:
|
|
54
|
-
local_config["database"] = {}
|
|
55
|
-
local_config = cast("ConfigFile", local_config)
|
|
56
60
|
|
|
61
|
+
# If the database config is not the default one, surface the error and exit.
|
|
62
|
+
db_config = config["database"] # FIXME: what if database is not in config?
|
|
57
63
|
if (
|
|
58
|
-
|
|
59
|
-
or local_config["database"].get("port")
|
|
60
|
-
or local_config["database"].get("username")
|
|
61
|
-
or db_config["hostname"] != "localhost"
|
|
64
|
+
db_config["hostname"] != "localhost"
|
|
62
65
|
or db_config["port"] != 5432
|
|
63
66
|
or db_config["username"] != "postgres"
|
|
64
67
|
):
|
|
65
68
|
raise DBOSInitializationError(
|
|
66
69
|
f"Could not connect to the database. Exception: {db_connection_error}"
|
|
67
70
|
)
|
|
71
|
+
|
|
68
72
|
print("[yellow]Postgres not detected locally[/yellow]")
|
|
69
73
|
|
|
70
74
|
# 3. If the database config is the default one, check if the user has Docker properly installed.
|
dbos/_dbos.py
CHANGED
|
@@ -49,6 +49,7 @@ from ._core import (
|
|
|
49
49
|
send,
|
|
50
50
|
set_event,
|
|
51
51
|
start_workflow,
|
|
52
|
+
start_workflow_async,
|
|
52
53
|
workflow_wrapper,
|
|
53
54
|
)
|
|
54
55
|
from ._queue import Queue, queue_thread
|
|
@@ -88,13 +89,23 @@ from ._context import (
|
|
|
88
89
|
assert_current_dbos_context,
|
|
89
90
|
get_local_dbos_context,
|
|
90
91
|
)
|
|
91
|
-
from ._dbos_config import
|
|
92
|
+
from ._dbos_config import (
|
|
93
|
+
ConfigFile,
|
|
94
|
+
DBOSConfig,
|
|
95
|
+
check_config_consistency,
|
|
96
|
+
is_dbos_configfile,
|
|
97
|
+
load_config,
|
|
98
|
+
overwrite_config,
|
|
99
|
+
process_config,
|
|
100
|
+
set_env_vars,
|
|
101
|
+
translate_dbos_config_to_config_file,
|
|
102
|
+
)
|
|
92
103
|
from ._error import (
|
|
93
104
|
DBOSConflictingRegistrationError,
|
|
94
105
|
DBOSException,
|
|
95
106
|
DBOSNonExistentWorkflowError,
|
|
96
107
|
)
|
|
97
|
-
from ._logger import add_otlp_to_all_loggers, dbos_logger
|
|
108
|
+
from ._logger import add_otlp_to_all_loggers, config_logger, dbos_logger, init_logger
|
|
98
109
|
from ._sys_db import SystemDatabase
|
|
99
110
|
|
|
100
111
|
# Most DBOS functions are just any callable F, so decorators / wrappers work on F
|
|
@@ -257,7 +268,7 @@ class DBOS:
|
|
|
257
268
|
def __new__(
|
|
258
269
|
cls: Type[DBOS],
|
|
259
270
|
*,
|
|
260
|
-
config: Optional[ConfigFile] = None,
|
|
271
|
+
config: Optional[Union[ConfigFile, DBOSConfig]] = None,
|
|
261
272
|
fastapi: Optional["FastAPI"] = None,
|
|
262
273
|
flask: Optional["Flask"] = None,
|
|
263
274
|
conductor_url: Optional[str] = None,
|
|
@@ -302,7 +313,7 @@ class DBOS:
|
|
|
302
313
|
def __init__(
|
|
303
314
|
self,
|
|
304
315
|
*,
|
|
305
|
-
config: Optional[ConfigFile] = None,
|
|
316
|
+
config: Optional[Union[ConfigFile, DBOSConfig]] = None,
|
|
306
317
|
fastapi: Optional["FastAPI"] = None,
|
|
307
318
|
flask: Optional["Flask"] = None,
|
|
308
319
|
conductor_url: Optional[str] = None,
|
|
@@ -312,12 +323,7 @@ class DBOS:
|
|
|
312
323
|
return
|
|
313
324
|
|
|
314
325
|
self._initialized: bool = True
|
|
315
|
-
|
|
316
|
-
config = load_config()
|
|
317
|
-
set_env_vars(config)
|
|
318
|
-
dbos_tracer.config(config)
|
|
319
|
-
dbos_logger.info("Initializing DBOS")
|
|
320
|
-
self.config: ConfigFile = config
|
|
326
|
+
|
|
321
327
|
self._launched: bool = False
|
|
322
328
|
self._debug_mode: bool = False
|
|
323
329
|
self._sys_db_field: Optional[SystemDatabase] = None
|
|
@@ -334,6 +340,36 @@ class DBOS:
|
|
|
334
340
|
self.conductor_key: Optional[str] = conductor_key
|
|
335
341
|
self.conductor_websocket: Optional[ConductorWebsocket] = None
|
|
336
342
|
|
|
343
|
+
init_logger()
|
|
344
|
+
|
|
345
|
+
unvalidated_config: Optional[ConfigFile] = None
|
|
346
|
+
|
|
347
|
+
if config is None:
|
|
348
|
+
# If no config is provided, load it from dbos-config.yaml
|
|
349
|
+
unvalidated_config = load_config(run_process_config=False)
|
|
350
|
+
elif is_dbos_configfile(config):
|
|
351
|
+
unvalidated_config = cast(ConfigFile, config)
|
|
352
|
+
if os.environ.get("DBOS__CLOUD") == "true":
|
|
353
|
+
unvalidated_config = overwrite_config(unvalidated_config)
|
|
354
|
+
check_config_consistency(name=unvalidated_config["name"])
|
|
355
|
+
else:
|
|
356
|
+
unvalidated_config = translate_dbos_config_to_config_file(
|
|
357
|
+
cast(DBOSConfig, config)
|
|
358
|
+
)
|
|
359
|
+
if os.environ.get("DBOS__CLOUD") == "true":
|
|
360
|
+
unvalidated_config = overwrite_config(unvalidated_config)
|
|
361
|
+
check_config_consistency(name=unvalidated_config["name"])
|
|
362
|
+
|
|
363
|
+
if unvalidated_config is not None:
|
|
364
|
+
self.config: ConfigFile = process_config(data=unvalidated_config)
|
|
365
|
+
else:
|
|
366
|
+
raise ValueError("No valid configuration was loaded.")
|
|
367
|
+
|
|
368
|
+
set_env_vars(self.config)
|
|
369
|
+
config_logger(self.config)
|
|
370
|
+
dbos_tracer.config(self.config)
|
|
371
|
+
dbos_logger.info("Initializing DBOS")
|
|
372
|
+
|
|
337
373
|
# If using FastAPI, set up middleware and lifecycle events
|
|
338
374
|
if self.fastapi is not None:
|
|
339
375
|
from ._fastapi import setup_fastapi_middleware
|
|
@@ -419,10 +455,17 @@ class DBOS:
|
|
|
419
455
|
if debug_mode:
|
|
420
456
|
return
|
|
421
457
|
|
|
422
|
-
admin_port = self.config
|
|
458
|
+
admin_port = self.config.get("runtimeConfig", {}).get("admin_port")
|
|
423
459
|
if admin_port is None:
|
|
424
460
|
admin_port = 3001
|
|
425
|
-
|
|
461
|
+
run_admin_server = self.config.get("runtimeConfig", {}).get(
|
|
462
|
+
"run_admin_server"
|
|
463
|
+
)
|
|
464
|
+
if run_admin_server:
|
|
465
|
+
try:
|
|
466
|
+
self._admin_server_field = AdminServer(dbos=self, port=admin_port)
|
|
467
|
+
except Exception as e:
|
|
468
|
+
dbos_logger.warning(f"Failed to start admin server: {e}")
|
|
426
469
|
|
|
427
470
|
workflow_ids = self._sys_db.get_pending_workflows(
|
|
428
471
|
GlobalParams.executor_id, GlobalParams.app_version
|
|
@@ -660,35 +703,26 @@ class DBOS:
|
|
|
660
703
|
f"{e.name} dependency not found. Please install {e.name} via your package manager."
|
|
661
704
|
) from e
|
|
662
705
|
|
|
663
|
-
@overload
|
|
664
|
-
@classmethod
|
|
665
|
-
def start_workflow(
|
|
666
|
-
cls,
|
|
667
|
-
func: Workflow[P, Coroutine[Any, Any, R]],
|
|
668
|
-
*args: P.args,
|
|
669
|
-
**kwargs: P.kwargs,
|
|
670
|
-
) -> WorkflowHandle[R]: ...
|
|
671
|
-
|
|
672
|
-
@overload
|
|
673
706
|
@classmethod
|
|
674
707
|
def start_workflow(
|
|
675
708
|
cls,
|
|
676
709
|
func: Workflow[P, R],
|
|
677
710
|
*args: P.args,
|
|
678
711
|
**kwargs: P.kwargs,
|
|
679
|
-
) -> WorkflowHandle[R]:
|
|
712
|
+
) -> WorkflowHandle[R]:
|
|
713
|
+
"""Invoke a workflow function in the background, returning a handle to the ongoing execution."""
|
|
714
|
+
return start_workflow(_get_dbos_instance(), func, None, True, *args, **kwargs)
|
|
680
715
|
|
|
681
716
|
@classmethod
|
|
682
|
-
def
|
|
717
|
+
async def start_workflow_async(
|
|
683
718
|
cls,
|
|
684
|
-
func: Workflow[P,
|
|
719
|
+
func: Workflow[P, Coroutine[Any, Any, R]],
|
|
685
720
|
*args: P.args,
|
|
686
721
|
**kwargs: P.kwargs,
|
|
687
|
-
) ->
|
|
688
|
-
"""Invoke a workflow function
|
|
689
|
-
return
|
|
690
|
-
|
|
691
|
-
start_workflow(_get_dbos_instance(), func, None, True, *args, **kwargs),
|
|
722
|
+
) -> WorkflowHandleAsync[R]:
|
|
723
|
+
"""Invoke a workflow function on the event loop, returning a handle to the ongoing execution."""
|
|
724
|
+
return await start_workflow_async(
|
|
725
|
+
_get_dbos_instance(), func, None, True, *args, **kwargs
|
|
692
726
|
)
|
|
693
727
|
|
|
694
728
|
@classmethod
|
|
@@ -923,7 +957,9 @@ class DBOS:
|
|
|
923
957
|
reg = _get_or_create_dbos_registry()
|
|
924
958
|
if reg.config is not None:
|
|
925
959
|
return reg.config
|
|
926
|
-
config =
|
|
960
|
+
config = (
|
|
961
|
+
load_config()
|
|
962
|
+
) # This will return the processed & validated config (with defaults)
|
|
927
963
|
reg.config = config
|
|
928
964
|
return config
|
|
929
965
|
|
|
@@ -1063,6 +1099,35 @@ class WorkflowHandle(Generic[R], Protocol):
|
|
|
1063
1099
|
...
|
|
1064
1100
|
|
|
1065
1101
|
|
|
1102
|
+
class WorkflowHandleAsync(Generic[R], Protocol):
|
|
1103
|
+
"""
|
|
1104
|
+
Handle to a workflow function.
|
|
1105
|
+
|
|
1106
|
+
`WorkflowHandleAsync` represents a current or previous workflow function invocation,
|
|
1107
|
+
allowing its status and result to be accessed.
|
|
1108
|
+
|
|
1109
|
+
Attributes:
|
|
1110
|
+
workflow_id(str): Workflow ID of the function invocation
|
|
1111
|
+
|
|
1112
|
+
"""
|
|
1113
|
+
|
|
1114
|
+
def __init__(self, workflow_id: str) -> None: ...
|
|
1115
|
+
|
|
1116
|
+
workflow_id: str
|
|
1117
|
+
|
|
1118
|
+
def get_workflow_id(self) -> str:
|
|
1119
|
+
"""Return the applicable workflow ID."""
|
|
1120
|
+
...
|
|
1121
|
+
|
|
1122
|
+
async def get_result(self) -> R:
|
|
1123
|
+
"""Return the result of the workflow function invocation, waiting if necessary."""
|
|
1124
|
+
...
|
|
1125
|
+
|
|
1126
|
+
async def get_status(self) -> WorkflowStatus:
|
|
1127
|
+
"""Return the current workflow function invocation status as `WorkflowStatus`."""
|
|
1128
|
+
...
|
|
1129
|
+
|
|
1130
|
+
|
|
1066
1131
|
class DBOSConfiguredInstance:
|
|
1067
1132
|
"""
|
|
1068
1133
|
Base class for classes containing DBOS member functions.
|