labtasker 0.2.7__tar.gz → 0.2.9__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 (217) hide show
  1. {labtasker-0.2.7 → labtasker-0.2.9}/PKG-INFO +6 -3
  2. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/__init__.py +3 -3
  3. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/api_models.py +134 -11
  4. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/cli/cli.py +2 -2
  5. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/cli/init.py +4 -4
  6. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/cli/loop.py +12 -81
  7. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/cli/task.py +81 -16
  8. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/cli/worker.py +10 -3
  9. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/client_api.py +13 -1
  10. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/api.py +12 -8
  11. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/cli_utils.py +44 -4
  12. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/context.py +5 -0
  13. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/job_runner.py +4 -7
  14. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/logging.py +2 -0
  15. labtasker-0.2.9/labtasker/client/core/pager.py +119 -0
  16. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/query_transpiler.py +57 -7
  17. labtasker-0.2.9/labtasker/client/core/utils.py +237 -0
  18. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/version_checker.py +43 -26
  19. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/templates/labtasker_root/client.toml +1 -1
  20. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/filtering.py +1 -1
  21. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/server/database.py +7 -4
  22. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/server/endpoints.py +2 -1
  23. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/utils.py +51 -19
  24. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker.egg-info/SOURCES.txt +2 -1
  25. {labtasker-0.2.7 → labtasker-0.2.9}/pyproject.toml +8 -4
  26. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_api_models.py +1 -1
  27. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_cli/test_task.py +49 -18
  28. labtasker-0.2.9/tests/test_client/test_core/test_event/test_event_listener_entity_data.py +108 -0
  29. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_event/test_various_actions.py +2 -2
  30. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_query_transpiler/test_matching.py +14 -0
  31. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_version_checker.py +6 -5
  32. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_server/test_endpoint/test_server.py +9 -9
  33. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_utils/test_utils.py +46 -46
  34. labtasker-0.2.7/labtasker/client/core/utils.py +0 -114
  35. labtasker-0.2.7/tests/test_client/test_core/test_event/test_event_integration.py +0 -0
  36. {labtasker-0.2.7 → labtasker-0.2.9}/LICENSE +0 -0
  37. {labtasker-0.2.7 → labtasker-0.2.9}/MANIFEST.in +0 -0
  38. {labtasker-0.2.7 → labtasker-0.2.9}/README.md +0 -0
  39. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/__main__.py +0 -0
  40. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/__init__.py +0 -0
  41. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/cli/__init__.py +0 -0
  42. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/cli/config.py +0 -0
  43. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/cli/event.py +0 -0
  44. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/cli/queue.py +0 -0
  45. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/__init__.py +0 -0
  46. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/cmd_parser/LabCmd.g4 +0 -0
  47. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/cmd_parser/LabCmdLexer.g4 +0 -0
  48. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/cmd_parser/__init__.py +0 -0
  49. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/cmd_parser/generated/LabCmd.py +0 -0
  50. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/cmd_parser/generated/LabCmdLexer.py +0 -0
  51. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/cmd_parser/generated/LabCmdListener.py +0 -0
  52. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/cmd_parser/generated/__init__.py +0 -0
  53. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/cmd_parser/parser.py +0 -0
  54. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/config.py +0 -0
  55. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/events.py +0 -0
  56. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/exceptions.py +0 -0
  57. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/heartbeat.py +0 -0
  58. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/paths.py +0 -0
  59. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/plugin_utils.py +0 -0
  60. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/resolver/__init__.py +0 -0
  61. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/resolver/models.py +0 -0
  62. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/core/resolver/utils.py +0 -0
  63. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/templates/labtasker_root/.gitignore +0 -0
  64. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/client/templates/labtasker_root/logs/.gitkeep +0 -0
  65. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/constants.py +0 -0
  66. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/security.py +0 -0
  67. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/server/__init__.py +0 -0
  68. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/server/cli.py +0 -0
  69. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/server/config.py +0 -0
  70. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/server/db_utils.py +0 -0
  71. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/server/dependencies.py +0 -0
  72. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/server/embedded_db.py +0 -0
  73. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/server/event_manager.py +0 -0
  74. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/server/fsm.py +0 -0
  75. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/server/logging.py +0 -0
  76. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/README.txt +0 -0
  77. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/__init__.py +0 -0
  78. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/BufferedTokenStream.py +0 -0
  79. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/CommonTokenFactory.py +0 -0
  80. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/CommonTokenStream.py +0 -0
  81. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/FileStream.py +0 -0
  82. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/InputStream.py +0 -0
  83. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/IntervalSet.py +0 -0
  84. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/LL1Analyzer.py +0 -0
  85. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/Lexer.py +0 -0
  86. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/ListTokenSource.py +0 -0
  87. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/Parser.py +0 -0
  88. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/ParserInterpreter.py +0 -0
  89. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/ParserRuleContext.py +0 -0
  90. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/PredictionContext.py +0 -0
  91. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/Recognizer.py +0 -0
  92. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/RuleContext.py +0 -0
  93. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/StdinStream.py +0 -0
  94. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/Token.py +0 -0
  95. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/TokenStreamRewriter.py +0 -0
  96. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/Utils.py +0 -0
  97. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/__init__.py +0 -0
  98. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/_pygrun.py +0 -0
  99. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/atn/ATN.py +0 -0
  100. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/atn/ATNConfig.py +0 -0
  101. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/atn/ATNConfigSet.py +0 -0
  102. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/atn/ATNDeserializationOptions.py +0 -0
  103. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/atn/ATNDeserializer.py +0 -0
  104. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/atn/ATNSimulator.py +0 -0
  105. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/atn/ATNState.py +0 -0
  106. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/atn/ATNType.py +0 -0
  107. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/atn/LexerATNSimulator.py +0 -0
  108. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/atn/LexerAction.py +0 -0
  109. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/atn/LexerActionExecutor.py +0 -0
  110. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/atn/ParserATNSimulator.py +0 -0
  111. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/atn/PredictionMode.py +0 -0
  112. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/atn/SemanticContext.py +0 -0
  113. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/atn/Transition.py +0 -0
  114. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/atn/__init__.py +0 -0
  115. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/dfa/DFA.py +0 -0
  116. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/dfa/DFASerializer.py +0 -0
  117. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/dfa/DFAState.py +0 -0
  118. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/dfa/__init__.py +0 -0
  119. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/error/DiagnosticErrorListener.py +0 -0
  120. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/error/ErrorListener.py +0 -0
  121. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/error/ErrorStrategy.py +0 -0
  122. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/error/Errors.py +0 -0
  123. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/error/__init__.py +0 -0
  124. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/tree/Chunk.py +0 -0
  125. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/tree/ParseTreeMatch.py +0 -0
  126. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/tree/ParseTreePattern.py +0 -0
  127. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/tree/ParseTreePatternMatcher.py +0 -0
  128. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/tree/RuleTagToken.py +0 -0
  129. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/tree/TokenTagToken.py +0 -0
  130. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/tree/Tree.py +0 -0
  131. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/tree/Trees.py +0 -0
  132. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/tree/__init__.py +0 -0
  133. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/xpath/XPath.py +0 -0
  134. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/xpath/XPathLexer.py +0 -0
  135. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/antlr4/xpath/__init__.py +0 -0
  136. {labtasker-0.2.7 → labtasker-0.2.9}/labtasker/vendor/vendor.txt +0 -0
  137. {labtasker-0.2.7 → labtasker-0.2.9}/setup.cfg +0 -0
  138. {labtasker-0.2.7 → labtasker-0.2.9}/tests/__init__.py +0 -0
  139. {labtasker-0.2.7 → labtasker-0.2.9}/tests/conftest.py +0 -0
  140. {labtasker-0.2.7 → labtasker-0.2.9}/tests/demo_pager_iterator.py +0 -0
  141. {labtasker-0.2.7 → labtasker-0.2.9}/tests/dummy_jobs/job_1.py +0 -0
  142. {labtasker-0.2.7 → labtasker-0.2.9}/tests/fixtures/__init__.py +0 -0
  143. {labtasker-0.2.7 → labtasker-0.2.9}/tests/fixtures/database/__init__.py +0 -0
  144. {labtasker-0.2.7 → labtasker-0.2.9}/tests/fixtures/database/mock.py +0 -0
  145. {labtasker-0.2.7 → labtasker-0.2.9}/tests/fixtures/database/real.py +0 -0
  146. {labtasker-0.2.7 → labtasker-0.2.9}/tests/fixtures/logging.py +0 -0
  147. {labtasker-0.2.7 → labtasker-0.2.9}/tests/fixtures/mock_datetime_now.py +0 -0
  148. {labtasker-0.2.7 → labtasker-0.2.9}/tests/fixtures/server/__init__.py +0 -0
  149. {labtasker-0.2.7 → labtasker-0.2.9}/tests/fixtures/server/async_app.py +0 -0
  150. {labtasker-0.2.7 → labtasker-0.2.9}/tests/fixtures/server/sync_app.py +0 -0
  151. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/__init__.py +0 -0
  152. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/conftest.py +0 -0
  153. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_cli/__init__.py +0 -0
  154. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_cli/conftest.py +0 -0
  155. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_cli/test_basic.py +0 -0
  156. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_cli/test_config.py +0 -0
  157. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_cli/test_event.py +0 -0
  158. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_cli/test_init.py +0 -0
  159. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_cli/test_loop.py +0 -0
  160. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_cli/test_queue.py +0 -0
  161. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_cli/test_worker.py +0 -0
  162. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/__init__.py +0 -0
  163. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_cli_utils.py +0 -0
  164. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_event/__init__.py +0 -0
  165. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_event/test_concurrency_job_flow_event.py +0 -0
  166. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_event/test_event_listener_basic.py +0 -0
  167. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_event/utils.py +0 -0
  168. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_heartbeat.py +0 -0
  169. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_job_runner.py +0 -0
  170. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_logging.py +0 -0
  171. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_loop_internal_error_handler.py +0 -0
  172. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_pager_iterator.py +0 -0
  173. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_parser.py +0 -0
  174. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_query_transpiler/__init__.py +0 -0
  175. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_query_transpiler/conftest.py +0 -0
  176. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_query_transpiler/test_behavior.py +0 -0
  177. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_query_transpiler/test_utils.py +0 -0
  178. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_query_transpiler/utils.py +0 -0
  179. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_resolver.py +0 -0
  180. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_runner_concurrency/__init__.py +0 -0
  181. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_runner_concurrency/run_concurrent.py +0 -0
  182. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_runner_concurrency/test_runner_concurrency_success_failure.py +0 -0
  183. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_runner_concurrency/test_runner_high_concurrency.py +0 -0
  184. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_runner_timeout/__init__.py +0 -0
  185. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_runner_timeout/conftest.py +0 -0
  186. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_runner_timeout/test_job_runner_timeout.py +0 -0
  187. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_runner_timeout/test_job_runner_with_resolver_timeout.py +0 -0
  188. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_runner_with_resolver.py +0 -0
  189. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_client/test_core/test_server_notification_and_client_version.py +0 -0
  190. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_filtering/__init__.py +0 -0
  191. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_filtering/exception_utils.py +0 -0
  192. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_filtering/test_exception_filtering.py +0 -0
  193. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_mock_time.py +0 -0
  194. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_security.py +0 -0
  195. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_server/__init__.py +0 -0
  196. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_server/conftest.py +0 -0
  197. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_server/test_database/__init__.py +0 -0
  198. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_server/test_database/conftest.py +0 -0
  199. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_server/test_database/test_database_basic.py +0 -0
  200. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_server/test_database/test_fetch_extra_filter.py +0 -0
  201. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_server/test_database/test_query_dict_to_mongo_filter.py +0 -0
  202. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_server/test_database/test_required_field_fetching.py +0 -0
  203. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_server/test_db_utils/__init__.py +0 -0
  204. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_server/test_db_utils/test_arg_match.py +0 -0
  205. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_server/test_db_utils/test_keys_to_query_dict.py +0 -0
  206. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_server/test_db_utils/test_keys_to_query_dict_deepest.py +0 -0
  207. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_server/test_db_utils/test_keys_to_query_dict_topmost.py +0 -0
  208. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_server/test_embedded_db.py +0 -0
  209. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_server/test_endpoint/__init__.py +0 -0
  210. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_server/test_endpoint/test_event_basic.py +0 -0
  211. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_server/test_endpoint/test_server_async.py +0 -0
  212. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_server/test_endpoint/test_server_async_ping.py +0 -0
  213. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_server/test_fsm.py +0 -0
  214. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_server/test_get_verified_queue_dependency.py +0 -0
  215. {labtasker-0.2.7 → labtasker-0.2.9}/tests/test_utils/__init__.py +0 -0
  216. {labtasker-0.2.7 → labtasker-0.2.9}/tests/utils.py +0 -0
  217. {labtasker-0.2.7 → labtasker-0.2.9}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labtasker
