dbos 0.21.0a3__tar.gz → 0.21.0a5__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.
Potentially problematic release.
This version of dbos might be problematic. Click here for more details.
- {dbos-0.21.0a3 → dbos-0.21.0a5}/PKG-INFO +1 -1
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_dbos.py +4 -5
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_dbos_config.py +17 -13
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_sys_db.py +97 -8
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_workflow_commands.py +51 -39
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/cli/cli.py +87 -19
- {dbos-0.21.0a3 → dbos-0.21.0a5}/pyproject.toml +1 -1
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/test_admin_server.py +38 -33
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/test_failures.py +21 -40
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/test_package.py +46 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/test_queue.py +188 -0
- dbos-0.21.0a5/tests/test_workflow_cmds.py +289 -0
- dbos-0.21.0a3/tests/test_workflow_cmds.py +0 -216
- {dbos-0.21.0a3 → dbos-0.21.0a5}/LICENSE +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/README.md +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/__init__.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_admin_server.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_app_db.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_classproperty.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_cloudutils/authentication.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_cloudutils/cloudutils.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_cloudutils/databases.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_context.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_core.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_croniter.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_db_wizard.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_error.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_fastapi.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_flask.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_kafka.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_kafka_message.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_logger.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_migrations/env.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_migrations/script.py.mako +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_outcome.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_queue.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_recovery.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_registrations.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_request.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_roles.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_scheduler.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_schemas/__init__.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_schemas/application_database.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_schemas/system_database.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_serialization.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/README.md +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/__package/main.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/_tracer.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/cli/_github_init.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/cli/_template_init.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/dbos-config.schema.json +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/dbos/py.typed +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/__init__.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/atexit_no_ctor.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/atexit_no_launch.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/classdefs.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/conftest.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/more_classdefs.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/queuedworkflow.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/test_async.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/test_classdecorators.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/test_concurrency.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/test_config.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/test_croniter.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/test_dbos.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/test_fastapi.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/test_fastapi_roles.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/test_flask.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/test_kafka.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/test_outcome.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/test_scheduler.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/test_schema_migration.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/test_singleton.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/test_spans.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/tests/test_sqlalchemy.py +0 -0
- {dbos-0.21.0a3 → dbos-0.21.0a5}/version/__init__.py +0 -0
|
@@ -800,14 +800,13 @@ class DBOS:
|
|
|
800
800
|
@classmethod
|
|
801
801
|
def cancel_workflow(cls, workflow_id: str) -> None:
|
|
802
802
|
"""Cancel a workflow by ID."""
|
|
803
|
-
_get_dbos_instance()._sys_db.
|
|
804
|
-
workflow_id, WorkflowStatusString.CANCELLED
|
|
805
|
-
)
|
|
803
|
+
_get_dbos_instance()._sys_db.cancel_workflow(workflow_id)
|
|
806
804
|
|
|
807
805
|
@classmethod
|
|
808
|
-
def resume_workflow(cls, workflow_id: str) ->
|
|
806
|
+
def resume_workflow(cls, workflow_id: str) -> WorkflowHandle[Any]:
|
|
809
807
|
"""Resume a workflow by ID."""
|
|
810
|
-
|
|
808
|
+
_get_dbos_instance()._sys_db.resume_workflow(workflow_id)
|
|
809
|
+
return execute_workflow_by_id(_get_dbos_instance(), workflow_id, False)
|
|
811
810
|
|
|
812
811
|
@classproperty
|
|
813
812
|
def logger(cls) -> Logger:
|
|
@@ -123,7 +123,10 @@ def get_dbos_database_url(config_file_path: str = DBOS_CONFIG_PATH) -> str:
|
|
|
123
123
|
|
|
124
124
|
|
|
125
125
|
def load_config(
|
|
126
|
-
config_file_path: str = DBOS_CONFIG_PATH,
|
|
126
|
+
config_file_path: str = DBOS_CONFIG_PATH,
|
|
127
|
+
*,
|
|
128
|
+
use_db_wizard: bool = True,
|
|
129
|
+
silent: bool = False,
|
|
127
130
|
) -> ConfigFile:
|
|
128
131
|
"""
|
|
129
132
|
Load the DBOS `ConfigFile` from the specified path (typically `dbos-config.yaml`).
|
|
@@ -188,18 +191,19 @@ def load_config(
|
|
|
188
191
|
# Load the DB connection file. Use its values for missing fields from dbos-config.yaml. Use defaults otherwise.
|
|
189
192
|
data = cast(ConfigFile, data)
|
|
190
193
|
db_connection = load_db_connection()
|
|
191
|
-
if
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
194
|
+
if not silent:
|
|
195
|
+
if data["database"].get("hostname"):
|
|
196
|
+
print(
|
|
197
|
+
"[bold blue]Loading database connection parameters from dbos-config.yaml[/bold blue]"
|
|
198
|
+
)
|
|
199
|
+
elif db_connection.get("hostname"):
|
|
200
|
+
print(
|
|
201
|
+
"[bold blue]Loading database connection parameters from .dbos/db_connection[/bold blue]"
|
|
202
|
+
)
|
|
203
|
+
else:
|
|
204
|
+
print(
|
|
205
|
+
"[bold blue]Using default database connection parameters (localhost)[/bold blue]"
|
|
206
|
+
)
|
|
203
207
|
|
|
204
208
|
data["database"]["hostname"] = (
|
|
205
209
|
data["database"].get("hostname") or db_connection.get("hostname") or "localhost"
|
|
@@ -126,6 +126,15 @@ class GetWorkflowsInput:
|
|
|
126
126
|
)
|
|
127
127
|
|
|
128
128
|
|
|
129
|
+
class GetQueuedWorkflowsInput(TypedDict):
|
|
130
|
+
queue_name: Optional[str]
|
|
131
|
+
status: Optional[str]
|
|
132
|
+
start_time: Optional[str] # Timestamp in ISO 8601 format
|
|
133
|
+
end_time: Optional[str] # Timestamp in ISO 8601 format
|
|
134
|
+
limit: Optional[int] # Return up to this many workflows IDs.
|
|
135
|
+
name: Optional[str] # The name of the workflow function
|
|
136
|
+
|
|
137
|
+
|
|
129
138
|
class GetWorkflowsOutput:
|
|
130
139
|
def __init__(self, workflow_uuids: List[str]):
|
|
131
140
|
self.workflow_uuids = workflow_uuids
|
|
@@ -390,20 +399,55 @@ class SystemDatabase:
|
|
|
390
399
|
if status["workflow_uuid"] in self._temp_txn_wf_ids:
|
|
391
400
|
self._exported_temp_txn_wf_status.add(status["workflow_uuid"])
|
|
392
401
|
|
|
393
|
-
def
|
|
402
|
+
def cancel_workflow(
|
|
394
403
|
self,
|
|
395
|
-
|
|
396
|
-
status: WorkflowStatusString,
|
|
404
|
+
workflow_id: str,
|
|
397
405
|
) -> None:
|
|
398
406
|
with self.engine.begin() as c:
|
|
399
|
-
|
|
407
|
+
# Remove the workflow from the queues table so it does not block the table
|
|
408
|
+
c.execute(
|
|
409
|
+
sa.delete(SystemSchema.workflow_queue).where(
|
|
410
|
+
SystemSchema.workflow_queue.c.workflow_uuid == workflow_id
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
# Set the workflow's status to CANCELLED
|
|
414
|
+
c.execute(
|
|
400
415
|
sa.update(SystemSchema.workflow_status)
|
|
401
|
-
.where(SystemSchema.workflow_status.c.workflow_uuid ==
|
|
416
|
+
.where(SystemSchema.workflow_status.c.workflow_uuid == workflow_id)
|
|
402
417
|
.values(
|
|
403
|
-
status=
|
|
418
|
+
status=WorkflowStatusString.CANCELLED.value,
|
|
419
|
+
)
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
def resume_workflow(
|
|
423
|
+
self,
|
|
424
|
+
workflow_id: str,
|
|
425
|
+
) -> None:
|
|
426
|
+
with self.engine.begin() as c:
|
|
427
|
+
# Check the status of the workflow. If it is complete, do nothing.
|
|
428
|
+
row = c.execute(
|
|
429
|
+
sa.select(
|
|
430
|
+
SystemSchema.workflow_status.c.status,
|
|
431
|
+
).where(SystemSchema.workflow_status.c.workflow_uuid == workflow_id)
|
|
432
|
+
).fetchone()
|
|
433
|
+
if (
|
|
434
|
+
row is None
|
|
435
|
+
or row[0] == WorkflowStatusString.SUCCESS.value
|
|
436
|
+
or row[0] == WorkflowStatusString.ERROR.value
|
|
437
|
+
):
|
|
438
|
+
return
|
|
439
|
+
# Remove the workflow from the queues table so resume can safely be called on an ENQUEUED workflow
|
|
440
|
+
c.execute(
|
|
441
|
+
sa.delete(SystemSchema.workflow_queue).where(
|
|
442
|
+
SystemSchema.workflow_queue.c.workflow_uuid == workflow_id
|
|
404
443
|
)
|
|
405
444
|
)
|
|
406
|
-
|
|
445
|
+
# Set the workflow's status to PENDING and clear its recovery attempts.
|
|
446
|
+
c.execute(
|
|
447
|
+
sa.update(SystemSchema.workflow_status)
|
|
448
|
+
.where(SystemSchema.workflow_status.c.workflow_uuid == workflow_id)
|
|
449
|
+
.values(status=WorkflowStatusString.PENDING.value, recovery_attempts=0)
|
|
450
|
+
)
|
|
407
451
|
|
|
408
452
|
def get_workflow_status(
|
|
409
453
|
self, workflow_uuid: str
|
|
@@ -623,7 +667,6 @@ class SystemDatabase:
|
|
|
623
667
|
query = sa.select(SystemSchema.workflow_status.c.workflow_uuid).order_by(
|
|
624
668
|
SystemSchema.workflow_status.c.created_at.desc()
|
|
625
669
|
)
|
|
626
|
-
|
|
627
670
|
if input.name:
|
|
628
671
|
query = query.where(SystemSchema.workflow_status.c.name == input.name)
|
|
629
672
|
if input.authenticated_user:
|
|
@@ -657,6 +700,52 @@ class SystemDatabase:
|
|
|
657
700
|
|
|
658
701
|
return GetWorkflowsOutput(workflow_uuids)
|
|
659
702
|
|
|
703
|
+
def get_queued_workflows(
|
|
704
|
+
self, input: GetQueuedWorkflowsInput
|
|
705
|
+
) -> GetWorkflowsOutput:
|
|
706
|
+
|
|
707
|
+
query = (
|
|
708
|
+
sa.select(SystemSchema.workflow_queue.c.workflow_uuid)
|
|
709
|
+
.join(
|
|
710
|
+
SystemSchema.workflow_status,
|
|
711
|
+
SystemSchema.workflow_queue.c.workflow_uuid
|
|
712
|
+
== SystemSchema.workflow_status.c.workflow_uuid,
|
|
713
|
+
)
|
|
714
|
+
.order_by(SystemSchema.workflow_status.c.created_at.desc())
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
if input.get("name"):
|
|
718
|
+
query = query.where(SystemSchema.workflow_status.c.name == input["name"])
|
|
719
|
+
|
|
720
|
+
if input.get("queue_name"):
|
|
721
|
+
query = query.where(
|
|
722
|
+
SystemSchema.workflow_queue.c.queue_name == input["queue_name"]
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
if input.get("status"):
|
|
726
|
+
query = query.where(
|
|
727
|
+
SystemSchema.workflow_status.c.status == input["status"]
|
|
728
|
+
)
|
|
729
|
+
if "start_time" in input and input["start_time"] is not None:
|
|
730
|
+
query = query.where(
|
|
731
|
+
SystemSchema.workflow_status.c.created_at
|
|
732
|
+
>= datetime.datetime.fromisoformat(input["start_time"]).timestamp()
|
|
733
|
+
* 1000
|
|
734
|
+
)
|
|
735
|
+
if "end_time" in input and input["end_time"] is not None:
|
|
736
|
+
query = query.where(
|
|
737
|
+
SystemSchema.workflow_status.c.created_at
|
|
738
|
+
<= datetime.datetime.fromisoformat(input["end_time"]).timestamp() * 1000
|
|
739
|
+
)
|
|
740
|
+
if input.get("limit"):
|
|
741
|
+
query = query.limit(input["limit"])
|
|
742
|
+
|
|
743
|
+
with self.engine.begin() as c:
|
|
744
|
+
rows = c.execute(query)
|
|
745
|
+
workflow_uuids = [row[0] for row in rows]
|
|
746
|
+
|
|
747
|
+
return GetWorkflowsOutput(workflow_uuids)
|
|
748
|
+
|
|
660
749
|
def get_pending_workflows(self, executor_id: str) -> list[str]:
|
|
661
750
|
with self.engine.begin() as c:
|
|
662
751
|
rows = c.execute(
|
|
@@ -1,23 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
import os
|
|
3
|
-
import sys
|
|
4
|
-
from typing import Any, List, Optional, cast
|
|
1
|
+
from typing import List, Optional, cast
|
|
5
2
|
|
|
6
3
|
import typer
|
|
7
|
-
from rich import print
|
|
8
4
|
|
|
9
|
-
from
|
|
10
|
-
|
|
11
|
-
from . import _serialization, load_config
|
|
12
|
-
from ._core import execute_workflow_by_id
|
|
13
|
-
from ._dbos_config import ConfigFile, _is_valid_app_name
|
|
5
|
+
from . import _serialization
|
|
6
|
+
from ._dbos_config import ConfigFile
|
|
14
7
|
from ._sys_db import (
|
|
8
|
+
GetQueuedWorkflowsInput,
|
|
15
9
|
GetWorkflowsInput,
|
|
16
10
|
GetWorkflowsOutput,
|
|
17
11
|
SystemDatabase,
|
|
18
12
|
WorkflowStatuses,
|
|
19
|
-
WorkflowStatusInternal,
|
|
20
|
-
WorkflowStatusString,
|
|
21
13
|
)
|
|
22
14
|
|
|
23
15
|
|
|
@@ -28,8 +20,8 @@ class WorkflowInformation:
|
|
|
28
20
|
workflowClassName: Optional[str]
|
|
29
21
|
workflowConfigName: Optional[str]
|
|
30
22
|
input: Optional[_serialization.WorkflowInputs] # JSON (jsonpickle)
|
|
31
|
-
output: Optional[str] # JSON (jsonpickle)
|
|
32
|
-
error: Optional[str] # JSON (jsonpickle)
|
|
23
|
+
output: Optional[str] = None # JSON (jsonpickle)
|
|
24
|
+
error: Optional[str] = None # JSON (jsonpickle)
|
|
33
25
|
executor_id: Optional[str]
|
|
34
26
|
app_version: Optional[str]
|
|
35
27
|
app_id: Optional[str]
|
|
@@ -41,19 +33,17 @@ class WorkflowInformation:
|
|
|
41
33
|
queue_name: Optional[str]
|
|
42
34
|
|
|
43
35
|
|
|
44
|
-
def
|
|
36
|
+
def list_workflows(
|
|
45
37
|
config: ConfigFile,
|
|
46
|
-
|
|
38
|
+
limit: int,
|
|
47
39
|
user: Optional[str],
|
|
48
40
|
starttime: Optional[str],
|
|
49
41
|
endtime: Optional[str],
|
|
50
42
|
status: Optional[str],
|
|
51
43
|
request: bool,
|
|
52
44
|
appversion: Optional[str],
|
|
45
|
+
name: Optional[str],
|
|
53
46
|
) -> List[WorkflowInformation]:
|
|
54
|
-
|
|
55
|
-
sys_db = None
|
|
56
|
-
|
|
57
47
|
try:
|
|
58
48
|
sys_db = SystemDatabase(config)
|
|
59
49
|
|
|
@@ -64,24 +54,55 @@ def _list_workflows(
|
|
|
64
54
|
if status is not None:
|
|
65
55
|
input.status = cast(WorkflowStatuses, status)
|
|
66
56
|
input.application_version = appversion
|
|
67
|
-
input.limit =
|
|
57
|
+
input.limit = limit
|
|
58
|
+
input.name = name
|
|
68
59
|
|
|
69
60
|
output: GetWorkflowsOutput = sys_db.get_workflows(input)
|
|
70
|
-
|
|
71
61
|
infos: List[WorkflowInformation] = []
|
|
62
|
+
for workflow_id in output.workflow_uuids:
|
|
63
|
+
info = _get_workflow_info(
|
|
64
|
+
sys_db, workflow_id, request
|
|
65
|
+
) # Call the method for each ID
|
|
66
|
+
if info is not None:
|
|
67
|
+
infos.append(info)
|
|
68
|
+
|
|
69
|
+
return infos
|
|
70
|
+
except Exception as e:
|
|
71
|
+
typer.echo(f"Error listing workflows: {e}")
|
|
72
|
+
return []
|
|
73
|
+
finally:
|
|
74
|
+
if sys_db:
|
|
75
|
+
sys_db.destroy()
|
|
72
76
|
|
|
73
|
-
if output.workflow_uuids is None:
|
|
74
|
-
typer.echo("No workflows found")
|
|
75
|
-
return {}
|
|
76
77
|
|
|
78
|
+
def list_queued_workflows(
|
|
79
|
+
config: ConfigFile,
|
|
80
|
+
limit: Optional[int] = None,
|
|
81
|
+
start_time: Optional[str] = None,
|
|
82
|
+
end_time: Optional[str] = None,
|
|
83
|
+
queue_name: Optional[str] = None,
|
|
84
|
+
status: Optional[str] = None,
|
|
85
|
+
name: Optional[str] = None,
|
|
86
|
+
request: bool = False,
|
|
87
|
+
) -> List[WorkflowInformation]:
|
|
88
|
+
try:
|
|
89
|
+
sys_db = SystemDatabase(config)
|
|
90
|
+
input: GetQueuedWorkflowsInput = {
|
|
91
|
+
"queue_name": queue_name,
|
|
92
|
+
"start_time": start_time,
|
|
93
|
+
"end_time": end_time,
|
|
94
|
+
"status": status,
|
|
95
|
+
"limit": limit,
|
|
96
|
+
"name": name,
|
|
97
|
+
}
|
|
98
|
+
output: GetWorkflowsOutput = sys_db.get_queued_workflows(input)
|
|
99
|
+
infos: List[WorkflowInformation] = []
|
|
77
100
|
for workflow_id in output.workflow_uuids:
|
|
78
101
|
info = _get_workflow_info(
|
|
79
102
|
sys_db, workflow_id, request
|
|
80
103
|
) # Call the method for each ID
|
|
81
|
-
|
|
82
104
|
if info is not None:
|
|
83
105
|
infos.append(info)
|
|
84
|
-
|
|
85
106
|
return infos
|
|
86
107
|
except Exception as e:
|
|
87
108
|
typer.echo(f"Error listing workflows: {e}")
|
|
@@ -91,17 +112,13 @@ def _list_workflows(
|
|
|
91
112
|
sys_db.destroy()
|
|
92
113
|
|
|
93
114
|
|
|
94
|
-
def
|
|
115
|
+
def get_workflow(
|
|
95
116
|
config: ConfigFile, uuid: str, request: bool
|
|
96
117
|
) -> Optional[WorkflowInformation]:
|
|
97
|
-
sys_db = None
|
|
98
|
-
|
|
99
118
|
try:
|
|
100
119
|
sys_db = SystemDatabase(config)
|
|
101
|
-
|
|
102
120
|
info = _get_workflow_info(sys_db, uuid, request)
|
|
103
121
|
return info
|
|
104
|
-
|
|
105
122
|
except Exception as e:
|
|
106
123
|
typer.echo(f"Error getting workflow: {e}")
|
|
107
124
|
return None
|
|
@@ -110,18 +127,13 @@ def _get_workflow(
|
|
|
110
127
|
sys_db.destroy()
|
|
111
128
|
|
|
112
129
|
|
|
113
|
-
def
|
|
114
|
-
# config = load_config()
|
|
115
|
-
sys_db = None
|
|
116
|
-
|
|
130
|
+
def cancel_workflow(config: ConfigFile, uuid: str) -> None:
|
|
117
131
|
try:
|
|
118
132
|
sys_db = SystemDatabase(config)
|
|
119
|
-
sys_db.
|
|
120
|
-
return
|
|
121
|
-
|
|
133
|
+
sys_db.cancel_workflow(uuid)
|
|
122
134
|
except Exception as e:
|
|
123
135
|
typer.echo(f"Failed to connect to DBOS system database: {e}")
|
|
124
|
-
|
|
136
|
+
raise e
|
|
125
137
|
finally:
|
|
126
138
|
if sys_db:
|
|
127
139
|
sys_db.destroy()
|
|
@@ -19,14 +19,21 @@ from .. import load_config
|
|
|
19
19
|
from .._app_db import ApplicationDatabase
|
|
20
20
|
from .._dbos_config import _is_valid_app_name
|
|
21
21
|
from .._sys_db import SystemDatabase, reset_system_database
|
|
22
|
-
from .._workflow_commands import
|
|
22
|
+
from .._workflow_commands import (
|
|
23
|
+
cancel_workflow,
|
|
24
|
+
get_workflow,
|
|
25
|
+
list_queued_workflows,
|
|
26
|
+
list_workflows,
|
|
27
|
+
)
|
|
23
28
|
from ..cli._github_init import create_template_from_github
|
|
24
29
|
from ._template_init import copy_template, get_project_name, get_templates_directory
|
|
25
30
|
|
|
26
31
|
app = typer.Typer()
|
|
27
32
|
workflow = typer.Typer()
|
|
33
|
+
queue = typer.Typer()
|
|
28
34
|
|
|
29
35
|
app.add_typer(workflow, name="workflow", help="Manage DBOS workflows")
|
|
36
|
+
workflow.add_typer(queue, name="queue", help="Manage enqueued workflows")
|
|
30
37
|
|
|
31
38
|
|
|
32
39
|
def _on_windows() -> bool:
|
|
@@ -272,18 +279,22 @@ def list(
|
|
|
272
279
|
help="Retrieve workflows with this application version",
|
|
273
280
|
),
|
|
274
281
|
] = None,
|
|
282
|
+
name: Annotated[
|
|
283
|
+
typing.Optional[str],
|
|
284
|
+
typer.Option(
|
|
285
|
+
"--name",
|
|
286
|
+
"-n",
|
|
287
|
+
help="Retrieve workflows with this name",
|
|
288
|
+
),
|
|
289
|
+
] = None,
|
|
275
290
|
request: Annotated[
|
|
276
291
|
bool,
|
|
277
292
|
typer.Option("--request", help="Retrieve workflow request information"),
|
|
278
293
|
] = True,
|
|
279
|
-
appdir: Annotated[
|
|
280
|
-
typing.Optional[str],
|
|
281
|
-
typer.Option("--app-dir", "-d", help="Specify the application root directory"),
|
|
282
|
-
] = None,
|
|
283
294
|
) -> None:
|
|
284
|
-
config = load_config()
|
|
285
|
-
workflows =
|
|
286
|
-
config, limit, user, starttime, endtime, status, request, appversion
|
|
295
|
+
config = load_config(silent=True)
|
|
296
|
+
workflows = list_workflows(
|
|
297
|
+
config, limit, user, starttime, endtime, status, request, appversion, name
|
|
287
298
|
)
|
|
288
299
|
print(jsonpickle.encode(workflows, unpicklable=False))
|
|
289
300
|
|
|
@@ -291,17 +302,13 @@ def list(
|
|
|
291
302
|
@workflow.command(help="Retrieve the status of a workflow")
|
|
292
303
|
def get(
|
|
293
304
|
uuid: Annotated[str, typer.Argument()],
|
|
294
|
-
appdir: Annotated[
|
|
295
|
-
typing.Optional[str],
|
|
296
|
-
typer.Option("--app-dir", "-d", help="Specify the application root directory"),
|
|
297
|
-
] = None,
|
|
298
305
|
request: Annotated[
|
|
299
306
|
bool,
|
|
300
307
|
typer.Option("--request", help="Retrieve workflow request information"),
|
|
301
308
|
] = True,
|
|
302
309
|
) -> None:
|
|
303
|
-
config = load_config()
|
|
304
|
-
print(jsonpickle.encode(
|
|
310
|
+
config = load_config(silent=True)
|
|
311
|
+
print(jsonpickle.encode(get_workflow(config, uuid, request), unpicklable=False))
|
|
305
312
|
|
|
306
313
|
|
|
307
314
|
@workflow.command(
|
|
@@ -309,13 +316,9 @@ def get(
|
|
|
309
316
|
)
|
|
310
317
|
def cancel(
|
|
311
318
|
uuid: Annotated[str, typer.Argument()],
|
|
312
|
-
appdir: Annotated[
|
|
313
|
-
typing.Optional[str],
|
|
314
|
-
typer.Option("--app-dir", "-d", help="Specify the application root directory"),
|
|
315
|
-
] = None,
|
|
316
319
|
) -> None:
|
|
317
320
|
config = load_config()
|
|
318
|
-
|
|
321
|
+
cancel_workflow(config, uuid)
|
|
319
322
|
print(f"Workflow {uuid} has been cancelled")
|
|
320
323
|
|
|
321
324
|
|
|
@@ -363,5 +366,70 @@ def restart(
|
|
|
363
366
|
print(f"Failed to resume workflow {uuid}. Status code: {response.status_code}")
|
|
364
367
|
|
|
365
368
|
|
|
369
|
+
@queue.command(name="list", help="List enqueued functions for your application")
|
|
370
|
+
def list_queue(
|
|
371
|
+
limit: Annotated[
|
|
372
|
+
typing.Optional[int],
|
|
373
|
+
typer.Option("--limit", "-l", help="Limit the results returned"),
|
|
374
|
+
] = None,
|
|
375
|
+
start_time: Annotated[
|
|
376
|
+
typing.Optional[str],
|
|
377
|
+
typer.Option(
|
|
378
|
+
"--start-time",
|
|
379
|
+
"-s",
|
|
380
|
+
help="Retrieve functions starting after this timestamp (ISO 8601 format)",
|
|
381
|
+
),
|
|
382
|
+
] = None,
|
|
383
|
+
end_time: Annotated[
|
|
384
|
+
typing.Optional[str],
|
|
385
|
+
typer.Option(
|
|
386
|
+
"--end-time",
|
|
387
|
+
"-e",
|
|
388
|
+
help="Retrieve functions starting before this timestamp (ISO 8601 format)",
|
|
389
|
+
),
|
|
390
|
+
] = None,
|
|
391
|
+
status: Annotated[
|
|
392
|
+
typing.Optional[str],
|
|
393
|
+
typer.Option(
|
|
394
|
+
"--status",
|
|
395
|
+
"-S",
|
|
396
|
+
help="Retrieve functions with this status (PENDING, SUCCESS, ERROR, RETRIES_EXCEEDED, ENQUEUED, or CANCELLED)",
|
|
397
|
+
),
|
|
398
|
+
] = None,
|
|
399
|
+
queue_name: Annotated[
|
|
400
|
+
typing.Optional[str],
|
|
401
|
+
typer.Option(
|
|
402
|
+
"--queue-name",
|
|
403
|
+
"-q",
|
|
404
|
+
help="Retrieve functions on this queue",
|
|
405
|
+
),
|
|
406
|
+
] = None,
|
|
407
|
+
name: Annotated[
|
|
408
|
+
typing.Optional[str],
|
|
409
|
+
typer.Option(
|
|
410
|
+
"--name",
|
|
411
|
+
"-n",
|
|
412
|
+
help="Retrieve functions on this queue",
|
|
413
|
+
),
|
|
414
|
+
] = None,
|
|
415
|
+
request: Annotated[
|
|
416
|
+
bool,
|
|
417
|
+
typer.Option("--request", help="Retrieve workflow request information"),
|
|
418
|
+
] = True,
|
|
419
|
+
) -> None:
|
|
420
|
+
config = load_config(silent=True)
|
|
421
|
+
workflows = list_queued_workflows(
|
|
422
|
+
config=config,
|
|
423
|
+
limit=limit,
|
|
424
|
+
start_time=start_time,
|
|
425
|
+
end_time=end_time,
|
|
426
|
+
queue_name=queue_name,
|
|
427
|
+
status=status,
|
|
428
|
+
request=request,
|
|
429
|
+
name=name,
|
|
430
|
+
)
|
|
431
|
+
print(jsonpickle.encode(workflows, unpicklable=False))
|
|
432
|
+
|
|
433
|
+
|
|
366
434
|
if __name__ == "__main__":
|
|
367
435
|
app()
|
|
@@ -151,54 +151,59 @@ runtimeConfig:
|
|
|
151
151
|
|
|
152
152
|
|
|
153
153
|
def test_admin_workflow_resume(dbos: DBOS, config: ConfigFile) -> None:
|
|
154
|
+
counter: int = 0
|
|
154
155
|
|
|
155
156
|
@DBOS.workflow()
|
|
156
157
|
def simple_workflow() -> None:
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
nonlocal counter
|
|
159
|
+
counter += 1
|
|
159
160
|
|
|
160
|
-
#
|
|
161
|
+
# Run the workflow and flush its results
|
|
161
162
|
simple_workflow()
|
|
162
|
-
|
|
163
|
+
assert counter == 1
|
|
164
|
+
dbos._sys_db.wait_for_buffer_flush()
|
|
163
165
|
|
|
164
|
-
#
|
|
165
|
-
output = _workflow_commands.
|
|
166
|
-
config, 10, None, None, None, None, False, None
|
|
166
|
+
# Verify the workflow has succeeded
|
|
167
|
+
output = _workflow_commands.list_workflows(
|
|
168
|
+
config, 10, None, None, None, None, False, None, None
|
|
167
169
|
)
|
|
168
170
|
assert len(output) == 1, f"Expected list length to be 1, but got {len(output)}"
|
|
169
|
-
|
|
170
171
|
assert output[0] != None, "Expected output to be not None"
|
|
171
|
-
|
|
172
172
|
wfUuid = output[0].workflowUUID
|
|
173
|
-
|
|
174
|
-
info = _workflow_commands._get_workflow(config, wfUuid, True)
|
|
173
|
+
info = _workflow_commands.get_workflow(config, wfUuid, True)
|
|
175
174
|
assert info is not None, "Expected output to be not None"
|
|
176
|
-
|
|
177
175
|
assert info.status == "SUCCESS", f"Expected status to be SUCCESS"
|
|
178
176
|
|
|
177
|
+
# Cancel the workflow. Verify it was cancelled
|
|
179
178
|
response = requests.post(
|
|
180
179
|
f"http://localhost:3001/workflows/{wfUuid}/cancel", json=[], timeout=5
|
|
181
180
|
)
|
|
182
181
|
assert response.status_code == 204
|
|
182
|
+
info = _workflow_commands.get_workflow(config, wfUuid, True)
|
|
183
|
+
assert info is not None
|
|
184
|
+
assert info.status == "CANCELLED", f"Expected status to be CANCELLED"
|
|
183
185
|
|
|
184
|
-
|
|
185
|
-
if info is not None:
|
|
186
|
-
assert info.status == "CANCELLED", f"Expected status to be CANCELLED"
|
|
187
|
-
else:
|
|
188
|
-
assert False, "Expected info to be not None"
|
|
189
|
-
|
|
186
|
+
# Resume the workflow. Verify that it succeeds again.
|
|
190
187
|
response = requests.post(
|
|
191
188
|
f"http://localhost:3001/workflows/{wfUuid}/resume", json=[], timeout=5
|
|
192
189
|
)
|
|
193
190
|
assert response.status_code == 204
|
|
191
|
+
dbos._sys_db.wait_for_buffer_flush()
|
|
192
|
+
assert counter == 2
|
|
193
|
+
info = _workflow_commands.get_workflow(config, wfUuid, True)
|
|
194
|
+
assert info is not None
|
|
195
|
+
assert info.status == "SUCCESS", f"Expected status to be SUCCESS"
|
|
194
196
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
197
|
+
# Resume the workflow. Verify it does not run and status remains SUCCESS
|
|
198
|
+
response = requests.post(
|
|
199
|
+
f"http://localhost:3001/workflows/{wfUuid}/resume", json=[], timeout=5
|
|
200
|
+
)
|
|
201
|
+
assert response.status_code == 204
|
|
202
|
+
dbos._sys_db.wait_for_buffer_flush()
|
|
203
|
+
info = _workflow_commands.get_workflow(config, wfUuid, True)
|
|
204
|
+
assert info is not None
|
|
205
|
+
assert info.status == "SUCCESS", f"Expected status to be SUCCESS"
|
|
206
|
+
assert counter == 2
|
|
202
207
|
|
|
203
208
|
|
|
204
209
|
def test_admin_workflow_restart(dbos: DBOS, config: ConfigFile) -> None:
|
|
@@ -213,8 +218,8 @@ def test_admin_workflow_restart(dbos: DBOS, config: ConfigFile) -> None:
|
|
|
213
218
|
time.sleep(1)
|
|
214
219
|
|
|
215
220
|
# get the workflow list
|
|
216
|
-
output = _workflow_commands.
|
|
217
|
-
config, 10, None, None, None, None, False, None
|
|
221
|
+
output = _workflow_commands.list_workflows(
|
|
222
|
+
config, 10, None, None, None, None, False, None, None
|
|
218
223
|
)
|
|
219
224
|
assert len(output) == 1, f"Expected list length to be 1, but got {len(output)}"
|
|
220
225
|
|
|
@@ -222,7 +227,7 @@ def test_admin_workflow_restart(dbos: DBOS, config: ConfigFile) -> None:
|
|
|
222
227
|
|
|
223
228
|
wfUuid = output[0].workflowUUID
|
|
224
229
|
|
|
225
|
-
info = _workflow_commands.
|
|
230
|
+
info = _workflow_commands.get_workflow(config, wfUuid, True)
|
|
226
231
|
assert info is not None, "Expected output to be not None"
|
|
227
232
|
|
|
228
233
|
assert info.status == "SUCCESS", f"Expected status to be SUCCESS"
|
|
@@ -232,7 +237,7 @@ def test_admin_workflow_restart(dbos: DBOS, config: ConfigFile) -> None:
|
|
|
232
237
|
)
|
|
233
238
|
assert response.status_code == 204
|
|
234
239
|
|
|
235
|
-
info = _workflow_commands.
|
|
240
|
+
info = _workflow_commands.get_workflow(config, wfUuid, True)
|
|
236
241
|
if info is not None:
|
|
237
242
|
assert info.status == "CANCELLED", f"Expected status to be CANCELLED"
|
|
238
243
|
else:
|
|
@@ -245,14 +250,14 @@ def test_admin_workflow_restart(dbos: DBOS, config: ConfigFile) -> None:
|
|
|
245
250
|
|
|
246
251
|
time.sleep(1)
|
|
247
252
|
|
|
248
|
-
info = _workflow_commands.
|
|
253
|
+
info = _workflow_commands.get_workflow(config, wfUuid, True)
|
|
249
254
|
if info is not None:
|
|
250
255
|
assert info.status == "CANCELLED", f"Expected status to be CANCELLED"
|
|
251
256
|
else:
|
|
252
257
|
assert False, "Expected info to be not None"
|
|
253
258
|
|
|
254
|
-
output = _workflow_commands.
|
|
255
|
-
config, 10, None, None, None, None, False, None
|
|
259
|
+
output = _workflow_commands.list_workflows(
|
|
260
|
+
config, 10, None, None, None, None, False, None, None
|
|
256
261
|
)
|
|
257
262
|
assert len(output) == 2, f"Expected list length to be 2, but got {len(output)}"
|
|
258
263
|
|
|
@@ -261,7 +266,7 @@ def test_admin_workflow_restart(dbos: DBOS, config: ConfigFile) -> None:
|
|
|
261
266
|
else:
|
|
262
267
|
new_wfUuid = output[0].workflowUUID
|
|
263
268
|
|
|
264
|
-
info = _workflow_commands.
|
|
269
|
+
info = _workflow_commands.get_workflow(config, new_wfUuid, True)
|
|
265
270
|
if info is not None:
|
|
266
271
|
assert info.status == "SUCCESS", f"Expected status to be SUCCESS"
|
|
267
272
|
else:
|