dbos 1.6.0a5__tar.gz → 1.7.0a2__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.
Files changed (108) hide show
  1. {dbos-1.6.0a5 → dbos-1.7.0a2}/PKG-INFO +1 -1
  2. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_admin_server.py +17 -8
  3. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_dbos.py +11 -1
  4. {dbos-1.6.0a5 → dbos-1.7.0a2}/pyproject.toml +1 -1
  5. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_admin_server.py +217 -15
  6. {dbos-1.6.0a5 → dbos-1.7.0a2}/LICENSE +0 -0
  7. {dbos-1.6.0a5 → dbos-1.7.0a2}/README.md +0 -0
  8. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/__init__.py +0 -0
  9. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/__main__.py +0 -0
  10. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_app_db.py +0 -0
  11. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_classproperty.py +0 -0
  12. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_client.py +0 -0
  13. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_conductor/conductor.py +0 -0
  14. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_conductor/protocol.py +0 -0
  15. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_context.py +0 -0
  16. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_core.py +0 -0
  17. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_croniter.py +0 -0
  18. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_dbos_config.py +0 -0
  19. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_debug.py +0 -0
  20. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_docker_pg_helper.py +0 -0
  21. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_error.py +0 -0
  22. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_event_loop.py +0 -0
  23. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_fastapi.py +0 -0
  24. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_flask.py +0 -0
  25. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_kafka.py +0 -0
  26. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_kafka_message.py +0 -0
  27. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_logger.py +0 -0
  28. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_migrations/env.py +0 -0
  29. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_migrations/script.py.mako +0 -0
  30. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
  31. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_migrations/versions/27ac6900c6ad_add_queue_dedup.py +0 -0
  32. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  33. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  34. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_migrations/versions/66478e1b95e5_consolidate_queues.py +0 -0
  35. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_migrations/versions/83f3732ae8e7_workflow_timeout.py +0 -0
  36. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_migrations/versions/933e86bdac6a_add_queue_priority.py +0 -0
  37. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  38. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  39. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  40. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_migrations/versions/d994145b47b6_consolidate_inputs.py +0 -0
  41. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  42. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
  43. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_outcome.py +0 -0
  44. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_queue.py +0 -0
  45. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_recovery.py +0 -0
  46. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_registrations.py +0 -0
  47. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_roles.py +0 -0
  48. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_scheduler.py +0 -0
  49. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_schemas/__init__.py +0 -0
  50. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_schemas/application_database.py +0 -0
  51. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_schemas/system_database.py +0 -0
  52. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_serialization.py +0 -0
  53. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_sys_db.py +0 -0
  54. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_templates/dbos-db-starter/README.md +0 -0
  55. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
  56. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_templates/dbos-db-starter/__package/main.py.dbos +0 -0
  57. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
  58. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
  59. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
  60. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
  61. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
  62. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
  63. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
  64. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_tracer.py +0 -0
  65. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_utils.py +0 -0
  66. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/_workflow_commands.py +0 -0
  67. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/cli/_github_init.py +0 -0
  68. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/cli/_template_init.py +0 -0
  69. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/cli/cli.py +0 -0
  70. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/dbos-config.schema.json +0 -0
  71. {dbos-1.6.0a5 → dbos-1.7.0a2}/dbos/py.typed +0 -0
  72. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/__init__.py +0 -0
  73. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/atexit_no_ctor.py +0 -0
  74. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/atexit_no_launch.py +0 -0
  75. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/classdefs.py +0 -0
  76. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/client_collateral.py +0 -0
  77. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/client_worker.py +0 -0
  78. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/conftest.py +0 -0
  79. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/dupname_classdefs1.py +0 -0
  80. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/dupname_classdefsa.py +0 -0
  81. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/more_classdefs.py +0 -0
  82. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/queuedworkflow.py +0 -0
  83. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_async.py +0 -0
  84. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_classdecorators.py +0 -0
  85. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_cli.py +0 -0
  86. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_client.py +0 -0
  87. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_concurrency.py +0 -0
  88. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_config.py +0 -0
  89. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_croniter.py +0 -0
  90. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_dbos.py +0 -0
  91. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_debug.py +0 -0
  92. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_docker_secrets.py +0 -0
  93. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_failures.py +0 -0
  94. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_fastapi.py +0 -0
  95. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_fastapi_roles.py +0 -0
  96. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_flask.py +0 -0
  97. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_kafka.py +0 -0
  98. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_outcome.py +0 -0
  99. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_package.py +0 -0
  100. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_queue.py +0 -0
  101. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_scheduler.py +0 -0
  102. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_schema_migration.py +0 -0
  103. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_singleton.py +0 -0
  104. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_spans.py +0 -0
  105. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_sqlalchemy.py +0 -0
  106. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_workflow_introspection.py +0 -0
  107. {dbos-1.6.0a5 → dbos-1.7.0a2}/tests/test_workflow_management.py +0 -0
  108. {dbos-1.6.0a5 → dbos-1.7.0a2}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 1.6.0a5