3
- Version: 0.2.7
3
+ Version: 0.2.9
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
@@ -40,6 +40,8 @@ Requires-Dist: mongomock<4.4.0,>=4.3.0
40
40
  Requires-Dist: jsonpickle<5.0.0,>=4.0.2
41
41
  Requires-Dist: mslex<2.0.0,>=1.3.0
42
42
  Requires-Dist: pexpect<5.0.0,>=4.9.0
43
+ Requires-Dist: pip>=25
44
+ Requires-Dist: dateparser<2.0.0,>=1.2.2
43
45
  Provides-Extra: dev
44
46
  Requires-Dist: pytest<9.0.0,>=8.0.0; extra == "dev"
45
47
  Requires-Dist: pytest-cov<7.0.0,>=5.0.0; extra == "dev"
@@ -50,9 +52,9 @@ Requires-Dist: flake8<8.0.0,>=7.0.0; extra == "dev"
50
52
  Requires-Dist: pre-commit<5.0.0,>=3.0.0; extra == "dev"
51
53
  Requires-Dist: freezegun<2.0.0,>=1.5.0; extra == "dev"
52
54
  Requires-Dist: pytest-docker<4.0.0,>=3.0.0; extra == "dev"
53
- Requires-Dist: pytest-asyncio<1.1.0,>=0.24.0; extra == "dev"
55
+ Requires-Dist: pytest-asyncio<1.2.0,>=0.24.0; extra == "dev"
54
56
  Requires-Dist: asgi-lifespan<3.0.0,>=2.1.0; extra == "dev"
