dbos 1.4.0a1__py3-none-any.whl → 1.5.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/_admin_server.py +108 -2
- dbos/_app_db.py +18 -0
- dbos/_conductor/conductor.py +37 -0
- dbos/_conductor/protocol.py +18 -0
- dbos/_core.py +5 -5
- dbos/_dbos.py +2 -2
- dbos/_dbos_config.py +5 -2
- dbos/_debug.py +4 -4
- dbos/_error.py +5 -2
- dbos/_logger.py +5 -2
- dbos/_outcome.py +11 -7
- dbos/_queue.py +0 -1
- dbos/_recovery.py +4 -6
- dbos/_sys_db.py +56 -0
- dbos/_tracer.py +5 -0
- dbos/_workflow_commands.py +35 -2
- dbos/cli/cli.py +31 -2
- {dbos-1.4.0a1.dist-info → dbos-1.5.0.dist-info}/METADATA +1 -1
- {dbos-1.4.0a1.dist-info → dbos-1.5.0.dist-info}/RECORD +22 -22
- {dbos-1.4.0a1.dist-info → dbos-1.5.0.dist-info}/WHEEL +0 -0
- {dbos-1.4.0a1.dist-info → dbos-1.5.0.dist-info}/entry_points.txt +0 -0
- {dbos-1.4.0a1.dist-info → dbos-1.5.0.dist-info}/licenses/LICENSE +0 -0
dbos/_admin_server.py
CHANGED
@@ -5,7 +5,9 @@ import re
|
|
5
5
|
import threading
|
6
6
|
from functools import partial
|
7
7
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
8
|
-
from typing import TYPE_CHECKING, Any, List, Optional, TypedDict
|
8
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, TypedDict
|
9
|
+
|
10
|
+
from dbos._workflow_commands import garbage_collect, global_timeout
|
9
11
|
|
10
12
|
from ._context import SetWorkflowID
|
11
13
|
from ._error import DBOSException
|
@@ -20,6 +22,10 @@ _health_check_path = "/dbos-healthz"
|
|
20
22
|
_workflow_recovery_path = "/dbos-workflow-recovery"
|
21
23
|
_deactivate_path = "/deactivate"
|
22
24
|
_workflow_queues_metadata_path = "/dbos-workflow-queues-metadata"
|
25
|
+
_garbage_collect_path = "/dbos-garbage-collect"
|
26
|
+
_global_timeout_path = "/dbos-global-timeout"
|
27
|
+
_queued_workflows_path = "/queues"
|
28
|
+
_workflows_path = "/workflows"
|
23
29
|
# /workflows/:workflow_id/cancel
|
24
30
|
# /workflows/:workflow_id/resume
|
25
31
|
# /workflows/:workflow_id/restart
|
@@ -100,10 +106,24 @@ class AdminRequestHandler(BaseHTTPRequestHandler):
|
|
100
106
|
steps_match = re.match(
|
101
107
|
r"^/workflows/(?P<workflow_id>[^/]+)/steps$", self.path
|
102
108
|
)
|
109
|
+
workflow_match = re.match(r"^/workflows/(?P<workflow_id>[^/]+)$", self.path)
|
103
110
|
|
104
111
|
if steps_match:
|
105
112
|
workflow_id = steps_match.group("workflow_id")
|
106
113
|
self._handle_steps(workflow_id)
|
114
|
+
elif workflow_match:
|
115
|
+
workflow_id = workflow_match.group("workflow_id")
|
116
|
+
workflows = self.dbos.list_workflows(workflow_ids=[workflow_id])
|
117
|
+
if not workflows:
|
118
|
+
self.send_response(404)
|
119
|
+
self._end_headers()
|
120
|
+
return
|
121
|
+
response_body = json.dumps(workflows[0].__dict__).encode("utf-8")
|
122
|
+
self.send_response(200)
|
123
|
+
self.send_header("Content-Type", "application/json")
|
124
|
+
self.send_header("Content-Length", str(len(response_body)))
|
125
|
+
self._end_headers()
|
126
|
+
self.wfile.write(response_body)
|
107
127
|
else:
|
108
128
|
self.send_response(404)
|
109
129
|
self._end_headers()
|
@@ -122,8 +142,50 @@ class AdminRequestHandler(BaseHTTPRequestHandler):
|
|
122
142
|
self.send_response(200)
|
123
143
|
self._end_headers()
|
124
144
|
self.wfile.write(json.dumps(workflow_ids).encode("utf-8"))
|
145
|
+
elif self.path == _workflows_path:
|
146
|
+
try:
|
147
|
+
filters = json.loads(post_data.decode("utf-8")) if post_data else {}
|
148
|
+
self._handle_workflows(filters)
|
149
|
+
except (json.JSONDecodeError, AttributeError) as e:
|
150
|
+
self.send_response(400)
|
151
|
+
self.send_header("Content-Type", "application/json")
|
152
|
+
self.end_headers()
|
153
|
+
self.wfile.write(
|
154
|
+
json.dumps({"error": f"Invalid JSON input: {str(e)}"}).encode(
|
155
|
+
"utf-8"
|
156
|
+
)
|
157
|
+
)
|
158
|
+
elif self.path == _queued_workflows_path:
|
159
|
+
try:
|
160
|
+
filters = json.loads(post_data.decode("utf-8")) if post_data else {}
|
161
|
+
self._handle_queued_workflows(filters)
|
162
|
+
except (json.JSONDecodeError, AttributeError) as e:
|
163
|
+
self.send_response(400)
|
164
|
+
self.send_header("Content-Type", "application/json")
|
165
|
+
self.end_headers()
|
166
|
+
self.wfile.write(
|
167
|
+
json.dumps({"error": f"Invalid JSON input: {str(e)}"}).encode(
|
168
|
+
"utf-8"
|
169
|
+
)
|
170
|
+
)
|
171
|
+
elif self.path == _garbage_collect_path:
|
172
|
+
inputs = json.loads(post_data.decode("utf-8"))
|
173
|
+
cutoff_epoch_timestamp_ms = inputs.get("cutoff_epoch_timestamp_ms", None)
|
174
|
+
rows_threshold = inputs.get("rows_threshold", None)
|
175
|
+
garbage_collect(
|
176
|
+
self.dbos,
|
177
|
+
cutoff_epoch_timestamp_ms=cutoff_epoch_timestamp_ms,
|
178
|
+
rows_threshold=rows_threshold,
|
179
|
+
)
|
180
|
+
self.send_response(204)
|
181
|
+
self._end_headers()
|
182
|
+
elif self.path == _global_timeout_path:
|
183
|
+
inputs = json.loads(post_data.decode("utf-8"))
|
184
|
+
cutoff_epoch_timestamp_ms = inputs.get("cutoff_epoch_timestamp_ms", None)
|
185
|
+
global_timeout(self.dbos, cutoff_epoch_timestamp_ms)
|
186
|
+
self.send_response(204)
|
187
|
+
self._end_headers()
|
125
188
|
else:
|
126
|
-
|
127
189
|
restart_match = re.match(
|
128
190
|
r"^/workflows/(?P<workflow_id>[^/]+)/restart$", self.path
|
129
191
|
)
|
@@ -262,6 +324,50 @@ class AdminRequestHandler(BaseHTTPRequestHandler):
|
|
262
324
|
self._end_headers()
|
263
325
|
self.wfile.write(json_steps)
|
264
326
|
|
327
|
+
def _handle_workflows(self, filters: Dict[str, Any]) -> None:
|
328
|
+
workflows = self.dbos.list_workflows(
|
329
|
+
workflow_ids=filters.get("workflow_ids"),
|
330
|
+
name=filters.get("name"),
|
331
|
+
start_time=filters.get("start_time"),
|
332
|
+
end_time=filters.get("end_time"),
|
333
|
+
status=filters.get("status"),
|
334
|
+
app_version=filters.get("application_version"),
|
335
|
+
limit=filters.get("limit"),
|
336
|
+
offset=filters.get("offset"),
|
337
|
+
sort_desc=filters.get("sort_desc", False),
|
338
|
+
workflow_id_prefix=filters.get("workflow_id_prefix"),
|
339
|
+
)
|
340
|
+
|
341
|
+
response_body = json.dumps(
|
342
|
+
[workflow.__dict__ for workflow in workflows]
|
343
|
+
).encode("utf-8")
|
344
|
+
self.send_response(200)
|
345
|
+
self.send_header("Content-Type", "application/json")
|
346
|
+
self.send_header("Content-Length", str(len(response_body)))
|
347
|
+
self._end_headers()
|
348
|
+
self.wfile.write(response_body)
|
349
|
+
|
350
|
+
def _handle_queued_workflows(self, filters: Dict[str, Any]) -> None:
|
351
|
+
workflows = self.dbos.list_queued_workflows(
|
352
|
+
queue_name=filters.get("queue_name"),
|
353
|
+
name=filters.get("name"),
|
354
|
+
start_time=filters.get("start_time"),
|
355
|
+
end_time=filters.get("end_time"),
|
356
|
+
status=filters.get("status"),
|
357
|
+
limit=filters.get("limit"),
|
358
|
+
offset=filters.get("offset"),
|
359
|
+
sort_desc=filters.get("sort_desc", False),
|
360
|
+
)
|
361
|
+
|
362
|
+
response_body = json.dumps(
|
363
|
+
[workflow.__dict__ for workflow in workflows]
|
364
|
+
).encode("utf-8")
|
365
|
+
self.send_response(200)
|
366
|
+
self.send_header("Content-Type", "application/json")
|
367
|
+
self.send_header("Content-Length", str(len(response_body)))
|
368
|
+
self._end_headers()
|
369
|
+
self.wfile.write(response_body)
|
370
|
+
|
265
371
|
|
266
372
|
# Be consistent with DBOS-TS response.
|
267
373
|
class PerfUtilization(TypedDict):
|
dbos/_app_db.py
CHANGED
@@ -256,3 +256,21 @@ class ApplicationDatabase:
|
|
256
256
|
)
|
257
257
|
|
258
258
|
conn.execute(insert_stmt)
|
259
|
+
|
260
|
+
def garbage_collect(
|
261
|
+
self, cutoff_epoch_timestamp_ms: int, pending_workflow_ids: list[str]
|
262
|
+
) -> None:
|
263
|
+
with self.engine.begin() as c:
|
264
|
+
delete_query = sa.delete(ApplicationSchema.transaction_outputs).where(
|
265
|
+
ApplicationSchema.transaction_outputs.c.created_at
|
266
|
+
< cutoff_epoch_timestamp_ms
|
267
|
+
)
|
268
|
+
|
269
|
+
if len(pending_workflow_ids) > 0:
|
270
|
+
delete_query = delete_query.where(
|
271
|
+
~ApplicationSchema.transaction_outputs.c.workflow_uuid.in_(
|
272
|
+
pending_workflow_ids
|
273
|
+
)
|
274
|
+
)
|
275
|
+
|
276
|
+
c.execute(delete_query)
|
dbos/_conductor/conductor.py
CHANGED
@@ -13,7 +13,9 @@ from websockets.sync.connection import Connection
|
|
13
13
|
from dbos._context import SetWorkflowID
|
14
14
|
from dbos._utils import GlobalParams
|
15
15
|
from dbos._workflow_commands import (
|
16
|
+
garbage_collect,
|
16
17
|
get_workflow,
|
18
|
+
global_timeout,
|
17
19
|
list_queued_workflows,
|
18
20
|
list_workflow_steps,
|
19
21
|
list_workflows,
|
@@ -356,6 +358,41 @@ class ConductorWebsocket(threading.Thread):
|
|
356
358
|
error_message=error_message,
|
357
359
|
)
|
358
360
|
websocket.send(list_steps_response.to_json())
|
361
|
+
elif msg_type == p.MessageType.RETENTION:
|
362
|
+
retention_message = p.RetentionRequest.from_json(message)
|
363
|
+
success = True
|
364
|
+
try:
|
365
|
+
garbage_collect(
|
366
|
+
self.dbos,
|
367
|
+
cutoff_epoch_timestamp_ms=retention_message.body[
|
368
|
+
"gc_cutoff_epoch_ms"
|
369
|
+
],
|
370
|
+
rows_threshold=retention_message.body[
|
371
|
+
"gc_rows_threshold"
|
372
|
+
],
|
373
|
+
)
|
374
|
+
if (
|
375
|
+
retention_message.body["timeout_cutoff_epoch_ms"]
|
376
|
+
is not None
|
377
|
+
):
|
378
|
+
global_timeout(
|
379
|
+
self.dbos,
|
380
|
+
retention_message.body[
|
381
|
+
"timeout_cutoff_epoch_ms"
|
382
|
+
],
|
383
|
+
)
|
384
|
+
except Exception as e:
|
385
|
+
error_message = f"Exception encountered during enforcing retention policy: {traceback.format_exc()}"
|
386
|
+
self.dbos.logger.error(error_message)
|
387
|
+
success = False
|
388
|
+
|
389
|
+
retention_response = p.RetentionResponse(
|
390
|
+
type=p.MessageType.RETENTION,
|
391
|
+
request_id=base_message.request_id,
|
392
|
+
success=success,
|
393
|
+
error_message=error_message,
|
394
|
+
)
|
395
|
+
websocket.send(retention_response.to_json())
|
359
396
|
else:
|
360
397
|
self.dbos.logger.warning(
|
361
398
|
f"Unexpected message type: {msg_type}"
|
dbos/_conductor/protocol.py
CHANGED
@@ -18,6 +18,7 @@ class MessageType(str, Enum):
|
|
18
18
|
EXIST_PENDING_WORKFLOWS = "exist_pending_workflows"
|
19
19
|
LIST_STEPS = "list_steps"
|
20
20
|
FORK_WORKFLOW = "fork_workflow"
|
21
|
+
RETENTION = "retention"
|
21
22
|
|
22
23
|
|
23
24
|
T = TypeVar("T", bound="BaseMessage")
|
@@ -280,3 +281,20 @@ class ForkWorkflowRequest(BaseMessage):
|
|
280
281
|
class ForkWorkflowResponse(BaseMessage):
|
281
282
|
new_workflow_id: Optional[str]
|
282
283
|
error_message: Optional[str] = None
|
284
|
+
|
285
|
+
|
286
|
+
class RetentionBody(TypedDict):
|
287
|
+
gc_cutoff_epoch_ms: Optional[int]
|
288
|
+
gc_rows_threshold: Optional[int]
|
289
|
+
timeout_cutoff_epoch_ms: Optional[int]
|
290
|
+
|
291
|
+
|
292
|
+
@dataclass
|
293
|
+
class RetentionRequest(BaseMessage):
|
294
|
+
body: RetentionBody
|
295
|
+
|
296
|
+
|
297
|
+
@dataclass
|
298
|
+
class RetentionResponse(BaseMessage):
|
299
|
+
success: bool
|
300
|
+
error_message: Optional[str] = None
|
dbos/_core.py
CHANGED
@@ -404,9 +404,9 @@ def _execute_workflow_wthread(
|
|
404
404
|
return dbos._background_event_loop.submit_coroutine(
|
405
405
|
cast(Pending[R], result)()
|
406
406
|
)
|
407
|
-
except Exception:
|
407
|
+
except Exception as e:
|
408
408
|
dbos.logger.error(
|
409
|
-
f"Exception encountered in asynchronous workflow:
|
409
|
+
f"Exception encountered in asynchronous workflow:", exc_info=e
|
410
410
|
)
|
411
411
|
raise
|
412
412
|
|
@@ -430,9 +430,9 @@ async def _execute_workflow_async(
|
|
430
430
|
_get_wf_invoke_func(dbos, status)
|
431
431
|
)
|
432
432
|
return await result()
|
433
|
-
except Exception:
|
433
|
+
except Exception as e:
|
434
434
|
dbos.logger.error(
|
435
|
-
f"Exception encountered in asynchronous workflow:
|
435
|
+
f"Exception encountered in asynchronous workflow:", exc_info=e
|
436
436
|
)
|
437
437
|
raise
|
438
438
|
|
@@ -1123,7 +1123,7 @@ def decorate_step(
|
|
1123
1123
|
stepOutcome = stepOutcome.retry(
|
1124
1124
|
max_attempts,
|
1125
1125
|
on_exception,
|
1126
|
-
lambda i: DBOSMaxStepRetriesExceeded(func.__name__, i),
|
1126
|
+
lambda i, e: DBOSMaxStepRetriesExceeded(func.__name__, i, e),
|
1127
1127
|
)
|
1128
1128
|
|
1129
1129
|
outcome = (
|
dbos/_dbos.py
CHANGED
@@ -521,8 +521,8 @@ class DBOS:
|
|
521
521
|
handler.flush()
|
522
522
|
add_otlp_to_all_loggers()
|
523
523
|
add_transformer_to_all_loggers()
|
524
|
-
except Exception:
|
525
|
-
dbos_logger.error(f"DBOS failed to launch:
|
524
|
+
except Exception as e:
|
525
|
+
dbos_logger.error(f"DBOS failed to launch:", exc_info=e)
|
526
526
|
raise
|
527
527
|
|
528
528
|
@classmethod
|
dbos/_dbos_config.py
CHANGED
@@ -31,6 +31,7 @@ class DBOSConfig(TypedDict, total=False):
|
|
31
31
|
otlp_logs_endpoints: List[str]: OTLP logs endpoints
|
32
32
|
admin_port (int): Admin port
|
33
33
|
run_admin_server (bool): Whether to run the DBOS admin server
|
34
|
+
otlp_attributes (dict[str, str]): A set of custom attributes to apply OTLP-exported logs and traces
|
34
35
|
"""
|
35
36
|
|
36
37
|
name: str
|
@@ -43,6 +44,7 @@ class DBOSConfig(TypedDict, total=False):
|
|
43
44
|
otlp_logs_endpoints: Optional[List[str]]
|
44
45
|
admin_port: Optional[int]
|
45
46
|
run_admin_server: Optional[bool]
|
47
|
+
otlp_attributes: Optional[dict[str, str]]
|
46
48
|
|
47
49
|
|
48
50
|
class RuntimeConfig(TypedDict, total=False):
|
@@ -84,6 +86,7 @@ class LoggerConfig(TypedDict, total=False):
|
|
84
86
|
class TelemetryConfig(TypedDict, total=False):
|
85
87
|
logs: Optional[LoggerConfig]
|
86
88
|
OTLPExporter: Optional[OTLPExporterConfig]
|
89
|
+
otlp_attributes: Optional[dict[str, str]]
|
87
90
|
|
88
91
|
|
89
92
|
class ConfigFile(TypedDict, total=False):
|
@@ -145,7 +148,8 @@ def translate_dbos_config_to_config_file(config: DBOSConfig) -> ConfigFile:
|
|
145
148
|
|
146
149
|
# Telemetry config
|
147
150
|
telemetry: TelemetryConfig = {
|
148
|
-
"OTLPExporter": {"tracesEndpoint": [], "logsEndpoint": []}
|
151
|
+
"OTLPExporter": {"tracesEndpoint": [], "logsEndpoint": []},
|
152
|
+
"otlp_attributes": config.get("otlp_attributes", {}),
|
149
153
|
}
|
150
154
|
# For mypy
|
151
155
|
assert telemetry["OTLPExporter"] is not None
|
@@ -431,7 +435,6 @@ def is_valid_database_url(database_url: str) -> bool:
|
|
431
435
|
url = make_url(database_url)
|
432
436
|
required_fields = [
|
433
437
|
("username", "Username must be specified in the connection URL"),
|
434
|
-
("password", "Password must be specified in the connection URL"),
|
435
438
|
("host", "Host must be specified in the connection URL"),
|
436
439
|
("database", "Database name must be specified in the connection URL"),
|
437
440
|
]
|
dbos/_debug.py
CHANGED
@@ -15,11 +15,11 @@ class PythonModule:
|
|
15
15
|
|
16
16
|
|
17
17
|
def debug_workflow(workflow_id: str, entrypoint: Union[str, PythonModule]) -> None:
|
18
|
-
# include the current directory (represented by empty string) in the search path
|
19
|
-
# if it not already included
|
20
|
-
if "" not in sys.path:
|
21
|
-
sys.path.insert(0, "")
|
22
18
|
if isinstance(entrypoint, str):
|
19
|
+
# ensure the entrypoint parent directory is in sys.path
|
20
|
+
parent = str(Path(entrypoint).parent)
|
21
|
+
if parent not in sys.path:
|
22
|
+
sys.path.insert(0, parent)
|
23
23
|
runpy.run_path(entrypoint)
|
24
24
|
elif isinstance(entrypoint, PythonModule):
|
25
25
|
runpy.run_module(entrypoint.module_name)
|
dbos/_error.py
CHANGED
@@ -150,9 +150,12 @@ class DBOSNotAuthorizedError(DBOSException):
|
|
150
150
|
class DBOSMaxStepRetriesExceeded(DBOSException):
|
151
151
|
"""Exception raised when a step was retried the maximimum number of times without success."""
|
152
152
|
|
153
|
-
def __init__(
|
153
|
+
def __init__(
|
154
|
+
self, step_name: str, max_retries: int, errors: list[Exception]
|
155
|
+
) -> None:
|
154
156
|
self.step_name = step_name
|
155
157
|
self.max_retries = max_retries
|
158
|
+
self.errors = errors
|
156
159
|
super().__init__(
|
157
160
|
f"Step {step_name} has exceeded its maximum of {max_retries} retries",
|
158
161
|
dbos_error_code=DBOSErrorCode.MaxStepRetriesExceeded.value,
|
@@ -160,7 +163,7 @@ class DBOSMaxStepRetriesExceeded(DBOSException):
|
|
160
163
|
|
161
164
|
def __reduce__(self) -> Any:
|
162
165
|
# Tell jsonpickle how to reconstruct this object
|
163
|
-
return (self.__class__, (self.step_name, self.max_retries))
|
166
|
+
return (self.__class__, (self.step_name, self.max_retries, self.errors))
|
164
167
|
|
165
168
|
|
166
169
|
class DBOSConflictingRegistrationError(DBOSException):
|
dbos/_logger.py
CHANGED
@@ -20,14 +20,17 @@ _otlp_handler, _dbos_log_transformer = None, None
|
|
20
20
|
|
21
21
|
|
22
22
|
class DBOSLogTransformer(logging.Filter):
|
23
|
-
def __init__(self) -> None:
|
23
|
+
def __init__(self, config: "ConfigFile") -> None:
|
24
24
|
super().__init__()
|
25
25
|
self.app_id = os.environ.get("DBOS__APPID", "")
|
26
|
+
self.otlp_attributes: dict[str, str] = config.get("telemetry", {}).get("otlp_attributes", {}) # type: ignore
|
26
27
|
|
27
28
|
def filter(self, record: Any) -> bool:
|
28
29
|
record.applicationID = self.app_id
|
29
30
|
record.applicationVersion = GlobalParams.app_version
|
30
31
|
record.executorID = GlobalParams.executor_id
|
32
|
+
for k, v in self.otlp_attributes.items():
|
33
|
+
setattr(record, k, v)
|
31
34
|
|
32
35
|
# If available, decorate the log entry with Workflow ID and Trace ID
|
33
36
|
from dbos._context import get_local_dbos_context
|
@@ -98,7 +101,7 @@ def config_logger(config: "ConfigFile") -> None:
|
|
98
101
|
|
99
102
|
# Attach DBOS-specific attributes to all log entries.
|
100
103
|
global _dbos_log_transformer
|
101
|
-
_dbos_log_transformer = DBOSLogTransformer()
|
104
|
+
_dbos_log_transformer = DBOSLogTransformer(config)
|
102
105
|
dbos_logger.addFilter(_dbos_log_transformer)
|
103
106
|
|
104
107
|
|
dbos/_outcome.py
CHANGED
@@ -37,7 +37,7 @@ class Outcome(Protocol[T]):
|
|
37
37
|
self,
|
38
38
|
attempts: int,
|
39
39
|
on_exception: Callable[[int, BaseException], float],
|
40
|
-
exceeded_retries: Callable[[int],
|
40
|
+
exceeded_retries: Callable[[int, list[Exception]], Exception],
|
41
41
|
) -> "Outcome[T]": ...
|
42
42
|
|
43
43
|
def intercept(
|
@@ -96,23 +96,25 @@ class Immediate(Outcome[T]):
|
|
96
96
|
func: Callable[[], T],
|
97
97
|
attempts: int,
|
98
98
|
on_exception: Callable[[int, BaseException], float],
|
99
|
-
exceeded_retries: Callable[[int],
|
99
|
+
exceeded_retries: Callable[[int, list[Exception]], Exception],
|
100
100
|
) -> T:
|
101
|
+
errors: list[Exception] = []
|
101
102
|
for i in range(attempts):
|
102
103
|
try:
|
103
104
|
with EnterDBOSStepRetry(i, attempts):
|
104
105
|
return func()
|
105
106
|
except Exception as exp:
|
107
|
+
errors.append(exp)
|
106
108
|
wait_time = on_exception(i, exp)
|
107
109
|
time.sleep(wait_time)
|
108
110
|
|
109
|
-
raise exceeded_retries(attempts)
|
111
|
+
raise exceeded_retries(attempts, errors)
|
110
112
|
|
111
113
|
def retry(
|
112
114
|
self,
|
113
115
|
attempts: int,
|
114
116
|
on_exception: Callable[[int, BaseException], float],
|
115
|
-
exceeded_retries: Callable[[int],
|
117
|
+
exceeded_retries: Callable[[int, list[Exception]], Exception],
|
116
118
|
) -> "Immediate[T]":
|
117
119
|
assert attempts > 0
|
118
120
|
return Immediate[T](
|
@@ -183,23 +185,25 @@ class Pending(Outcome[T]):
|
|
183
185
|
func: Callable[[], Coroutine[Any, Any, T]],
|
184
186
|
attempts: int,
|
185
187
|
on_exception: Callable[[int, BaseException], float],
|
186
|
-
exceeded_retries: Callable[[int],
|
188
|
+
exceeded_retries: Callable[[int, list[Exception]], Exception],
|
187
189
|
) -> T:
|
190
|
+
errors: list[Exception] = []
|
188
191
|
for i in range(attempts):
|
189
192
|
try:
|
190
193
|
with EnterDBOSStepRetry(i, attempts):
|
191
194
|
return await func()
|
192
195
|
except Exception as exp:
|
196
|
+
errors.append(exp)
|
193
197
|
wait_time = on_exception(i, exp)
|
194
198
|
await asyncio.sleep(wait_time)
|
195
199
|
|
196
|
-
raise exceeded_retries(attempts)
|
200
|
+
raise exceeded_retries(attempts, errors)
|
197
201
|
|
198
202
|
def retry(
|
199
203
|
self,
|
200
204
|
attempts: int,
|
201
205
|
on_exception: Callable[[int, BaseException], float],
|
202
|
-
exceeded_retries: Callable[[int],
|
206
|
+
exceeded_retries: Callable[[int, list[Exception]], Exception],
|
203
207
|
) -> "Pending[T]":
|
204
208
|
assert attempts > 0
|
205
209
|
return Pending[T](
|
dbos/_queue.py
CHANGED
dbos/_recovery.py
CHANGED
@@ -1,7 +1,5 @@
|
|
1
|
-
import os
|
2
1
|
import threading
|
3
2
|
import time
|
4
|
-
import traceback
|
5
3
|
from typing import TYPE_CHECKING, Any, List
|
6
4
|
|
7
5
|
from dbos._utils import GlobalParams
|
@@ -39,9 +37,9 @@ def startup_recovery_thread(
|
|
39
37
|
time.sleep(1)
|
40
38
|
except Exception as e:
|
41
39
|
dbos.logger.error(
|
42
|
-
f"Exception encountered when recovering workflows:
|
40
|
+
f"Exception encountered when recovering workflows:", exc_info=e
|
43
41
|
)
|
44
|
-
raise
|
42
|
+
raise
|
45
43
|
|
46
44
|
|
47
45
|
def recover_pending_workflows(
|
@@ -59,9 +57,9 @@ def recover_pending_workflows(
|
|
59
57
|
workflow_handles.append(handle)
|
60
58
|
except Exception as e:
|
61
59
|
dbos.logger.error(
|
62
|
-
f"Exception encountered when recovering workflows:
|
60
|
+
f"Exception encountered when recovering workflows:", exc_info=e
|
63
61
|
)
|
64
|
-
raise
|
62
|
+
raise
|
65
63
|
dbos.logger.info(
|
66
64
|
f"Recovering {len(pending_workflows)} workflows for executor {executor_id} from version {GlobalParams.app_version}"
|
67
65
|
)
|
dbos/_sys_db.py
CHANGED
@@ -1852,6 +1852,62 @@ class SystemDatabase:
|
|
1852
1852
|
dbos_logger.error(f"Error connecting to the DBOS system database: {e}")
|
1853
1853
|
raise
|
1854
1854
|
|
1855
|
+
def garbage_collect(
|
1856
|
+
self, cutoff_epoch_timestamp_ms: Optional[int], rows_threshold: Optional[int]
|
1857
|
+
) -> Optional[tuple[int, list[str]]]:
|
1858
|
+
if rows_threshold is not None:
|
1859
|
+
with self.engine.begin() as c:
|
1860
|
+
# Get the created_at timestamp of the rows_threshold newest row
|
1861
|
+
result = c.execute(
|
1862
|
+
sa.select(SystemSchema.workflow_status.c.created_at)
|
1863
|
+
.order_by(SystemSchema.workflow_status.c.created_at.desc())
|
1864
|
+
.limit(1)
|
1865
|
+
.offset(rows_threshold - 1)
|
1866
|
+
).fetchone()
|
1867
|
+
|
1868
|
+
if result is not None:
|
1869
|
+
rows_based_cutoff = result[0]
|
1870
|
+
# Use the more restrictive cutoff (higher timestamp = more recent = more deletion)
|
1871
|
+
if (
|
1872
|
+
cutoff_epoch_timestamp_ms is None
|
1873
|
+
or rows_based_cutoff > cutoff_epoch_timestamp_ms
|
1874
|
+
):
|
1875
|
+
cutoff_epoch_timestamp_ms = rows_based_cutoff
|
1876
|
+
|
1877
|
+
if cutoff_epoch_timestamp_ms is None:
|
1878
|
+
return None
|
1879
|
+
|
1880
|
+
with self.engine.begin() as c:
|
1881
|
+
# Delete all workflows older than cutoff that are NOT PENDING or ENQUEUED
|
1882
|
+
c.execute(
|
1883
|
+
sa.delete(SystemSchema.workflow_status)
|
1884
|
+
.where(
|
1885
|
+
SystemSchema.workflow_status.c.created_at
|
1886
|
+
< cutoff_epoch_timestamp_ms
|
1887
|
+
)
|
1888
|
+
.where(
|
1889
|
+
~SystemSchema.workflow_status.c.status.in_(
|
1890
|
+
[
|
1891
|
+
WorkflowStatusString.PENDING.value,
|
1892
|
+
WorkflowStatusString.ENQUEUED.value,
|
1893
|
+
]
|
1894
|
+
)
|
1895
|
+
)
|
1896
|
+
)
|
1897
|
+
|
1898
|
+
# Then, get the IDs of all remaining old workflows
|
1899
|
+
pending_enqueued_result = c.execute(
|
1900
|
+
sa.select(SystemSchema.workflow_status.c.workflow_uuid).where(
|
1901
|
+
SystemSchema.workflow_status.c.created_at
|
1902
|
+
< cutoff_epoch_timestamp_ms
|
1903
|
+
)
|
1904
|
+
).fetchall()
|
1905
|
+
|
1906
|
+
# Return the final cutoff and workflow IDs
|
1907
|
+
return cutoff_epoch_timestamp_ms, [
|
1908
|
+
row[0] for row in pending_enqueued_result
|
1909
|
+
]
|
1910
|
+
|
1855
1911
|
|
1856
1912
|
def reset_system_database(postgres_db_url: sa.URL, sysdb_name: str) -> None:
|
1857
1913
|
try:
|
dbos/_tracer.py
CHANGED
@@ -19,11 +19,14 @@ if TYPE_CHECKING:
|
|
19
19
|
|
20
20
|
class DBOSTracer:
|
21
21
|
|
22
|
+
otlp_attributes: dict[str, str] = {}
|
23
|
+
|
22
24
|
def __init__(self) -> None:
|
23
25
|
self.app_id = os.environ.get("DBOS__APPID", None)
|
24
26
|
self.provider: Optional[TracerProvider] = None
|
25
27
|
|
26
28
|
def config(self, config: ConfigFile) -> None:
|
29
|
+
self.otlp_attributes = config.get("telemetry", {}).get("otlp_attributes", {}) # type: ignore
|
27
30
|
if not isinstance(trace.get_tracer_provider(), TracerProvider):
|
28
31
|
resource = Resource(
|
29
32
|
attributes={
|
@@ -63,6 +66,8 @@ class DBOSTracer:
|
|
63
66
|
for k, v in attributes.items():
|
64
67
|
if k != "name" and v is not None and isinstance(v, (str, bool, int, float)):
|
65
68
|
span.set_attribute(k, v)
|
69
|
+
for k, v in self.otlp_attributes.items():
|
70
|
+
span.set_attribute(k, v)
|
66
71
|
return span
|
67
72
|
|
68
73
|
def end_span(self, span: Span) -> None:
|
dbos/_workflow_commands.py
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
+
import time
|
1
2
|
import uuid
|
2
|
-
from
|
3
|
+
from datetime import datetime
|
4
|
+
from typing import TYPE_CHECKING, List, Optional
|
3
5
|
|
4
6
|
from dbos._context import get_local_dbos_context
|
5
|
-
from dbos._error import DBOSException
|
6
7
|
|
7
8
|
from ._app_db import ApplicationDatabase
|
8
9
|
from ._sys_db import (
|
@@ -11,8 +12,12 @@ from ._sys_db import (
|
|
11
12
|
StepInfo,
|
12
13
|
SystemDatabase,
|
13
14
|
WorkflowStatus,
|
15
|
+
WorkflowStatusString,
|
14
16
|
)
|
15
17
|
|
18
|
+
if TYPE_CHECKING:
|
19
|
+
from ._dbos import DBOS
|
20
|
+
|
16
21
|
|
17
22
|
def list_workflows(
|
18
23
|
sys_db: SystemDatabase,
|
@@ -118,3 +123,31 @@ def fork_workflow(
|
|
118
123
|
application_version=application_version,
|
119
124
|
)
|
120
125
|
return forked_workflow_id
|
126
|
+
|
127
|
+
|
128
|
+
def garbage_collect(
|
129
|
+
dbos: "DBOS",
|
130
|
+
cutoff_epoch_timestamp_ms: Optional[int],
|
131
|
+
rows_threshold: Optional[int],
|
132
|
+
) -> None:
|
133
|
+
if cutoff_epoch_timestamp_ms is None and rows_threshold is None:
|
134
|
+
return
|
135
|
+
result = dbos._sys_db.garbage_collect(
|
136
|
+
cutoff_epoch_timestamp_ms=cutoff_epoch_timestamp_ms,
|
137
|
+
rows_threshold=rows_threshold,
|
138
|
+
)
|
139
|
+
if result is not None:
|
140
|
+
cutoff_epoch_timestamp_ms, pending_workflow_ids = result
|
141
|
+
dbos._app_db.garbage_collect(cutoff_epoch_timestamp_ms, pending_workflow_ids)
|
142
|
+
|
143
|
+
|
144
|
+
def global_timeout(dbos: "DBOS", cutoff_epoch_timestamp_ms: int) -> None:
|
145
|
+
cutoff_iso = datetime.fromtimestamp(cutoff_epoch_timestamp_ms / 1000).isoformat()
|
146
|
+
for workflow in dbos.list_workflows(
|
147
|
+
status=WorkflowStatusString.PENDING.value, end_time=cutoff_iso
|
148
|
+
):
|
149
|
+
dbos.cancel_workflow(workflow.workflow_id)
|
150
|
+
for workflow in dbos.list_workflows(
|
151
|
+
status=WorkflowStatusString.ENQUEUED.value, end_time=cutoff_iso
|
152
|
+
):
|
153
|
+
dbos.cancel_workflow(workflow.workflow_id)
|
dbos/cli/cli.py
CHANGED
@@ -18,7 +18,12 @@ from dbos._debug import debug_workflow, parse_start_command
|
|
18
18
|
|
19
19
|
from .._app_db import ApplicationDatabase
|
20
20
|
from .._client import DBOSClient
|
21
|
-
from .._dbos_config import
|
21
|
+
from .._dbos_config import (
|
22
|
+
_app_name_to_db_name,
|
23
|
+
_is_valid_app_name,
|
24
|
+
is_valid_database_url,
|
25
|
+
load_config,
|
26
|
+
)
|
22
27
|
from .._docker_pg_helper import start_docker_pg, stop_docker_pg
|
23
28
|
from .._schemas.system_database import SystemSchema
|
24
29
|
from .._sys_db import SystemDatabase, reset_system_database
|
@@ -28,12 +33,36 @@ from ._template_init import copy_template, get_project_name, get_templates_direc
|
|
28
33
|
|
29
34
|
|
30
35
|
def _get_db_url(db_url: Optional[str]) -> str:
|
36
|
+
"""
|
37
|
+
Get the database URL to use for the DBOS application.
|
38
|
+
Order of precedence:
|
39
|
+
- If the `db_url` argument is provided, use it.
|
40
|
+
- If the `dbos-config.yaml` file is present, use the `database_url` from it.
|
41
|
+
- If the `DBOS_DATABASE_URL` environment variable is set, use it.
|
42
|
+
|
43
|
+
Otherwise fallback to the same default Postgres URL than the DBOS library.
|
44
|
+
Note that for the latter to be possible, a configuration file must have been found, with an application name set.
|
45
|
+
"""
|
31
46
|
database_url = db_url
|
47
|
+
_app_db_name = None
|
48
|
+
if database_url is None:
|
49
|
+
# Load from config file if present
|
50
|
+
try:
|
51
|
+
config = load_config(run_process_config=False, silent=True)
|
52
|
+
database_url = config.get("database_url")
|
53
|
+
_app_db_name = _app_name_to_db_name(config["name"])
|
54
|
+
except (FileNotFoundError, OSError):
|
55
|
+
# Config file doesn't exist, continue with other fallbacks
|
56
|
+
pass
|
32
57
|
if database_url is None:
|
33
58
|
database_url = os.getenv("DBOS_DATABASE_URL")
|
59
|
+
if database_url is None and _app_db_name is not None:
|
60
|
+
# Fallback on the same defaults than the DBOS library
|
61
|
+
_password = os.environ.get("PGPASSWORD", "dbos")
|
62
|
+
database_url = f"postgres://postgres:{_password}@localhost:5432/{_app_db_name}?connect_timeout=10&sslmode=prefer"
|
34
63
|
if database_url is None:
|
35
64
|
raise ValueError(
|
36
|
-
"Missing database URL: please set it using the --db-url flag
|
65
|
+
"Missing database URL: please set it using the --db-url flag, the DBOS_DATABASE_URL environment variable, or in your dbos-config.yaml file."
|
37
66
|
)
|
38
67
|
assert is_valid_database_url(database_url)
|
39
68
|
return database_url
|
@@ -1,29 +1,29 @@
|
|
1
|
-
dbos-1.
|
2
|
-
dbos-1.
|
3
|
-
dbos-1.
|
4
|
-
dbos-1.
|
1
|
+
dbos-1.5.0.dist-info/METADATA,sha256=Dn02XFDJEtFjCEZYBOqsdTHrg-qMgQ3Rd58d6HS7JRk,13265
|
2
|
+
dbos-1.5.0.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
|
3
|
+
dbos-1.5.0.dist-info/entry_points.txt,sha256=_QOQ3tVfEjtjBlr1jS4sHqHya9lI2aIEIWkz8dqYp14,58
|
4
|
+
dbos-1.5.0.dist-info/licenses/LICENSE,sha256=VGZit_a5-kdw9WT6fY5jxAWVwGQzgLFyPWrcVVUhVNU,1067
|
5
5
|
dbos/__init__.py,sha256=NssPCubaBxdiKarOWa-wViz1hdJSkmBGcpLX_gQ4NeA,891
|
6
6
|
dbos/__main__.py,sha256=G7Exn-MhGrVJVDbgNlpzhfh8WMX_72t3_oJaFT9Lmt8,653
|
7
|
-
dbos/_admin_server.py,sha256=
|
8
|
-
dbos/_app_db.py,sha256=
|
7
|
+
dbos/_admin_server.py,sha256=l46ZX4NpvBP9W8cl9gE7OqMNwUCevLMt2VztM7crBv0,15465
|
8
|
+
dbos/_app_db.py,sha256=htblDPfqrpb_uZoFcvaud7cgQ-PDyn6Bn-cBidxdCTA,10603
|
9
9
|
dbos/_classproperty.py,sha256=f0X-_BySzn3yFDRKB2JpCbLYQ9tLwt1XftfshvY7CBs,626
|
10
10
|
dbos/_client.py,sha256=cQxw1Nbh_vKZ03lONt0EmUhwXBk3B3NczZrmfXXeefY,14667
|
11
|
-
dbos/_conductor/conductor.py,sha256=
|
12
|
-
dbos/_conductor/protocol.py,sha256=
|
11
|
+
dbos/_conductor/conductor.py,sha256=y_T-8kEHwKWt6W8LtcFMctB_6EvYFWsuGLxiFuuKKBU,23702
|
12
|
+
dbos/_conductor/protocol.py,sha256=DOTprPSd7oHDcvwWSyZpnlPds_JfILtcKzHZa-qBsF4,7330
|
13
13
|
dbos/_context.py,sha256=5ajoWAmToAfzzmMLylnJZoL4Ny9rBwZWuG05sXadMIA,24798
|
14
|
-
dbos/_core.py,sha256=
|
14
|
+
dbos/_core.py,sha256=eFRhljdi8vjpmBEzu-wt_feC7-uDmKqbiybz92KCRwI,48523
|
15
15
|
dbos/_croniter.py,sha256=XHAyUyibs_59sJQfSNWkP7rqQY6_XrlfuuCxk4jYqek,47559
|
16
|
-
dbos/_dbos.py,sha256=
|
17
|
-
dbos/_dbos_config.py,sha256=
|
18
|
-
dbos/_debug.py,sha256=
|
16
|
+
dbos/_dbos.py,sha256=HHMo-PLUa6m5jFgdA-YYAwaTx2kyA1Te1VslAyd0hIw,47257
|
17
|
+
dbos/_dbos_config.py,sha256=JUG4V1rrP0p1AYESgih4ea80qOH_13UsgoIIm8X84pw,20562
|
18
|
+
dbos/_debug.py,sha256=99j2SChWmCPAlZoDmjsJGe77tpU2LEa8E2TtLAnnh7o,1831
|
19
19
|
dbos/_docker_pg_helper.py,sha256=tLJXWqZ4S-ExcaPnxg_i6cVxL6ZxrYlZjaGsklY-s2I,6115
|
20
|
-
dbos/_error.py,sha256=
|
20
|
+
dbos/_error.py,sha256=nS7KuXJHhuNXZRErxdEUGT38Hb0VPyxNwSyADiVpHcE,8581
|
21
21
|
dbos/_event_loop.py,sha256=cvaFN9-II3MsHEOq8QoICc_8qSKrjikMlLfuhC3Y8Dk,2923
|
22
22
|
dbos/_fastapi.py,sha256=T7YlVY77ASqyTqq0aAPclZ9YzlXdGTT0lEYSwSgt1EE,3151
|
23
23
|
dbos/_flask.py,sha256=Npnakt-a3W5OykONFRkDRnumaDhTQmA0NPdUCGRYKXE,1652
|
24
24
|
dbos/_kafka.py,sha256=pz0xZ9F3X9Ky1k-VSbeF3tfPhP3UPr3lUUhUfE41__U,4198
|
25
25
|
dbos/_kafka_message.py,sha256=NYvOXNG3Qn7bghn1pv3fg4Pbs86ILZGcK4IB-MLUNu0,409
|
26
|
-
dbos/_logger.py,sha256=
|
26
|
+
dbos/_logger.py,sha256=Dp6bHZKUtcm5gWwYHj_HA5Wj5OMuJGUrpl2g2i4xDZg,4620
|
27
27
|
dbos/_migrations/env.py,sha256=38SIGVbmn_VV2x2u1aHLcPOoWgZ84eCymf3g_NljmbU,1626
|
28
28
|
dbos/_migrations/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
|
29
29
|
dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py,sha256=ICLPl8CN9tQXMsLDsAj8z1TsL831-Z3F8jSBvrR-wyw,736
|
@@ -39,9 +39,9 @@ dbos/_migrations/versions/d76646551a6c_workflow_queue.py,sha256=G942nophZ2uC2vc4
|
|
39
39
|
dbos/_migrations/versions/d994145b47b6_consolidate_inputs.py,sha256=_J0jP247fuo66fzOmLlKFO9FJ_CRBXlqa2lnLrcXugQ,672
|
40
40
|
dbos/_migrations/versions/eab0cc1d9a14_job_queue.py,sha256=uvhFOtqbBreCePhAxZfIT0qCAI7BiZTou9wt6QnbY7c,1412
|
41
41
|
dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py,sha256=m90Lc5YH0ZISSq1MyxND6oq3RZrZKrIqEsZtwJ1jWxA,1049
|
42
|
-
dbos/_outcome.py,sha256=
|
43
|
-
dbos/_queue.py,sha256=
|
44
|
-
dbos/_recovery.py,sha256=
|
42
|
+
dbos/_outcome.py,sha256=Kz3aL7517q9UEFTx3Cq9zzztjWyWVOx_08fZyHo9dvg,7035
|
43
|
+
dbos/_queue.py,sha256=Kq7aldTDLRF7cZtkXmsCy6wV2PR24enkhghEG25NtaU,4080
|
44
|
+
dbos/_recovery.py,sha256=TBNjkmSEqBU-g5YXExsLJ9XoCe4iekqtREsskXZECEg,2507
|
45
45
|
dbos/_registrations.py,sha256=CZt1ElqDjCT7hz6iyT-1av76Yu-iuwu_c9lozO87wvM,7303
|
46
46
|
dbos/_roles.py,sha256=iOsgmIAf1XVzxs3gYWdGRe1B880YfOw5fpU7Jwx8_A8,2271
|
47
47
|
dbos/_scheduler.py,sha256=SR1oRZRcVzYsj-JauV2LA8JtwTkt8mru7qf6H1AzQ1U,2027
|
@@ -49,7 +49,7 @@ dbos/_schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
49
49
|
dbos/_schemas/application_database.py,sha256=SypAS9l9EsaBHFn9FR8jmnqt01M74d9AF1AMa4m2hhI,1040
|
50
50
|
dbos/_schemas/system_database.py,sha256=rbFKggONdvvbb45InvGz0TM6a7c-Ux9dcaL-h_7Z7pU,4438
|
51
51
|
dbos/_serialization.py,sha256=bWuwhXSQcGmiazvhJHA5gwhrRWxtmFmcCFQSDJnqqkU,3666
|
52
|
-
dbos/_sys_db.py,sha256=
|
52
|
+
dbos/_sys_db.py,sha256=now889o6Mlmcdopp8xF5_0LAE67KeVH9Vm-4svIqo5s,80170
|
53
53
|
dbos/_templates/dbos-db-starter/README.md,sha256=GhxhBj42wjTt1fWEtwNriHbJuKb66Vzu89G4pxNHw2g,930
|
54
54
|
dbos/_templates/dbos-db-starter/__package/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
55
55
|
dbos/_templates/dbos-db-starter/__package/main.py.dbos,sha256=aQnBPSSQpkB8ERfhf7gB7P9tsU6OPKhZscfeh0yiaD8,2702
|
@@ -60,13 +60,13 @@ dbos/_templates/dbos-db-starter/migrations/env.py.dbos,sha256=IBB_gz9RjC20HPfOTG
|
|
60
60
|
dbos/_templates/dbos-db-starter/migrations/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
|
61
61
|
dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py,sha256=MpS7LGaJS0CpvsjhfDkp9EJqvMvVCjRPfUp4c0aE2ys,941
|
62
62
|
dbos/_templates/dbos-db-starter/start_postgres_docker.py,sha256=lQVLlYO5YkhGPEgPqwGc7Y8uDKse9HsWv5fynJEFJHM,1681
|
63
|
-
dbos/_tracer.py,sha256=
|
63
|
+
dbos/_tracer.py,sha256=RnlcaOJEx_58hr2J9L9g6E7gjAHAeEtEGugJZmCwNfQ,2963
|
64
64
|
dbos/_utils.py,sha256=uywq1QrjMwy17btjxW4bES49povlQwYwYbvKwMT6C2U,1575
|
65
|
-
dbos/_workflow_commands.py,sha256=
|
65
|
+
dbos/_workflow_commands.py,sha256=Fi-sQxQvFkDkMlCv7EyRJIWxqk3fG6DGlgvvwkjWbS4,4485
|
66
66
|
dbos/cli/_github_init.py,sha256=Y_bDF9gfO2jB1id4FV5h1oIxEJRWyqVjhb7bNEa5nQ0,3224
|
67
67
|
dbos/cli/_template_init.py,sha256=7JBcpMqP1r2mfCnvWatu33z8ctEGHJarlZYKgB83cXE,2972
|
68
|
-
dbos/cli/cli.py,sha256=
|
68
|
+
dbos/cli/cli.py,sha256=IcfaX4rrSrk6f24S2jrlR33snYMyNyEIx_lNQtuVr2E,22081
|
69
69
|
dbos/dbos-config.schema.json,sha256=CjaspeYmOkx6Ip_pcxtmfXJTn_YGdSx_0pcPBF7KZmo,6060
|
70
70
|
dbos/py.typed,sha256=QfzXT1Ktfk3Rj84akygc7_42z0lRpCq0Ilh8OXI6Zas,44
|
71
71
|
version/__init__.py,sha256=L4sNxecRuqdtSFdpUGX3TtBi9KL3k7YsZVIvv-fv9-A,1678
|
72
|
-
dbos-1.
|
72
|
+
dbos-1.5.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|