dbos 1.6.0a3__tar.gz → 1.6.0a4__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.0a3 → dbos-1.6.0a4}/PKG-INFO +1 -1
  2. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_core.py +41 -27
  3. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_dbos.py +17 -4
  4. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_kafka.py +2 -1
  5. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_registrations.py +5 -3
  6. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_roles.py +3 -2
  7. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_scheduler.py +11 -8
  8. {dbos-1.6.0a3 → dbos-1.6.0a4}/pyproject.toml +1 -1
  9. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_classdecorators.py +1 -0
  10. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_dbos.py +56 -4
  11. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_failures.py +1 -1
  12. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_fastapi_roles.py +3 -3
  13. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_scheduler.py +15 -1
  14. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_spans.py +21 -15
  15. {dbos-1.6.0a3 → dbos-1.6.0a4}/LICENSE +0 -0
  16. {dbos-1.6.0a3 → dbos-1.6.0a4}/README.md +0 -0
  17. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/__init__.py +0 -0
  18. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/__main__.py +0 -0
  19. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_admin_server.py +0 -0
  20. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_app_db.py +0 -0
  21. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_classproperty.py +0 -0
  22. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_client.py +0 -0
  23. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_conductor/conductor.py +0 -0
  24. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_conductor/protocol.py +0 -0
  25. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_context.py +0 -0
  26. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_croniter.py +0 -0
  27. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_dbos_config.py +0 -0
  28. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_debug.py +0 -0
  29. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_docker_pg_helper.py +0 -0
  30. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_error.py +0 -0
  31. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_event_loop.py +0 -0
  32. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_fastapi.py +0 -0
  33. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_flask.py +0 -0
  34. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_kafka_message.py +0 -0
  35. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_logger.py +0 -0
  36. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/env.py +0 -0
  37. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/script.py.mako +0 -0
  38. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
  39. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/27ac6900c6ad_add_queue_dedup.py +0 -0
  40. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  41. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  42. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/66478e1b95e5_consolidate_queues.py +0 -0
  43. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/83f3732ae8e7_workflow_timeout.py +0 -0
  44. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/933e86bdac6a_add_queue_priority.py +0 -0
  45. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  46. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  47. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  48. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/d994145b47b6_consolidate_inputs.py +0 -0
  49. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  50. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
  51. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_outcome.py +0 -0
  52. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_queue.py +0 -0
  53. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_recovery.py +0 -0
  54. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_schemas/__init__.py +0 -0
  55. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_schemas/application_database.py +0 -0
  56. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_schemas/system_database.py +0 -0
  57. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_serialization.py +0 -0
  58. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_sys_db.py +0 -0
  59. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_templates/dbos-db-starter/README.md +0 -0
  60. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
  61. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_templates/dbos-db-starter/__package/main.py.dbos +0 -0
  62. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
  63. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
  64. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
  65. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
  66. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
  67. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
  68. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
  69. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_tracer.py +0 -0
  70. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_utils.py +0 -0
  71. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_workflow_commands.py +0 -0
  72. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/cli/_github_init.py +0 -0
  73. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/cli/_template_init.py +0 -0
  74. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/cli/cli.py +0 -0
  75. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/dbos-config.schema.json +0 -0
  76. {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/py.typed +0 -0
  77. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/__init__.py +0 -0
  78. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/atexit_no_ctor.py +0 -0
  79. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/atexit_no_launch.py +0 -0
  80. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/classdefs.py +0 -0
  81. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/client_collateral.py +0 -0
  82. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/client_worker.py +0 -0
  83. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/conftest.py +0 -0
  84. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/dupname_classdefs1.py +0 -0
  85. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/dupname_classdefsa.py +0 -0
  86. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/more_classdefs.py +0 -0
  87. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/queuedworkflow.py +0 -0
  88. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_admin_server.py +0 -0
  89. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_async.py +0 -0
  90. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_cli.py +0 -0
  91. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_client.py +0 -0
  92. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_concurrency.py +0 -0
  93. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_config.py +0 -0
  94. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_croniter.py +0 -0
  95. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_debug.py +0 -0
  96. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_docker_secrets.py +0 -0
  97. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_fastapi.py +0 -0
  98. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_flask.py +0 -0
  99. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_kafka.py +0 -0
  100. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_outcome.py +0 -0
  101. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_package.py +0 -0
  102. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_queue.py +0 -0
  103. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_schema_migration.py +0 -0
  104. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_singleton.py +0 -0
  105. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_sqlalchemy.py +0 -0
  106. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_workflow_introspection.py +0 -0
  107. {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_workflow_management.py +0 -0
  108. {dbos-1.6.0a3 → dbos-1.6.0a4}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 1.6.0a3
3
+ Version: 1.6.0a4
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -392,7 +392,7 @@ def _execute_workflow_wthread(
392
392
  **kwargs: Any,
393
393
  ) -> R:
394
394
  attributes: TracedAttributes = {
395
- "name": func.__name__,
395
+ "name": get_dbos_func_name(func),
396
396
  "operationType": OperationType.WORKFLOW.value,
397
397
  }
398
398
  with DBOSContextSwap(ctx):
@@ -425,7 +425,7 @@ async def _execute_workflow_async(
425
425
  **kwargs: Any,
426
426
  ) -> R:
427
427
  attributes: TracedAttributes = {
428
- "name": func.__name__,
428
+ "name": get_dbos_func_name(func),
429
429
  "operationType": OperationType.WORKFLOW.value,
430
430
  }
431
431
  with DBOSContextSwap(ctx):
@@ -532,7 +532,8 @@ def start_workflow(
532
532
  fi = get_func_info(func)
533
533
  if fi is None:
534
534
  raise DBOSWorkflowFunctionNotFoundError(
535
- "<NONE>", f"start_workflow: function {func.__name__} is not registered"
535
+ "<NONE>",
536
+ f"start_workflow: function {func.__name__} is not registered",
536
537
  )
537
538
 
538
539
  func = cast("Workflow[P, R]", func.__orig_func) # type: ignore
@@ -627,7 +628,8 @@ async def start_workflow_async(
627
628
  fi = get_func_info(func)
628
629
  if fi is None:
629
630
  raise DBOSWorkflowFunctionNotFoundError(
630
- "<NONE>", f"start_workflow: function {func.__name__} is not registered"
631
+ "<NONE>",
632
+ f"start_workflow: function {func.__name__} is not registered",
631
633
  )
632
634
 
633
635
  func = cast("Workflow[P, R]", func.__orig_func) # type: ignore
@@ -731,13 +733,13 @@ def workflow_wrapper(
731
733
  assert fi is not None
732
734
  if dbosreg.dbos is None:
733
735
  raise DBOSException(
734
- f"Function {func.__name__} invoked before DBOS initialized"
736
+ f"Function {get_dbos_func_name(func)} invoked before DBOS initialized"
735
737
  )
736
738
  dbos = dbosreg.dbos
737
739
 
738
740
  rr: Optional[str] = check_required_roles(func, fi)
739
741
  attributes: TracedAttributes = {
740
- "name": func.__name__,
742
+ "name": get_dbos_func_name(func),
741
743
  "operationType": OperationType.WORKFLOW.value,
742
744
  }
743
745
  inputs: WorkflowInputs = {
@@ -837,27 +839,30 @@ def workflow_wrapper(
837
839
 
838
840
 
839
841
  def decorate_workflow(
840
- reg: "DBOSRegistry", max_recovery_attempts: Optional[int]
842
+ reg: "DBOSRegistry", name: Optional[str], max_recovery_attempts: Optional[int]
841
843
  ) -> Callable[[Callable[P, R]], Callable[P, R]]:
842
844
  def _workflow_decorator(func: Callable[P, R]) -> Callable[P, R]:
843
845
  wrapped_func = workflow_wrapper(reg, func, max_recovery_attempts)
844
- reg.register_wf_function(func.__qualname__, wrapped_func, "workflow")
846
+ func_name = name if name is not None else func.__qualname__
847
+ set_dbos_func_name(func, func_name)
848
+ set_dbos_func_name(wrapped_func, func_name)
849
+ reg.register_wf_function(func_name, wrapped_func, "workflow")
845
850
  return wrapped_func
846
851
 
847
852
  return _workflow_decorator
848
853
 
849
854
 
850
855
  def decorate_transaction(
851
- dbosreg: "DBOSRegistry", isolation_level: "IsolationLevel" = "SERIALIZABLE"
856
+ dbosreg: "DBOSRegistry", name: Optional[str], isolation_level: "IsolationLevel"
852
857
  ) -> Callable[[F], F]:
853
858
  def decorator(func: F) -> F:
854
859
 
855
- transaction_name = func.__qualname__
860
+ transaction_name = name if name is not None else func.__qualname__
856
861
 
857
862
  def invoke_tx(*args: Any, **kwargs: Any) -> Any:
858
863
  if dbosreg.dbos is None:
859
864
  raise DBOSException(
860
- f"Function {func.__name__} invoked before DBOS initialized"
865
+ f"Function {transaction_name} invoked before DBOS initialized"
861
866
  )
862
867
 
863
868
  dbos = dbosreg.dbos
@@ -865,12 +870,12 @@ def decorate_transaction(
865
870
  status = dbos._sys_db.get_workflow_status(ctx.workflow_id)
866
871
  if status and status["status"] == WorkflowStatusString.CANCELLED.value:
867
872
  raise DBOSWorkflowCancelledError(
868
- f"Workflow {ctx.workflow_id} is cancelled. Aborting transaction {func.__name__}."
873
+ f"Workflow {ctx.workflow_id} is cancelled. Aborting transaction {transaction_name}."
869
874
  )
870
875
 
871
876
  with dbos._app_db.sessionmaker() as session:
872
877
  attributes: TracedAttributes = {
873
- "name": func.__name__,
878
+ "name": transaction_name,
874
879
  "operationType": OperationType.TRANSACTION.value,
875
880
  }
876
881
  with EnterDBOSTransaction(session, attributes=attributes):
@@ -971,7 +976,7 @@ def decorate_transaction(
971
976
  raise
972
977
  except InvalidRequestError as invalid_request_error:
973
978
  dbos.logger.error(
974
- f"InvalidRequestError in transaction {func.__qualname__} \033[1m Hint: Do not call commit() or rollback() within a DBOS transaction.\033[0m"
979
+ f"InvalidRequestError in transaction {transaction_name} \033[1m Hint: Do not call commit() or rollback() within a DBOS transaction.\033[0m"
975
980
  )
976
981
  txn_error = invalid_request_error
977
982
  raise
@@ -991,7 +996,7 @@ def decorate_transaction(
991
996
 
992
997
  if inspect.iscoroutinefunction(func):
993
998
  raise DBOSException(
994
- f"Function {func.__name__} is a coroutine function, but DBOS.transaction does not support coroutine functions"
999
+ f"Function {transaction_name} is a coroutine function, but DBOS.transaction does not support coroutine functions"
995
1000
  )
996
1001
 
997
1002
  fi = get_or_create_func_info(func)
@@ -1010,15 +1015,19 @@ def decorate_transaction(
1010
1015
  with DBOSAssumeRole(rr):
1011
1016
  return invoke_tx(*args, **kwargs)
1012
1017
  else:
1013
- tempwf = dbosreg.workflow_info_map.get("<temp>." + func.__qualname__)
1018
+ tempwf = dbosreg.workflow_info_map.get("<temp>." + transaction_name)
1014
1019
  assert tempwf
1015
1020
  return tempwf(*args, **kwargs)
1016
1021
 
1022
+ set_dbos_func_name(func, transaction_name)
1023
+ set_dbos_func_name(wrapper, transaction_name)
1024
+
1017
1025
  def temp_wf(*args: Any, **kwargs: Any) -> Any:
1018
1026
  return wrapper(*args, **kwargs)
1019
1027
 
1020
1028
  wrapped_wf = workflow_wrapper(dbosreg, temp_wf)
1021
- set_dbos_func_name(temp_wf, "<temp>." + func.__qualname__)
1029
+ set_dbos_func_name(temp_wf, "<temp>." + transaction_name)
1030
+ set_dbos_func_name(wrapped_wf, "<temp>." + transaction_name)
1022
1031
  set_temp_workflow_type(temp_wf, "transaction")
1023
1032
  dbosreg.register_wf_function(
1024
1033
  get_dbos_func_name(temp_wf), wrapped_wf, "transaction"
@@ -1035,24 +1044,25 @@ def decorate_transaction(
1035
1044
  def decorate_step(
1036
1045
  dbosreg: "DBOSRegistry",
1037
1046
  *,
1038
- retries_allowed: bool = False,
1039
- interval_seconds: float = 1.0,
1040
- max_attempts: int = 3,
1041
- backoff_rate: float = 2.0,
1047
+ name: Optional[str],
1048
+ retries_allowed: bool,
1049
+ interval_seconds: float,
1050
+ max_attempts: int,
1051
+ backoff_rate: float,
1042
1052
  ) -> Callable[[Callable[P, R]], Callable[P, R]]:
1043
1053
  def decorator(func: Callable[P, R]) -> Callable[P, R]:
1044
1054
 
1045
- step_name = func.__qualname__
1055
+ step_name = name if name is not None else func.__qualname__
1046
1056
 
1047
1057
  def invoke_step(*args: Any, **kwargs: Any) -> Any:
1048
1058
  if dbosreg.dbos is None:
1049
1059
  raise DBOSException(
1050
- f"Function {func.__name__} invoked before DBOS initialized"
1060
+ f"Function {step_name} invoked before DBOS initialized"
1051
1061
  )
1052
1062
  dbos = dbosreg.dbos
1053
1063
 
1054
1064
  attributes: TracedAttributes = {
1055
- "name": func.__name__,
1065
+ "name": step_name,
1056
1066
  "operationType": OperationType.STEP.value,
1057
1067
  }
1058
1068
 
@@ -1131,7 +1141,7 @@ def decorate_step(
1131
1141
  stepOutcome = stepOutcome.retry(
1132
1142
  max_attempts,
1133
1143
  on_exception,
1134
- lambda i, e: DBOSMaxStepRetriesExceeded(func.__name__, i, e),
1144
+ lambda i, e: DBOSMaxStepRetriesExceeded(step_name, i, e),
1135
1145
  )
1136
1146
 
1137
1147
  outcome = (
@@ -1160,7 +1170,7 @@ def decorate_step(
1160
1170
  with DBOSAssumeRole(rr):
1161
1171
  return invoke_step(*args, **kwargs)
1162
1172
  else:
1163
- tempwf = dbosreg.workflow_info_map.get("<temp>." + func.__qualname__)
1173
+ tempwf = dbosreg.workflow_info_map.get("<temp>." + step_name)
1164
1174
  assert tempwf
1165
1175
  return tempwf(*args, **kwargs)
1166
1176
 
@@ -1168,6 +1178,9 @@ def decorate_step(
1168
1178
  _mark_coroutine(wrapper) if inspect.iscoroutinefunction(func) else wrapper # type: ignore
1169
1179
  )
1170
1180
 
1181
+ set_dbos_func_name(func, step_name)
1182
+ set_dbos_func_name(wrapper, step_name)
1183
+
1171
1184
  def temp_wf_sync(*args: Any, **kwargs: Any) -> Any:
1172
1185
  return wrapper(*args, **kwargs)
1173
1186
 
@@ -1181,7 +1194,8 @@ def decorate_step(
1181
1194
 
1182
1195
  temp_wf = temp_wf_async if inspect.iscoroutinefunction(func) else temp_wf_sync
1183
1196
  wrapped_wf = workflow_wrapper(dbosreg, temp_wf)
1184
- set_dbos_func_name(temp_wf, "<temp>." + func.__qualname__)
1197
+ set_dbos_func_name(temp_wf, "<temp>." + step_name)
1198
+ set_dbos_func_name(wrapped_wf, "<temp>." + step_name)
1185
1199
  set_temp_workflow_type(temp_wf, "step")
1186
1200
  dbosreg.register_wf_function(get_dbos_func_name(temp_wf), wrapped_wf, "step")
1187
1201
  wrapper.__orig_func = temp_wf # type: ignore
@@ -360,6 +360,7 @@ class DBOS:
360
360
 
361
361
  temp_send_wf = workflow_wrapper(self._registry, send_temp_workflow)
362
362
  set_dbos_func_name(send_temp_workflow, TEMP_SEND_WF_NAME)
363
+ set_dbos_func_name(temp_send_wf, TEMP_SEND_WF_NAME)
363
364
  set_temp_workflow_type(send_temp_workflow, "send")
364
365
  self._registry.register_wf_function(TEMP_SEND_WF_NAME, temp_send_wf, "send")
365
366
 
@@ -589,14 +590,22 @@ class DBOS:
589
590
  # Decorators for DBOS functionality
590
591
  @classmethod
591
592
  def workflow(
592
- cls, *, max_recovery_attempts: Optional[int] = DEFAULT_MAX_RECOVERY_ATTEMPTS
593
+ cls,
594
+ *,
595
+ name: Optional[str] = None,
596
+ max_recovery_attempts: Optional[int] = DEFAULT_MAX_RECOVERY_ATTEMPTS,
593
597
  ) -> Callable[[Callable[P, R]], Callable[P, R]]:
594
598
  """Decorate a function for use as a DBOS workflow."""
595
- return decorate_workflow(_get_or_create_dbos_registry(), max_recovery_attempts)
599
+ return decorate_workflow(
600
+ _get_or_create_dbos_registry(), name, max_recovery_attempts
601
+ )
596
602
 
597
603
  @classmethod
598
604
  def transaction(
599
- cls, isolation_level: IsolationLevel = "SERIALIZABLE"
605
+ cls,
606
+ isolation_level: IsolationLevel = "SERIALIZABLE",
607
+ *,
608
+ name: Optional[str] = None,
600
609
  ) -> Callable[[F], F]:
601
610
  """
602
611
  Decorate a function for use as a DBOS transaction.
@@ -605,12 +614,15 @@ class DBOS:
605
614
  isolation_level(IsolationLevel): Transaction isolation level
606
615
 
607
616
  """
608
- return decorate_transaction(_get_or_create_dbos_registry(), isolation_level)
617
+ return decorate_transaction(
618
+ _get_or_create_dbos_registry(), name, isolation_level
619
+ )
609
620
 
610
621
  @classmethod
611
622
  def step(
612
623
  cls,
613
624
  *,
625
+ name: Optional[str] = None,
614
626
  retries_allowed: bool = False,
615
627
  interval_seconds: float = 1.0,
616
628
  max_attempts: int = 3,
@@ -629,6 +641,7 @@ class DBOS:
629
641
 
630
642
  return decorate_step(
631
643
  _get_or_create_dbos_registry(),
644
+ name=name,
632
645
  retries_allowed=retries_allowed,
633
646
  interval_seconds=interval_seconds,
634
647
  max_attempts=max_attempts,
@@ -13,6 +13,7 @@ from ._context import SetWorkflowID
13
13
  from ._error import DBOSInitializationError
14
14
  from ._kafka_message import KafkaMessage
15
15
  from ._logger import dbos_logger
16
+ from ._registrations import get_dbos_func_name
16
17
 
17
18
  _KafkaConsumerWorkflow = Callable[[KafkaMessage], None]
18
19
 
@@ -44,7 +45,7 @@ def _kafka_consumer_loop(
44
45
  config["auto.offset.reset"] = "earliest"
45
46
 
46
47
  if config.get("group.id") is None:
47
- config["group.id"] = safe_group_name(func.__qualname__, topics)
48
+ config["group.id"] = safe_group_name(get_dbos_func_name(func), topics)
48
49
  dbos_logger.warning(
49
50
  f"Consumer group ID not found. Using generated group.id {config['group.id']}"
50
51
  )
@@ -4,15 +4,17 @@ from enum import Enum
4
4
  from types import FunctionType
5
5
  from typing import Any, Callable, List, Literal, Optional, Tuple, Type, cast
6
6
 
7
+ from dbos._error import DBOSWorkflowFunctionNotFoundError
8
+
7
9
  DEFAULT_MAX_RECOVERY_ATTEMPTS = 100
8
10
 
9
11
 
10
12
  def get_dbos_func_name(f: Any) -> str:
11
13
  if hasattr(f, "dbos_function_name"):
12
14
  return str(getattr(f, "dbos_function_name"))
13
- if hasattr(f, "__qualname__"):
14
- return str(getattr(f, "__qualname__"))
15
- return "<unknown>"
15
+ raise DBOSWorkflowFunctionNotFoundError(
16
+ "<NONE>", f"function {f.__name__} is not registered"
17
+ )
16
18
 
17
19
 
18
20
  def set_dbos_func_name(f: Any, name: str) -> None:
@@ -10,6 +10,7 @@ from ._context import DBOSAssumeRole, get_local_dbos_context
10
10
  from ._registrations import (
11
11
  DBOSFuncInfo,
12
12
  get_class_info_for_func,
13
+ get_dbos_func_name,
13
14
  get_or_create_class_info,
14
15
  get_or_create_func_info,
15
16
  )
@@ -36,7 +37,7 @@ def check_required_roles(
36
37
  ctx = get_local_dbos_context()
37
38
  if ctx is None or ctx.authenticated_roles is None:
38
39
  raise DBOSNotAuthorizedError(
39
- f"Function {func.__name__} requires a role, but was called in a context without authentication information"
40
+ f"Function {get_dbos_func_name(func)} requires a role, but was called in a context without authentication information"
40
41
  )
41
42
 
42
43
  for r in required_roles:
@@ -44,7 +45,7 @@ def check_required_roles(
44
45
  return r
45
46
 
46
47
  raise DBOSNotAuthorizedError(
47
- f"Function {func.__name__} has required roles, but user is not authenticated for any of them"
48
+ f"Function {get_dbos_func_name(func)} has required roles, but user is not authenticated for any of them"
48
49
  )
49
50
 
50
51
 
@@ -11,6 +11,7 @@ if TYPE_CHECKING:
11
11
 
12
12
  from ._context import SetWorkflowID
13
13
  from ._croniter import croniter # type: ignore
14
+ from ._registrations import get_dbos_func_name
14
15
 
15
16
  ScheduledWorkflow = Callable[[datetime, datetime], None]
16
17
 
@@ -24,20 +25,22 @@ def scheduler_loop(
24
25
  iter = croniter(cron, datetime.now(timezone.utc), second_at_beginning=True)
25
26
  except Exception as e:
26
27
  dbos_logger.error(
27
- f'Cannot run scheduled function {func.__name__}. Invalid crontab "{cron}"'
28
+ f'Cannot run scheduled function {get_dbos_func_name(func)}. Invalid crontab "{cron}"'
28
29
  )
29
30
  while not stop_event.is_set():
30
31
  nextExecTime = iter.get_next(datetime)
31
32
  sleepTime = nextExecTime - datetime.now(timezone.utc)
32
33
  if stop_event.wait(timeout=sleepTime.total_seconds()):
33
34
  return
34
- with SetWorkflowID(f"sched-{func.__qualname__}-{nextExecTime.isoformat()}"):
35
- try:
35
+ try:
36
+ with SetWorkflowID(
37
+ f"sched-{get_dbos_func_name(func)}-{nextExecTime.isoformat()}"
38
+ ):
36
39
  scheduler_queue.enqueue(func, nextExecTime, datetime.now(timezone.utc))
37
- except Exception:
38
- dbos_logger.warning(
39
- f"Exception encountered in scheduler thread: {traceback.format_exc()})"
40
- )
40
+ except Exception:
41
+ dbos_logger.warning(
42
+ f"Exception encountered in scheduler thread: {traceback.format_exc()})"
43
+ )
41
44
 
42
45
 
43
46
  def scheduled(
@@ -48,7 +51,7 @@ def scheduled(
48
51
  croniter(cron, datetime.now(timezone.utc), second_at_beginning=True)
49
52
  except Exception as e:
50
53
  raise ValueError(
51
- f'Invalid crontab "{cron}" for scheduled function function {func.__name__}.'
54
+ f'Invalid crontab "{cron}" for scheduled function function {get_dbos_func_name(func)}.'
52
55
  )
53
56
 
54
57
  global scheduler_queue
@@ -27,7 +27,7 @@ dependencies = [
27
27
  ]
28
28
  requires-python = ">=3.9"
29
29
  readme = "README.md"
30
- version = "1.6.0a3"
30
+ version = "1.6.0a4"
31
31
 
32
32
  [project.license]
33
33
  text = "MIT"
@@ -15,6 +15,7 @@ from tests.conftest import queue_entries_are_cleaned_up
15
15
 
16
16
  def test_required_roles(dbos: DBOS) -> None:
17
17
  @DBOS.required_roles(["user"])
18
+ @DBOS.workflow(name="tfunc")
18
19
  def tfunc(var: str) -> str:
19
20
  assert assert_current_dbos_context().assumed_role == "user"
20
21
  return var
@@ -15,6 +15,7 @@ import sqlalchemy as sa
15
15
  from dbos import (
16
16
  DBOS,
17
17
  DBOSConfig,
18
+ Queue,
18
19
  SetWorkflowID,
19
20
  SetWorkflowTimeout,
20
21
  WorkflowHandle,
@@ -1190,10 +1191,13 @@ def test_debug_logging(
1190
1191
  result1 = test_workflow()
1191
1192
 
1192
1193
  assert result1 == "Step: Hello, Transaction: World"
1193
- assert "Running step" in caplog.text and "name: step_function" in caplog.text
1194
+ assert (
1195
+ "Running step" in caplog.text
1196
+ and f"name: {step_function.__qualname__}" in caplog.text
1197
+ )
1194
1198
  assert (
1195
1199
  "Running transaction" in caplog.text
1196
- and "name: transaction_function" in caplog.text
1200
+ and f"name: {transaction_function.__qualname__}" in caplog.text
1197
1201
  )
1198
1202
  assert "Running sleep" in caplog.text
1199
1203
  assert "Running set_event" in caplog.text
@@ -1233,10 +1237,13 @@ def test_debug_logging(
1233
1237
  result3 = test_workflow()
1234
1238
 
1235
1239
  assert result3 == result1
1236
- assert "Replaying step" in caplog.text and "name: step_function" in caplog.text
1240
+ assert (
1241
+ "Replaying step" in caplog.text
1242
+ and f"name: {step_function.__qualname__}" in caplog.text
1243
+ )
1237
1244
  assert (
1238
1245
  "Replaying transaction" in caplog.text
1239
- and "name: transaction_function" in caplog.text
1246
+ and f"name: {transaction_function.__qualname__}" in caplog.text in caplog.text
1240
1247
  )
1241
1248
  assert "Replaying sleep" in caplog.text
1242
1249
  assert "Replaying set_event" in caplog.text
@@ -1556,3 +1563,48 @@ def test_workflow_timeout(dbos: DBOS) -> None:
1556
1563
  assert assert_current_dbos_context().workflow_timeout_ms is None
1557
1564
  assert assert_current_dbos_context().workflow_timeout_ms == 1000
1558
1565
  assert get_local_dbos_context() is None
1566
+
1567
+
1568
+ def test_custom_names(dbos: DBOS) -> None:
1569
+ workflow_name = "workflow_name"
1570
+ step_name = "step_name"
1571
+ txn_name = "txn_name"
1572
+ queue = Queue("test-queue")
1573
+
1574
+ @DBOS.workflow(name=workflow_name)
1575
+ def workflow() -> str:
1576
+ return DBOS.workflow_id
1577
+
1578
+ handle = queue.enqueue(workflow)
1579
+ assert handle.get_status().name == workflow_name
1580
+ assert handle.get_result() == handle.workflow_id
1581
+
1582
+ @DBOS.step(name=step_name)
1583
+ def step() -> str:
1584
+ return DBOS.workflow_id
1585
+
1586
+ handle = queue.enqueue(step)
1587
+ assert handle.get_status().name == f"<temp>.{step_name}"
1588
+ assert handle.get_result() == handle.workflow_id
1589
+
1590
+ @DBOS.transaction(name=txn_name)
1591
+ def txn() -> str:
1592
+ return DBOS.workflow_id
1593
+
1594
+ handle = queue.enqueue(txn)
1595
+ assert handle.get_status().name == f"<temp>.{txn_name}"
1596
+ assert handle.get_result() == handle.workflow_id
1597
+
1598
+ # Verify we can declare another workflow with the same function name
1599
+ # but a different custom name
1600
+
1601
+ another_workflow = "another_workflow"
1602
+
1603
+ @DBOS.workflow(name=another_workflow)
1604
+ def workflow(x: int) -> int:
1605
+ return x
1606
+
1607
+ value = 5
1608
+ handle = DBOS.start_workflow(workflow, value) # type: ignore
1609
+ assert handle.get_status().name == another_workflow
1610
+ assert handle.get_result() == value # type: ignore
@@ -325,7 +325,7 @@ def test_step_retries(dbos: DBOS) -> None:
325
325
  def enqueue_failing_step() -> None:
326
326
  queue.enqueue(failing_step).get_result()
327
327
 
328
- error_message = f"Step {failing_step.__name__} has exceeded its maximum of {max_attempts} retries"
328
+ error_message = f"Step {failing_step.__qualname__} has exceeded its maximum of {max_attempts} retries"
329
329
 
330
330
  # Test calling the step directly
331
331
  with pytest.raises(DBOSMaxStepRetriesExceeded) as excinfo:
@@ -123,7 +123,7 @@ def test_simple_endpoint(dbos_fastapi: Tuple[DBOS, FastAPI]) -> None:
123
123
  assert span.attributes is not None
124
124
 
125
125
  span = spans[0]
126
- assert span.name == "test_user_endpoint"
126
+ assert span.name == test_user_endpoint.__qualname__
127
127
  assert span.parent is not None
128
128
  assert span.parent.span_id == spans[1].context.span_id
129
129
  assert span.attributes is not None
@@ -171,7 +171,7 @@ def test_simple_endpoint(dbos_fastapi: Tuple[DBOS, FastAPI]) -> None:
171
171
  assert response.status_code == 403
172
172
  assert (
173
173
  response.text
174
- == '{"message":"Function test_admin_endpoint has required roles, but user is not authenticated for any of them","dbos_error_code":"8","dbos_error":"DBOSNotAuthorizedError"}'
174
+ == '{"message":"Function test_simple_endpoint.<locals>.test_admin_endpoint has required roles, but user is not authenticated for any of them","dbos_error_code":"8","dbos_error":"DBOSNotAuthorizedError"}'
175
175
  )
176
176
 
177
177
 
@@ -293,7 +293,7 @@ def test_jwt_endpoint(dbos_fastapi: Tuple[DBOS, FastAPI]) -> None:
293
293
  assert response.status_code == 403
294
294
  assert (
295
295
  response.text
296
- == '{"message":"Function test_admin_endpoint has required roles, but user is not authenticated for any of them","dbos_error_code":"8","dbos_error":"DBOSNotAuthorizedError"}'
296
+ == '{"message":"Function test_jwt_endpoint.<locals>.test_admin_endpoint has required roles, but user is not authenticated for any of them","dbos_error_code":"8","dbos_error":"DBOSNotAuthorizedError"}'
297
297
  )
298
298
 
299
299
 
@@ -7,6 +7,7 @@ from sqlalchemy.engine import Engine
7
7
 
8
8
  # Public API
9
9
  from dbos import DBOS
10
+ from dbos._error import DBOSWorkflowFunctionNotFoundError
10
11
 
11
12
 
12
13
  def simulate_db_restart(engine: Engine, downtime: float) -> None:
@@ -155,6 +156,19 @@ def test_scheduled_transaction(dbos: DBOS) -> None:
155
156
  assert txn_counter > 2 and txn_counter <= 4
156
157
 
157
158
 
159
+ def test_scheduled_step(dbos: DBOS) -> None:
160
+ step_counter: int = 0
161
+
162
+ @DBOS.scheduled("* * * * * *")
163
+ @DBOS.step()
164
+ def test_step(scheduled: datetime, actual: datetime) -> None:
165
+ nonlocal step_counter
166
+ step_counter += 1
167
+
168
+ time.sleep(4)
169
+ assert step_counter > 2 and step_counter <= 4
170
+
171
+
158
172
  def test_scheduled_workflow_exception(dbos: DBOS) -> None:
159
173
  wf_counter: int = 0
160
174
 
@@ -240,5 +254,5 @@ def my_function():
240
254
  pass
241
255
  """
242
256
  # Use exec to run the code and catch the expected exception
243
- with pytest.raises(ValueError, match="Invalid crontab"):
257
+ with pytest.raises(DBOSWorkflowFunctionNotFoundError) as excinfo:
244
258
  exec(code)
@@ -41,7 +41,7 @@ def test_spans(config: DBOSConfig) -> None:
41
41
 
42
42
  spans = exporter.get_finished_spans()
43
43
 
44
- assert len(spans) == 4
44
+ assert len(spans) == 5
45
45
 
46
46
  for span in spans:
47
47
  assert span.attributes is not None
@@ -50,15 +50,17 @@ def test_spans(config: DBOSConfig) -> None:
50
50
  assert span.context is not None
51
51
  assert span.attributes["foo"] == "bar"
52
52
 
53
- assert spans[0].name == test_step.__name__
53
+ assert spans[0].name == test_step.__qualname__
54
54
  assert spans[1].name == "a new span"
55
- assert spans[2].name == test_workflow.__name__
56
- assert spans[3].name == test_step.__name__
55
+ assert spans[2].name == test_workflow.__qualname__
56
+ assert spans[3].name == test_step.__qualname__
57
+ assert spans[4].name == f"<temp>.{test_step.__qualname__}"
57
58
 
58
59
  assert spans[0].parent.span_id == spans[2].context.span_id # type: ignore
59
60
  assert spans[1].parent.span_id == spans[2].context.span_id # type: ignore
60
61
  assert spans[2].parent == None
61
- assert spans[3].parent == None
62
+ assert spans[3].parent.span_id == spans[4].context.span_id # type: ignore
63
+ assert spans[4].parent == None
62
64
 
63
65
 
64
66
  @pytest.mark.asyncio
@@ -87,7 +89,7 @@ async def test_spans_async(dbos: DBOS) -> None:
87
89
 
88
90
  spans = exporter.get_finished_spans()
89
91
 
90
- assert len(spans) == 4
92
+ assert len(spans) == 5
91
93
 
92
94
  for span in spans:
93
95
  assert span.attributes is not None
@@ -95,15 +97,17 @@ async def test_spans_async(dbos: DBOS) -> None:
95
97
  assert span.attributes["executorID"] == GlobalParams.executor_id
96
98
  assert span.context is not None
97
99
 
98
- assert spans[0].name == test_step.__name__
100
+ assert spans[0].name == test_step.__qualname__
99
101
  assert spans[1].name == "a new span"
100
- assert spans[2].name == test_workflow.__name__
101
- assert spans[3].name == test_step.__name__
102
+ assert spans[2].name == test_workflow.__qualname__
103
+ assert spans[3].name == test_step.__qualname__
104
+ assert spans[4].name == f"<temp>.{test_step.__qualname__}"
102
105
 
103
106
  assert spans[0].parent.span_id == spans[2].context.span_id # type: ignore
104
107
  assert spans[1].parent.span_id == spans[2].context.span_id # type: ignore
105
108
  assert spans[2].parent == None
106
- assert spans[3].parent == None
109
+ assert spans[3].parent.span_id == spans[4].context.span_id # type: ignore
110
+ assert spans[4].parent == None
107
111
 
108
112
 
109
113
  def test_temp_wf_fastapi(dbos_fastapi: Tuple[DBOS, FastAPI]) -> None:
@@ -127,15 +131,17 @@ def test_temp_wf_fastapi(dbos_fastapi: Tuple[DBOS, FastAPI]) -> None:
127
131
 
128
132
  spans = exporter.get_finished_spans()
129
133
 
130
- assert len(spans) == 2
134
+ assert len(spans) == 3
131
135
 
132
136
  for span in spans:
133
137
  assert span.attributes is not None
134
138
  assert span.attributes["applicationVersion"] == GlobalParams.app_version
135
139
  assert span.context is not None
136
140
 
137
- assert spans[0].name == test_step_endpoint.__name__
138
- assert spans[1].name == "/step"
141
+ assert spans[0].name == test_step_endpoint.__qualname__
142
+ assert spans[1].name == f"<temp>.{test_step_endpoint.__qualname__}"
143
+ assert spans[2].name == "/step"
139
144
 
140
- assert spans[0].parent.span_id == spans[1].context.span_id # type:ignore
141
- assert spans[1].parent == None
145
+ assert spans[0].parent.span_id == spans[1].context.span_id # type: ignore
146
+ assert spans[1].parent.span_id == spans[2].context.span_id # type: ignore
147
+ assert spans[2].parent == None
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