55
- Requires-Dist: tox<4.28.0,>=4.24.0; extra == "dev"
57
+ Requires-Dist: tox<4.29.0,>=4.24.0; extra == "dev"
56
58
  Requires-Dist: pytest-dependency<0.7.0,>=0.6.0; extra == "dev"
57
59
  Requires-Dist: pytest-sugar<2.0.0,>=1.0.0; extra == "dev"
58
60
  Provides-Extra: doc
@@ -62,6 +64,7 @@ Requires-Dist: mike<2.2.0,>=2.1.3; extra == "doc"
62
64
  Provides-Extra: plugins
63
65
  Requires-Dist: labtasker-plugin-task-count; extra == "plugins"
64
66
  Requires-Dist: labtasker-plugin-script-generate; extra == "plugins"
67
+ Requires-Dist: labtasker-plugin-task-recency; extra == "plugins"
65
68
  Dynamic: license-file
66
69
 
67
70
  <p align="center"><em>Make your ML experiment wrapper scripts smarter with...</em></p>
@@ -1,13 +1,13 @@
1
- __version__ = "0.2.7"
1
+ __version__ = "0.2.9"
2
2
 
3
3
  from labtasker.client.client_api import *
4
4
  from labtasker.client.core.config import get_client_config
5
5
  from labtasker.client.core.exceptions import *
