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.

Files changed (90) hide show
  1. {dbos-0.21.0a4 → dbos-0.21.0a5}/PKG-INFO +1 -1
  2. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_dbos_config.py +17 -13
  3. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_sys_db.py +55 -1
  4. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_workflow_commands.py +43 -13
  5. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/cli/cli.py +84 -16
  6. {dbos-0.21.0a4 → dbos-0.21.0a5}/pyproject.toml +1 -1
  7. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_admin_server.py +3 -3
  8. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_package.py +46 -0
  9. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_workflow_cmds.py +100 -27
  10. {dbos-0.21.0a4 → dbos-0.21.0a5}/LICENSE +0 -0
  11. {dbos-0.21.0a4 → dbos-0.21.0a5}/README.md +0 -0
  12. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/__init__.py +0 -0
  13. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_admin_server.py +0 -0
  14. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_app_db.py +0 -0
  15. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_classproperty.py +0 -0
  16. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_cloudutils/authentication.py +0 -0
  17. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_cloudutils/cloudutils.py +0 -0
  18. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_cloudutils/databases.py +0 -0
  19. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_context.py +0 -0
  20. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_core.py +0 -0
  21. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_croniter.py +0 -0
  22. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_db_wizard.py +0 -0
  23. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_dbos.py +0 -0
  24. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_error.py +0 -0
  25. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_fastapi.py +0 -0
  26. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_flask.py +0 -0
  27. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_kafka.py +0 -0
  28. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_kafka_message.py +0 -0
  29. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_logger.py +0 -0
  30. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_migrations/env.py +0 -0
  31. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_migrations/script.py.mako +0 -0
  32. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
  33. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  34. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  35. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  36. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  37. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  38. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  39. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_outcome.py +0 -0
  40. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_queue.py +0 -0
  41. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_recovery.py +0 -0
  42. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_registrations.py +0 -0
  43. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_request.py +0 -0
  44. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_roles.py +0 -0
  45. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_scheduler.py +0 -0
  46. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_schemas/__init__.py +0 -0
  47. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_schemas/application_database.py +0 -0
  48. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_schemas/system_database.py +0 -0
  49. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_serialization.py +0 -0
  50. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/README.md +0 -0
  51. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
  52. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/__package/main.py +0 -0
  53. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
  54. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
  55. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
  56. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
  57. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
  58. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
  59. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
  60. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/_tracer.py +0 -0
  61. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/cli/_github_init.py +0 -0
  62. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/cli/_template_init.py +0 -0
  63. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/dbos-config.schema.json +0 -0
  64. {dbos-0.21.0a4 → dbos-0.21.0a5}/dbos/py.typed +0 -0
  65. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/__init__.py +0 -0
  66. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/atexit_no_ctor.py +0 -0
  67. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/atexit_no_launch.py +0 -0
  68. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/classdefs.py +0 -0
  69. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/conftest.py +0 -0
  70. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/more_classdefs.py +0 -0
  71. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/queuedworkflow.py +0 -0
  72. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_async.py +0 -0
  73. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_classdecorators.py +0 -0
  74. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_concurrency.py +0 -0
  75. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_config.py +0 -0
  76. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_croniter.py +0 -0
  77. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_dbos.py +0 -0
  78. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_failures.py +0 -0
  79. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_fastapi.py +0 -0
  80. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_fastapi_roles.py +0 -0
  81. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_flask.py +0 -0
  82. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_kafka.py +0 -0
  83. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_outcome.py +0 -0
  84. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_queue.py +0 -0
  85. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_scheduler.py +0 -0
  86. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_schema_migration.py +0 -0
  87. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_singleton.py +0 -0
  88. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_spans.py +0 -0
  89. {dbos-0.21.0a4 → dbos-0.21.0a5}/tests/test_sqlalchemy.py +0 -0
  90. {dbos-0.21.0a4 → dbos-0.21.0a5}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 0.21.0a4
3
+ Version: 0.21.0a5
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -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, *, use_db_wizard: bool = True
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 data["database"].get("hostname"):
192
- print(
193
- "[bold blue]Loading database connection parameters from dbos-config.yaml[/bold blue]"
194
- )
195
- elif db_connection.get("hostname"):
196
- print(
197
- "[bold blue]Loading database connection parameters from .dbos/db_connection[/bold blue]"
198
- )
199
- else:
200
- print(
201
- "[bold blue]Using default database connection parameters (localhost)[/bold blue]"
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
- li: int,
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 = li
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 cancel_workflow, get_workflow, list_workflows
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()
@@ -27,7 +27,7 @@ dependencies = [
27
27
  ]
28
28
  requires-python = ">=3.9"
29
29
  readme = "README.md"
30
- version = "0.21.0a4"
30
+ version = "0.21.0a5"
31
31
 
32
32
  [project.license]
33
33
  text = "MIT"
@@ -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 uuid
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 test_list_workflow_status(dbos: DBOS, config: ConfigFile) -> None:
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, "SUCCESS", False, 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
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