3
+ Version: 1.7.0a2
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -3,12 +3,14 @@ from __future__ import annotations
3
3
  import json
4
4
  import re
5
5
  import threading
6
+ from dataclasses import asdict
6
7
  from functools import partial
7
8
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
8
9
  from typing import TYPE_CHECKING, Any, Dict, List, Optional, TypedDict
9
10
 
10
11
  from dbos._workflow_commands import garbage_collect, global_timeout
11
12
 
13
+ from ._conductor import protocol as conductor_protocol
12
14
  from ._context import SetWorkflowID
13
15
  from ._error import DBOSException
14
16
  from ._logger import dbos_logger
@@ -326,20 +328,24 @@ class AdminRequestHandler(BaseHTTPRequestHandler):
326
328
 
327
329
  def _handle_workflows(self, filters: Dict[str, Any]) -> None:
328
330
  workflows = self.dbos.list_workflows(
329
- workflow_ids=filters.get("workflow_ids"),
330
- name=filters.get("name"),
331
+ workflow_ids=filters.get("workflow_uuids"),
332
+ user=filters.get("authenticated_user"),
331
333
  start_time=filters.get("start_time"),
332
334
  end_time=filters.get("end_time"),
333
335
  status=filters.get("status"),
334
336
  app_version=filters.get("application_version"),
337
+ name=filters.get("workflow_name"),
335
338
  limit=filters.get("limit"),
336
339
  offset=filters.get("offset"),
337
340
  sort_desc=filters.get("sort_desc", False),
338
341
  workflow_id_prefix=filters.get("workflow_id_prefix"),
339
342
  )
340
-
343
+ workflows_output = [
344
+ conductor_protocol.WorkflowsOutput.from_workflow_information(i)
345
+ for i in workflows
346
+ ]
341
347
  response_body = json.dumps(
342
- [workflow.__dict__ for workflow in workflows]
348
+ [workflow.__dict__ for workflow in workflows_output]
343
349
  ).encode("utf-8")
344
350
  self.send_response(200)
345
351
  self.send_header("Content-Type", "application/json")
@@ -349,18 +355,21 @@ class AdminRequestHandler(BaseHTTPRequestHandler):
349
355
 
350
356
  def _handle_queued_workflows(self, filters: Dict[str, Any]) -> None:
351
357
  workflows = self.dbos.list_queued_workflows(
352
- queue_name=filters.get("queue_name"),
353
- name=filters.get("name"),
354
358
  start_time=filters.get("start_time"),
355
359
  end_time=filters.get("end_time"),
356
360
  status=filters.get("status"),
361
+ name=filters.get("workflow_name"),
357
362
  limit=filters.get("limit"),
358
363
  offset=filters.get("offset"),
364
+ queue_name=filters.get("queue_name"),
359
365
  sort_desc=filters.get("sort_desc", False),
360
366
  )
361
-
367
+ workflows_output = [
368
+ conductor_protocol.WorkflowsOutput.from_workflow_information(i)
369
+ for i in workflows
370
+ ]
362
371
  response_body = json.dumps(
363
- [workflow.__dict__ for workflow in workflows]
372
+ [workflow.__dict__ for workflow in workflows_output]
364
373
  ).encode("utf-8")
