dbos 0.24.0a15__py3-none-any.whl → 0.25.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.
- dbos/__init__.py +5 -1
- dbos/__main__.py +3 -0
- dbos/_admin_server.py +28 -2
- dbos/_app_db.py +14 -15
- dbos/_client.py +206 -0
- dbos/_conductor/conductor.py +33 -2
- dbos/_conductor/protocol.py +47 -7
- dbos/_context.py +48 -0
- dbos/_core.py +173 -48
- dbos/_db_wizard.py +3 -7
- dbos/_dbos.py +134 -85
- dbos/_dbos_config.py +2 -1
- dbos/_fastapi.py +4 -1
- dbos/_logger.py +14 -0
- dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +46 -0
- dbos/_outcome.py +6 -2
- dbos/_queue.py +5 -5
- dbos/_schemas/system_database.py +2 -0
- dbos/_sys_db.py +159 -178
- dbos/_templates/dbos-db-starter/__package/main.py +6 -11
- dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +2 -4
- dbos/_workflow_commands.py +90 -63
- dbos/cli/_template_init.py +8 -3
- dbos/cli/cli.py +22 -6
- {dbos-0.24.0a15.dist-info → dbos-0.25.0.dist-info}/METADATA +2 -1
- {dbos-0.24.0a15.dist-info → dbos-0.25.0.dist-info}/RECORD +29 -27
- {dbos-0.24.0a15.dist-info → dbos-0.25.0.dist-info}/WHEEL +1 -1
- {dbos-0.24.0a15.dist-info → dbos-0.25.0.dist-info}/entry_points.txt +0 -0
- {dbos-0.24.0a15.dist-info → dbos-0.25.0.dist-info}/licenses/LICENSE +0 -0
dbos/__init__.py
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
from . import _error as error
|
|
2
|
+
from ._client import DBOSClient, EnqueueOptions
|
|
2
3
|
from ._context import DBOSContextEnsure, DBOSContextSetAuth, SetWorkflowID
|
|
3
|
-
from ._dbos import DBOS, DBOSConfiguredInstance, WorkflowHandle
|
|
4
|
+
from ._dbos import DBOS, DBOSConfiguredInstance, WorkflowHandle
|
|
4
5
|
from ._dbos_config import ConfigFile, DBOSConfig, get_dbos_database_url, load_config
|
|
5
6
|
from ._kafka_message import KafkaMessage
|
|
6
7
|
from ._queue import Queue
|
|
7
8
|
from ._sys_db import GetWorkflowsInput, WorkflowStatusString
|
|
9
|
+
from ._workflow_commands import WorkflowStatus
|
|
8
10
|
|
|
9
11
|
__all__ = [
|
|
10
12
|
"ConfigFile",
|
|
11
13
|
"DBOSConfig",
|
|
12
14
|
"DBOS",
|
|
15
|
+
"DBOSClient",
|
|
13
16
|
"DBOSConfiguredInstance",
|
|
14
17
|
"DBOSContextEnsure",
|
|
15
18
|
"DBOSContextSetAuth",
|
|
19
|
+
"EnqueueOptions",
|
|
16
20
|
"GetWorkflowsInput",
|
|
17
21
|
"KafkaMessage",
|
|
18
22
|
"SetWorkflowID",
|
dbos/__main__.py
CHANGED
dbos/_admin_server.py
CHANGED
|
@@ -20,6 +20,7 @@ _workflow_queues_metadata_path = "/dbos-workflow-queues-metadata"
|
|
|
20
20
|
# /workflows/:workflow_id/cancel
|
|
21
21
|
# /workflows/:workflow_id/resume
|
|
22
22
|
# /workflows/:workflow_id/restart
|
|
23
|
+
# /workflows/:workflow_id/steps
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
class AdminServer:
|
|
@@ -86,8 +87,16 @@ class AdminRequestHandler(BaseHTTPRequestHandler):
|
|
|
86
87
|
self._end_headers()
|
|
87
88
|
self.wfile.write(json.dumps(queue_metadata_array).encode("utf-8"))
|
|
88
89
|
else:
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
steps_match = re.match(
|
|
91
|
+
r"^/workflows/(?P<workflow_id>[^/]+)/steps$", self.path
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if steps_match:
|
|
95
|
+
workflow_id = steps_match.group("workflow_id")
|
|
96
|
+
self._handle_steps(workflow_id)
|
|
97
|
+
else:
|
|
98
|
+
self.send_response(404)
|
|
99
|
+
self._end_headers()
|
|
91
100
|
|
|
92
101
|
def do_POST(self) -> None:
|
|
93
102
|
content_length = int(
|
|
@@ -149,6 +158,23 @@ class AdminRequestHandler(BaseHTTPRequestHandler):
|
|
|
149
158
|
self.send_response(204)
|
|
150
159
|
self._end_headers()
|
|
151
160
|
|
|
161
|
+
def _handle_steps(self, workflow_id: str) -> None:
|
|
162
|
+
steps = self.dbos._sys_db.get_workflow_steps(workflow_id)
|
|
163
|
+
|
|
164
|
+
updated_steps = [
|
|
165
|
+
{
|
|
166
|
+
**step,
|
|
167
|
+
"output": str(step["output"]) if step["output"] is not None else None,
|
|
168
|
+
"error": str(step["error"]) if step["error"] is not None else None,
|
|
169
|
+
}
|
|
170
|
+
for step in steps
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
json_steps = json.dumps(updated_steps).encode("utf-8")
|
|
174
|
+
self.send_response(200)
|
|
175
|
+
self._end_headers()
|
|
176
|
+
self.wfile.write(json_steps)
|
|
177
|
+
|
|
152
178
|
|
|
153
179
|
# Be consistent with DBOS-TS response.
|
|
154
180
|
class PerfUtilization(TypedDict):
|
dbos/_app_db.py
CHANGED
|
@@ -27,19 +27,18 @@ class RecordedResult(TypedDict):
|
|
|
27
27
|
|
|
28
28
|
class ApplicationDatabase:
|
|
29
29
|
|
|
30
|
-
def __init__(self,
|
|
31
|
-
self.config = config
|
|
30
|
+
def __init__(self, database: DatabaseConfig, *, debug_mode: bool = False):
|
|
32
31
|
|
|
33
|
-
app_db_name =
|
|
32
|
+
app_db_name = database["app_db_name"]
|
|
34
33
|
|
|
35
34
|
# If the application database does not already exist, create it
|
|
36
35
|
if not debug_mode:
|
|
37
36
|
postgres_db_url = sa.URL.create(
|
|
38
37
|
"postgresql+psycopg",
|
|
39
|
-
username=
|
|
40
|
-
password=
|
|
41
|
-
host=
|
|
42
|
-
port=
|
|
38
|
+
username=database["username"],
|
|
39
|
+
password=database["password"],
|
|
40
|
+
host=database["hostname"],
|
|
41
|
+
port=database["port"],
|
|
43
42
|
database="postgres",
|
|
44
43
|
)
|
|
45
44
|
postgres_db_engine = sa.create_engine(postgres_db_url)
|
|
@@ -55,25 +54,25 @@ class ApplicationDatabase:
|
|
|
55
54
|
# Create a connection pool for the application database
|
|
56
55
|
app_db_url = sa.URL.create(
|
|
57
56
|
"postgresql+psycopg",
|
|
58
|
-
username=
|
|
59
|
-
password=
|
|
60
|
-
host=
|
|
61
|
-
port=
|
|
57
|
+
username=database["username"],
|
|
58
|
+
password=database["password"],
|
|
59
|
+
host=database["hostname"],
|
|
60
|
+
port=database["port"],
|
|
62
61
|
database=app_db_name,
|
|
63
62
|
)
|
|
64
63
|
|
|
65
64
|
connect_args = {}
|
|
66
65
|
if (
|
|
67
|
-
"connectionTimeoutMillis" in
|
|
68
|
-
and
|
|
66
|
+
"connectionTimeoutMillis" in database
|
|
67
|
+
and database["connectionTimeoutMillis"]
|
|
69
68
|
):
|
|
70
69
|
connect_args["connect_timeout"] = int(
|
|
71
|
-
|
|
70
|
+
database["connectionTimeoutMillis"] / 1000
|
|
72
71
|
)
|
|
73
72
|
|
|
74
73
|
self.engine = sa.create_engine(
|
|
75
74
|
app_db_url,
|
|
76
|
-
pool_size=
|
|
75
|
+
pool_size=database["app_db_pool_size"],
|
|
77
76
|
max_overflow=0,
|
|
78
77
|
pool_timeout=30,
|
|
79
78
|
connect_args=connect_args,
|
dbos/_client.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Any, Generic, Optional, TypedDict, TypeVar
|
|
5
|
+
|
|
6
|
+
if sys.version_info < (3, 11):
|
|
7
|
+
from typing_extensions import NotRequired
|
|
8
|
+
else:
|
|
9
|
+
from typing import NotRequired
|
|
10
|
+
|
|
11
|
+
from dbos import _serialization
|
|
12
|
+
from dbos._dbos import WorkflowHandle, WorkflowHandleAsync
|
|
13
|
+
from dbos._dbos_config import parse_database_url_to_dbconfig
|
|
14
|
+
from dbos._error import DBOSNonExistentWorkflowError
|
|
15
|
+
from dbos._registrations import DEFAULT_MAX_RECOVERY_ATTEMPTS
|
|
16
|
+
from dbos._serialization import WorkflowInputs
|
|
17
|
+
from dbos._sys_db import SystemDatabase, WorkflowStatusInternal, WorkflowStatusString
|
|
18
|
+
from dbos._workflow_commands import WorkflowStatus, get_workflow
|
|
19
|
+
|
|
20
|
+
R = TypeVar("R", covariant=True) # A generic type for workflow return values
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class EnqueueOptions(TypedDict):
|
|
24
|
+
workflow_name: str
|
|
25
|
+
workflow_class_name: NotRequired[str]
|
|
26
|
+
queue_name: str
|
|
27
|
+
app_version: NotRequired[str]
|
|
28
|
+
workflow_id: NotRequired[str]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class WorkflowHandleClientPolling(Generic[R]):
|
|
32
|
+
|
|
33
|
+
def __init__(self, workflow_id: str, sys_db: SystemDatabase):
|
|
34
|
+
self.workflow_id = workflow_id
|
|
35
|
+
self._sys_db = sys_db
|
|
36
|
+
|
|
37
|
+
def get_workflow_id(self) -> str:
|
|
38
|
+
return self.workflow_id
|
|
39
|
+
|
|
40
|
+
def get_result(self) -> R:
|
|
41
|
+
res: R = self._sys_db.await_workflow_result(self.workflow_id)
|
|
42
|
+
return res
|
|
43
|
+
|
|
44
|
+
def get_status(self) -> "WorkflowStatus":
|
|
45
|
+
status = get_workflow(self._sys_db, self.workflow_id, True)
|
|
46
|
+
if status is None:
|
|
47
|
+
raise DBOSNonExistentWorkflowError(self.workflow_id)
|
|
48
|
+
return status
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class WorkflowHandleClientAsyncPolling(Generic[R]):
|
|
52
|
+
|
|
53
|
+
def __init__(self, workflow_id: str, sys_db: SystemDatabase):
|
|
54
|
+
self.workflow_id = workflow_id
|
|
55
|
+
self._sys_db = sys_db
|
|
56
|
+
|
|
57
|
+
def get_workflow_id(self) -> str:
|
|
58
|
+
return self.workflow_id
|
|
59
|
+
|
|
60
|
+
async def get_result(self) -> R:
|
|
61
|
+
res: R = await asyncio.to_thread(
|
|
62
|
+
self._sys_db.await_workflow_result, self.workflow_id
|
|
63
|
+
)
|
|
64
|
+
return res
|
|
65
|
+
|
|
66
|
+
async def get_status(self) -> "WorkflowStatus":
|
|
67
|
+
status = await asyncio.to_thread(
|
|
68
|
+
get_workflow, self._sys_db, self.workflow_id, True
|
|
69
|
+
)
|
|
70
|
+
if status is None:
|
|
71
|
+
raise DBOSNonExistentWorkflowError(self.workflow_id)
|
|
72
|
+
return status
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class DBOSClient:
|
|
76
|
+
def __init__(self, database_url: str, *, system_database: Optional[str] = None):
|
|
77
|
+
db_config = parse_database_url_to_dbconfig(database_url)
|
|
78
|
+
if system_database is not None:
|
|
79
|
+
db_config["sys_db_name"] = system_database
|
|
80
|
+
self._sys_db = SystemDatabase(db_config)
|
|
81
|
+
|
|
82
|
+
def destroy(self) -> None:
|
|
83
|
+
self._sys_db.destroy()
|
|
84
|
+
|
|
85
|
+
def _enqueue(self, options: EnqueueOptions, *args: Any, **kwargs: Any) -> str:
|
|
86
|
+
workflow_name = options["workflow_name"]
|
|
87
|
+
queue_name = options["queue_name"]
|
|
88
|
+
|
|
89
|
+
workflow_class_name = options.get("workflow_class_name")
|
|
90
|
+
app_version = options.get("app_version")
|
|
91
|
+
max_recovery_attempts = options.get("max_recovery_attempts")
|
|
92
|
+
if max_recovery_attempts is None:
|
|
93
|
+
max_recovery_attempts = DEFAULT_MAX_RECOVERY_ATTEMPTS
|
|
94
|
+
workflow_id = options.get("workflow_id")
|
|
95
|
+
if workflow_id is None:
|
|
96
|
+
workflow_id = str(uuid.uuid4())
|
|
97
|
+
|
|
98
|
+
status: WorkflowStatusInternal = {
|
|
99
|
+
"workflow_uuid": workflow_id,
|
|
100
|
+
"status": WorkflowStatusString.ENQUEUED.value,
|
|
101
|
+
"name": workflow_name,
|
|
102
|
+
"class_name": workflow_class_name,
|
|
103
|
+
"queue_name": queue_name,
|
|
104
|
+
"app_version": app_version,
|
|
105
|
+
"config_name": None,
|
|
106
|
+
"authenticated_user": None,
|
|
107
|
+
"assumed_role": None,
|
|
108
|
+
"authenticated_roles": None,
|
|
109
|
+
"request": None,
|
|
110
|
+
"output": None,
|
|
111
|
+
"error": None,
|
|
112
|
+
"created_at": None,
|
|
113
|
+
"updated_at": None,
|
|
114
|
+
"executor_id": None,
|
|
115
|
+
"recovery_attempts": None,
|
|
116
|
+
"app_id": None,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
inputs: WorkflowInputs = {
|
|
120
|
+
"args": args,
|
|
121
|
+
"kwargs": kwargs,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
wf_status = self._sys_db.insert_workflow_status(status)
|
|
125
|
+
self._sys_db.update_workflow_inputs(
|
|
126
|
+
workflow_id, _serialization.serialize_args(inputs)
|
|
127
|
+
)
|
|
128
|
+
if wf_status == WorkflowStatusString.ENQUEUED.value:
|
|
129
|
+
self._sys_db.enqueue(workflow_id, queue_name)
|
|
130
|
+
return workflow_id
|
|
131
|
+
|
|
132
|
+
def enqueue(
|
|
133
|
+
self, options: EnqueueOptions, *args: Any, **kwargs: Any
|
|
134
|
+
) -> WorkflowHandle[R]:
|
|
135
|
+
workflow_id = self._enqueue(options, *args, **kwargs)
|
|
136
|
+
return WorkflowHandleClientPolling[R](workflow_id, self._sys_db)
|
|
137
|
+
|
|
138
|
+
async def enqueue_async(
|
|
139
|
+
self, options: EnqueueOptions, *args: Any, **kwargs: Any
|
|
140
|
+
) -> WorkflowHandleAsync[R]:
|
|
141
|
+
workflow_id = await asyncio.to_thread(self._enqueue, options, *args, **kwargs)
|
|
142
|
+
return WorkflowHandleClientAsyncPolling[R](workflow_id, self._sys_db)
|
|
143
|
+
|
|
144
|
+
def retrieve_workflow(self, workflow_id: str) -> WorkflowHandle[R]:
|
|
145
|
+
status = get_workflow(self._sys_db, workflow_id, True)
|
|
146
|
+
if status is None:
|
|
147
|
+
raise DBOSNonExistentWorkflowError(workflow_id)
|
|
148
|
+
return WorkflowHandleClientPolling[R](workflow_id, self._sys_db)
|
|
149
|
+
|
|
150
|
+
async def retrieve_workflow_async(self, workflow_id: str) -> WorkflowHandleAsync[R]:
|
|
151
|
+
status = asyncio.to_thread(get_workflow, self._sys_db, workflow_id, True)
|
|
152
|
+
if status is None:
|
|
153
|
+
raise DBOSNonExistentWorkflowError(workflow_id)
|
|
154
|
+
return WorkflowHandleClientAsyncPolling[R](workflow_id, self._sys_db)
|
|
155
|
+
|
|
156
|
+
def send(
|
|
157
|
+
self,
|
|
158
|
+
destination_id: str,
|
|
159
|
+
message: Any,
|
|
160
|
+
topic: Optional[str] = None,
|
|
161
|
+
idempotency_key: Optional[str] = None,
|
|
162
|
+
) -> None:
|
|
163
|
+
idempotency_key = idempotency_key if idempotency_key else str(uuid.uuid4())
|
|
164
|
+
status: WorkflowStatusInternal = {
|
|
165
|
+
"workflow_uuid": f"{destination_id}-{idempotency_key}",
|
|
166
|
+
"status": WorkflowStatusString.SUCCESS.value,
|
|
167
|
+
"name": "temp_workflow-send-client",
|
|
168
|
+
"class_name": None,
|
|
169
|
+
"queue_name": None,
|
|
170
|
+
"config_name": None,
|
|
171
|
+
"authenticated_user": None,
|
|
172
|
+
"assumed_role": None,
|
|
173
|
+
"authenticated_roles": None,
|
|
174
|
+
"request": None,
|
|
175
|
+
"output": None,
|
|
176
|
+
"error": None,
|
|
177
|
+
"created_at": None,
|
|
178
|
+
"updated_at": None,
|
|
179
|
+
"executor_id": None,
|
|
180
|
+
"recovery_attempts": None,
|
|
181
|
+
"app_id": None,
|
|
182
|
+
"app_version": None,
|
|
183
|
+
}
|
|
184
|
+
self._sys_db.insert_workflow_status(status)
|
|
185
|
+
self._sys_db.send(status["workflow_uuid"], 0, destination_id, message, topic)
|
|
186
|
+
|
|
187
|
+
async def send_async(
|
|
188
|
+
self,
|
|
189
|
+
destination_id: str,
|
|
190
|
+
message: Any,
|
|
191
|
+
topic: Optional[str] = None,
|
|
192
|
+
idempotency_key: Optional[str] = None,
|
|
193
|
+
) -> None:
|
|
194
|
+
return await asyncio.to_thread(
|
|
195
|
+
self.send, destination_id, message, topic, idempotency_key
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def get_event(self, workflow_id: str, key: str, timeout_seconds: float = 60) -> Any:
|
|
199
|
+
return self._sys_db.get_event(workflow_id, key, timeout_seconds)
|
|
200
|
+
|
|
201
|
+
async def get_event_async(
|
|
202
|
+
self, workflow_id: str, key: str, timeout_seconds: float = 60
|
|
203
|
+
) -> Any:
|
|
204
|
+
return await asyncio.to_thread(
|
|
205
|
+
self.get_event, workflow_id, key, timeout_seconds
|
|
206
|
+
)
|
dbos/_conductor/conductor.py
CHANGED
|
@@ -9,7 +9,12 @@ from websockets.sync.client import connect
|
|
|
9
9
|
from websockets.sync.connection import Connection
|
|
10
10
|
|
|
11
11
|
from dbos._utils import GlobalParams
|
|
12
|
-
from dbos._workflow_commands import
|
|
12
|
+
from dbos._workflow_commands import (
|
|
13
|
+
get_workflow,
|
|
14
|
+
list_queued_workflows,
|
|
15
|
+
list_workflow_steps,
|
|
16
|
+
list_workflows,
|
|
17
|
+
)
|
|
13
18
|
|
|
14
19
|
from . import protocol as p
|
|
15
20
|
|
|
@@ -203,7 +208,7 @@ class ConductorWebsocket(threading.Thread):
|
|
|
203
208
|
info = get_workflow(
|
|
204
209
|
self.dbos._sys_db,
|
|
205
210
|
get_workflow_message.workflow_id,
|
|
206
|
-
|
|
211
|
+
get_request=False,
|
|
207
212
|
)
|
|
208
213
|
except Exception as e:
|
|
209
214
|
error_message = f"Exception encountered when getting workflow {get_workflow_message.workflow_id}: {traceback.format_exc()}"
|
|
@@ -243,6 +248,32 @@ class ConductorWebsocket(threading.Thread):
|
|
|
243
248
|
)
|
|
244
249
|
)
|
|
245
250
|
websocket.send(exist_pending_workflows_response.to_json())
|
|
251
|
+
elif msg_type == p.MessageType.LIST_STEPS:
|
|
252
|
+
list_steps_message = p.ListStepsRequest.from_json(message)
|
|
253
|
+
step_info = None
|
|
254
|
+
try:
|
|
255
|
+
step_info = list_workflow_steps(
|
|
256
|
+
self.dbos._sys_db,
|
|
257
|
+
list_steps_message.workflow_id,
|
|
258
|
+
)
|
|
259
|
+
except Exception as e:
|
|
260
|
+
error_message = f"Exception encountered when getting workflow {list_steps_message.workflow_id}: {traceback.format_exc()}"
|
|
261
|
+
self.dbos.logger.error(error_message)
|
|
262
|
+
|
|
263
|
+
list_steps_response = p.ListStepsResponse(
|
|
264
|
+
type=p.MessageType.LIST_STEPS,
|
|
265
|
+
request_id=base_message.request_id,
|
|
266
|
+
output=(
|
|
267
|
+
[
|
|
268
|
+
p.WorkflowSteps.from_step_info(i)
|
|
269
|
+
for i in step_info
|
|
270
|
+
]
|
|
271
|
+
if step_info is not None
|
|
272
|
+
else None
|
|
273
|
+
),
|
|
274
|
+
error_message=error_message,
|
|
275
|
+
)
|
|
276
|
+
websocket.send(list_steps_response.to_json())
|
|
246
277
|
else:
|
|
247
278
|
self.dbos.logger.warning(
|
|
248
279
|
f"Unexpected message type: {msg_type}"
|
dbos/_conductor/protocol.py
CHANGED
|
@@ -3,7 +3,8 @@ from dataclasses import asdict, dataclass
|
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from typing import List, Optional, Type, TypedDict, TypeVar
|
|
5
5
|
|
|
6
|
-
from dbos.
|
|
6
|
+
from dbos._sys_db import StepInfo
|
|
7
|
+
from dbos._workflow_commands import WorkflowStatus
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class MessageType(str, Enum):
|
|
@@ -16,6 +17,7 @@ class MessageType(str, Enum):
|
|
|
16
17
|
RESTART = "restart"
|
|
17
18
|
GET_WORKFLOW = "get_workflow"
|
|
18
19
|
EXIST_PENDING_WORKFLOWS = "exist_pending_workflows"
|
|
20
|
+
LIST_STEPS = "list_steps"
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
T = TypeVar("T", bound="BaseMessage")
|
|
@@ -141,27 +143,33 @@ class WorkflowsOutput:
|
|
|
141
143
|
ExecutorID: Optional[str]
|
|
142
144
|
|
|
143
145
|
@classmethod
|
|
144
|
-
def from_workflow_information(cls, info:
|
|
146
|
+
def from_workflow_information(cls, info: WorkflowStatus) -> "WorkflowsOutput":
|
|
145
147
|
# Convert fields to strings as needed
|
|
146
148
|
created_at_str = str(info.created_at) if info.created_at is not None else None
|
|
147
149
|
updated_at_str = str(info.updated_at) if info.updated_at is not None else None
|
|
148
150
|
inputs_str = str(info.input) if info.input is not None else None
|
|
149
151
|
outputs_str = str(info.output) if info.output is not None else None
|
|
152
|
+
error_str = str(info.error) if info.error is not None else None
|
|
150
153
|
request_str = str(info.request) if info.request is not None else None
|
|
154
|
+
roles_str = (
|
|
155
|
+
str(info.authenticated_roles)
|
|
156
|
+
if info.authenticated_roles is not None
|
|
157
|
+
else None
|
|
158
|
+
)
|
|
151
159
|
|
|
152
160
|
return cls(
|
|
153
161
|
WorkflowUUID=info.workflow_id,
|
|
154
162
|
Status=info.status,
|
|
155
|
-
WorkflowName=info.
|
|
156
|
-
WorkflowClassName=info.
|
|
157
|
-
WorkflowConfigName=info.
|
|
163
|
+
WorkflowName=info.name,
|
|
164
|
+
WorkflowClassName=info.class_name,
|
|
165
|
+
WorkflowConfigName=info.config_name,
|
|
158
166
|
AuthenticatedUser=info.authenticated_user,
|
|
159
167
|
AssumedRole=info.assumed_role,
|
|
160
|
-
AuthenticatedRoles=
|
|
168
|
+
AuthenticatedRoles=roles_str,
|
|
161
169
|
Input=inputs_str,
|
|
162
170
|
Output=outputs_str,
|
|
163
171
|
Request=request_str,
|
|
164
|
-
Error=
|
|
172
|
+
Error=error_str,
|
|
165
173
|
CreatedAt=created_at_str,
|
|
166
174
|
UpdatedAt=updated_at_str,
|
|
167
175
|
QueueName=info.queue_name,
|
|
@@ -170,6 +178,27 @@ class WorkflowsOutput:
|
|
|
170
178
|
)
|
|
171
179
|
|
|
172
180
|
|
|
181
|
+
@dataclass
|
|
182
|
+
class WorkflowSteps:
|
|
183
|
+
function_id: int
|
|
184
|
+
function_name: str
|
|
185
|
+
output: Optional[str]
|
|
186
|
+
error: Optional[str]
|
|
187
|
+
child_workflow_id: Optional[str]
|
|
188
|
+
|
|
189
|
+
@classmethod
|
|
190
|
+
def from_step_info(cls, info: StepInfo) -> "WorkflowSteps":
|
|
191
|
+
output_str = str(info["output"]) if info["output"] is not None else None
|
|
192
|
+
error_str = str(info["error"]) if info["error"] is not None else None
|
|
193
|
+
return cls(
|
|
194
|
+
function_id=info["function_id"],
|
|
195
|
+
function_name=info["function_name"],
|
|
196
|
+
output=output_str,
|
|
197
|
+
error=error_str,
|
|
198
|
+
child_workflow_id=info["child_workflow_id"],
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
173
202
|
@dataclass
|
|
174
203
|
class ListWorkflowsRequest(BaseMessage):
|
|
175
204
|
body: ListWorkflowsBody
|
|
@@ -224,3 +253,14 @@ class ExistPendingWorkflowsRequest(BaseMessage):
|
|
|
224
253
|
class ExistPendingWorkflowsResponse(BaseMessage):
|
|
225
254
|
exist: bool
|
|
226
255
|
error_message: Optional[str] = None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@dataclass
|
|
259
|
+
class ListStepsRequest(BaseMessage):
|
|
260
|
+
workflow_id: str
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@dataclass
|
|
264
|
+
class ListStepsResponse(BaseMessage):
|
|
265
|
+
output: Optional[List[WorkflowSteps]]
|
|
266
|
+
error_message: Optional[str] = None
|
dbos/_context.py
CHANGED
|
@@ -5,6 +5,7 @@ import os
|
|
|
5
5
|
import uuid
|
|
6
6
|
from contextlib import AbstractContextManager
|
|
7
7
|
from contextvars import ContextVar
|
|
8
|
+
from dataclasses import dataclass
|
|
8
9
|
from enum import Enum
|
|
9
10
|
from types import TracebackType
|
|
10
11
|
from typing import List, Literal, Optional, Type, TypedDict
|
|
@@ -48,6 +49,23 @@ class TracedAttributes(TypedDict, total=False):
|
|
|
48
49
|
authenticatedUserAssumedRole: Optional[str]
|
|
49
50
|
|
|
50
51
|
|
|
52
|
+
@dataclass
|
|
53
|
+
class StepStatus:
|
|
54
|
+
"""
|
|
55
|
+
Status of a step execution.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
step_id: The unique ID of this step in its workflow.
|
|
59
|
+
current_attempt: For steps with automatic retries, which attempt number (zero-indexed) is currently executing.
|
|
60
|
+
max_attempts: For steps with automatic retries, the maximum number of attempts that will be made before the step fails.
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
step_id: int
|
|
65
|
+
current_attempt: Optional[int]
|
|
66
|
+
max_attempts: Optional[int]
|
|
67
|
+
|
|
68
|
+
|
|
51
69
|
class DBOSContext:
|
|
52
70
|
def __init__(self) -> None:
|
|
53
71
|
self.executor_id = GlobalParams.executor_id
|
|
@@ -73,6 +91,7 @@ class DBOSContext:
|
|
|
73
91
|
self.authenticated_user: Optional[str] = None
|
|
74
92
|
self.authenticated_roles: Optional[List[str]] = None
|
|
75
93
|
self.assumed_role: Optional[str] = None
|
|
94
|
+
self.step_status: Optional[StepStatus] = None
|
|
76
95
|
|
|
77
96
|
def create_child(self) -> DBOSContext:
|
|
78
97
|
rv = DBOSContext()
|
|
@@ -92,6 +111,9 @@ class DBOSContext:
|
|
|
92
111
|
rv.assumed_role = self.assumed_role
|
|
93
112
|
return rv
|
|
94
113
|
|
|
114
|
+
def has_parent(self) -> bool:
|
|
115
|
+
return len(self.parent_workflow_id) > 0
|
|
116
|
+
|
|
95
117
|
def assign_workflow_id(self) -> str:
|
|
96
118
|
if len(self.id_assigned_for_next_workflow) > 0:
|
|
97
119
|
wfid = self.id_assigned_for_next_workflow
|
|
@@ -147,10 +169,12 @@ class DBOSContext:
|
|
|
147
169
|
attributes: TracedAttributes,
|
|
148
170
|
) -> None:
|
|
149
171
|
self.curr_step_function_id = fid
|
|
172
|
+
self.step_status = StepStatus(fid, None, None)
|
|
150
173
|
self._start_span(attributes)
|
|
151
174
|
|
|
152
175
|
def end_step(self, exc_value: Optional[BaseException]) -> None:
|
|
153
176
|
self.curr_step_function_id = -1
|
|
177
|
+
self.step_status = None
|
|
154
178
|
self._end_span(exc_value)
|
|
155
179
|
|
|
156
180
|
def start_transaction(
|
|
@@ -429,6 +453,30 @@ class EnterDBOSStep:
|
|
|
429
453
|
return False # Did not handle
|
|
430
454
|
|
|
431
455
|
|
|
456
|
+
class EnterDBOSStepRetry:
|
|
457
|
+
def __init__(self, current_attempt: int, max_attempts: int) -> None:
|
|
458
|
+
self.current_attempt = current_attempt
|
|
459
|
+
self.max_attempts = max_attempts
|
|
460
|
+
|
|
461
|
+
def __enter__(self) -> None:
|
|
462
|
+
ctx = get_local_dbos_context()
|
|
463
|
+
if ctx is not None and ctx.step_status is not None:
|
|
464
|
+
ctx.step_status.current_attempt = self.current_attempt
|
|
465
|
+
ctx.step_status.max_attempts = self.max_attempts
|
|
466
|
+
|
|
467
|
+
def __exit__(
|
|
468
|
+
self,
|
|
469
|
+
exc_type: Optional[Type[BaseException]],
|
|
470
|
+
exc_value: Optional[BaseException],
|
|
471
|
+
traceback: Optional[TracebackType],
|
|
472
|
+
) -> Literal[False]:
|
|
473
|
+
ctx = get_local_dbos_context()
|
|
474
|
+
if ctx is not None and ctx.step_status is not None:
|
|
475
|
+
ctx.step_status.current_attempt = None
|
|
476
|
+
ctx.step_status.max_attempts = None
|
|
477
|
+
return False # Did not handle
|
|
478
|
+
|
|
479
|
+
|
|
432
480
|
class EnterDBOSTransaction:
|
|
433
481
|
def __init__(self, sqls: Session, attributes: TracedAttributes) -> None:
|
|
434
482
|
self.sqls = sqls
|