6
6
  from labtasker.client.core.paths import get_labtasker_client_config_path
7
- from labtasker.client.core.version_checker import check_pypi_status
7
+ from labtasker.client.core.version_checker import check_package_version
8
8
  from labtasker.filtering import install_traceback_filter, set_traceback_filter_hook
9
9
 
10
- check_pypi_status()
10
+ check_package_version()
11
11
 
12
12
  # by default, traceback filter is enabled.
13
13
  # you may disable it via client config
@@ -1,20 +1,56 @@
1
+ import warnings
1
2
  from datetime import datetime
2
- from typing import Any, Dict, List, Literal, Optional, Tuple, Union
3
+ from typing import Any, Dict, List, Literal, Optional, Tuple, TypeVar, Union
3
4
 
4
5
  from packaging.version import Version
5
- from pydantic import BaseModel, ConfigDict, Field, SecretStr, field_validator
6
+ from pydantic import (
7
+ BaseModel,
8
+ ConfigDict,
9
+ Field,
10
+ SecretStr,
11
+ ValidationInfo,
12
+ field_validator,
13
+ model_validator,
14
+ )
6
15
 
7
16
  from labtasker import __version__
8
17
  from labtasker.constants import Priority
9
- from labtasker.utils import validate_dict_keys
18
+ from labtasker.utils import _disable_check_var, validate_dict_keys
10
19
 
11
20
 
12
21
  class BaseApiModel(BaseModel):
13
- """
14
- Base API model for all API models.
15
- """
22
+ """Base API model for all API models."""
23
+
24
+ model_config = ConfigDict(populate_by_name=True, extra="ignore")
25
+ unknown_fields: Dict[str, Any] = Field(
26
+ default_factory=dict, exclude=True
27
+ ) # marked as excluded from serialization
28
+
29
+ @model_validator(mode="before")
30
+ @classmethod
31
+ def collect_unknown_fields(
32
+ cls, data: Dict[str, Any], info: ValidationInfo
33
+ ) -> Dict[str, Any]:
34
+ if _disable_check_var.get(): # if check is disabled, return data
35
+ return data
36
+
37
+ if isinstance(data, dict):
38
+ # Get the set of model field names
39
+ model_field_names = set(cls.model_fields.keys()) # type: ignore
16
40
 
17
- model_config = ConfigDict(populate_by_name=True)
41
+ unknown_fields = {
42
+ k: v for k, v in data.items() if k not in model_field_names
43
+ }
44
+
45
+ if unknown_fields:
46
+ warnings.warn(
47
+ f"Unknown fields provided to {cls.__name__}: {', '.join(unknown_fields.keys())}; "
48
+ f"It could be that your client and server version mismatch. Try upgrading both to latest versions.",
49
+ UserWarning,
50
+ )
51
+ data["_unknown_fields"] = unknown_fields
52
+
53
+ return data
18
54
 
19
55
 
20
56
  class BaseRequestModel(BaseApiModel):