365
374
  self.send_response(200)
366
375
  self.send_header("Content-Type", "application/json")
@@ -7,7 +7,6 @@ import inspect
7
7
  import os
8
8
  import sys
9
9
  import threading
10
- import traceback
11
10
  import uuid
12
11
  from concurrent.futures import ThreadPoolExecutor
13
12
  from logging import Logger
@@ -28,6 +27,7 @@ from typing import (
28
27
  )
29
28
 
30
29
  from opentelemetry.trace import Span
30
+ from rich import print
31
31
 
32
32
  from dbos._conductor.conductor import ConductorWebsocket
33
33
  from dbos._sys_db import WorkflowStatus
@@ -517,6 +517,16 @@ class DBOS:
517
517
 
518
518
  dbos_logger.info("DBOS launched!")
519
519
 
520
+ if self.conductor_key is None and os.environ.get("DBOS__CLOUD") != "true":
521
+ # Hint the user to open the URL to register and set up Conductor
522
+ app_name = self._config["name"]
523
+ conductor_registration_url = (
524
+ f"https://console.dbos.dev/self-host?appname={app_name}"
525
+ )
526
+ print(
527
+ f"[bold]To view and manage workflows, connect to DBOS Conductor at:[/bold] [bold blue]{conductor_registration_url}[/bold blue]"
528
+ )
529
+
520
530
  # Flush handlers and add OTLP to all loggers if enabled
521
531
  # to enable their export in DBOS Cloud
522
532
  for handler in dbos_logger.handlers:
@@ -27,7 +27,7 @@ dependencies = [
27
27
  ]
28
28
  requires-python = ">=3.9"
29
29
  readme = "README.md"
30
- version = "1.6.0a5"
30
+ version = "1.7.0a2"
31
31
 
32
32
  [project.license]
33
33
  text = "MIT"
@@ -3,7 +3,8 @@ import socket
3
3
  import threading
4
4
  import time
5
5
  import uuid
6
- from datetime import datetime, timezone
6
+ from datetime import datetime, timedelta, timezone
7
+ from typing import Any, Dict
7
8
 
8
9
  import pytest
9
10
  import requests
@@ -462,13 +463,13 @@ def test_list_workflows(dbos: DBOS) -> None:
462
463
  pass
463
464
 
464
465
  @DBOS.workflow()
465
- def test_workflow_2() -> None:
466
- pass
466
+ def test_workflow_2(my_time: datetime) -> str:
467
+ return DBOS.workflow_id + " completed at " + my_time.isoformat()
467
468
 
468
469
  # Start workflows
469
470
  handle_1 = DBOS.start_workflow(test_workflow_1)
470
471
  time.sleep(2) # Sleep for 2 seconds between workflows
471
- handle_2 = DBOS.start_workflow(test_workflow_2)
472
+ handle_2 = DBOS.start_workflow(test_workflow_2, datetime.now())
472
473
 
473
474
  # Wait for workflows to complete
474
475
  handle_1.get_result()
@@ -492,8 +493,8 @@ def test_list_workflows(dbos: DBOS) -> None:
492
493
  ).isoformat()
493
494
 
494
495
  # Test POST /workflows with filters
