dbos 0.21.0a4__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.0a4 → dbos-0.21.0a5}/PKG-INFO +1 -1
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_dbos_config.py +17 -13
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_sys_db.py +55 -1
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_workflow_commands.py +43 -13
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/cli/cli.py +84 -16
- {dbos-0.21.0a4 → dbos-0.21.0a5}/pyproject.toml +1 -1
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_admin_server.py +3 -3
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_package.py +46 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_workflow_cmds.py +100 -27
- {dbos-0.21.0a4 → dbos-0.21.0a5}/LICENSE +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/README.md +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/__init__.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_admin_server.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_app_db.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_classproperty.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_cloudutils/authentication.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_cloudutils/cloudutils.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_cloudutils/databases.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_context.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_core.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_croniter.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_db_wizard.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_dbos.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_error.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_fastapi.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_flask.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_kafka.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_kafka_message.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_logger.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_migrations/env.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_migrations/script.py.mako +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_outcome.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_queue.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_recovery.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_registrations.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_request.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_roles.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_scheduler.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_schemas/__init__.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_schemas/application_database.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_schemas/system_database.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_serialization.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/README.md +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/__package/main.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_tracer.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/cli/_github_init.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/cli/_template_init.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/dbos-config.schema.json +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/py.typed +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/__init__.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/atexit_no_ctor.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/atexit_no_launch.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/classdefs.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/conftest.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/more_classdefs.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/queuedworkflow.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_async.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_classdecorators.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_concurrency.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_config.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_croniter.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_dbos.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_failures.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_fastapi.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_fastapi_roles.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_flask.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_kafka.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_outcome.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_queue.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_scheduler.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_schema_migration.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_singleton.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_spans.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_sqlalchemy.py +0 -0
- {dbos-0.21.0a4 → dbos-0.21.0a5}/version/__init__.py +0 -0
|
@@ -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
|
|
@@ -658,7 +667,6 @@ class SystemDatabase:
|
|
|
658
667
|
query = sa.select(SystemSchema.workflow_status.c.workflow_uuid).order_by(
|
|
659
668
|
SystemSchema.workflow_status.c.created_at.desc()
|
|
660
669
|
)
|
|
661
|
-
|
|
662
670
|
if input.name:
|
|
663
671
|
query = query.where(SystemSchema.workflow_status.c.name == input.name)
|
|
664
672
|
if input.authenticated_user:
|
|
@@ -692,6 +700,52 @@ class SystemDatabase:
|
|
|
692
700
|
|
|
693
701
|
return GetWorkflowsOutput(workflow_uuids)
|
|
694
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
|
+
|
|
695
749
|
def get_pending_workflows(self, executor_id: str) -> list[str]:
|
|
696
750
|
with self.engine.begin() as c:
|
|
697
751
|
rows = c.execute(
|
|
@@ -5,6 +5,7 @@ import typer
|
|
|
5
5
|
from . import _serialization
|
|
6
6
|
from ._dbos_config import ConfigFile
|
|
7
7
|
from ._sys_db import (
|
|
8
|
+
GetQueuedWorkflowsInput,
|
|
8
9
|
GetWorkflowsInput,
|
|
9
10
|
GetWorkflowsOutput,
|
|
10
11
|
SystemDatabase,
|
|
@@ -19,8 +20,8 @@ class WorkflowInformation:
|
|
|
19
20
|
workflowClassName: Optional[str]
|
|
20
21
|
workflowConfigName: Optional[str]
|
|
21
22
|
input: Optional[_serialization.WorkflowInputs] # JSON (jsonpickle)
|
|
22
|
-
output: Optional[str] # JSON (jsonpickle)
|
|
23
|
-
error: Optional[str] # JSON (jsonpickle)
|
|
23
|
+
output: Optional[str] = None # JSON (jsonpickle)
|
|
24
|
+
error: Optional[str] = None # JSON (jsonpickle)
|
|
24
25
|
executor_id: Optional[str]
|
|
25
26
|
app_version: Optional[str]
|
|
26
27
|
app_id: Optional[str]
|
|
@@ -34,17 +35,15 @@ class WorkflowInformation:
|
|
|
34
35
|
|
|
35
36
|
def list_workflows(
|
|
36
37
|
config: ConfigFile,
|
|
37
|
-
|
|
38
|
+
limit: int,
|
|
38
39
|
user: Optional[str],
|
|
39
40
|
starttime: Optional[str],
|
|
40
41
|
endtime: Optional[str],
|
|
41
42
|
status: Optional[str],
|
|
42
43
|
request: bool,
|
|
43
44
|
appversion: Optional[str],
|
|
45
|
+
name: Optional[str],
|
|
44
46
|
) -> List[WorkflowInformation]:
|
|
45
|
-
|
|
46
|
-
sys_db = None
|
|
47
|
-
|
|
48
47
|
try:
|
|
49
48
|
sys_db = SystemDatabase(config)
|
|
50
49
|
|
|
@@ -55,24 +54,55 @@ def list_workflows(
|
|
|
55
54
|
if status is not None:
|
|
56
55
|
input.status = cast(WorkflowStatuses, status)
|
|
57
56
|
input.application_version = appversion
|
|
58
|
-
input.limit =
|
|
57
|
+
input.limit = limit
|
|
58
|
+
input.name = name
|
|
59
59
|
|
|
60
60
|
output: GetWorkflowsOutput = sys_db.get_workflows(input)
|
|
61
|
-
|
|
62
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()
|
|
63
76
|
|
|
64
|
-
if output.workflow_uuids is None:
|
|
65
|
-
typer.echo("No workflows found")
|
|
66
|
-
return {}
|
|
67
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] = []
|
|
68
100
|
for workflow_id in output.workflow_uuids:
|
|
69
101
|
info = _get_workflow_info(
|
|
70
102
|
sys_db, workflow_id, request
|
|
71
103
|
) # Call the method for each ID
|
|
72
|
-
|
|
73
104
|
if info is not None:
|
|
74
105
|
infos.append(info)
|
|
75
|
-
|
|
76
106
|
return infos
|
|
77
107
|
except Exception as e:
|
|
78
108
|
typer.echo(f"Error listing workflows: {e}")
|
|
@@ -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()
|
|
295
|
+
config = load_config(silent=True)
|
|
285
296
|
workflows = list_workflows(
|
|
286
|
-
config, limit, user, starttime, endtime, status, request, appversion
|
|
297
|
+
config, limit, user, starttime, endtime, status, request, appversion, name
|
|
287
298
|
)
|
|
288
299
|
print(jsonpickle.encode(workflows, unpicklable=False))
|
|
289
300
|
|
|
@@ -291,16 +302,12 @@ 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()
|
|
310
|
+
config = load_config(silent=True)
|
|
304
311
|
print(jsonpickle.encode(get_workflow(config, uuid, request), unpicklable=False))
|
|
305
312
|
|
|
306
313
|
|
|
@@ -309,10 +316,6 @@ 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)
|
|
@@ -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()
|
|
@@ -165,7 +165,7 @@ def test_admin_workflow_resume(dbos: DBOS, config: ConfigFile) -> None:
|
|
|
165
165
|
|
|
166
166
|
# Verify the workflow has succeeded
|
|
167
167
|
output = _workflow_commands.list_workflows(
|
|
168
|
-
config, 10, None, None, None, None, False, None
|
|
168
|
+
config, 10, None, None, None, None, False, None, None
|
|
169
169
|
)
|
|
170
170
|
assert len(output) == 1, f"Expected list length to be 1, but got {len(output)}"
|
|
171
171
|
assert output[0] != None, "Expected output to be not None"
|
|
@@ -219,7 +219,7 @@ def test_admin_workflow_restart(dbos: DBOS, config: ConfigFile) -> None:
|
|
|
219
219
|
|
|
220
220
|
# get the workflow list
|
|
221
221
|
output = _workflow_commands.list_workflows(
|
|
222
|
-
config, 10, None, None, None, None, False, None
|
|
222
|
+
config, 10, None, None, None, None, False, None, None
|
|
223
223
|
)
|
|
224
224
|
assert len(output) == 1, f"Expected list length to be 1, but got {len(output)}"
|
|
225
225
|
|
|
@@ -257,7 +257,7 @@ def test_admin_workflow_restart(dbos: DBOS, config: ConfigFile) -> None:
|
|
|
257
257
|
assert False, "Expected info to be not None"
|
|
258
258
|
|
|
259
259
|
output = _workflow_commands.list_workflows(
|
|
260
|
-
config, 10, None, None, None, None, False, None
|
|
260
|
+
config, 10, None, None, None, None, False, None, None
|
|
261
261
|
)
|
|
262
262
|
assert len(output) == 2, f"Expected list length to be 2, but got {len(output)}"
|
|
263
263
|
|
|
@@ -8,6 +8,7 @@ import time
|
|
|
8
8
|
import urllib.error
|
|
9
9
|
import urllib.request
|
|
10
10
|
|
|
11
|
+
import requests
|
|
11
12
|
import sqlalchemy as sa
|
|
12
13
|
import yaml
|
|
13
14
|
|
|
@@ -147,3 +148,48 @@ def test_reset(postgres_db_engine: sa.Engine) -> None:
|
|
|
147
148
|
)
|
|
148
149
|
).scalar()
|
|
149
150
|
assert result == 0
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_list_commands() -> None:
|
|
154
|
+
app_name = "reset-app"
|
|
155
|
+
with tempfile.TemporaryDirectory() as temp_path:
|
|
156
|
+
subprocess.check_call(
|
|
157
|
+
["dbos", "init", app_name, "--template", "dbos-toolbox"],
|
|
158
|
+
cwd=temp_path,
|
|
159
|
+
)
|
|
160
|
+
subprocess.check_call(["dbos", "reset", "-y"], cwd=temp_path)
|
|
161
|
+
subprocess.check_call(["dbos", "migrate"], cwd=temp_path)
|
|
162
|
+
|
|
163
|
+
# Get some workflows enqueued on the toolbox, then kill the toolbox
|
|
164
|
+
process = subprocess.Popen(["dbos", "start"], cwd=temp_path)
|
|
165
|
+
try:
|
|
166
|
+
session = requests.Session()
|
|
167
|
+
for i in range(10):
|
|
168
|
+
try:
|
|
169
|
+
session.get(
|
|
170
|
+
"http://localhost:8000/queue", timeout=1
|
|
171
|
+
).raise_for_status()
|
|
172
|
+
break
|
|
173
|
+
except requests.exceptions.Timeout:
|
|
174
|
+
break
|
|
175
|
+
except requests.exceptions.ConnectionError as e:
|
|
176
|
+
if i == 9:
|
|
177
|
+
raise
|
|
178
|
+
print(f"Attempt {i+1} failed: {e}. Retrying in 1 second...")
|
|
179
|
+
time.sleep(1)
|
|
180
|
+
time.sleep(1) # So the queued workflows can start
|
|
181
|
+
finally:
|
|
182
|
+
os.kill(process.pid, signal.SIGINT)
|
|
183
|
+
process.wait()
|
|
184
|
+
|
|
185
|
+
# Verify the output is valid JSON
|
|
186
|
+
output = subprocess.check_output(["dbos", "workflow", "list"], cwd=temp_path)
|
|
187
|
+
data = json.loads(output)
|
|
188
|
+
assert isinstance(data, list) and len(data) == 10
|
|
189
|
+
|
|
190
|
+
# Verify the output is valid JSON
|
|
191
|
+
output = subprocess.check_output(
|
|
192
|
+
["dbos", "workflow", "queue", "list"], cwd=temp_path
|
|
193
|
+
)
|
|
194
|
+
data = json.loads(output)
|
|
195
|
+
assert isinstance(data, list) and len(data) == 10
|
|
@@ -1,22 +1,9 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
import threading
|
|
3
2
|
import time
|
|
4
|
-
import
|
|
5
|
-
from datetime import datetime, timedelta
|
|
6
|
-
from typing import List, cast
|
|
7
|
-
|
|
8
|
-
import pytest
|
|
9
|
-
import sqlalchemy as sa
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
10
4
|
|
|
11
5
|
# Public API
|
|
12
|
-
from dbos import
|
|
13
|
-
DBOS,
|
|
14
|
-
ConfigFile,
|
|
15
|
-
SetWorkflowID,
|
|
16
|
-
WorkflowHandle,
|
|
17
|
-
WorkflowStatusString,
|
|
18
|
-
_workflow_commands,
|
|
19
|
-
)
|
|
6
|
+
from dbos import DBOS, ConfigFile, Queue, WorkflowStatusString, _workflow_commands
|
|
20
7
|
|
|
21
8
|
|
|
22
9
|
def test_list_workflow(dbos: DBOS, config: ConfigFile) -> None:
|
|
@@ -32,7 +19,7 @@ def test_list_workflow(dbos: DBOS, config: ConfigFile) -> None:
|
|
|
32
19
|
time.sleep(1) # wait for the workflow to complete
|
|
33
20
|
# get the workflow list
|
|
34
21
|
output = _workflow_commands.list_workflows(
|
|
35
|
-
config, 10, None, None, None, None, False, None
|
|
22
|
+
config, 10, None, None, None, None, False, None, None
|
|
36
23
|
)
|
|
37
24
|
assert len(output) == 1, f"Expected list length to be 1, but got {len(output)}"
|
|
38
25
|
assert output[0] != None, "Expected output to be not None"
|
|
@@ -55,12 +42,12 @@ def test_list_workflow_limit(dbos: DBOS, config: ConfigFile) -> None:
|
|
|
55
42
|
time.sleep(1) # wait for the workflow to complete
|
|
56
43
|
# get the workflow list
|
|
57
44
|
output = _workflow_commands.list_workflows(
|
|
58
|
-
config, 2, None, None, None, None, False, None
|
|
45
|
+
config, 2, None, None, None, None, False, None, None
|
|
59
46
|
)
|
|
60
47
|
assert len(output) == 2, f"Expected list length to be 1, but got {len(output)}"
|
|
61
48
|
|
|
62
49
|
|
|
63
|
-
def
|
|
50
|
+
def test_list_workflow_status_name(dbos: DBOS, config: ConfigFile) -> None:
|
|
64
51
|
print("Testing list_workflow")
|
|
65
52
|
|
|
66
53
|
@DBOS.workflow()
|
|
@@ -73,12 +60,22 @@ def test_list_workflow_status(dbos: DBOS, config: ConfigFile) -> None:
|
|
|
73
60
|
time.sleep(1) # wait for the workflow to complete
|
|
74
61
|
# get the workflow list
|
|
75
62
|
output = _workflow_commands.list_workflows(
|
|
76
|
-
config, 10, None, None, None, "PENDING", False, None
|
|
63
|
+
config, 10, None, None, None, "PENDING", False, None, None
|
|
64
|
+
)
|
|
65
|
+
assert len(output) == 0, f"Expected list length to be 0, but got {len(output)}"
|
|
66
|
+
|
|
67
|
+
output = _workflow_commands.list_workflows(
|
|
68
|
+
config, 10, None, None, None, "SUCCESS", False, None, None
|
|
69
|
+
)
|
|
70
|
+
assert len(output) == 1, f"Expected list length to be 1, but got {len(output)}"
|
|
71
|
+
|
|
72
|
+
output = _workflow_commands.list_workflows(
|
|
73
|
+
config, 10, None, None, None, None, False, None, "no"
|
|
77
74
|
)
|
|
78
75
|
assert len(output) == 0, f"Expected list length to be 0, but got {len(output)}"
|
|
79
76
|
|
|
80
77
|
output = _workflow_commands.list_workflows(
|
|
81
|
-
config, 10, None, None, None,
|
|
78
|
+
config, 10, None, None, None, None, False, None, simple_workflow.__qualname__
|
|
82
79
|
)
|
|
83
80
|
assert len(output) == 1, f"Expected list length to be 1, but got {len(output)}"
|
|
84
81
|
|
|
@@ -103,7 +100,7 @@ def test_list_workflow_start_end_times(dbos: DBOS, config: ConfigFile) -> None:
|
|
|
103
100
|
print(endtime)
|
|
104
101
|
|
|
105
102
|
output = _workflow_commands.list_workflows(
|
|
106
|
-
config, 10, None, starttime, endtime, None, False, None
|
|
103
|
+
config, 10, None, starttime, endtime, None, False, None, None
|
|
107
104
|
)
|
|
108
105
|
assert len(output) == 1, f"Expected list length to be 1, but got {len(output)}"
|
|
109
106
|
|
|
@@ -111,7 +108,7 @@ def test_list_workflow_start_end_times(dbos: DBOS, config: ConfigFile) -> None:
|
|
|
111
108
|
newendtime = starttime
|
|
112
109
|
|
|
113
110
|
output = _workflow_commands.list_workflows(
|
|
114
|
-
config, 10, None, newstarttime, newendtime, None, False, None
|
|
111
|
+
config, 10, None, newstarttime, newendtime, None, False, None, None
|
|
115
112
|
)
|
|
116
113
|
assert len(output) == 0, f"Expected list length to be 0, but got {len(output)}"
|
|
117
114
|
|
|
@@ -142,19 +139,19 @@ def test_list_workflow_end_times_positive(dbos: DBOS, config: ConfigFile) -> Non
|
|
|
142
139
|
time_3 = datetime.now().isoformat()
|
|
143
140
|
|
|
144
141
|
output = _workflow_commands.list_workflows(
|
|
145
|
-
config, 10, None, time_0, time_1, None, False, None
|
|
142
|
+
config, 10, None, time_0, time_1, None, False, None, None
|
|
146
143
|
)
|
|
147
144
|
|
|
148
145
|
assert len(output) == 0, f"Expected list length to be 0, but got {len(output)}"
|
|
149
146
|
|
|
150
147
|
output = _workflow_commands.list_workflows(
|
|
151
|
-
config, 10, None, time_1, time_2, None, False, None
|
|
148
|
+
config, 10, None, time_1, time_2, None, False, None, None
|
|
152
149
|
)
|
|
153
150
|
|
|
154
151
|
assert len(output) == 1, f"Expected list length to be 1, but got {len(output)}"
|
|
155
152
|
|
|
156
153
|
output = _workflow_commands.list_workflows(
|
|
157
|
-
config, 10, None, time_1, time_3, None, False, None
|
|
154
|
+
config, 10, None, time_1, time_3, None, False, None, None
|
|
158
155
|
)
|
|
159
156
|
assert len(output) == 2, f"Expected list length to be 2, but got {len(output)}"
|
|
160
157
|
|
|
@@ -172,7 +169,7 @@ def test_get_workflow(dbos: DBOS, config: ConfigFile) -> None:
|
|
|
172
169
|
time.sleep(1) # wait for the workflow to complete
|
|
173
170
|
# get the workflow list
|
|
174
171
|
output = _workflow_commands.list_workflows(
|
|
175
|
-
config, 10, None, None, None, None, False, None
|
|
172
|
+
config, 10, None, None, None, None, False, None, None
|
|
176
173
|
)
|
|
177
174
|
assert len(output) == 1, f"Expected list length to be 1, but got {len(output)}"
|
|
178
175
|
|
|
@@ -200,7 +197,7 @@ def test_cancel_workflow(dbos: DBOS, config: ConfigFile) -> None:
|
|
|
200
197
|
simple_workflow()
|
|
201
198
|
# get the workflow list
|
|
202
199
|
output = _workflow_commands.list_workflows(
|
|
203
|
-
config, 10, None, None, None, None, False, None
|
|
200
|
+
config, 10, None, None, None, None, False, None, None
|
|
204
201
|
)
|
|
205
202
|
# assert len(output) == 1, f"Expected list length to be 1, but got {len(output)}"
|
|
206
203
|
|
|
@@ -214,3 +211,79 @@ def test_cancel_workflow(dbos: DBOS, config: ConfigFile) -> None:
|
|
|
214
211
|
assert info is not None, "Expected info to be not None"
|
|
215
212
|
if info is not None:
|
|
216
213
|
assert info.status == "CANCELLED", f"Expected status to be CANCELLED"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_queued_workflows(dbos: DBOS, config: ConfigFile) -> None:
|
|
217
|
+
queued_steps = 5
|
|
218
|
+
step_events = [threading.Event() for _ in range(queued_steps)]
|
|
219
|
+
event = threading.Event()
|
|
220
|
+
queue = Queue("test_queue")
|
|
221
|
+
|
|
222
|
+
@DBOS.workflow()
|
|
223
|
+
def test_workflow() -> list[int]:
|
|
224
|
+
handles = []
|
|
225
|
+
for i in range(queued_steps):
|
|
226
|
+
h = queue.enqueue(blocking_step, i)
|
|
227
|
+
handles.append(h)
|
|
228
|
+
return [h.get_result() for h in handles]
|
|
229
|
+
|
|
230
|
+
@DBOS.step()
|
|
231
|
+
def blocking_step(i: int) -> int:
|
|
232
|
+
step_events[i].set()
|
|
233
|
+
event.wait()
|
|
234
|
+
return i
|
|
235
|
+
|
|
236
|
+
# The workflow enqueues blocking steps, wait for all to start
|
|
237
|
+
handle = DBOS.start_workflow(test_workflow)
|
|
238
|
+
for e in step_events:
|
|
239
|
+
e.wait()
|
|
240
|
+
|
|
241
|
+
# Verify all blocking steps are enqueued and have the right data
|
|
242
|
+
workflows = _workflow_commands.list_queued_workflows(config)
|
|
243
|
+
assert len(workflows) == queued_steps
|
|
244
|
+
for i, workflow in enumerate(workflows):
|
|
245
|
+
assert workflow.status == WorkflowStatusString.PENDING.value
|
|
246
|
+
assert workflow.queue_name == queue.name
|
|
247
|
+
assert workflow.input is not None
|
|
248
|
+
# Verify newest queue entries appear first
|
|
249
|
+
assert workflow.input["args"][0] == queued_steps - i - 1
|
|
250
|
+
assert workflow.output is None
|
|
251
|
+
assert workflow.error is None
|
|
252
|
+
assert "blocking_step" in workflow.workflowName
|
|
253
|
+
|
|
254
|
+
# Test every filter
|
|
255
|
+
workflows = _workflow_commands.list_queued_workflows(
|
|
256
|
+
config, status=WorkflowStatusString.PENDING.value
|
|
257
|
+
)
|
|
258
|
+
assert len(workflows) == queued_steps
|
|
259
|
+
workflows = _workflow_commands.list_queued_workflows(
|
|
260
|
+
config, status=WorkflowStatusString.ENQUEUED.value
|
|
261
|
+
)
|
|
262
|
+
assert len(workflows) == 0
|
|
263
|
+
workflows = _workflow_commands.list_queued_workflows(config, queue_name=queue.name)
|
|
264
|
+
assert len(workflows) == queued_steps
|
|
265
|
+
workflows = _workflow_commands.list_queued_workflows(config, queue_name="no")
|
|
266
|
+
assert len(workflows) == 0
|
|
267
|
+
workflows = _workflow_commands.list_queued_workflows(
|
|
268
|
+
config, name=f"<temp>.{blocking_step.__qualname__}"
|
|
269
|
+
)
|
|
270
|
+
assert len(workflows) == queued_steps
|
|
271
|
+
workflows = _workflow_commands.list_queued_workflows(config, name="no")
|
|
272
|
+
assert len(workflows) == 0
|
|
273
|
+
now = datetime.now(timezone.utc)
|
|
274
|
+
start_time = (now - timedelta(seconds=10)).isoformat()
|
|
275
|
+
end_time = (now + timedelta(seconds=10)).isoformat()
|
|
276
|
+
workflows = _workflow_commands.list_queued_workflows(
|
|
277
|
+
config, start_time=start_time, end_time=end_time
|
|
278
|
+
)
|
|
279
|
+
assert len(workflows) == queued_steps
|
|
280
|
+
workflows = _workflow_commands.list_queued_workflows(
|
|
281
|
+
config, start_time=now.isoformat(), end_time=end_time
|
|
282
|
+
)
|
|
283
|
+
assert len(workflows) == 0
|
|
284
|
+
|
|
285
|
+
# Confirm the workflow finishes and nothing is enqueued afterwards
|
|
286
|
+
event.set()
|
|
287
|
+
assert handle.get_result() == [0, 1, 2, 3, 4]
|
|
288
|
+
workflows = _workflow_commands.list_queued_workflows(config)
|
|
289
|
+
assert len(workflows) == 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|