dbos 1.7.0a4__tar.gz → 1.8.0__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.
- {dbos-1.7.0a4 → dbos-1.8.0}/PKG-INFO +1 -1
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_admin_server.py +3 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_client.py +27 -4
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_conductor/conductor.py +6 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_conductor/protocol.py +5 -2
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_context.py +37 -12
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_core.py +4 -1
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_dbos.py +112 -30
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_dbos_config.py +24 -1
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_error.py +5 -5
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_sys_db.py +65 -40
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_workflow_commands.py +9 -2
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/cli/cli.py +4 -4
- {dbos-1.7.0a4 → dbos-1.8.0}/pyproject.toml +1 -1
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/conftest.py +2 -1
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_admin_server.py +33 -10
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_async.py +6 -2
- dbos-1.8.0/tests/test_async_workflow_management.py +264 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_classdecorators.py +27 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_client.py +20 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_config.py +16 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_dbos.py +26 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_failures.py +7 -4
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_queue.py +8 -1
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_scheduler.py +3 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_singleton.py +0 -24
- dbos-1.8.0/tests/test_spans.py +272 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_workflow_introspection.py +70 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_workflow_management.py +1 -1
- dbos-1.7.0a4/tests/test_spans.py +0 -147
- {dbos-1.7.0a4 → dbos-1.8.0}/LICENSE +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/README.md +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/__init__.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/__main__.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_app_db.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_classproperty.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_croniter.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_debug.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_docker_pg_helper.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_event_loop.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_fastapi.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_flask.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_kafka.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_kafka_message.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_logger.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/env.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/script.py.mako +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/27ac6900c6ad_add_queue_dedup.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/66478e1b95e5_consolidate_queues.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/83f3732ae8e7_workflow_timeout.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/933e86bdac6a_add_queue_priority.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/d994145b47b6_consolidate_inputs.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_outcome.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_queue.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_recovery.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_registrations.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_roles.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_scheduler.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_schemas/__init__.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_schemas/application_database.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_schemas/system_database.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_serialization.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_templates/dbos-db-starter/README.md +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_templates/dbos-db-starter/__package/main.py.dbos +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_tracer.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/_utils.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/cli/_github_init.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/cli/_template_init.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/dbos-config.schema.json +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/dbos/py.typed +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/__init__.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/atexit_no_ctor.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/atexit_no_launch.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/classdefs.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/client_collateral.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/client_worker.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/dupname_classdefs1.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/dupname_classdefsa.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/more_classdefs.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/queuedworkflow.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_cli.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_concurrency.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_croniter.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_debug.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_docker_secrets.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_fastapi.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_fastapi_roles.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_flask.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_kafka.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_outcome.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_package.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_schema_migration.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/tests/test_sqlalchemy.py +0 -0
- {dbos-1.7.0a4 → dbos-1.8.0}/version/__init__.py +0 -0
|
@@ -343,6 +343,8 @@ class AdminRequestHandler(BaseHTTPRequestHandler):
|
|
|
343
343
|
offset=filters.get("offset"),
|
|
344
344
|
sort_desc=filters.get("sort_desc", False),
|
|
345
345
|
workflow_id_prefix=filters.get("workflow_id_prefix"),
|
|
346
|
+
load_input=filters.get("load_input", False),
|
|
347
|
+
load_output=filters.get("load_output", False),
|
|
346
348
|
)
|
|
347
349
|
workflows_output = [
|
|
348
350
|
conductor_protocol.WorkflowsOutput.from_workflow_information(i)
|
|
@@ -367,6 +369,7 @@ class AdminRequestHandler(BaseHTTPRequestHandler):
|
|
|
367
369
|
offset=filters.get("offset"),
|
|
368
370
|
queue_name=filters.get("queue_name"),
|
|
369
371
|
sort_desc=filters.get("sort_desc", False),
|
|
372
|
+
load_input=filters.get("load_input", False),
|
|
370
373
|
)
|
|
371
374
|
workflows_output = [
|
|
372
375
|
conductor_protocol.WorkflowsOutput.from_workflow_information(i)
|
|
@@ -13,7 +13,7 @@ else:
|
|
|
13
13
|
|
|
14
14
|
from dbos import _serialization
|
|
15
15
|
from dbos._dbos import WorkflowHandle, WorkflowHandleAsync
|
|
16
|
-
from dbos._dbos_config import is_valid_database_url
|
|
16
|
+
from dbos._dbos_config import get_system_database_url, is_valid_database_url
|
|
17
17
|
from dbos._error import DBOSException, DBOSNonExistentWorkflowError
|
|
18
18
|
from dbos._registrations import DEFAULT_MAX_RECOVERY_ATTEMPTS
|
|
19
19
|
from dbos._serialization import WorkflowInputs
|
|
@@ -97,17 +97,28 @@ class WorkflowHandleClientAsyncPolling(Generic[R]):
|
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
class DBOSClient:
|
|
100
|
-
def __init__(
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
database_url: str,
|
|
103
|
+
*,
|
|
104
|
+
system_database_url: Optional[str] = None,
|
|
105
|
+
system_database: Optional[str] = None,
|
|
106
|
+
):
|
|
101
107
|
assert is_valid_database_url(database_url)
|
|
102
108
|
# We only create database connections but do not run migrations
|
|
103
109
|
self._sys_db = SystemDatabase(
|
|
104
|
-
|
|
110
|
+
system_database_url=get_system_database_url(
|
|
111
|
+
{
|
|
112
|
+
"system_database_url": system_database_url,
|
|
113
|
+
"database_url": database_url,
|
|
114
|
+
"database": {"sys_db_name": system_database},
|
|
115
|
+
}
|
|
116
|
+
),
|
|
105
117
|
engine_kwargs={
|
|
106
118
|
"pool_timeout": 30,
|
|
107
119
|
"max_overflow": 0,
|
|
108
120
|
"pool_size": 2,
|
|
109
121
|
},
|
|
110
|
-
sys_db_name=system_database,
|
|
111
122
|
)
|
|
112
123
|
self._sys_db.check_connection()
|
|
113
124
|
self._app_db = ApplicationDatabase(
|
|
@@ -294,6 +305,8 @@ class DBOSClient:
|
|
|
294
305
|
offset: Optional[int] = None,
|
|
295
306
|
sort_desc: bool = False,
|
|
296
307
|
workflow_id_prefix: Optional[str] = None,
|
|
308
|
+
load_input: bool = True,
|
|
309
|
+
load_output: bool = True,
|
|
297
310
|
) -> List[WorkflowStatus]:
|
|
298
311
|
return list_workflows(
|
|
299
312
|
self._sys_db,
|
|
@@ -308,6 +321,8 @@ class DBOSClient:
|
|
|
308
321
|
offset=offset,
|
|
309
322
|
sort_desc=sort_desc,
|
|
310
323
|
workflow_id_prefix=workflow_id_prefix,
|
|
324
|
+
load_input=load_input,
|
|
325
|
+
load_output=load_output,
|
|
311
326
|
)
|
|
312
327
|
|
|
313
328
|
async def list_workflows_async(
|
|
@@ -324,6 +339,8 @@ class DBOSClient:
|
|
|
324
339
|
offset: Optional[int] = None,
|
|
325
340
|
sort_desc: bool = False,
|
|
326
341
|
workflow_id_prefix: Optional[str] = None,
|
|
342
|
+
load_input: bool = True,
|
|
343
|
+
load_output: bool = True,
|
|
327
344
|
) -> List[WorkflowStatus]:
|
|
328
345
|
return await asyncio.to_thread(
|
|
329
346
|
self.list_workflows,
|
|
@@ -338,6 +355,8 @@ class DBOSClient:
|
|
|
338
355
|
offset=offset,
|
|
339
356
|
sort_desc=sort_desc,
|
|
340
357
|
workflow_id_prefix=workflow_id_prefix,
|
|
358
|
+
load_input=load_input,
|
|
359
|
+
load_output=load_output,
|
|
341
360
|
)
|
|
342
361
|
|
|
343
362
|
def list_queued_workflows(
|
|
@@ -351,6 +370,7 @@ class DBOSClient:
|
|
|
351
370
|
limit: Optional[int] = None,
|
|
352
371
|
offset: Optional[int] = None,
|
|
353
372
|
sort_desc: bool = False,
|
|
373
|
+
load_input: bool = True,
|
|
354
374
|
) -> List[WorkflowStatus]:
|
|
355
375
|
return list_queued_workflows(
|
|
356
376
|
self._sys_db,
|
|
@@ -362,6 +382,7 @@ class DBOSClient:
|
|
|
362
382
|
limit=limit,
|
|
363
383
|
offset=offset,
|
|
364
384
|
sort_desc=sort_desc,
|
|
385
|
+
load_input=load_input,
|
|
365
386
|
)
|
|
366
387
|
|
|
367
388
|
async def list_queued_workflows_async(
|
|
@@ -375,6 +396,7 @@ class DBOSClient:
|
|
|
375
396
|
limit: Optional[int] = None,
|
|
376
397
|
offset: Optional[int] = None,
|
|
377
398
|
sort_desc: bool = False,
|
|
399
|
+
load_input: bool = True,
|
|
378
400
|
) -> List[WorkflowStatus]:
|
|
379
401
|
return await asyncio.to_thread(
|
|
380
402
|
self.list_queued_workflows,
|
|
@@ -386,6 +408,7 @@ class DBOSClient:
|
|
|
386
408
|
limit=limit,
|
|
387
409
|
offset=offset,
|
|
388
410
|
sort_desc=sort_desc,
|
|
411
|
+
load_input=load_input,
|
|
389
412
|
)
|
|
390
413
|
|
|
391
414
|
def list_workflow_steps(self, workflow_id: str) -> List[StepInfo]:
|
|
@@ -223,6 +223,8 @@ class ConductorWebsocket(threading.Thread):
|
|
|
223
223
|
body = list_workflows_message.body
|
|
224
224
|
infos = []
|
|
225
225
|
try:
|
|
226
|
+
load_input = body.get("load_input", False)
|
|
227
|
+
load_output = body.get("load_output", False)
|
|
226
228
|
infos = list_workflows(
|
|
227
229
|
self.dbos._sys_db,
|
|
228
230
|
workflow_ids=body["workflow_uuids"],
|
|
@@ -235,6 +237,8 @@ class ConductorWebsocket(threading.Thread):
|
|
|
235
237
|
limit=body["limit"],
|
|
236
238
|
offset=body["offset"],
|
|
237
239
|
sort_desc=body["sort_desc"],
|
|
240
|
+
load_input=load_input,
|
|
241
|
+
load_output=load_output,
|
|
238
242
|
)
|
|
239
243
|
except Exception as e:
|
|
240
244
|
error_message = f"Exception encountered when listing workflows: {traceback.format_exc()}"
|
|
@@ -257,6 +261,7 @@ class ConductorWebsocket(threading.Thread):
|
|
|
257
261
|
q_body = list_queued_workflows_message.body
|
|
258
262
|
infos = []
|
|
259
263
|
try:
|
|
264
|
+
q_load_input = q_body.get("load_input", False)
|
|
260
265
|
infos = list_queued_workflows(
|
|
261
266
|
self.dbos._sys_db,
|
|
262
267
|
start_time=q_body["start_time"],
|
|
@@ -267,6 +272,7 @@ class ConductorWebsocket(threading.Thread):
|
|
|
267
272
|
offset=q_body["offset"],
|
|
268
273
|
queue_name=q_body["queue_name"],
|
|
269
274
|
sort_desc=q_body["sort_desc"],
|
|
275
|
+
load_input=q_load_input,
|
|
270
276
|
)
|
|
271
277
|
except Exception as e:
|
|
272
278
|
error_message = f"Exception encountered when listing queued workflows: {traceback.format_exc()}"
|
|
@@ -110,7 +110,7 @@ class RestartResponse(BaseMessage):
|
|
|
110
110
|
error_message: Optional[str] = None
|
|
111
111
|
|
|
112
112
|
|
|
113
|
-
class ListWorkflowsBody(TypedDict):
|
|
113
|
+
class ListWorkflowsBody(TypedDict, total=False):
|
|
114
114
|
workflow_uuids: List[str]
|
|
115
115
|
workflow_name: Optional[str]
|
|
116
116
|
authenticated_user: Optional[str]
|
|
@@ -121,6 +121,8 @@ class ListWorkflowsBody(TypedDict):
|
|
|
121
121
|
limit: Optional[int]
|
|
122
122
|
offset: Optional[int]
|
|
123
123
|
sort_desc: bool
|
|
124
|
+
load_input: bool
|
|
125
|
+
load_output: bool
|
|
124
126
|
|
|
125
127
|
|
|
126
128
|
@dataclass
|
|
@@ -209,7 +211,7 @@ class ListWorkflowsResponse(BaseMessage):
|
|
|
209
211
|
error_message: Optional[str] = None
|
|
210
212
|
|
|
211
213
|
|
|
212
|
-
class ListQueuedWorkflowsBody(TypedDict):
|
|
214
|
+
class ListQueuedWorkflowsBody(TypedDict, total=False):
|
|
213
215
|
workflow_name: Optional[str]
|
|
214
216
|
start_time: Optional[str]
|
|
215
217
|
end_time: Optional[str]
|
|
@@ -218,6 +220,7 @@ class ListQueuedWorkflowsBody(TypedDict):
|
|
|
218
220
|
limit: Optional[int]
|
|
219
221
|
offset: Optional[int]
|
|
220
222
|
sort_desc: bool
|
|
223
|
+
load_input: bool
|
|
221
224
|
|
|
222
225
|
|
|
223
226
|
@dataclass
|
|
@@ -10,7 +10,7 @@ from enum import Enum
|
|
|
10
10
|
from types import TracebackType
|
|
11
11
|
from typing import List, Literal, Optional, Type, TypedDict
|
|
12
12
|
|
|
13
|
-
from opentelemetry.trace import Span, Status, StatusCode
|
|
13
|
+
from opentelemetry.trace import Span, Status, StatusCode, use_span
|
|
14
14
|
from sqlalchemy.orm import Session
|
|
15
15
|
|
|
16
16
|
from dbos._utils import GlobalParams
|
|
@@ -68,6 +68,20 @@ class StepStatus:
|
|
|
68
68
|
max_attempts: Optional[int]
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
@dataclass
|
|
72
|
+
class ContextSpan:
|
|
73
|
+
"""
|
|
74
|
+
A span that is used to track the context of a workflow or step execution.
|
|
75
|
+
|
|
76
|
+
Attributes:
|
|
77
|
+
span: The OpenTelemetry span object.
|
|
78
|
+
context_manager: The context manager that is used to manage the span's lifecycle.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
span: Span
|
|
82
|
+
context_manager: AbstractContextManager[Span]
|
|
83
|
+
|
|
84
|
+
|
|
71
85
|
class DBOSContext:
|
|
72
86
|
def __init__(self) -> None:
|
|
73
87
|
self.executor_id = GlobalParams.executor_id
|
|
@@ -86,7 +100,7 @@ class DBOSContext:
|
|
|
86
100
|
self.curr_step_function_id: int = -1
|
|
87
101
|
self.curr_tx_function_id: int = -1
|
|
88
102
|
self.sql_session: Optional[Session] = None
|
|
89
|
-
self.
|
|
103
|
+
self.context_spans: list[ContextSpan] = []
|
|
90
104
|
|
|
91
105
|
self.authenticated_user: Optional[str] = None
|
|
92
106
|
self.authenticated_roles: Optional[List[str]] = None
|
|
@@ -202,8 +216,8 @@ class DBOSContext:
|
|
|
202
216
|
self._end_span(exc_value)
|
|
203
217
|
|
|
204
218
|
def get_current_span(self) -> Optional[Span]:
|
|
205
|
-
if len(self.
|
|
206
|
-
return self.
|
|
219
|
+
if len(self.context_spans) > 0:
|
|
220
|
+
return self.context_spans[-1].span
|
|
207
221
|
return None
|
|
208
222
|
|
|
209
223
|
def _start_span(self, attributes: TracedAttributes) -> None:
|
|
@@ -218,27 +232,38 @@ class DBOSContext:
|
|
|
218
232
|
)
|
|
219
233
|
attributes["authenticatedUserAssumedRole"] = self.assumed_role
|
|
220
234
|
span = dbos_tracer.start_span(
|
|
221
|
-
attributes,
|
|
235
|
+
attributes,
|
|
236
|
+
parent=self.context_spans[-1].span if len(self.context_spans) > 0 else None,
|
|
237
|
+
)
|
|
238
|
+
# Activate the current span
|
|
239
|
+
cm = use_span(
|
|
240
|
+
span,
|
|
241
|
+
end_on_exit=False,
|
|
242
|
+
record_exception=False,
|
|
243
|
+
set_status_on_exception=False,
|
|
222
244
|
)
|
|
223
|
-
self.
|
|
245
|
+
self.context_spans.append(ContextSpan(span, cm))
|
|
246
|
+
cm.__enter__()
|
|
224
247
|
|
|
225
248
|
def _end_span(self, exc_value: Optional[BaseException]) -> None:
|
|
249
|
+
context_span = self.context_spans.pop()
|
|
226
250
|
if exc_value is None:
|
|
227
|
-
|
|
251
|
+
context_span.span.set_status(Status(StatusCode.OK))
|
|
228
252
|
else:
|
|
229
|
-
|
|
253
|
+
context_span.span.set_status(
|
|
230
254
|
Status(StatusCode.ERROR, description=str(exc_value))
|
|
231
255
|
)
|
|
232
|
-
dbos_tracer.end_span(
|
|
256
|
+
dbos_tracer.end_span(context_span.span)
|
|
257
|
+
context_span.context_manager.__exit__(None, None, None)
|
|
233
258
|
|
|
234
259
|
def set_authentication(
|
|
235
260
|
self, user: Optional[str], roles: Optional[List[str]]
|
|
236
261
|
) -> None:
|
|
237
262
|
self.authenticated_user = user
|
|
238
263
|
self.authenticated_roles = roles
|
|
239
|
-
if user is not None and len(self.
|
|
240
|
-
self.
|
|
241
|
-
self.
|
|
264
|
+
if user is not None and len(self.context_spans) > 0:
|
|
265
|
+
self.context_spans[-1].span.set_attribute("authenticatedUser", user)
|
|
266
|
+
self.context_spans[-1].span.set_attribute(
|
|
242
267
|
"authenticatedUserRoles", json.dumps(roles) if roles is not None else ""
|
|
243
268
|
)
|
|
244
269
|
|
|
@@ -1157,13 +1157,16 @@ def decorate_step(
|
|
|
1157
1157
|
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
1158
1158
|
rr: Optional[str] = check_required_roles(func, fi)
|
|
1159
1159
|
# Entering step is allowed:
|
|
1160
|
+
# No DBOS, just call the original function directly
|
|
1160
1161
|
# In a step already, just call the original function directly.
|
|
1161
1162
|
# In a workflow (that is not in a step already)
|
|
1162
1163
|
# Not in a workflow (we will start the single op workflow)
|
|
1164
|
+
if not dbosreg.dbos or not dbosreg.dbos._launched:
|
|
1165
|
+
# Call the original function directly
|
|
1166
|
+
return func(*args, **kwargs)
|
|
1163
1167
|
ctx = get_local_dbos_context()
|
|
1164
1168
|
if ctx and ctx.is_step():
|
|
1165
1169
|
# Call the original function directly
|
|
1166
|
-
|
|
1167
1170
|
return func(*args, **kwargs)
|
|
1168
1171
|
if ctx and ctx.is_within_workflow():
|
|
1169
1172
|
assert ctx.is_workflow(), "Steps must be called from within workflows"
|
|
@@ -91,6 +91,7 @@ from ._context import (
|
|
|
91
91
|
from ._dbos_config import (
|
|
92
92
|
ConfigFile,
|
|
93
93
|
DBOSConfig,
|
|
94
|
+
get_system_database_url,
|
|
94
95
|
overwrite_config,
|
|
95
96
|
process_config,
|
|
96
97
|
translate_dbos_config_to_config_file,
|
|
@@ -424,9 +425,8 @@ class DBOS:
|
|
|
424
425
|
assert self._config["database_url"] is not None
|
|
425
426
|
assert self._config["database"]["sys_db_engine_kwargs"] is not None
|
|
426
427
|
self._sys_db_field = SystemDatabase(
|
|
427
|
-
|
|
428
|
+
system_database_url=get_system_database_url(self._config),
|
|
428
429
|
engine_kwargs=self._config["database"]["sys_db_engine_kwargs"],
|
|
429
|
-
sys_db_name=self._config["database"]["sys_db_name"],
|
|
430
430
|
debug_mode=debug_mode,
|
|
431
431
|
)
|
|
432
432
|
assert self._config["database"]["db_engine_kwargs"] is not None
|
|
@@ -966,6 +966,12 @@ class DBOS:
|
|
|
966
966
|
fn, "DBOS.cancelWorkflow"
|
|
967
967
|
)
|
|
968
968
|
|
|
969
|
+
@classmethod
|
|
970
|
+
async def cancel_workflow_async(cls, workflow_id: str) -> None:
|
|
971
|
+
"""Cancel a workflow by ID."""
|
|
972
|
+
await cls._configure_asyncio_thread_pool()
|
|
973
|
+
await asyncio.to_thread(cls.cancel_workflow, workflow_id)
|
|
974
|
+
|
|
969
975
|
@classmethod
|
|
970
976
|
async def _configure_asyncio_thread_pool(cls) -> None:
|
|
971
977
|
"""
|
|
@@ -987,11 +993,23 @@ class DBOS:
|
|
|
987
993
|
_get_dbos_instance()._sys_db.call_function_as_step(fn, "DBOS.resumeWorkflow")
|
|
988
994
|
return cls.retrieve_workflow(workflow_id)
|
|
989
995
|
|
|
996
|
+
@classmethod
|
|
997
|
+
async def resume_workflow_async(cls, workflow_id: str) -> WorkflowHandleAsync[Any]:
|
|
998
|
+
"""Resume a workflow by ID."""
|
|
999
|
+
await cls._configure_asyncio_thread_pool()
|
|
1000
|
+
await asyncio.to_thread(cls.resume_workflow, workflow_id)
|
|
1001
|
+
return await cls.retrieve_workflow_async(workflow_id)
|
|
1002
|
+
|
|
990
1003
|
@classmethod
|
|
991
1004
|
def restart_workflow(cls, workflow_id: str) -> WorkflowHandle[Any]:
|
|
992
1005
|
"""Restart a workflow with a new workflow ID"""
|
|
993
1006
|
return cls.fork_workflow(workflow_id, 1)
|
|
994
1007
|
|
|
1008
|
+
@classmethod
|
|
1009
|
+
async def restart_workflow_async(cls, workflow_id: str) -> WorkflowHandleAsync[Any]:
|
|
1010
|
+
"""Restart a workflow with a new workflow ID"""
|
|
1011
|
+
return await cls.fork_workflow_async(workflow_id, 1)
|
|
1012
|
+
|
|
995
1013
|
@classmethod
|
|
996
1014
|
def fork_workflow(
|
|
997
1015
|
cls,
|
|
@@ -1017,6 +1035,23 @@ class DBOS:
|
|
|
1017
1035
|
)
|
|
1018
1036
|
return cls.retrieve_workflow(new_id)
|
|
1019
1037
|
|
|
1038
|
+
@classmethod
|
|
1039
|
+
async def fork_workflow_async(
|
|
1040
|
+
cls,
|
|
1041
|
+
workflow_id: str,
|
|
1042
|
+
start_step: int,
|
|
1043
|
+
*,
|
|
1044
|
+
application_version: Optional[str] = None,
|
|
1045
|
+
) -> WorkflowHandleAsync[Any]:
|
|
1046
|
+
"""Restart a workflow with a new workflow ID from a specific step"""
|
|
1047
|
+
await cls._configure_asyncio_thread_pool()
|
|
1048
|
+
new_id = await asyncio.to_thread(
|
|
1049
|
+
lambda: cls.fork_workflow(
|
|
1050
|
+
workflow_id, start_step, application_version=application_version
|
|
1051
|
+
).get_workflow_id()
|
|
1052
|
+
)
|
|
1053
|
+
return await cls.retrieve_workflow_async(new_id)
|
|
1054
|
+
|
|
1020
1055
|
@classmethod
|
|
1021
1056
|
def list_workflows(
|
|
1022
1057
|
cls,
|
|
@@ -1032,6 +1067,8 @@ class DBOS:
|
|
|
1032
1067
|
offset: Optional[int] = None,
|
|
1033
1068
|
sort_desc: bool = False,
|
|
1034
1069
|
workflow_id_prefix: Optional[str] = None,
|
|
1070
|
+
load_input: bool = True,
|
|
1071
|
+
load_output: bool = True,
|
|
1035
1072
|
) -> List[WorkflowStatus]:
|
|
1036
1073
|
def fn() -> List[WorkflowStatus]:
|
|
1037
1074
|
return list_workflows(
|
|
@@ -1047,12 +1084,50 @@ class DBOS:
|
|
|
1047
1084
|
offset=offset,
|
|
1048
1085
|
sort_desc=sort_desc,
|
|
1049
1086
|
workflow_id_prefix=workflow_id_prefix,
|
|
1087
|
+
load_input=load_input,
|
|
1088
|
+
load_output=load_output,
|
|
1050
1089
|
)
|
|
1051
1090
|
|
|
1052
1091
|
return _get_dbos_instance()._sys_db.call_function_as_step(
|
|
1053
1092
|
fn, "DBOS.listWorkflows"
|
|
1054
1093
|
)
|
|
1055
1094
|
|
|
1095
|
+
@classmethod
|
|
1096
|
+
async def list_workflows_async(
|
|
1097
|
+
cls,
|
|
1098
|
+
*,
|
|
1099
|
+
workflow_ids: Optional[List[str]] = None,
|
|
1100
|
+
status: Optional[Union[str, List[str]]] = None,
|
|
1101
|
+
start_time: Optional[str] = None,
|
|
1102
|
+
end_time: Optional[str] = None,
|
|
1103
|
+
name: Optional[str] = None,
|
|
1104
|
+
app_version: Optional[str] = None,
|
|
1105
|
+
user: Optional[str] = None,
|
|
1106
|
+
limit: Optional[int] = None,
|
|
1107
|
+
offset: Optional[int] = None,
|
|
1108
|
+
sort_desc: bool = False,
|
|
1109
|
+
workflow_id_prefix: Optional[str] = None,
|
|
1110
|
+
load_input: bool = True,
|
|
1111
|
+
load_output: bool = True,
|
|
1112
|
+
) -> List[WorkflowStatus]:
|
|
1113
|
+
await cls._configure_asyncio_thread_pool()
|
|
1114
|
+
return await asyncio.to_thread(
|
|
1115
|
+
cls.list_workflows,
|
|
1116
|
+
workflow_ids=workflow_ids,
|
|
1117
|
+
status=status,
|
|
1118
|
+
start_time=start_time,
|
|
1119
|
+
end_time=end_time,
|
|
1120
|
+
name=name,
|
|
1121
|
+
app_version=app_version,
|
|
1122
|
+
user=user,
|
|
1123
|
+
limit=limit,
|
|
1124
|
+
offset=offset,
|
|
1125
|
+
sort_desc=sort_desc,
|
|
1126
|
+
workflow_id_prefix=workflow_id_prefix,
|
|
1127
|
+
load_input=load_input,
|
|
1128
|
+
load_output=load_output,
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1056
1131
|
@classmethod
|
|
1057
1132
|
def list_queued_workflows(
|
|
1058
1133
|
cls,
|
|
@@ -1065,6 +1140,7 @@ class DBOS:
|
|
|
1065
1140
|
limit: Optional[int] = None,
|
|
1066
1141
|
offset: Optional[int] = None,
|
|
1067
1142
|
sort_desc: bool = False,
|
|
1143
|
+
load_input: bool = True,
|
|
1068
1144
|
) -> List[WorkflowStatus]:
|
|
1069
1145
|
def fn() -> List[WorkflowStatus]:
|
|
1070
1146
|
return list_queued_workflows(
|
|
@@ -1077,12 +1153,41 @@ class DBOS:
|
|
|
1077
1153
|
limit=limit,
|
|
1078
1154
|
offset=offset,
|
|
1079
1155
|
sort_desc=sort_desc,
|
|
1156
|
+
load_input=load_input,
|
|
1080
1157
|
)
|
|
1081
1158
|
|
|
1082
1159
|
return _get_dbos_instance()._sys_db.call_function_as_step(
|
|
1083
1160
|
fn, "DBOS.listQueuedWorkflows"
|
|
1084
1161
|
)
|
|
1085
1162
|
|
|
1163
|
+
@classmethod
|
|
1164
|
+
async def list_queued_workflows_async(
|
|
1165
|
+
cls,
|
|
1166
|
+
*,
|
|
1167
|
+
queue_name: Optional[str] = None,
|
|
1168
|
+
status: Optional[Union[str, List[str]]] = None,
|
|
1169
|
+
start_time: Optional[str] = None,
|
|
1170
|
+
end_time: Optional[str] = None,
|
|
1171
|
+
name: Optional[str] = None,
|
|
1172
|
+
limit: Optional[int] = None,
|
|
1173
|
+
offset: Optional[int] = None,
|
|
1174
|
+
sort_desc: bool = False,
|
|
1175
|
+
load_input: bool = True,
|
|
1176
|
+
) -> List[WorkflowStatus]:
|
|
1177
|
+
await cls._configure_asyncio_thread_pool()
|
|
1178
|
+
return await asyncio.to_thread(
|
|
1179
|
+
cls.list_queued_workflows,
|
|
1180
|
+
queue_name=queue_name,
|
|
1181
|
+
status=status,
|
|
1182
|
+
start_time=start_time,
|
|
1183
|
+
end_time=end_time,
|
|
1184
|
+
name=name,
|
|
1185
|
+
limit=limit,
|
|
1186
|
+
offset=offset,
|
|
1187
|
+
sort_desc=sort_desc,
|
|
1188
|
+
load_input=load_input,
|
|
1189
|
+
)
|
|
1190
|
+
|
|
1086
1191
|
@classmethod
|
|
1087
1192
|
def list_workflow_steps(cls, workflow_id: str) -> List[StepInfo]:
|
|
1088
1193
|
def fn() -> List[StepInfo]:
|
|
@@ -1094,6 +1199,11 @@ class DBOS:
|
|
|
1094
1199
|
fn, "DBOS.listWorkflowSteps"
|
|
1095
1200
|
)
|
|
1096
1201
|
|
|
1202
|
+
@classmethod
|
|
1203
|
+
async def list_workflow_steps_async(cls, workflow_id: str) -> List[StepInfo]:
|
|
1204
|
+
await cls._configure_asyncio_thread_pool()
|
|
1205
|
+
return await asyncio.to_thread(cls.list_workflow_steps, workflow_id)
|
|
1206
|
+
|
|
1097
1207
|
@classproperty
|
|
1098
1208
|
def logger(cls) -> Logger:
|
|
1099
1209
|
"""Return the DBOS `Logger` for the current context."""
|
|
@@ -1266,31 +1376,3 @@ class DBOSConfiguredInstance:
|
|
|
1266
1376
|
def __init__(self, config_name: str) -> None:
|
|
1267
1377
|
self.config_name = config_name
|
|
1268
1378
|
DBOS.register_instance(self)
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
# Apps that import DBOS probably don't exit. If they do, let's see if
|
|
1272
|
-
# it looks like startup was abandoned or a call was forgotten...
|
|
1273
|
-
def _dbos_exit_hook() -> None:
|
|
1274
|
-
if _dbos_global_registry is None:
|
|
1275
|
-
# Probably used as or for a support module
|
|
1276
|
-
return
|
|
1277
|
-
if _dbos_global_instance is None:
|
|
1278
|
-
print("DBOS exiting; functions were registered but DBOS() was not called")
|
|
1279
|
-
dbos_logger.warning(
|
|
1280
|
-
"DBOS exiting; functions were registered but DBOS() was not called"
|
|
1281
|
-
)
|
|
1282
|
-
return
|
|
1283
|
-
if not _dbos_global_instance._launched:
|
|
1284
|
-
if _dbos_global_instance.fastapi is not None:
|
|
1285
|
-
# FastAPI lifespan middleware will call launch/destroy, so we can ignore this.
|
|
1286
|
-
# This is likely to happen during fastapi dev runs, where the reloader loads the module multiple times.
|
|
1287
|
-
return
|
|
1288
|
-
print("DBOS exiting; DBOS exists but launch() was not called")
|
|
1289
|
-
dbos_logger.warning("DBOS exiting; DBOS exists but launch() was not called")
|
|
1290
|
-
return
|
|
1291
|
-
# If we get here, we're exiting normally
|
|
1292
|
-
_dbos_global_instance.destroy()
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
# Register the exit hook
|
|
1296
|
-
atexit.register(_dbos_exit_hook)
|
|
@@ -23,7 +23,8 @@ class DBOSConfig(TypedDict, total=False):
|
|
|
23
23
|
Attributes:
|
|
24
24
|
name (str): Application name
|
|
25
25
|
database_url (str): Database connection string
|
|
26
|
-
|
|
26
|
+
system_database_url (str): Connection string for the system database (if different from the application database)
|
|
27
|
+
sys_db_name (str): System database name (deprecated)
|
|
27
28
|
sys_db_pool_size (int): System database pool size
|
|
28
29
|
db_engine_kwargs (Dict[str, Any]): SQLAlchemy engine kwargs (See https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine)
|
|
29
30
|
log_level (str): Log level
|
|
@@ -36,6 +37,7 @@ class DBOSConfig(TypedDict, total=False):
|
|
|
36
37
|
|
|
37
38
|
name: str
|
|
38
39
|
database_url: Optional[str]
|
|
40
|
+
system_database_url: Optional[str]
|
|
39
41
|
sys_db_name: Optional[str]
|
|
40
42
|
sys_db_pool_size: Optional[int]
|
|
41
43
|
db_engine_kwargs: Optional[Dict[str, Any]]
|
|
@@ -111,6 +113,7 @@ class ConfigFile(TypedDict, total=False):
|
|
|
111
113
|
runtimeConfig: RuntimeConfig
|
|
112
114
|
database: DatabaseConfig
|
|
113
115
|
database_url: Optional[str]
|
|
116
|
+
system_database_url: Optional[str]
|
|
114
117
|
telemetry: Optional[TelemetryConfig]
|
|
115
118
|
env: Dict[str, str]
|
|
116
119
|
|
|
@@ -136,6 +139,8 @@ def translate_dbos_config_to_config_file(config: DBOSConfig) -> ConfigFile:
|
|
|
136
139
|
|
|
137
140
|
if "database_url" in config:
|
|
138
141
|
translated_config["database_url"] = config.get("database_url")
|
|
142
|
+
if "system_database_url" in config:
|
|
143
|
+
translated_config["system_database_url"] = config.get("system_database_url")
|
|
139
144
|
|
|
140
145
|
# Runtime config
|
|
141
146
|
translated_config["runtimeConfig"] = {"run_admin_server": True}
|
|
@@ -488,6 +493,8 @@ def overwrite_config(provided_config: ConfigFile) -> ConfigFile:
|
|
|
488
493
|
"DBOS_DATABASE_URL environment variable is not set. This is required to connect to the database."
|
|
489
494
|
)
|
|
490
495
|
provided_config["database_url"] = db_url
|
|
496
|
+
if "system_database_url" in provided_config:
|
|
497
|
+
del provided_config["system_database_url"]
|
|
491
498
|
|
|
492
499
|
# Telemetry config
|
|
493
500
|
if "telemetry" not in provided_config or provided_config["telemetry"] is None:
|
|
@@ -537,3 +544,19 @@ def overwrite_config(provided_config: ConfigFile) -> ConfigFile:
|
|
|
537
544
|
del provided_config["env"]
|
|
538
545
|
|
|
539
546
|
return provided_config
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def get_system_database_url(config: ConfigFile) -> str:
|
|
550
|
+
if "system_database_url" in config and config["system_database_url"] is not None:
|
|
551
|
+
return config["system_database_url"]
|
|
552
|
+
else:
|
|
553
|
+
assert config["database_url"] is not None
|
|
554
|
+
app_db_url = make_url(config["database_url"])
|
|
555
|
+
if config["database"].get("sys_db_name") is not None:
|
|
556
|
+
sys_db_name = config["database"]["sys_db_name"]
|
|
557
|
+
else:
|
|
558
|
+
assert app_db_url.database is not None
|
|
559
|
+
sys_db_name = app_db_url.database + SystemSchema.sysdb_suffix
|
|
560
|
+
return app_db_url.set(database=sys_db_name).render_as_string(
|
|
561
|
+
hide_password=False
|
|
562
|
+
)
|
|
@@ -55,7 +55,7 @@ class DBOSErrorCode(Enum):
|
|
|
55
55
|
InitializationError = 3
|
|
56
56
|
WorkflowFunctionNotFound = 4
|
|
57
57
|
NonExistentWorkflowError = 5
|
|
58
|
-
|
|
58
|
+
MaxRecoveryAttemptsExceeded = 6
|
|
59
59
|
MaxStepRetriesExceeded = 7
|
|
60
60
|
NotAuthorized = 8
|
|
61
61
|
ConflictingWorkflowError = 9
|
|
@@ -121,13 +121,13 @@ class DBOSNonExistentWorkflowError(DBOSException):
|
|
|
121
121
|
)
|
|
122
122
|
|
|
123
123
|
|
|
124
|
-
class
|
|
125
|
-
"""Exception raised when a workflow
|
|
124
|
+
class MaxRecoveryAttemptsExceededError(DBOSException):
|
|
125
|
+
"""Exception raised when a workflow exceeds its max recovery attempts."""
|
|
126
126
|
|
|
127
127
|
def __init__(self, wf_id: str, max_retries: int):
|
|
128
128
|
super().__init__(
|
|
129
|
-
f"Workflow {wf_id} has
|
|
130
|
-
dbos_error_code=DBOSErrorCode.
|
|
129
|
+
f"Workflow {wf_id} has exceeded its maximum of {max_retries} execution or recovery attempts. Further attempts to execute or recover it will fail. See documentation for details: https://docs.dbos.dev/python/reference/decorators",
|
|
130
|
+
dbos_error_code=DBOSErrorCode.MaxRecoveryAttemptsExceeded.value,
|
|
131
131
|
)
|
|
132
132
|
|
|
133
133
|
|