495
- filters = {
496
- "workflow_ids": workflow_ids,
496
+ filters: Dict[str, Any] = {
497
+ "workflow_uuids": workflow_ids,
497
498
  "start_time": start_time_filter,
498
499
  }
499
500
  response = requests.post("http://localhost:3001/workflows", json=filters, timeout=5)
@@ -501,7 +502,24 @@ def test_list_workflows(dbos: DBOS) -> None:
501
502
 
502
503
  workflows = response.json()
503
504
  assert len(workflows) == 1, f"Expected 1 workflows, but got {len(workflows)}"
504
- assert workflows[0]["workflow_id"] == handle_2.workflow_id, "Workflow ID mismatch"
505
+
506
+ # Make sure it contains all the expected fields
507
+ assert workflows[0]["WorkflowUUID"] == handle_2.workflow_id, "Workflow ID mismatch"
508
+ assert workflows[0]["WorkflowName"] == test_workflow_2.__qualname__
509
+ assert workflows[0]["Status"] == "SUCCESS"
510
+ assert workflows[0]["WorkflowClassName"] is None
511
+ assert workflows[0]["WorkflowConfigName"] is None
512
+ assert workflows[0]["AuthenticatedUser"] is None
513
+ assert workflows[0]["AssumedRole"] is None
514
+ assert workflows[0]["AuthenticatedRoles"] is None
515
+ assert workflows[0]["Input"] is not None and len(workflows[0]["Input"]) > 0
516
+ assert workflows[0]["Output"] is not None and len(workflows[0]["Output"]) > 0
517
+ assert workflows[0]["Error"] is None
518
+ assert workflows[0]["CreatedAt"] is not None and len(workflows[0]["CreatedAt"]) > 0
519
+ assert workflows[0]["UpdatedAt"] is not None and len(workflows[0]["UpdatedAt"]) > 0
520
+ assert workflows[0]["QueueName"] is None
521
+ assert workflows[0]["ApplicationVersion"] == GlobalParams.app_version
522
+ assert workflows[0]["ExecutorID"] == GlobalParams.executor_id
505
523
 
506
524
  # Test POST /workflows without filters
507
525
  response = requests.post("http://localhost:3001/workflows", json={}, timeout=5)
@@ -512,7 +530,106 @@ def test_list_workflows(dbos: DBOS) -> None:
512
530
  workflows_list
513
531
  ), f"Expected {len(workflows_list)} workflows, but got {len(workflows)}"
514
532
  for workflow in workflows:
515
- assert workflow["workflow_id"] in workflow_ids, "Workflow ID mismatch"
533
+ assert workflow["WorkflowUUID"] in workflow_ids, "Workflow ID mismatch"
534
+
535
+ # Verify sort_desc inverts the order
536
+ filters = {
537
+ "sort_desc": True,
538
+ }
539
+ response = requests.post("http://localhost:3001/workflows", json=filters, timeout=5)
540
+ assert response.status_code == 200
541
+ workflows = response.json()
542
+ assert len(workflows) == len(workflows_list)
543
+ assert (
544
+ workflows[0]["WorkflowUUID"] == handle_2.workflow_id
545
+ ), "First workflow should be the last one started"
546
+
547
+ # Test all filters
548
+ filters = {
549
+ "workflow_uuids": ["not-a-valid-uuid"],
550
+ }
551
+ response = requests.post("http://localhost:3001/workflows", json=filters, timeout=5)
552
+ assert response.status_code == 200
553
+ workflows = response.json()
554
+ assert len(workflows) == 0, "Expected no workflows for invalid UUID"
555
+
556
+ filters = {
557
+ "workflow_uuids": [handle_1.workflow_id, handle_2.workflow_id],
558
+ }
559
+ response = requests.post("http://localhost:3001/workflows", json=filters, timeout=5)
560
+ assert response.status_code == 200
561
+ workflows = response.json()
562
+ assert len(workflows) == 2
563
+
564
+ filters = {
565
+ "authenticated_user": "no-user",
566
+ }
567
+ response = requests.post("http://localhost:3001/workflows", json=filters, timeout=5)
568
+ assert response.status_code == 200
569
+ workflows = response.json()
570
+ assert len(workflows) == 0
571
+
572
+ filters = {
573
+ "workflow_name": test_workflow_1.__qualname__,
574
+ }
575
+ response = requests.post("http://localhost:3001/workflows", json=filters, timeout=5)
576
+ assert response.status_code == 200
577
+ workflows = response.json()
578
+ assert len(workflows) == 1
579
+ assert workflows[0]["WorkflowUUID"] == handle_1.workflow_id
580
+
581
+ filters = {
582
+ "end_time": (datetime.now(timezone.utc) - timedelta(minutes=10)).isoformat()
583
+ }
584
+ response = requests.post("http://localhost:3001/workflows", json=filters, timeout=5)
585
+ assert response.status_code == 200
586
+ workflows = response.json()
587
+ assert len(workflows) == 0
588
+
589
+ filters = {
590
+ "start_time": (datetime.now(timezone.utc) + timedelta(minutes=10)).isoformat(),
591
+ }
592
+ response = requests.post("http://localhost:3001/workflows", json=filters, timeout=5)
593
+ assert response.status_code == 200
594
+ workflows = response.json()
595
+ assert len(workflows) == 0
596
+
597
+ filters = {
598
+ "status": ["SUCCESS", "CANCELLED"],
599
+ }
600
+ response = requests.post("http://localhost:3001/workflows", json=filters, timeout=5)
601
+ assert response.status_code == 200
602
+ workflows = response.json()
603
+ assert len(workflows) == 2
604
+
605
+ filters = {
606
+ "application_version": GlobalParams.app_version,
607
+ }
608
+ response = requests.post("http://localhost:3001/workflows", json=filters, timeout=5)
609
+ assert response.status_code == 200
610
+ workflows = response.json()
611
+ assert len(workflows) == 2
612
+
613
+ filters = {
614
+ "limit": 1,
615
+ "offset": 1,
616
+ }
617
+ response = requests.post("http://localhost:3001/workflows", json=filters, timeout=5)
618
+ assert response.status_code == 200
619
+ workflows = response.json()
620
+ assert len(workflows) == 1
621
+ assert workflows[0]["WorkflowUUID"] == handle_2.workflow_id
622
+
623
+ filters = {
624
+ "workflow_id_prefix": handle_1.workflow_id[
625
+ :10
626
+ ], # First 10 characters of the workflow name
627
+ }
628
+ response = requests.post("http://localhost:3001/workflows", json=filters, timeout=5)
629
+ assert response.status_code == 200
630
+ workflows = response.json()
631
+ assert len(workflows) == 1
632
+ assert workflows[0]["WorkflowUUID"] == handle_1.workflow_id
516
633
 
