labtasker 0.2.11__tar.gz → 0.2.13__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 (216) hide show
  1. {labtasker-0.2.11 → labtasker-0.2.13}/PKG-INFO +15 -15
  2. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/__init__.py +4 -1
  3. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/cli/worker.py +7 -0
  4. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/api.py +9 -2
  5. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/heartbeat.py +16 -5
  6. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/job_runner.py +9 -2
  7. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/paths.py +3 -2
  8. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/filtering.py +10 -5
  9. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/server/database.py +51 -11
  10. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/server/endpoints.py +5 -7
  11. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/server/fsm.py +40 -3
  12. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker.egg-info/SOURCES.txt +2 -1
  13. {labtasker-0.2.11 → labtasker-0.2.13}/pyproject.toml +15 -16
  14. labtasker-0.2.13/tests/test_client/test_core/test_heartbeat_e2e.py +73 -0
  15. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_fsm.py +1 -1
  16. {labtasker-0.2.11 → labtasker-0.2.13}/LICENSE +0 -0
  17. {labtasker-0.2.11 → labtasker-0.2.13}/MANIFEST.in +0 -0
  18. {labtasker-0.2.11 → labtasker-0.2.13}/README.md +0 -0
  19. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/__main__.py +0 -0
  20. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/api_models.py +0 -0
  21. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/__init__.py +0 -0
  22. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/cli/__init__.py +0 -0
  23. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/cli/cli.py +0 -0
  24. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/cli/config.py +0 -0
  25. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/cli/event.py +0 -0
  26. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/cli/init.py +0 -0
  27. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/cli/loop.py +0 -0
  28. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/cli/queue.py +0 -0
  29. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/cli/task.py +0 -0
  30. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/client_api.py +0 -0
  31. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/__init__.py +0 -0
  32. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/cli_utils.py +0 -0
  33. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/cmd_parser/LabCmd.g4 +0 -0
  34. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/cmd_parser/LabCmdLexer.g4 +0 -0
  35. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/cmd_parser/__init__.py +0 -0
  36. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/cmd_parser/generated/LabCmd.py +0 -0
  37. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/cmd_parser/generated/LabCmdLexer.py +0 -0
  38. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/cmd_parser/generated/LabCmdListener.py +0 -0
  39. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/cmd_parser/generated/__init__.py +0 -0
  40. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/cmd_parser/parser.py +0 -0
  41. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/config.py +0 -0
  42. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/context.py +0 -0
  43. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/events.py +0 -0
  44. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/exceptions.py +0 -0
  45. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/logging.py +0 -0
  46. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/pager.py +0 -0
  47. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/plugin_utils.py +0 -0
  48. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/query_transpiler.py +0 -0
  49. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/resolver/__init__.py +0 -0
  50. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/resolver/models.py +0 -0
  51. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/resolver/utils.py +0 -0
  52. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/utils.py +0 -0
  53. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/version_checker.py +0 -0
  54. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/templates/labtasker_root/.gitignore +0 -0
  55. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/templates/labtasker_root/client.toml +0 -0
  56. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/templates/labtasker_root/logs/.gitkeep +0 -0
  57. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/constants.py +0 -0
  58. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/security.py +0 -0
  59. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/server/__init__.py +0 -0
  60. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/server/cli.py +0 -0
  61. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/server/config.py +0 -0
  62. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/server/db_utils.py +0 -0
  63. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/server/dependencies.py +0 -0
  64. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/server/embedded_db.py +0 -0
  65. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/server/event_manager.py +0 -0
  66. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/server/logging.py +0 -0
  67. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/utils.py +0 -0
  68. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/README.txt +0 -0
  69. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/__init__.py +0 -0
  70. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/BufferedTokenStream.py +0 -0
  71. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/CommonTokenFactory.py +0 -0
  72. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/CommonTokenStream.py +0 -0
  73. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/FileStream.py +0 -0
  74. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/InputStream.py +0 -0
  75. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/IntervalSet.py +0 -0
  76. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/LL1Analyzer.py +0 -0
  77. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/Lexer.py +0 -0
  78. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/ListTokenSource.py +0 -0
  79. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/Parser.py +0 -0
  80. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/ParserInterpreter.py +0 -0
  81. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/ParserRuleContext.py +0 -0
  82. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/PredictionContext.py +0 -0
  83. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/Recognizer.py +0 -0
  84. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/RuleContext.py +0 -0
  85. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/StdinStream.py +0 -0
  86. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/Token.py +0 -0
  87. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/TokenStreamRewriter.py +0 -0
  88. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/Utils.py +0 -0
  89. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/__init__.py +0 -0
  90. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/_pygrun.py +0 -0
  91. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/ATN.py +0 -0
  92. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/ATNConfig.py +0 -0
  93. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/ATNConfigSet.py +0 -0
  94. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/ATNDeserializationOptions.py +0 -0
  95. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/ATNDeserializer.py +0 -0
  96. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/ATNSimulator.py +0 -0
  97. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/ATNState.py +0 -0
  98. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/ATNType.py +0 -0
  99. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/LexerATNSimulator.py +0 -0
  100. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/LexerAction.py +0 -0
  101. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/LexerActionExecutor.py +0 -0
  102. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/ParserATNSimulator.py +0 -0
  103. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/PredictionMode.py +0 -0
  104. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/SemanticContext.py +0 -0
  105. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/Transition.py +0 -0
  106. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/__init__.py +0 -0
  107. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/dfa/DFA.py +0 -0
  108. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/dfa/DFASerializer.py +0 -0
  109. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/dfa/DFAState.py +0 -0
  110. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/dfa/__init__.py +0 -0
  111. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/error/DiagnosticErrorListener.py +0 -0
  112. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/error/ErrorListener.py +0 -0
  113. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/error/ErrorStrategy.py +0 -0
  114. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/error/Errors.py +0 -0
  115. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/error/__init__.py +0 -0
  116. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/tree/Chunk.py +0 -0
  117. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/tree/ParseTreeMatch.py +0 -0
  118. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/tree/ParseTreePattern.py +0 -0
  119. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/tree/ParseTreePatternMatcher.py +0 -0
  120. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/tree/RuleTagToken.py +0 -0
  121. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/tree/TokenTagToken.py +0 -0
  122. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/tree/Tree.py +0 -0
  123. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/tree/Trees.py +0 -0
  124. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/tree/__init__.py +0 -0
  125. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/xpath/XPath.py +0 -0
  126. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/xpath/XPathLexer.py +0 -0
  127. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/xpath/__init__.py +0 -0
  128. {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/vendor.txt +0 -0
  129. {labtasker-0.2.11 → labtasker-0.2.13}/setup.cfg +0 -0
  130. {labtasker-0.2.11 → labtasker-0.2.13}/tests/__init__.py +0 -0
  131. {labtasker-0.2.11 → labtasker-0.2.13}/tests/conftest.py +0 -0
  132. {labtasker-0.2.11 → labtasker-0.2.13}/tests/demo_pager_iterator.py +0 -0
  133. {labtasker-0.2.11 → labtasker-0.2.13}/tests/dummy_jobs/job_1.py +0 -0
  134. {labtasker-0.2.11 → labtasker-0.2.13}/tests/fixtures/__init__.py +0 -0
  135. {labtasker-0.2.11 → labtasker-0.2.13}/tests/fixtures/database/__init__.py +0 -0
  136. {labtasker-0.2.11 → labtasker-0.2.13}/tests/fixtures/database/mock.py +0 -0
  137. {labtasker-0.2.11 → labtasker-0.2.13}/tests/fixtures/database/real.py +0 -0
  138. {labtasker-0.2.11 → labtasker-0.2.13}/tests/fixtures/logging.py +0 -0
  139. {labtasker-0.2.11 → labtasker-0.2.13}/tests/fixtures/mock_datetime_now.py +0 -0
  140. {labtasker-0.2.11 → labtasker-0.2.13}/tests/fixtures/server/__init__.py +0 -0
  141. {labtasker-0.2.11 → labtasker-0.2.13}/tests/fixtures/server/async_app.py +0 -0
  142. {labtasker-0.2.11 → labtasker-0.2.13}/tests/fixtures/server/sync_app.py +0 -0
  143. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_api_models.py +0 -0
  144. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/__init__.py +0 -0
  145. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/conftest.py +0 -0
  146. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_cli/__init__.py +0 -0
  147. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_cli/conftest.py +0 -0
  148. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_cli/test_basic.py +0 -0
  149. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_cli/test_config.py +0 -0
  150. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_cli/test_event.py +0 -0
  151. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_cli/test_init.py +0 -0
  152. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_cli/test_loop.py +0 -0
  153. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_cli/test_queue.py +0 -0
  154. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_cli/test_task.py +0 -0
  155. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_cli/test_worker.py +0 -0
  156. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/__init__.py +0 -0
  157. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_cli_utils.py +0 -0
  158. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_event/__init__.py +0 -0
  159. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_event/test_concurrency_job_flow_event.py +0 -0
  160. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_event/test_event_listener_basic.py +0 -0
  161. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_event/test_event_listener_entity_data.py +0 -0
  162. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_event/test_various_actions.py +0 -0
  163. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_event/utils.py +0 -0
  164. /labtasker-0.2.11/tests/test_client/test_core/test_heartbeat.py → /labtasker-0.2.13/tests/test_client/test_core/test_heartbeat_unit.py +0 -0
  165. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_job_runner.py +0 -0
  166. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_logging.py +0 -0
  167. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_loop_internal_error_handler.py +0 -0
  168. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_pager_iterator.py +0 -0
  169. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_parser.py +0 -0
  170. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_query_transpiler/__init__.py +0 -0
  171. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_query_transpiler/conftest.py +0 -0
  172. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_query_transpiler/test_behavior.py +0 -0
  173. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_query_transpiler/test_matching.py +0 -0
  174. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_query_transpiler/test_utils.py +0 -0
  175. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_query_transpiler/utils.py +0 -0
  176. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_resolver.py +0 -0
  177. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_runner_concurrency/__init__.py +0 -0
  178. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_runner_concurrency/run_concurrent.py +0 -0
  179. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_runner_concurrency/test_runner_concurrency_success_failure.py +0 -0
  180. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_runner_concurrency/test_runner_high_concurrency.py +0 -0
  181. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_runner_timeout/__init__.py +0 -0
  182. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_runner_timeout/conftest.py +0 -0
  183. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_runner_timeout/test_job_runner_timeout.py +0 -0
  184. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_runner_timeout/test_job_runner_with_resolver_timeout.py +0 -0
  185. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_runner_with_resolver.py +0 -0
  186. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_server_notification_and_client_version.py +0 -0
  187. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_version_checker.py +0 -0
  188. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_filtering/__init__.py +0 -0
  189. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_filtering/exception_utils.py +0 -0
  190. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_filtering/test_exception_filtering.py +0 -0
  191. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_mock_time.py +0 -0
  192. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_security.py +0 -0
  193. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/__init__.py +0 -0
  194. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/conftest.py +0 -0
  195. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_database/__init__.py +0 -0
  196. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_database/conftest.py +0 -0
  197. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_database/test_database_basic.py +0 -0
  198. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_database/test_fetch_extra_filter.py +0 -0
  199. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_database/test_query_dict_to_mongo_filter.py +0 -0
  200. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_database/test_required_field_fetching.py +0 -0
  201. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_db_utils/__init__.py +0 -0
  202. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_db_utils/test_arg_match.py +0 -0
  203. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_db_utils/test_keys_to_query_dict.py +0 -0
  204. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_db_utils/test_keys_to_query_dict_deepest.py +0 -0
  205. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_db_utils/test_keys_to_query_dict_topmost.py +0 -0
  206. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_embedded_db.py +0 -0
  207. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_endpoint/__init__.py +0 -0
  208. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_endpoint/test_event_basic.py +0 -0
  209. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_endpoint/test_server.py +0 -0
  210. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_endpoint/test_server_async.py +0 -0
  211. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_endpoint/test_server_async_ping.py +0 -0
  212. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_get_verified_queue_dependency.py +0 -0
  213. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_utils/__init__.py +0 -0
  214. {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_utils/test_utils.py +0 -0
  215. {labtasker-0.2.11 → labtasker-0.2.13}/tests/utils.py +0 -0
  216. {labtasker-0.2.11 → labtasker-0.2.13}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labtasker
3
- Version: 0.2.11
3
+ Version: 0.2.13
4
4
  Summary: A task queue system for lab experiments
5
5
  Author-email: Your Name <your.email@example.com>
6
6
  License: Apache License 2.0
@@ -19,19 +19,19 @@ Requires-Python: <4.0,>=3.10
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
21
  Requires-Dist: pymongo<5.0.0,>=4.0.0
22
- Requires-Dist: fastapi<0.117.0,>=0.115.0
23
- Requires-Dist: uvicorn[standard]<0.36.0,>=0.15.0
22
+ Requires-Dist: fastapi<0.133.0,>=0.115.0
23
+ Requires-Dist: uvicorn[standard]<0.42.0,>=0.15.0
24
24
  Requires-Dist: click<9.0.0,>=8.2.0
25
25
  Requires-Dist: passlib<2.0.0,>=1.7.0
26
26
  Requires-Dist: pydantic-settings<3.0.0,>=2.8.0
27
27
  Requires-Dist: httpx[socks]<0.29.0,>=0.28.0
28
- Requires-Dist: typer<0.17.0,>=0.16.0
28
+ Requires-Dist: typer<0.25.0,>=0.16.0
29
29
  Requires-Dist: loguru<0.8.0,>=0.7.0
30
- Requires-Dist: ruamel-yaml<0.19.0,>=0.18.10
30
+ Requires-Dist: ruamel-yaml<0.20.0,>=0.18.10
31
31
  Requires-Dist: pyyaml<7.0.0,>=6.0.0
32
- Requires-Dist: tomlkit<0.14.0,>=0.13.2
32
+ Requires-Dist: tomlkit<0.15.0,>=0.13.2
33
33
  Requires-Dist: importlib-metadata<9.0.0,>=8.5.0
34
- Requires-Dist: packaging<26.0,>=24.2
34
+ Requires-Dist: packaging<27.0,>=24.2
35
35
  Requires-Dist: sse-starlette<4.0.0,>=2.1.3
36
36
  Requires-Dist: httpx-sse<0.5.0,>=0.4.0
37
37
  Requires-Dist: stamina<26.0.0,>=25.1.0
@@ -43,24 +43,24 @@ Requires-Dist: pexpect<5.0.0,>=4.9.0
43
43
  Requires-Dist: pip>=25
44
44
  Requires-Dist: dateparser<2.0.0,>=1.2.2
45
45
  Provides-Extra: dev
46
- Requires-Dist: pytest<9.0.0,>=8.0.0; extra == "dev"
47
- Requires-Dist: pytest-cov<7.0.0,>=5.0.0; extra == "dev"
48
- Requires-Dist: black<26.0.0,>=24.0.0; extra == "dev"
49
- Requires-Dist: isort<7.0.0,>=5.13.0; extra == "dev"
46
+ Requires-Dist: pytest<10.0.0,>=8.0.0; extra == "dev"
47
+ Requires-Dist: pytest-cov<8.0.0,>=5.0.0; extra == "dev"
48
+ Requires-Dist: black<27.0.0,>=24.0.0; extra == "dev"
49
+ Requires-Dist: isort<9.0.0,>=5.13.0; extra == "dev"
50
50
  Requires-Dist: mypy<2.0.0,>=1.14.0; extra == "dev"
51
51
  Requires-Dist: flake8<8.0.0,>=7.0.0; extra == "dev"
52
52
  Requires-Dist: pre-commit<5.0.0,>=3.0.0; extra == "dev"
53
53
  Requires-Dist: freezegun<2.0.0,>=1.5.0; extra == "dev"
54
54
  Requires-Dist: pytest-docker<4.0.0,>=3.0.0; extra == "dev"
55
- Requires-Dist: pytest-asyncio<1.2.0,>=0.24.0; extra == "dev"
55
+ Requires-Dist: pytest-asyncio<1.4.0,>=0.24.0; extra == "dev"
56
56
  Requires-Dist: asgi-lifespan<3.0.0,>=2.1.0; extra == "dev"
57
- Requires-Dist: tox<4.29.0,>=4.24.0; extra == "dev"
57
+ Requires-Dist: tox<4.46.0,>=4.24.0; extra == "dev"
58
58
  Requires-Dist: pytest-dependency<0.7.0,>=0.6.0; extra == "dev"
59
59
  Requires-Dist: pytest-sugar<2.0.0,>=1.0.0; extra == "dev"
60
60
  Requires-Dist: rust-just<2.0.0,>=1.42.4; extra == "dev"
61
61
  Provides-Extra: doc
62
- Requires-Dist: mkdocs-material<9.7.0,>=9.6.5; extra == "doc"
63
- Requires-Dist: mkdocs-glightbox<0.5.0,>=0.4.0; extra == "doc"
62
+ Requires-Dist: mkdocs-material<9.8.0,>=9.6.5; extra == "doc"
63
+ Requires-Dist: mkdocs-glightbox<0.6.0,>=0.4.0; extra == "doc"
64
64
  Requires-Dist: mike<2.2.0,>=2.1.3; extra == "doc"
65
65
  Provides-Extra: plugins
66
66
  Requires-Dist: labtasker-plugin-task-count; extra == "plugins"
@@ -1,4 +1,7 @@
1
- __version__ = "0.2.11"
1
+ from importlib.metadata import version
2
+
3
+ __application__ = "labtasker"
4
+ __version__: str = version(__application__)
2
5
 
3
6
  from labtasker.client.client_api import *
4
7
  from labtasker.client.core.config import get_client_config
@@ -102,6 +102,12 @@ def ls(
102
102
  "--name",
103
103
  help="Filter by worker name.",
104
104
  ),
105
+ status: Optional[str] = typer.Option(
106
+ None,
107
+ "--status",
108
+ "-s",
109
+ help="Filter by worker status. One of `active`, `suspended`, `crashed`.",
110
+ ),
105
111
  extra_filter: Optional[str] = typer.Option(
106
112
  None,
107
113
  "--extra-filter",
@@ -164,6 +170,7 @@ def ls(
164
170
  ls_workers,
165
171
  worker_id=worker_id,
166
172
  worker_name=worker_name,
173
+ status=status,
167
174
  extra_filter=extra_filter,
168
175
  ),
169
176
  offset=offset,
@@ -64,11 +64,15 @@ __all__ = [
64
64
  ]
65
65
 
66
66
 
67
+ def _is_network_transient_error(exception):
68
+ return isinstance(exception, (httpx.TransportError, ConnectionError, TimeoutError))
69
+
70
+
67
71
  def _network_err_retry(func):
68
72
  @wraps(func)
69
73
  def wrapper(*args, **kwargs):
70
74
  return stamina.retry(
71
- on=httpx.TransportError,
75
+ on=_is_network_transient_error,
72
76
  attempts=10,
73
77
  timeout=100.0,
74
78
  wait_initial=0.5,
@@ -278,12 +282,15 @@ def report_task_status(
278
282
  @_network_err_retry
279
283
  def refresh_task_heartbeat(
280
284
  task_id: str,
285
+ worker_id: Optional[str] = None,
281
286
  client: Optional[httpx.Client] = None,
282
287
  ) -> None:
283
288
  """Refresh the heartbeat of a task."""
284
289
  if client is None:
285
290
  client = get_httpx_client()
286
- response = client.post(f"/api/v1/queues/me/tasks/{task_id}/heartbeat")
291
+ response = client.post(
292
+ f"/api/v1/queues/me/tasks/{task_id}/heartbeat", params={"worker_id": worker_id}
293
+ )
287
294
  raise_for_status(response)
288
295
 
289
296
 
@@ -18,8 +18,9 @@ __all__ = [
18
18
 
19
19
  class Heartbeat:
20
20
 
21
- def __init__(self, task_id, heartbeat_interval):
21
+ def __init__(self, task_id, worker_id, heartbeat_interval):
22
22
  self.task_id = task_id
23
+ self.worker_id = worker_id
23
24
  self.heartbeat_interval = heartbeat_interval
24
25
 
25
26
  self._thread = None
@@ -39,6 +40,7 @@ class Heartbeat:
39
40
  self._thread.start()
40
41
 
41
42
  def delay(self, interval: float) -> bool:
43
+ """Returns False if it should exit."""
42
44
  slice_t = 0.05 # check for stop event
43
45
  start_time = time.perf_counter()
44
46
  while True:
@@ -67,11 +69,12 @@ class Heartbeat:
67
69
  """Refresh heartbeat periodically"""
68
70
  while True:
69
71
  try:
70
- refresh_task_heartbeat(task_id=self.task_id)
72
+ refresh_task_heartbeat(task_id=self.task_id, worker_id=self.worker_id)
71
73
  except Exception as e:
72
- logger.error(f"Heartbeat failed for task {self.task_id}: {e}")
74
+ logger.error(f"Failed to refresh heartbeat: {str(e)}")
75
+ raise
73
76
 
74
- # check if heartbeat should stop
77
+ # Check if heartbeat should stop
75
78
  if not self.delay(self.heartbeat_interval):
76
79
  break
77
80
 
@@ -85,6 +88,9 @@ class Heartbeat:
85
88
  except FileNotFoundError:
86
89
  pass
87
90
 
91
+ def is_alive(self):
92
+ return self._thread and self._thread.is_alive()
93
+
88
94
 
89
95
  _current_heartbeat: ContextVar[Optional[Heartbeat]] = ContextVar(
90
96
  "heartbeat", default=None
@@ -92,7 +98,10 @@ _current_heartbeat: ContextVar[Optional[Heartbeat]] = ContextVar(
92
98
 
93
99
 
94
100
  def start_heartbeat(
95
- task_id, heartbeat_interval: Optional[float] = None, raise_error=True
101
+ task_id,
102
+ worker_id: Optional[str] = None,
103
+ heartbeat_interval: Optional[float] = None,
104
+ raise_error=True,
96
105
  ):
97
106
  logger.debug("Try starting heartbeat.")
98
107
  if _current_heartbeat.get() is not None:
@@ -102,12 +111,14 @@ def start_heartbeat(
102
111
 
103
112
  heartbeat_manager = Heartbeat(
104
113
  task_id=task_id,
114
+ worker_id=worker_id,
105
115
  heartbeat_interval=heartbeat_interval
106
116
  or get_client_config().task.heartbeat_interval,
107
117
  )
108
118
  heartbeat_manager.start()
109
119
  _current_heartbeat.set(heartbeat_manager)
110
120
  logger.debug("Heartbeat started.")
121
+ return heartbeat_manager
111
122
 
112
123
 
113
124
  def end_heartbeat(raise_error=True):
@@ -220,7 +220,9 @@ def loop_run(
220
220
  dump_task_info()
221
221
 
222
222
  with log_to_file(file_path=get_labtasker_log_dir() / "run.log"):
223
- start_heartbeat(task_id=current_task_id())
223
+ start_heartbeat(
224
+ task_id=current_task_id(), worker_id=current_worker_id()
225
+ )
224
226
  success_flag = False
225
227
  try:
226
228
  func_args = (task.args, *args) if pass_args_dict else args
@@ -339,7 +341,8 @@ def loop_run(
339
341
  if success_flag:
340
342
  # Default finish. Can be overridden by the user if called somewhere deep in the wrapped func().
341
343
  finish(status="success")
342
- end_heartbeat()
344
+ # finish() already calls end_heartbeat(), but use raise_error=False as safety net
345
+ end_heartbeat(raise_error=False)
343
346
  except _LabtaskerLoopExit:
344
347
  # clean up the worker
345
348
  if auto_create_worker: # worker is managed automatically
@@ -389,6 +392,10 @@ def finish(
389
392
  "You can either use @labtasker.loop() decorator or labtasker loop cli to run job."
390
393
  )
391
394
 
395
+ # Stop heartbeat before reporting status to avoid race condition
396
+ # where heartbeat thread tries to refresh a non-running task
397
+ end_heartbeat(raise_error=False)
398
+
392
399
  summary_file_path = get_labtasker_log_dir() / "summary.json"
393
400
  if summary_file_path.exists():
394
401
  # Skip if summary.json exists. Might be already called from subprocess.
@@ -6,12 +6,13 @@ from pathlib import Path
6
6
  from labtasker.client.core.exceptions import LabtaskerRuntimeError
7
7
  from labtasker.utils import get_current_time
8
8
 
9
- _LABTASKER_ROOT = Path(os.environ.get("LABTASKER_ROOT", ".labtasker"))
9
+ # Use absolute path to avoid issues when working directory changes
10
+ _LABTASKER_ROOT = Path(os.environ.get("LABTASKER_ROOT", ".labtasker")).resolve()
10
11
 
11
12
  _labtasker_log_dir = contextvars.ContextVar(
12
13
  "labtasker_root",
13
14
  default=(
14
- Path(os.environ["LABTASKER_LOG_DIR"])
15
+ Path(os.environ["LABTASKER_LOG_DIR"]).resolve()
15
16
  if "LABTASKER_LOG_DIR" in os.environ
16
17
  else None
17
18
  ),
@@ -4,13 +4,16 @@ from contextlib import contextmanager
4
4
  from types import TracebackType
5
5
  from typing import Optional, Set, Type
6
6
 
7
- from rich.traceback import Traceback
8
- from typer.main import console_stderr
9
-
10
7
  _registered_sensitive_texts: Set[str] = set()
11
8
  _hook_enabled = True
12
9
 
13
10
 
11
+ def _get_rich_stderr_console():
12
+ from rich.console import Console
13
+
14
+ return Console(stderr=True)
15
+
16
+
14
17
  def register_sensitive_text(text: str):
15
18
  """Register a sensitive text to be filtered out of tracebacks"""
16
19
  _registered_sensitive_texts.add(text)
@@ -75,7 +78,7 @@ def install_traceback_filter():
75
78
  if sanitized_exc.__str__() != sanitize_text(
76
79
  sanitized_exc.__str__()
77
80
  ) or sanitized_exc.__repr__() != sanitize_text(sanitized_exc.__repr__()):
78
- console_stderr.print(
81
+ _get_rich_stderr_console().print(
79
82
  "[bold orange1]Warning:[/bold orange1] Traceback output has been suppressed due to an unexpected error in the traceback filtering hook. The traceback was intercepted and prevented from displaying. To view tracebacks, set `enable_traceback_filter` to `false` in your .labtasker/client.toml configuration file."
80
83
  )
81
84
  return
@@ -86,6 +89,8 @@ def install_traceback_filter():
86
89
  ]
87
90
 
88
91
  # Use rich traceback for pretty output
92
+ from rich.traceback import Traceback
93
+
89
94
  rich_tb = Traceback.from_exception(
90
95
  type(sanitized_exc),
91
96
  sanitized_exc,
@@ -94,7 +99,7 @@ def install_traceback_filter():
94
99
  suppress=suppress_internal_dir_names,
95
100
  width=80, # Set a default width
96
101
  )
97
- console_stderr.print(rich_tb)
102
+ _get_rich_stderr_console().print(rich_tb)
98
103
 
99
104
  # Install the custom excepthook
100
105
  sys.excepthook = filtered_excepthook
@@ -674,22 +674,52 @@ class DBService:
674
674
  @retry_on_transient
675
675
  @validate_arg
676
676
  def refresh_task_heartbeat(
677
- self,
678
- queue_id: str,
679
- task_id: str,
680
- ) -> bool:
677
+ self, queue_id: str, task_id: str, worker_id: Optional[str] = None
678
+ ):
681
679
  """Update task heartbeat timestamp."""
680
+ query = {"_id": task_id, "queue_id": queue_id, "status": "running"}
681
+
682
682
  with self._client.start_session() as session:
683
683
  with session.start_transaction():
684
- return (
685
- self._tasks.update_one(
686
- {"_id": task_id, "queue_id": queue_id},
687
- {"$set": {"last_heartbeat": get_current_time()}},
688
- session=session,
689
- ).modified_count
690
- > 0
684
+ # Find the task in a single query
685
+ task = self._tasks.find_one(query)
686
+ if not task:
687
+ raise HTTPException(
688
+ status_code=HTTP_404_NOT_FOUND,
689
+ detail=f"Task '{task_id}' not found in queue '{queue_id}' or not in 'running' state",
690
+ )
691
+
692
+ # Validate worker if provided
693
+ if worker_id:
694
+ if task["worker_id"] != worker_id:
695
+ raise HTTPException(
696
+ status_code=HTTP_403_FORBIDDEN,
697
+ detail=f"Task '{task_id}' is assigned to worker '{task['worker_id']}', not '{worker_id}'",
698
+ )
699
+
700
+ # Check worker status in a single query
701
+ worker = self._workers.find_one(
702
+ {"_id": worker_id, "status": WorkerState.ACTIVE}
703
+ )
704
+ if not worker:
705
+ raise HTTPException(
706
+ status_code=HTTP_404_NOT_FOUND,
707
+ detail=f"Worker '{worker_id}' not found or not active",
708
+ )
709
+
710
+ # Update the task heartbeat
711
+ result = self._tasks.update_one(
712
+ query,
713
+ {"$set": {"last_heartbeat": get_current_time()}},
714
+ session=session,
691
715
  )
692
716
 
717
+ if result.modified_count == 0:
718
+ raise HTTPException(
719
+ status_code=HTTP_404_NOT_FOUND,
720
+ detail=f"Failed to update heartbeat for task '{task_id}' - it may have changed state during the operation",
721
+ )
722
+
693
723
  @retry_on_transient
694
724
  @validate_arg
695
725
  def worker_report_task_status(
@@ -921,6 +951,10 @@ class DBService:
921
951
  return_document=ReturnDocument.AFTER,
922
952
  )
923
953
 
954
+ assert (
955
+ updated_task is not None
956
+ ), f"Task {task_id} not found after update"
957
+
924
958
  # if the FSM state is modified by user manually
925
959
  if not reset_pending and updated_task["status"] != task["status"]:
926
960
  event_handle = fsm.transition_to(updated_task["status"])
@@ -992,6 +1026,8 @@ class DBService:
992
1026
  return_document=ReturnDocument.AFTER,
993
1027
  )
994
1028
 
1029
+ assert updated_worker is not None, f"Worker {worker_id} not found after update"
1030
+
995
1031
  # Update the event with entity data and publish
996
1032
  event_handle.update_fsm_event(updated_worker)
997
1033
 
@@ -1162,6 +1198,10 @@ class DBService:
1162
1198
  session=session,
1163
1199
  )
1164
1200
 
1201
+ assert (
1202
+ updated_task is not None
1203
+ ), f"Task {task['_id']} not found after update"
1204
+
1165
1205
  event_handle.update_fsm_event(updated_task)
1166
1206
  fsm_event_handles.append(event_handle)
1167
1207
 
@@ -1,9 +1,9 @@
1
1
  import asyncio
2
2
  import uuid
3
3
  from contextlib import asynccontextmanager
4
- from typing import Any, Dict, List
4
+ from typing import Any, Dict, List, Optional
5
5
 
6
- from fastapi import Depends, FastAPI, HTTPException, Request
6
+ from fastapi import Depends, FastAPI, HTTPException, Query, Request
7
7
  from sse_starlette.sse import EventSourceResponse
8
8
  from starlette.status import (
9
9
  HTTP_201_CREATED,
@@ -309,16 +309,14 @@ def report_task_status(
309
309
  )
310
310
  def refresh_task_heartbeat(
311
311
  task_id: str,
312
+ worker_id: Optional[str] = Query(None), # use query param
312
313
  queue: Dict[str, Any] = Depends(get_verified_queue_dependency),
313
314
  db: DBService = Depends(get_db),
314
315
  ):
315
316
  """Update task heartbeat timestamp."""
316
- done = db.refresh_task_heartbeat(
317
- queue_id=queue["_id"],
318
- task_id=task_id,
317
+ db.refresh_task_heartbeat(
318
+ queue_id=queue["_id"], task_id=task_id, worker_id=worker_id
319
319
  )
320
- if not done:
321
- raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="Task not found.")
322
320
 
323
321
 
324
322
  @app.get(
@@ -43,7 +43,12 @@ class StateTransitionEventHandle:
43
43
  self.commit()
44
44
 
45
45
  def commit(self):
46
- event_data = StateTransitionEvent(
46
+ event_data = self._create_event_data()
47
+ self._publish_event(event_data)
48
+ self._entity_data = None
49
+
50
+ def _create_event_data(self):
51
+ return StateTransitionEvent(
47
52
  entity_type=self.entity_type,
48
53
  queue_id=self.queue_id,
49
54
  entity_id=self.entity_id,
@@ -54,9 +59,17 @@ class StateTransitionEventHandle:
54
59
  entity_data=self._entity_data,
55
60
  )
56
61
 
62
+ def _publish_event(self, event_data):
57
63
  # Use fully synchronous event publishing
58
64
  event_manager.publish_event(self.queue_id, event_data)
59
- self._entity_data = None
65
+
66
+
67
+ class NullEventHandle(StateTransitionEventHandle):
68
+ """A placeholder that does nothing. (Used for cases where triggering event publishing is undesired)"""
69
+
70
+ def _publish_event(self, event_data):
71
+ # Override to do nothing
72
+ pass
60
73
 
61
74
 
62
75
  class State(str, Enum):
@@ -140,6 +153,18 @@ class BaseFSM:
140
153
  def state(self):
141
154
  return self._state
142
155
 
156
+ def null_transition(self) -> NullEventHandle:
157
+ """Perform a null transition and return a handle"""
158
+ return NullEventHandle(
159
+ entity_type=self.ENTITY_TYPE,
160
+ entity_id=self.entity_id,
161
+ queue_id=self.queue_id,
162
+ old_state=str(self._state),
163
+ new_state=str(self._state),
164
+ transition_time=get_current_time(),
165
+ metadata=self.metadata,
166
+ )
167
+
143
168
  def transition_to(self, new_state: State) -> StateTransitionEventHandle:
144
169
  """Perform state transition and return a handle"""
145
170
  old_state = self._state
@@ -190,6 +215,7 @@ class TaskFSM(BaseFSM):
190
215
  TaskState.FAILED: {
191
216
  TaskState.PENDING,
192
217
  TaskState.CANCELLED,
218
+ TaskState.FAILED, # null transition (for more tolerance)
193
219
  }, # Can be reset and requeued
194
220
  TaskState.CANCELLED: {
195
221
  TaskState.PENDING,
@@ -280,11 +306,15 @@ class TaskFSM(BaseFSM):
280
306
  Transitions:
281
307
  - RUNNING -> PENDING (if retries < max_retries)
282
308
  - RUNNING -> FAILED (if retries >= max_retries)
309
+ - FAILED -> FAILED (null transition, does nothing)
283
310
  - Others -> InvalidStateTransition (invalid)
284
311
 
285
312
  Note: FAILED state can transition back to PENDING for retries
286
313
  until max_retries is reached.
287
314
  """
315
+ if self.state == TaskState.FAILED:
316
+ return self.null_transition()
317
+
288
318
  if self.state != TaskState.RUNNING:
289
319
  raise InvalidStateTransition(f"Cannot fail task in {self.state} state")
290
320
 
@@ -305,7 +335,10 @@ class WorkerFSM(BaseFSM):
305
335
  WorkerState.CRASHED,
306
336
  },
307
337
  WorkerState.SUSPENDED: {WorkerState.ACTIVE}, # Manual transition
308
- WorkerState.CRASHED: {WorkerState.ACTIVE}, # Manual transition
338
+ WorkerState.CRASHED: {
339
+ WorkerState.ACTIVE, # Manual transition
340
+ WorkerState.CRASHED, # null transition (for more tolerance)
341
+ },
309
342
  }
310
343
 
311
344
  def __init__(
@@ -366,7 +399,11 @@ class WorkerFSM(BaseFSM):
366
399
  Transitions:
367
400
  - ACTIVE -> ACTIVE
368
401
  - ACTIVE -> CRASHED (retries >= max_retries)
402
+ - CRASHED -> CRASHED (null transition, does nothing)
369
403
  """
404
+ if self.state == WorkerState.CRASHED:
405
+ return self.null_transition()
406
+
370
407
  if self.state != WorkerState.ACTIVE:
371
408
  raise InvalidStateTransition(f"Cannot fail worker in {self.state} state")
372
409
 
@@ -154,7 +154,8 @@ tests/test_client/test_cli/test_task.py
154
154
  tests/test_client/test_cli/test_worker.py
155
155
  tests/test_client/test_core/__init__.py
156
156
  tests/test_client/test_core/test_cli_utils.py
157
- tests/test_client/test_core/test_heartbeat.py
157
+ tests/test_client/test_core/test_heartbeat_e2e.py
158
+ tests/test_client/test_core/test_heartbeat_unit.py
158
159
  tests/test_client/test_core/test_job_runner.py
159
160
  tests/test_client/test_core/test_logging.py
160
161
  tests/test_client/test_core/test_loop_internal_error_handler.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "labtasker"
7
- version = "0.2.11"
7
+ version = "0.2.13"
8
8
  description = "A task queue system for lab experiments"
9
9
  authors = [{ name = "Your Name", email = "your.email@example.com" }]
10
10
  license = { text = "Apache License 2.0" }
@@ -23,19 +23,19 @@ classifiers = [
23
23
 
24
24
  dependencies = [
25
25
  "pymongo (>=4.0.0,<5.0.0)",
26
- "fastapi (>=0.115.0,<0.117.0)",
27
- "uvicorn[standard] (>=0.15.0,<0.36.0)",
26
+ "fastapi (>=0.115.0,<0.133.0)",
27
+ "uvicorn[standard] (>=0.15.0,<0.42.0)",
28
28
  "click (>=8.2.0,<9.0.0)",
29
29
  "passlib (>=1.7.0,<2.0.0)",
30
30
  "pydantic-settings (>=2.8.0,<3.0.0)",
31
31
  "httpx[socks] (>=0.28.0,<0.29.0)",
32
- "typer (>=0.16.0,<0.17.0)",
32
+ "typer (>=0.16.0,<0.25.0)",
33
33
  "loguru (>=0.7.0,<0.8.0)",
34
- "ruamel-yaml (>=0.18.10,<0.19.0)",
34
+ "ruamel-yaml (>=0.18.10,<0.20.0)",
35
35
  "pyyaml (>=6.0.0,<7.0.0)",
36
- "tomlkit (>=0.13.2,<0.14.0)",
36
+ "tomlkit (>=0.13.2,<0.15.0)",
37
37
  "importlib-metadata (>=8.5.0,<9.0.0)",
38
- "packaging (>=24.2,<26.0)",
38
+ "packaging (>=24.2,<27.0)",
39
39
  "sse-starlette (>=2.1.3,<4.0.0)",
40
40
  "httpx-sse (>=0.4.0,<0.5.0)",
41
41
  "stamina (>=25.1.0,<26.0.0)",
@@ -50,25 +50,25 @@ dependencies = [
50
50
 
51
51
  [project.optional-dependencies]
52
52
  dev = [
53
- "pytest (>=8.0.0,<9.0.0)",
54
- "pytest-cov (>=5.0.0,<7.0.0)",
55
- "black (>=24.0.0,<26.0.0)",
56
- "isort (>=5.13.0,<7.0.0)",
53
+ "pytest (>=8.0.0,<10.0.0)",
54
+ "pytest-cov (>=5.0.0,<8.0.0)",
55
+ "black (>=24.0.0,<27.0.0)",
56
+ "isort (>=5.13.0,<9.0.0)",
57
57
  "mypy (>=1.14.0,<2.0.0)",
58
58
  "flake8 (>=7.0.0,<8.0.0)",
59
59
  "pre-commit (>=3.0.0,<5.0.0)",
60
60
  "freezegun (>=1.5.0,<2.0.0)",
61
61
  "pytest-docker (>=3.0.0,<4.0.0)",
62
- "pytest-asyncio (>=0.24.0,<1.2.0)",
62
+ "pytest-asyncio (>=0.24.0,<1.4.0)",
63
63
  "asgi-lifespan (>=2.1.0,<3.0.0)",
64
- "tox (>=4.24.0,<4.29.0)",
64
+ "tox (>=4.24.0,<4.46.0)",
65
65
  "pytest-dependency (>=0.6.0,<0.7.0)",
66
66
  "pytest-sugar (>=1.0.0,<2.0.0)",
67
67
  "rust-just (>=1.42.4,<2.0.0)",
68
68
  ]
69
69
  doc = [
70
- "mkdocs-material (>=9.6.5,<9.7.0)",
71
- "mkdocs-glightbox (>=0.4.0,<0.5.0)",
70
+ "mkdocs-material (>=9.6.5,<9.8.0)",
71
+ "mkdocs-glightbox (>=0.4.0,<0.6.0)",
72
72
  "mike (>=2.1.3,<2.2.0)",
73
73
  ]
74
74
 
@@ -133,7 +133,6 @@ disable_error_code = [
133
133
  "no-redef",
134
134
  "import-untyped"
135
135
  ]
136
- python_version = "0.2.11"
137
136
  warn_unused_configs = true
138
137
  ignore_missing_imports = true
139
138
  show_error_codes = true