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.
- {labtasker-0.2.11 → labtasker-0.2.13}/PKG-INFO +15 -15
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/__init__.py +4 -1
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/cli/worker.py +7 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/api.py +9 -2
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/heartbeat.py +16 -5
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/job_runner.py +9 -2
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/paths.py +3 -2
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/filtering.py +10 -5
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/server/database.py +51 -11
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/server/endpoints.py +5 -7
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/server/fsm.py +40 -3
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker.egg-info/SOURCES.txt +2 -1
- {labtasker-0.2.11 → labtasker-0.2.13}/pyproject.toml +15 -16
- labtasker-0.2.13/tests/test_client/test_core/test_heartbeat_e2e.py +73 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_fsm.py +1 -1
- {labtasker-0.2.11 → labtasker-0.2.13}/LICENSE +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/MANIFEST.in +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/README.md +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/__main__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/api_models.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/cli/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/cli/cli.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/cli/config.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/cli/event.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/cli/init.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/cli/loop.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/cli/queue.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/cli/task.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/client_api.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/cli_utils.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/cmd_parser/LabCmd.g4 +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/cmd_parser/LabCmdLexer.g4 +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/cmd_parser/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/cmd_parser/generated/LabCmd.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/cmd_parser/generated/LabCmdLexer.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/cmd_parser/generated/LabCmdListener.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/cmd_parser/generated/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/cmd_parser/parser.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/config.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/context.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/events.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/exceptions.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/logging.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/pager.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/plugin_utils.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/query_transpiler.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/resolver/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/resolver/models.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/resolver/utils.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/utils.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/core/version_checker.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/templates/labtasker_root/.gitignore +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/templates/labtasker_root/client.toml +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/client/templates/labtasker_root/logs/.gitkeep +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/constants.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/security.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/server/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/server/cli.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/server/config.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/server/db_utils.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/server/dependencies.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/server/embedded_db.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/server/event_manager.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/server/logging.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/utils.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/README.txt +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/BufferedTokenStream.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/CommonTokenFactory.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/CommonTokenStream.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/FileStream.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/InputStream.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/IntervalSet.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/LL1Analyzer.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/Lexer.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/ListTokenSource.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/Parser.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/ParserInterpreter.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/ParserRuleContext.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/PredictionContext.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/Recognizer.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/RuleContext.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/StdinStream.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/Token.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/TokenStreamRewriter.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/Utils.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/_pygrun.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/ATN.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/ATNConfig.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/ATNConfigSet.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/ATNDeserializationOptions.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/ATNDeserializer.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/ATNSimulator.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/ATNState.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/ATNType.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/LexerATNSimulator.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/LexerAction.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/LexerActionExecutor.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/ParserATNSimulator.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/PredictionMode.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/SemanticContext.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/Transition.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/atn/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/dfa/DFA.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/dfa/DFASerializer.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/dfa/DFAState.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/dfa/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/error/DiagnosticErrorListener.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/error/ErrorListener.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/error/ErrorStrategy.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/error/Errors.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/error/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/tree/Chunk.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/tree/ParseTreeMatch.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/tree/ParseTreePattern.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/tree/ParseTreePatternMatcher.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/tree/RuleTagToken.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/tree/TokenTagToken.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/tree/Tree.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/tree/Trees.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/tree/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/xpath/XPath.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/xpath/XPathLexer.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/antlr4/xpath/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/labtasker/vendor/vendor.txt +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/setup.cfg +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/conftest.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/demo_pager_iterator.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/dummy_jobs/job_1.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/fixtures/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/fixtures/database/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/fixtures/database/mock.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/fixtures/database/real.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/fixtures/logging.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/fixtures/mock_datetime_now.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/fixtures/server/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/fixtures/server/async_app.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/fixtures/server/sync_app.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_api_models.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/conftest.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_cli/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_cli/conftest.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_cli/test_basic.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_cli/test_config.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_cli/test_event.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_cli/test_init.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_cli/test_loop.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_cli/test_queue.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_cli/test_task.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_cli/test_worker.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_cli_utils.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_event/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_event/test_concurrency_job_flow_event.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_event/test_event_listener_basic.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_event/test_event_listener_entity_data.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_event/test_various_actions.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_event/utils.py +0 -0
- /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
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_job_runner.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_logging.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_loop_internal_error_handler.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_pager_iterator.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_parser.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_query_transpiler/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_query_transpiler/conftest.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_query_transpiler/test_behavior.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_query_transpiler/test_matching.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_query_transpiler/test_utils.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_query_transpiler/utils.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_resolver.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_runner_concurrency/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_runner_concurrency/run_concurrent.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_runner_concurrency/test_runner_concurrency_success_failure.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_runner_concurrency/test_runner_high_concurrency.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_runner_timeout/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_runner_timeout/conftest.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_runner_timeout/test_job_runner_timeout.py +0 -0
- {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
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_runner_with_resolver.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_server_notification_and_client_version.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_client/test_core/test_version_checker.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_filtering/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_filtering/exception_utils.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_filtering/test_exception_filtering.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_mock_time.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_security.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/conftest.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_database/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_database/conftest.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_database/test_database_basic.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_database/test_fetch_extra_filter.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_database/test_query_dict_to_mongo_filter.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_database/test_required_field_fetching.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_db_utils/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_db_utils/test_arg_match.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_db_utils/test_keys_to_query_dict.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_db_utils/test_keys_to_query_dict_deepest.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_db_utils/test_keys_to_query_dict_topmost.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_embedded_db.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_endpoint/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_endpoint/test_event_basic.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_endpoint/test_server.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_endpoint/test_server_async.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_endpoint/test_server_async_ping.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_server/test_get_verified_queue_dependency.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_utils/__init__.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/test_utils/test_utils.py +0 -0
- {labtasker-0.2.11 → labtasker-0.2.13}/tests/utils.py +0 -0
- {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.
|
|
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.
|
|
23
|
-
Requires-Dist: uvicorn[standard]<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.
|
|
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.
|
|
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.
|
|
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<
|
|
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<
|
|
47
|
-
Requires-Dist: pytest-cov<
|
|
48
|
-
Requires-Dist: black<
|
|
49
|
-
Requires-Dist: isort<
|
|
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.
|
|
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.
|
|
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.
|
|
63
|
-
Requires-Dist: mkdocs-glightbox<0.
|
|
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"
|
|
@@ -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=
|
|
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(
|
|
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"
|
|
74
|
+
logger.error(f"Failed to refresh heartbeat: {str(e)}")
|
|
75
|
+
raise
|
|
73
76
|
|
|
74
|
-
#
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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: {
|
|
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/
|
|
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.
|
|
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.
|
|
27
|
-
"uvicorn[standard] (>=0.15.0,<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.
|
|
32
|
+
"typer (>=0.16.0,<0.25.0)",
|
|
33
33
|
"loguru (>=0.7.0,<0.8.0)",
|
|
34
|
-
"ruamel-yaml (>=0.18.10,<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.
|
|
36
|
+
"tomlkit (>=0.13.2,<0.15.0)",
|
|
37
37
|
"importlib-metadata (>=8.5.0,<9.0.0)",
|
|
38
|
-
"packaging (>=24.2,<
|
|
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,<
|
|
54
|
-
"pytest-cov (>=5.0.0,<
|
|
55
|
-
"black (>=24.0.0,<
|
|
56
|
-
"isort (>=5.13.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.
|
|
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.
|
|
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.
|
|
71
|
-
"mkdocs-glightbox (>=0.4.0,<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
|