517
634
 
518
635
  def test_get_workflow_by_id(dbos: DBOS) -> None:
@@ -606,15 +723,15 @@ def test_queued_workflows_endpoint(dbos: DBOS) -> None:
606
723
  test_queue2 = Queue("test-queue-2", concurrency=1)
607
724
 
608
725
  @DBOS.workflow()
609
- def blocking_workflow() -> str:
726
+ def blocking_workflow(i: int) -> str:
610
727
  while True:
611
728
  time.sleep(0.1)
612
729
 
613
730
  # Enqueue some workflows to create queued entries
614
- handles = []
615
- handles.append(test_queue1.enqueue(blocking_workflow))
616
- handles.append(test_queue1.enqueue(blocking_workflow))
617
- handles.append(test_queue2.enqueue(blocking_workflow))
731
+ handles: list[WorkflowHandle[str]] = []
732
+ handles.append(test_queue1.enqueue(blocking_workflow, 1))
733
+ handles.append(test_queue1.enqueue(blocking_workflow, 2))
734
+ handles.append(test_queue2.enqueue(blocking_workflow, 3))
618
735
 
619
736
  # Test basic queued workflows endpoint
620
737
  response = requests.post("http://localhost:3001/queues", json={}, timeout=5)
@@ -628,16 +745,101 @@ def test_queued_workflows_endpoint(dbos: DBOS) -> None:
628
745
  len(queued_workflows) == 3
629
746
  ), f"Expected 3 queued workflows, got {len(queued_workflows)}"
630
747
 