@@ -119,13 +155,100 @@ class TaskSubmitRequest(
119
155
  priority: int = Priority.MEDIUM
120
156
 
121
157
 
122
- class TaskFetchRequest(BaseRequestModel):
158
+ T = TypeVar("T", bound="DatetimeSerializationMixin")
159
+
160
+
161
+ class DatetimeSerializationMixin:
162
+ """
163
+ A mixin that enables proper serialization and deserialization of datetime objects
164
+ in the extra_filter field.
165
+
166
+ This mixin should be used with BaseModel.
167
+ Example: class MyModel(DatetimeSerializationMixin, BaseModel):
168
+ """
169
+
170
+ # Define a model validator to process datetime markers in input data
171
+ @model_validator(mode="before")
172
+ @classmethod
173
+ def process_datetime_markers(cls, data: Any) -> Any:
174
+ """Process incoming data to convert datetime markers in extra_filter field"""
175
+ if (
176
+ isinstance(data, dict)
177
+ and "extra_filter" in data
178
+ and data["extra_filter"] is not None
179
+ ):
180
+ data["extra_filter"] = cls._process_datetime_markers(data["extra_filter"])
181
+ return data
182
+
183
+ # Add a field validator for extra_filter field
184
+ @field_validator("extra_filter", mode="before")
185
+ @classmethod
186
+ def validate_extra_filter(
187
+ cls, value: Optional[Dict[str, Any]]
188
+ ) -> Optional[Dict[str, Any]]:
189
+ """Validate and process extra_filter field"""
190
+ if value is None:
191
+ return None
192
+ return cls._process_datetime_markers(value)
193
+
194
+ @classmethod
195
+ def _process_datetime_markers(cls, obj: Any) -> Any:
196
+ """Process data recursively to convert datetime markers to datetime objects"""
197
+ if isinstance(obj, dict):
198
+ # Check if it's a datetime marker
199
+ if "_dt" in obj and len(obj) == 1 and isinstance(obj["_dt"], str):
200
+ try:
201
+ return datetime.fromisoformat(obj["_dt"])
202
+ except ValueError:
203
+ pass
204
+ # Process regular dictionary
205
+ return {k: cls._process_datetime_markers(v) for k, v in obj.items()}
206
+ elif isinstance(obj, list):
207
+ return [cls._process_datetime_markers(item) for item in obj]
208
+ return obj
209
+
210
+ def dump_to_json_dict(self) -> Dict[str, Any]:
211
+ """
212
+ Dump the model to a JSON-serializable dictionary with datetime objects
213
+ in extra_filter converted to {"_dt": iso_format} representation.
214
+
215
+ This method doesn't modify the model_config, but provides a way to
216
+ properly serialize datetime objects in the extra_filter field.
217
+
218
+ Returns:
219
+ Dict[str, Any]: A JSON-serializable dictionary
220
+ """
221
+ # First get the standard model dump
222
+ data = self.model_dump() # type: ignore[attr-defined]
223
+
224
+ # Process extra_filter field if it exists
225
+ if "extra_filter" in data and data["extra_filter"] is not None:
226
+ data["extra_filter"] = self._process_datetime_objects(data["extra_filter"])
227
+
228
+ return data
229
+
230
+ def _process_datetime_objects(self, obj: Any) -> Any:
231
+ """Process data recursively to convert datetime objects to markers"""
232
+ if isinstance(obj, dict):
233
+ return {k: self._process_datetime_objects(v) for k, v in obj.items()}
234
+ elif isinstance(obj, list):
235
+ return [self._process_datetime_objects(item) for item in obj]
236
+ elif isinstance(obj, datetime):
237
+ return {"_dt": obj.isoformat()}
238
+ elif isinstance(obj, tuple):
239
+ # Convert tuples to lists for JSON serialization
240
+ return [self._process_datetime_objects(item) for item in obj]
241
+ return obj
242
+
243
+
244
+ class TaskFetchRequest(DatetimeSerializationMixin, BaseRequestModel): # type: ignore[misc]
123
245
  worker_id: Optional[str] = None
124
246
  eta_max: Optional[str] = None
125
247
  heartbeat_timeout: Optional[float] = None
126
248
  start_heartbeat: bool = True
127
249
  required_fields: Optional[List[str]] = None
128
250
  extra_filter: Optional[Dict[str, Any]] = None
251
+ cmd: Optional[Union[str, List[str]]] = None
129
252
 
130
253
 
131
254
  class Task(
@@ -149,7 +272,7 @@ class Task(
149
272
  priority: int
150
273
  metadata: Dict
151
274
  args: Dict
152
- cmd: Union[str, List[str]]
275
+ cmd: Optional[Union[str, List[str]]]
153
276
  summary: Dict
154
277
  worker_id: Optional[str]
155
278
 
@@ -204,7 +327,7 @@ class TaskFetchResponse(BaseResponseModel):
204
327
  task: Optional[Task] = None
205
328
 
206
329
 
207
- class TaskLsRequest(BaseRequestModel):
330
+ class TaskLsRequest(DatetimeSerializationMixin, BaseRequestModel): # type: ignore[misc]
208
331
  offset: int = Field(0, ge=0)
209
332
  limit: int = Field(100, gt=0, le=1000)
210
333
  task_id: Optional[str] = None
@@ -266,7 +389,7 @@ class WorkerStatusUpdateRequest(BaseRequestModel):
266
389
  status: str = Field(..., pattern=r"^(active|suspended|crashed)$")
267
390
 
268
391
 
269
- class WorkerLsRequest(BaseRequestModel):
392
+ class WorkerLsRequest(DatetimeSerializationMixin, BaseRequestModel): # type: ignore[misc]
270
393
  offset: int = Field(0, ge=0)
271
394
  limit: int = Field(100, gt=0, le=1000)
272
395
  worker_id: Optional[str] = None
@@ -12,7 +12,7 @@ from labtasker.client.core.api import health_check
12
12
  from labtasker.client.core.config import requires_client_config
13
13
  from labtasker.client.core.exceptions import LabtaskerNetworkError
14
14
  from labtasker.client.core.logging import stderr_console, stdout_console
15
- from labtasker.client.core.version_checker import check_pypi_status
15
+ from labtasker.client.core.version_checker import check_package_version
16
16
 
17
17
  app = typer.Typer(context_settings={"help_option_names": ["-h", "--help"]})
18
18
 
@@ -20,7 +20,7 @@ app = typer.Typer(context_settings={"help_option_names": ["-h", "--help"]})
20
20
  def version_callback(value: bool):
21
21
  if value:
22
22
  stdout_console.print(f"Labtasker Version: {__version__}")
23
- check_pypi_status(force_check=True, blocking=True)
23
+ check_package_version(force_check=True, blocking=True)
24
24
  raise typer.Exit()
25
25
 
26
26
 
@@ -211,12 +211,12 @@ def setup_queue(base_url, base_url_verified) -> Tuple[str, str]:
211
211
  def confirm_set_traceback_filter() -> bool:
212
212
  choices = [
213
213
  Choice(
214
- "Yes: I want better exception formatting and sensitive text filtering.",
215
- data=True,
214
+ "No: I want more compatibility than security.",
215
+ data=False,
216
216
  ),
217
217
  Choice(
218
- "No: I just want compatibility.",
219
- data=False,
218
+ "Yes: I want better exception formatting and sensitive text filtering.",
219
+ data=True,
220
220
  ),
221
221
  ]
222
222
  choice = ListPrompt(
@@ -1,14 +1,11 @@
1
1
  """Implements `labtasker loop xxx`"""
2
2
 
3
- import json
4
3
  import os
5
- import subprocess
6
4
  import sys
7
5
  from collections import defaultdict
8
6
  from pathlib import Path
9
7
  from typing import List, Optional
10
8
 
11
- import pexpect
12
9
  import typer
13
10
  from typing_extensions import Annotated
14
11
 
@@ -16,6 +13,7 @@ from labtasker.client.cli.cli import app
16
13
  from labtasker.client.core.cli_utils import (
17
14
  cli_utils_decorator,
18
15
  eta_max_validation,
16
+ is_piped_io,
19
17
  parse_filter,
20
18
  )
21
19
  from labtasker.client.core.cmd_parser import cmd_interpolate
@@ -26,9 +24,14 @@ from labtasker.client.core.job_runner import loop_run
26
24
  from labtasker.client.core.logging import (
27
25
  logger,
28
26
  set_verbose,
29
- stderr_console,
30
27
  verbose_print,
31
28
  )
29
+ from labtasker.client.core.utils import (
30
+ check_pty_available,
31
+ json_serializer,
32
+ run_with_pty,
33
+ run_with_subprocess,
34
+ )
32
35
 
33
36
 
34
37
  class InfiniteDefaultDict(defaultdict):
@@ -44,76 +47,6 @@ class InfiniteDefaultDict(defaultdict):
44
47
  return super().get(key, default)
45
48
 
46
49
 
47
- def _check_pty_available(opt: bool) -> bool:
48
- if opt and os.name == "nt":
49
- stderr_console.print(
50
- "[bold orange1]Warning:[/bold orange1] PTY is not available on Windows. "
51
- "Disabling PTY support."
52
- )
53
- return False
54
- return opt
55
-
56
-
57
- def _stream_child_output(child) -> None:
58
- """Stream the output of a pexpect child in real-time, supporting progress bars."""
59
- try:
60
- while True:
61
- try:
62
- output = child.read_nonblocking(size=1024, timeout=0.1)
63
- if output:
64
- # keep \r
65
- sys.stdout.write(output)
66
- sys.stdout.flush()
67
- except pexpect.TIMEOUT:
68
- continue
69
- except pexpect.EOF:
70
- break
71
- finally:
72
- child.close()
73
-
74
-
75
- def _run_with_pty(cmd, shell_exec=None, use_shell=False):
76
- """Run a command with PTY support for interactive programs."""
77
- if use_shell:
78
- shell_exec = shell_exec or "/bin/sh"
79
- child = pexpect.spawn(shell_exec, ["-c", cmd], encoding="utf-8")
80
- else:
81
- child = pexpect.spawn(cmd[0], cmd[1:], encoding="utf-8")
82
-
83
- _stream_child_output(child)
84
-
85
- return child.exitstatus
86
-
87
-
88
- def _run_with_subprocess(cmd, shell_exec=None, use_shell=False):
89
- """Run a command using standard subprocess approach."""
90
- with subprocess.Popen(
91
- args=cmd,
92
- stdin=subprocess.PIPE,
93
- stdout=subprocess.PIPE,
94
- stderr=subprocess.PIPE,
95
- text=True,
96
- executable=shell_exec,
97
- shell=use_shell,
98
- ) as process:
99
- while True:
100
- output = process.stdout.readline()
101
- error = process.stderr.readline()
102
-
103
- if output:
104
- sys.stdout.write(output.strip())
105
- sys.stdout.flush()
106
- if error:
107
- sys.stderr.write(error.strip())
108
- sys.stderr.flush()
109
-
110
- # Break when process completes and streams are empty
111
- if process.poll() is not None and not output and not error:
112
- break
113
-
114
- return process.returncode
115
-
116
-
117
50
  @app.command()
118
51
  @cli_utils_decorator
119
52
  def loop(
@@ -163,7 +96,7 @@ def loop(
163
96
  ),
164
97
  use_pty: bool = typer.Option(
165
98
  os.name == "posix", # enabled by default on POSIX systems
166
- callback=_check_pty_available,
99
+ callback=check_pty_available,
167
100
  help="Use pseudo terminal on POSIX systems for better interactive program support.",
168
101
  ),
169
102
  verbose: bool = typer.Option( # noqa
@@ -201,7 +134,7 @@ def loop(
201
134
  input_cmd = f.read().strip()
202
135
 
203
136
  # 3. Try reading from stdin if shell mode is enabled
204
- if not input_cmd and not sys.stdin.isatty():
137
+ if not input_cmd and is_piped_io():
205
138
  input_cmd = sys.stdin.read().strip()
206
139
 
207
140
  # Final validation: ensure a command is present
@@ -211,7 +144,7 @@ def loop(
211
144
  )
212
145
 
213
146
  parsed_filter = parse_filter(extra_filter)
214
- verbose_print(f"Parsed filter: {json.dumps(parsed_filter, indent=4)}")
147
+ verbose_print(f"Parsed filter: {json_serializer(parsed_filter, indent=4)}")
215
148
 
216
149
  if heartbeat_timeout is None:
217
150
  heartbeat_timeout = get_client_config().task.heartbeat_interval * 3
@@ -244,12 +177,10 @@ def loop(
244
177
  try:
245
178
  if use_pty:
246
179
  # Use pexpect with PTY on POSIX systems
247
- exit_code = _run_with_pty(interpolated_cmd, executable, use_shell)
180
+ exit_code = run_with_pty(interpolated_cmd, executable, use_shell)
248
181
  else:
249
182
  # Standard subprocess approach for non-PTY execution
250
- exit_code = _run_with_subprocess(
251
- interpolated_cmd, executable, use_shell
252
- )
183
+ exit_code = run_with_subprocess(interpolated_cmd, executable, use_shell)
253
184
 
254
185
  if exit_code != 0:
255
186
  raise _LabtaskerJobFailed(
@@ -1,7 +1,6 @@
1
1
  """Manage tasks (CRUD operations)."""
2
2
 
3
3
  import io
4
- import json
5
4
  import os
6
5
  import sys
7
6
  import tempfile
@@ -22,7 +21,6 @@ from typing_extensions import Annotated
22
21
  from labtasker.api_models import Task, TaskUpdateRequest
23
22
  from labtasker.client.core.api import (
24
23
  delete_task,
25
- get_queue,
26
24
  ls_tasks,
27
25
  submit_task,
28
26
  update_tasks,
@@ -31,6 +29,8 @@ from labtasker.client.core.cli_utils import (
31
29
  LsFmtChoices,
32
30
  cli_utils_decorator,
33
31
  confirm,
32
+ get_editor,
33
+ is_piped_io,
34
34
  ls_format_iter,
35
35
  pager_iterator,
36
36
  parse_dict,
@@ -47,6 +47,8 @@ from labtasker.client.core.logging import (
47
47
  stdout_console,
48
48
  verbose_print,
49
49
  )
50
+ from labtasker.client.core.pager import echo_via_pager_no_check_isatty
51
+ from labtasker.client.core.utils import json_serializer
50
52
  from labtasker.constants import Priority
51
53
 
52
54
  app = typer.Typer()
@@ -237,6 +239,10 @@ def ls(
237
239
  help="Only show task IDs that match the query, rather than full entry. "
238
240
  "Useful when using in bash scripts.",
239
241
  ),
242
+ ansi: bool = typer.Option(
243
+ True,
244
+ help="Enable ANSI colors.",
245
+ ),
240
246
  pager: bool = typer.Option(
241
247
  True,
242
248
  help="Enable pagination.",
@@ -268,6 +274,11 @@ def ls(
268
274
  callback=set_verbose,
269
275
  is_eager=True,
270
276
  ),
277
+ piped_in: bool = typer.Option(
278
+ False,
279
+ "--piped-in",
280
+ help="Read from stdin. Useful in cascaded commands to display final result. e.g. `labtasker xxx | labtasker ls --piped-in`",
281
+ ),
271
282
  ):
272
283
  """
273
284
  List and filter tasks in the queue.
@@ -298,10 +309,34 @@ def ls(
298
309
  else:
299
310
  parsed_sort = parse_sort(sort)
300
311
 
301
- get_queue() # validate auth and queue existence, prevent err swallowed by pager
302
-
303
312
  extra_filter = parse_filter(extra_filter)
304
- verbose_print(f"Parsed filter: {json.dumps(extra_filter, indent=4)}")
313
+ verbose_print(f"Parsed filter: {json_serializer(extra_filter, indent=4)}")
314
+
315
+ # Check for piped input (task ids could be passed through cascaded commands e.g. labtasker xxx | labtasker yyy)
316
+ if piped_in:
317
+ if any([task_id, task_name, status, extra_filter]):
318
+ raise typer.BadParameter(
319
+ "Cannot use --task-id, --task-name, --status, or --extra-filter with piped input --piped-in enabled."
320
+ )
321
+ # Read task IDs from stdin (pipe)
322
+ piped_task_ids = [line.strip() for line in sys.stdin if line.strip()]
323
+ verbose_print(f"Received {len(piped_task_ids)} task IDs from pipe")
324
+
325
+ # If we have piped task IDs but no task_id is specified yet
326
+ if piped_task_ids:
327
+ # If there's only one task ID from pipe and no task_id specified, use it directly
328
+ if len(piped_task_ids) == 1 and not task_id:
329
+ task_id = piped_task_ids[0]
330
+ verbose_print(f"Using single piped task ID: {task_id}")
331
+ # If there are multiple task IDs, use them to build an extra filter
332
+ elif len(piped_task_ids) > 1:
333
+ # Build a filter to include all piped task IDs
334
+ extra_filter = {"_id": {"$in": piped_task_ids}}
335
+
336
+ verbose_print(
337
+ f"Created filter from {len(piped_task_ids)} piped task IDs"
338
+ )
339
+
305
340
  page_iter = pager_iterator(
306
341
  fetch_function=partial(
307
342
  ls_tasks,
@@ -321,16 +356,18 @@ def ls(
321
356
  raise typer.Exit() # exit directly without other printing
322
357
 
323
358
  if pager:
324
- click.echo_via_pager(
359
+ echo_via_pager_no_check_isatty(
325
360
  ls_format_iter[fmt](
326
361
  page_iter,
327
362
  use_rich=False,
328
- )
363
+ ansi=ansi,
364
+ ),
329
365
  )
330
366
  else:
331
367
  for item in ls_format_iter[fmt](
332
368
  page_iter,
333
369
  use_rich=True,
370
+ ansi=ansi,
334
371
  ):
335
372
  stdout_console.print(item)
336
373
 
@@ -397,9 +434,9 @@ def update(
397
434
  "-q",
398
435
  help="Disable interactive mode and confirmations. Set this to true if you are using this in a bash script.",
399
436
  ),
400
- editor: Optional[str] = typer.Option(
401
- None,
402
- help="Editor to use for modifying task data incase you didn't specify --update.",
437
+ editor: str = typer.Option(
438
+ get_editor(),
439
+ help="Editor to use for modifying task data in case you didn't specify --update.",
403
440
  ),
404
441
  verbose: bool = typer.Option(
405
442
  False,
@@ -417,10 +454,14 @@ def update(
417
454
  priority, and other settings. You can update multiple tasks at once by using
418
455
  filters, and either specify updates directly or use an interactive editor.
419
456
 
457
+ You can also pipe task IDs from other commands, for example:
458
+ `labtasker task ls -q | labtasker task update -u 'status=cancelled'`
459
+
420
460
  Examples:
421
461
  labtasker task update --id "task-123" -- priority=10
422
462
  labtasker task update --status pending -- metadata.tag=important
423
463
  labtasker task update --name "training" --editor vim # Open in editor
464
+ labtasker task ls -q | labtasker task update -u status=cancelled # `task ls` can be replaced with one of the self-implemented plugin commands.
424
465
  """
425
466
  if updates and option_updates:
426
467
  raise typer.BadParameter(
@@ -434,7 +475,28 @@ def update(
434
475
 
435
476
  updates = updates or option_updates
436
477
  extra_filter = parse_filter(extra_filter)
437
- verbose_print(f"Parsed filter: {json.dumps(extra_filter, indent=4)}")
478
+ verbose_print(f"Parsed filter: {json_serializer(extra_filter, indent=4)}")
479
+
480
+ # Check for piped input (task ids could be passed through cascaded commands e.g. labtasker xxx | labtasker yyy)
481
+ if is_piped_io() and not any([task_id, task_name, status, extra_filter]):
482
+ # Read task IDs from stdin (pipe)
483
+ piped_task_ids = [line.strip() for line in sys.stdin if line.strip()]
484
+ verbose_print(f"Received {len(piped_task_ids)} task IDs from pipe")
485
+
486
+ # If we have piped task IDs but no task_id is specified yet
487
+ if piped_task_ids:
488
+ # If there's only one task ID from pipe and no task_id specified, use it directly
489
+ if len(piped_task_ids) == 1 and not task_id:
490
+ task_id = piped_task_ids[0]
491
+ verbose_print(f"Using single piped task ID: {task_id}")
492
+ # If there are multiple task IDs, use them to build an extra filter
493
+ elif len(piped_task_ids) > 1:
494
+ # Build a filter to include all piped task IDs
495
+ extra_filter = {"_id": {"$in": piped_task_ids}}
496
+
497
+ verbose_print(
498
+ f"Created filter from {len(piped_task_ids)} piped task IDs"
499
+ )
438
500
 
439
501
  # readonly fields
440
502
  readonly_fields: Set[str] = (
@@ -541,7 +603,7 @@ def handle_editor_mode(old_tasks, readonly_fields, editor):
541
603
  try:
542
604
  task_updates.append(
543
605
  TaskUpdateRequest(
544
- _id=old_tasks[i].task_id,
606
+ task_id=old_tasks[i].task_id,
545
607
  replace_fields=replace_fields,
546
608
  **ud,
547
609
  )
@@ -569,12 +631,12 @@ def handle_editor_mode(old_tasks, readonly_fields, editor):
569
631
  raise typer.Abort()
570
632
  # Continue the loop with the current file state
571
633
 
634
+ return task_updates
635
+
572
636
  finally:
573
637
  if temp_file_path and temp_file_path.exists():
574
638
  temp_file_path.unlink()
575
639
 
576
- return task_updates
577
-
578
640
 
579
641
  def handle_non_editor_mode(old_tasks, updates, readonly_fields):
580
642
  """Handles update tasks without an editor and returns task updates."""
@@ -595,7 +657,7 @@ def handle_non_editor_mode(old_tasks, updates, readonly_fields):
595
657
  for i, task in enumerate(old_tasks):
596
658
  task_updates.append(
597
659
  TaskUpdateRequest(
598
- _id=task.task_id, replace_fields=replace_fields, **update_dict
660
+ task_id=task.task_id, replace_fields=replace_fields, **update_dict
599
661
  )
600
662
  )
601
663
 
@@ -655,6 +717,9 @@ def display_updated_tasks(updated_tasks, update_dicts):
655
717
  console = rich.console.Console()
656
718
  with console.capture() as capture:
657
719
  console.print(Syntax(s.getvalue(), "yaml"))
720
+
721
+ # use normal pager instead of echo_via_pager_no_check_isatty
722
+ # because it would break the test cases for update task
658
723
  click.echo_via_pager(capture.get())
659
724
 
660
725
 
@@ -662,7 +727,7 @@ def display_updated_tasks(updated_tasks, update_dicts):
662
727
  @cli_utils_decorator
663
728
  def delete(
664
729
  task_ids: List[str] = typer.Argument(
665
- ... if sys.stdin.isatty() else None,
730
+ ... if not is_piped_io() else None,
666
731
  help="IDs of the task to delete.",
667
732
  ),
668
733
  yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),