631
- # Test with filters
632
- filters = {"queue_name": "test-queue-1", "limit": 1}
748
+ # Make sure it contains all the expected fields
749
+ assert queued_workflows[0]["WorkflowName"] == blocking_workflow.__qualname__
750
+ assert (
751
+ queued_workflows[0]["WorkflowUUID"] == handles[0].workflow_id
752
+ ), "Workflow ID mismatch"
753
+ assert (
754
+ queued_workflows[0]["Status"] == "ENQUEUED"
755
+ or queued_workflows[0]["Status"] == "PENDING"
756
+ )
757
+ assert queued_workflows[0]["WorkflowClassName"] is None
758
+ assert queued_workflows[0]["WorkflowConfigName"] is None
759
+ assert queued_workflows[0]["AuthenticatedUser"] is None
760
+ assert queued_workflows[0]["AssumedRole"] is None
761
+ assert queued_workflows[0]["AuthenticatedRoles"] is None
762
+ assert (
763
+ queued_workflows[0]["Input"] is not None
764
+ and len(queued_workflows[0]["Input"]) > 0
765
+ )
766
+ assert "1" in queued_workflows[0]["Input"]
767
+ assert queued_workflows[0]["Output"] is None
768
+ assert queued_workflows[0]["Error"] is None
769
+ assert (
770
+ queued_workflows[0]["CreatedAt"] is not None
771
+ and len(queued_workflows[0]["CreatedAt"]) > 0
772
+ )
773
+ assert (
774
+ queued_workflows[0]["UpdatedAt"] is not None
775
+ and len(queued_workflows[0]["UpdatedAt"]) > 0
776
+ )
777
+ assert queued_workflows[0]["QueueName"] == test_queue1.name
778
+ assert queued_workflows[0]["ApplicationVersion"] == GlobalParams.app_version
779
+ assert queued_workflows[0]["ExecutorID"] == GlobalParams.executor_id
780
+
781
+ # Verify sort_desc inverts the order
782
+ filters: Dict[str, Any] = {
783
+ "sort_desc": True,
784
+ }
785
+ response = requests.post("http://localhost:3001/queues", json=filters, timeout=5)
786
+ assert response.status_code == 200
787
+ filtered_workflows = response.json()
788
+ assert len(filtered_workflows) == len(handles)
789
+ assert (
790
+ filtered_workflows[0]["WorkflowUUID"] == handles[2].workflow_id
791
+ ), "First workflow should be the last one enqueued"
792
+
793
+ # Test all filters
794
+ filters = {
795
+ "workflow_name": blocking_workflow.__qualname__,
796
+ }
797
+ response = requests.post("http://localhost:3001/queues", json=filters, timeout=5)
798
+ assert response.status_code == 200
799
+ filtered_workflows = response.json()
800
+ assert len(filtered_workflows) == len(handles)
801
+
802
+ filters = {
803
+ "end_time": (datetime.now(timezone.utc) - timedelta(minutes=10)).isoformat(),
804
+ }
633
805
  response = requests.post("http://localhost:3001/queues", json=filters, timeout=5)
634
806
  assert response.status_code == 200
807
+ filtered_workflows = response.json()
808
+ assert len(filtered_workflows) == 0
809
+
810
+ filters = {
811
+ "start_time": (datetime.now(timezone.utc) + timedelta(minutes=10)).isoformat(),
812
+ }
813
+ response = requests.post("http://localhost:3001/queues", json=filters, timeout=5)
814
+ assert response.status_code == 200
815
+ filtered_workflows = response.json()
816
+ assert len(filtered_workflows) == 0
635
817
 
818
+ filters = {
819
+ "status": ["PENDING", "ENQUEUED"],
820
+ }
821
+ response = requests.post("http://localhost:3001/queues", json=filters, timeout=5)
822
+ assert response.status_code == 200
823
+ filtered_workflows = response.json()
824
+ assert len(filtered_workflows) == len(handles)
825
+
826
+ filters = {
827
+ "queue_name": test_queue1.name,
828
+ }
829
+ response = requests.post("http://localhost:3001/queues", json=filters, timeout=5)
830
+ assert response.status_code == 200
831
+ filtered_workflows = response.json()
832
+ assert len(filtered_workflows) == 2
833
+
834
+ filters = {"queue_name": test_queue1.name, "limit": 1, "offset": 1}
835
+ response = requests.post("http://localhost:3001/queues", json=filters, timeout=5)
836
+ assert response.status_code == 200
636
837
  filtered_workflows = response.json()
637
838
  assert isinstance(filtered_workflows, list), "Response should be a list"
638
839
  assert (
639
840
  len(filtered_workflows) == 1
640
841
  ), f"Expected 1 workflow, got {len(filtered_workflows)}"
842
+ assert filtered_workflows[0]["WorkflowUUID"] == handles[1].workflow_id
641
843
 
642
844
  # Test with non-existent queue name
643
845
  filters = {"queue_name": "non-existent-queue"}
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes