labtasker 0.1.2__tar.gz → 0.1.3__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 (156) hide show
  1. labtasker-0.1.3/.gitmodules +4 -0
  2. {labtasker-0.1.2 → labtasker-0.1.3}/MANIFEST.in +2 -0
  3. {labtasker-0.1.2 → labtasker-0.1.3}/PKG-INFO +3 -1
  4. labtasker-0.1.3/demo/advanced/custom_resolver/submit_job.py +11 -0
  5. labtasker-0.1.3/demo/advanced/custom_resolver/w.py +38 -0
  6. labtasker-0.1.3/demo/advanced/custom_resolver/wo.py +29 -0
  7. labtasker-0.1.3/demo/basic/python_demo/run_job.py +25 -0
  8. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/__init__.py +4 -2
  9. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/api_models.py +89 -22
  10. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/cli/cli.py +2 -1
  11. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/cli/loop.py +4 -4
  12. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/cli/queue.py +9 -1
  13. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/cli/task.py +4 -0
  14. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/cli/worker.py +6 -0
  15. labtasker-0.1.3/labtasker/client/client_api.py +118 -0
  16. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/core/api.py +35 -22
  17. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/core/cli_utils.py +1 -1
  18. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/core/config.py +6 -0
  19. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/core/heartbeat.py +20 -5
  20. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/core/job_runner.py +7 -10
  21. labtasker-0.1.3/labtasker/client/core/resolver/__init__.py +6 -0
  22. labtasker-0.1.3/labtasker/client/core/resolver/models.py +37 -0
  23. labtasker-0.1.3/labtasker/client/core/resolver/utils.py +316 -0
  24. labtasker-0.1.3/labtasker/client/core/utils.py +80 -0
  25. labtasker-0.1.3/labtasker/client/core/version_checker.py +137 -0
  26. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/templates/labtasker_root/.gitignore +1 -1
  27. labtasker-0.1.3/labtasker/client/templates/labtasker_root/client.toml +20 -0
  28. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/constants.py +4 -0
  29. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/server/database.py +30 -8
  30. labtasker-0.1.3/labtasker/server/db_utils.py +249 -0
  31. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/utils.py +24 -118
  32. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker.egg-info/SOURCES.txt +37 -24
  33. {labtasker-0.1.2 → labtasker-0.1.3}/pyproject.toml +5 -1
  34. labtasker-0.1.3/tests/test_api_models.py +19 -0
  35. {labtasker-0.1.2 → labtasker-0.1.3}/tests/test_client/test_cli/test_loop.py +1 -1
  36. {labtasker-0.1.2/tests → labtasker-0.1.3/tests/test_client/test_core}/test_cli_utils.py +1 -1
  37. {labtasker-0.1.2 → labtasker-0.1.3}/tests/test_client/test_core/test_heartbeat.py +23 -9
  38. {labtasker-0.1.2 → labtasker-0.1.3}/tests/test_client/test_core/test_job_runner.py +5 -4
  39. {labtasker-0.1.2 → labtasker-0.1.3}/tests/test_client/test_core/test_job_runner_timeout.py +4 -3
  40. labtasker-0.1.3/tests/test_client/test_core/test_job_runner_with_resolver_timeout.py +128 -0
  41. labtasker-0.1.3/tests/test_client/test_core/test_resolver.py +405 -0
  42. labtasker-0.1.3/tests/test_client/test_core/test_runner_with_resolver.py +598 -0
  43. labtasker-0.1.3/tests/test_client/test_core/test_server_notification_and_client_version.py +70 -0
  44. labtasker-0.1.3/tests/test_client/test_core/test_version_checker.py +66 -0
  45. {labtasker-0.1.2/tests → labtasker-0.1.3/tests/test_client}/test_parser.py +2 -5
  46. {labtasker-0.1.2/tests → labtasker-0.1.3/tests/test_server}/test_database/test_database.py +159 -6
  47. {labtasker-0.1.2/tests/test_utils → labtasker-0.1.3/tests/test_server/test_db_utils}/test_arg_match.py +31 -2
  48. labtasker-0.1.3/tests/test_server/test_db_utils/test_keys_to_query_dict.py +30 -0
  49. labtasker-0.1.2/tests/test_utils/test_keys_to_query_dict.py → labtasker-0.1.3/tests/test_server/test_db_utils/test_keys_to_query_dict_deepest.py +7 -6
  50. labtasker-0.1.3/tests/test_server/test_db_utils/test_keys_to_query_dict_topmost.py +44 -0
  51. labtasker-0.1.3/tests/test_utils/__init__.py +0 -0
  52. labtasker-0.1.2/demo/python_demo/run_job.py +0 -20
  53. labtasker-0.1.2/labtasker/client/client_api.py +0 -30
  54. labtasker-0.1.2/labtasker/client/templates/labtasker_root/client.toml +0 -14
  55. labtasker-0.1.2/labtasker/server/db_utils.py +0 -91
  56. labtasker-0.1.2/plugins/labtasker_plugin_task_count/.gitignore +0 -186
  57. labtasker-0.1.2/plugins/labtasker_plugin_task_count/README.md +0 -13
  58. labtasker-0.1.2/plugins/labtasker_plugin_task_count/pyproject.toml +0 -32
  59. labtasker-0.1.2/plugins/labtasker_plugin_task_count/src/labtasker_plugin_task_count/impl.py +0 -13
  60. labtasker-0.1.2/plugins/labtasker_plugin_task_count/src/labtasker_plugin_task_count/main.py +0 -30
  61. {labtasker-0.1.2 → labtasker-0.1.3}/.coveragerc +0 -0
  62. {labtasker-0.1.2 → labtasker-0.1.3}/.dockerignore +0 -0
  63. {labtasker-0.1.2 → labtasker-0.1.3}/.gitattributes +0 -0
  64. {labtasker-0.1.2 → labtasker-0.1.3}/.gitignore +0 -0
  65. {labtasker-0.1.2 → labtasker-0.1.3}/.pre-commit-config.yaml +0 -0
  66. {labtasker-0.1.2 → labtasker-0.1.3}/.pytest.ini +0 -0
  67. {labtasker-0.1.2 → labtasker-0.1.3}/.python-version +0 -0
  68. {labtasker-0.1.2 → labtasker-0.1.3}/Dockerfile +0 -0
  69. {labtasker-0.1.2 → labtasker-0.1.3}/PRIVACY.md +0 -0
  70. {labtasker-0.1.2 → labtasker-0.1.3}/README.md +0 -0
  71. {labtasker-0.1.2/demo → labtasker-0.1.3/demo/basic}/bash_demo/job_main.py +0 -0
  72. {labtasker-0.1.2/demo → labtasker-0.1.3/demo/basic}/bash_demo/run_job.sh +0 -0
  73. {labtasker-0.1.2/demo → labtasker-0.1.3/demo/basic}/bash_demo/submit_job.sh +0 -0
  74. {labtasker-0.1.2/demo → labtasker-0.1.3/demo/basic}/python_demo/submit_job.py +0 -0
  75. {labtasker-0.1.2 → labtasker-0.1.3}/docker/mongodb/docker-entrypoint-wrapped.sh +0 -0
  76. {labtasker-0.1.2 → labtasker-0.1.3}/docker/mongodb/healthcheck.sh +0 -0
  77. {labtasker-0.1.2 → labtasker-0.1.3}/docker/mongodb/init.d/init-keyfile.sh +0 -0
  78. {labtasker-0.1.2 → labtasker-0.1.3}/docker/mongodb/post-init.d/init-mongo.sh +0 -0
  79. {labtasker-0.1.2 → labtasker-0.1.3}/docker-compose.yml +0 -0
  80. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/__main__.py +0 -0
  81. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/__init__.py +0 -0
  82. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/cli/__init__.py +0 -0
  83. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/cli/config.py +0 -0
  84. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/core/__init__.py +0 -0
  85. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/core/cmd_parser/LabCmd.g4 +0 -0
  86. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/core/cmd_parser/LabCmd.py +0 -0
  87. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/core/cmd_parser/LabCmdLexer.g4 +0 -0
  88. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/core/cmd_parser/LabCmdLexer.py +0 -0
  89. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/core/cmd_parser/LabCmdListener.py +0 -0
  90. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/core/cmd_parser/__init__.py +0 -0
  91. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/core/cmd_parser/parser.py +0 -0
  92. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/core/context.py +0 -0
  93. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/core/exceptions.py +0 -0
  94. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/core/logging.py +0 -0
  95. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/core/paths.py +0 -0
  96. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/core/plugin_utils.py +0 -0
  97. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/client/templates/labtasker_root/logs/.gitkeep +0 -0
  98. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/concurrent.py +0 -0
  99. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/filtering.py +0 -0
  100. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/security.py +0 -0
  101. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/server/__init__.py +0 -0
  102. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/server/config.py +0 -0
  103. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/server/dependencies.py +0 -0
  104. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/server/endpoints.py +0 -0
  105. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/server/fsm.py +0 -0
  106. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/server/logging.py +0 -0
  107. {labtasker-0.1.2 → labtasker-0.1.3}/labtasker/server/run.py +0 -0
  108. {labtasker-0.1.2 → labtasker-0.1.3}/scripts/check_version.py +0 -0
  109. {labtasker-0.1.2 → labtasker-0.1.3}/server.example.env +0 -0
  110. {labtasker-0.1.2 → labtasker-0.1.3}/setup.cfg +0 -0
  111. {labtasker-0.1.2/plugins/labtasker_plugin_task_count/src/labtasker_plugin_task_count → labtasker-0.1.3/tests}/__init__.py +0 -0
  112. {labtasker-0.1.2 → labtasker-0.1.3}/tests/conftest.py +0 -0
  113. {labtasker-0.1.2 → labtasker-0.1.3}/tests/demo_pager_iterator.py +0 -0
  114. {labtasker-0.1.2 → labtasker-0.1.3}/tests/dummy_jobs/job_1.py +0 -0
  115. {labtasker-0.1.2/tests → labtasker-0.1.3/tests/fixtures}/__init__.py +0 -0
  116. {labtasker-0.1.2 → labtasker-0.1.3}/tests/fixtures/database/__init__.py +0 -0
  117. {labtasker-0.1.2 → labtasker-0.1.3}/tests/fixtures/database/mock.py +0 -0
  118. {labtasker-0.1.2 → labtasker-0.1.3}/tests/fixtures/database/real.py +0 -0
  119. {labtasker-0.1.2 → labtasker-0.1.3}/tests/fixtures/logging.py +0 -0
  120. {labtasker-0.1.2 → labtasker-0.1.3}/tests/fixtures/mock_datetime_now.py +0 -0
  121. {labtasker-0.1.2 → labtasker-0.1.3}/tests/fixtures/server/__init__.py +0 -0
  122. {labtasker-0.1.2 → labtasker-0.1.3}/tests/fixtures/server/async_app.py +0 -0
  123. {labtasker-0.1.2 → labtasker-0.1.3}/tests/fixtures/server/sync_app.py +0 -0
  124. {labtasker-0.1.2/tests/fixtures → labtasker-0.1.3/tests/test_client}/__init__.py +0 -0
  125. {labtasker-0.1.2 → labtasker-0.1.3}/tests/test_client/conftest.py +0 -0
  126. {labtasker-0.1.2/tests/test_client → labtasker-0.1.3/tests/test_client/test_cli}/__init__.py +0 -0
  127. {labtasker-0.1.2 → labtasker-0.1.3}/tests/test_client/test_cli/conftest.py +0 -0
  128. {labtasker-0.1.2 → labtasker-0.1.3}/tests/test_client/test_cli/test_basic.py +0 -0
  129. {labtasker-0.1.2 → labtasker-0.1.3}/tests/test_client/test_cli/test_config.py +0 -0
  130. {labtasker-0.1.2 → labtasker-0.1.3}/tests/test_client/test_cli/test_queue.py +0 -0
  131. {labtasker-0.1.2 → labtasker-0.1.3}/tests/test_client/test_cli/test_task.py +0 -0
  132. {labtasker-0.1.2 → labtasker-0.1.3}/tests/test_client/test_cli/test_worker.py +0 -0
  133. {labtasker-0.1.2/tests/test_client/test_cli → labtasker-0.1.3/tests/test_client/test_core}/__init__.py +0 -0
  134. {labtasker-0.1.2 → labtasker-0.1.3}/tests/test_client/test_core/test_logging.py +0 -0
  135. {labtasker-0.1.2 → labtasker-0.1.3}/tests/test_client/test_core/test_pager_iterator.py +0 -0
  136. {labtasker-0.1.2/tests/test_client/test_core → labtasker-0.1.3/tests/test_filtering}/__init__.py +0 -0
  137. {labtasker-0.1.2 → labtasker-0.1.3}/tests/test_filtering/exception_utils.py +0 -0
  138. {labtasker-0.1.2 → labtasker-0.1.3}/tests/test_filtering/test_exception_filtering.py +0 -0
  139. {labtasker-0.1.2 → labtasker-0.1.3}/tests/test_mock_time.py +0 -0
  140. {labtasker-0.1.2 → labtasker-0.1.3}/tests/test_security.py +0 -0
  141. {labtasker-0.1.2/tests/test_database → labtasker-0.1.3/tests/test_server}/__init__.py +0 -0
  142. {labtasker-0.1.2 → labtasker-0.1.3}/tests/test_server/conftest.py +0 -0
  143. {labtasker-0.1.2/tests/test_filtering → labtasker-0.1.3/tests/test_server/test_database}/__init__.py +0 -0
  144. {labtasker-0.1.2/tests → labtasker-0.1.3/tests/test_server}/test_database/conftest.py +0 -0
  145. {labtasker-0.1.2/tests → labtasker-0.1.3/tests/test_server}/test_database/test_benchmark_threadpool.py +0 -0
  146. {labtasker-0.1.2/tests → labtasker-0.1.3/tests/test_server}/test_database/test_query_dict_to_mongo_filter.py +0 -0
  147. {labtasker-0.1.2/tests/test_server → labtasker-0.1.3/tests/test_server/test_db_utils}/__init__.py +0 -0
  148. {labtasker-0.1.2/tests/test_utils → labtasker-0.1.3/tests/test_server/test_endpoint}/__init__.py +0 -0
  149. {labtasker-0.1.2/tests/test_server → labtasker-0.1.3/tests/test_server/test_endpoint}/test_server.py +0 -0
  150. {labtasker-0.1.2/tests/test_server → labtasker-0.1.3/tests/test_server/test_endpoint}/test_server_async.py +0 -0
  151. {labtasker-0.1.2/tests/test_server → labtasker-0.1.3/tests/test_server/test_endpoint}/test_server_async_ping.py +0 -0
  152. {labtasker-0.1.2/tests → labtasker-0.1.3/tests/test_server}/test_fsm.py +0 -0
  153. {labtasker-0.1.2 → labtasker-0.1.3}/tests/test_server/test_get_verified_queue_dependency.py +0 -0
  154. {labtasker-0.1.2 → labtasker-0.1.3}/tests/test_threadpool.py +0 -0
  155. {labtasker-0.1.2 → labtasker-0.1.3}/tests/test_utils/test_utils.py +0 -0
  156. {labtasker-0.1.2 → labtasker-0.1.3}/tox.ini +0 -0
@@ -0,0 +1,4 @@
1
+ [submodule "plugins/labtasker_plugin_task_count"]
2
+ path = plugins/labtasker_plugin_task_count
3
+ url = https://github.com/fkcptlst/labtasker-plugin-task-count.git
4
+ branch = main
@@ -11,6 +11,8 @@ prune .venv
11
11
  prune .tox
12
12
  prune .benchmarks
13
13
 
14
+ prune plugins
15
+
14
16
  exclude Makefile
15
17
  exclude coverage.xml
16
18
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: labtasker
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: A task queue system for lab experiments
5
5
  Author-email: Your Name <your.email@example.com>
6
6
  License: MIT
@@ -54,6 +54,8 @@ Requires-Dist: pytest-dependency<0.7.0,>=0.6.0; extra == "dev"
54
54
  Provides-Extra: doc
55
55
  Requires-Dist: mkdocs-material<9.7.0,>=9.6.5; extra == "doc"
56
56
  Requires-Dist: mike<2.2.0,>=2.1.3; extra == "doc"
57
+ Provides-Extra: plugins
58
+ Requires-Dist: labtasker-plugin-task-count; extra == "plugins"
57
59
 
58
60
  # Labtasker
59
61
 
@@ -0,0 +1,11 @@
1
+ import labtasker
2
+
3
+ if __name__ == "__main__":
4
+ for i in range(5):
5
+ print(f"Submitting i={i}")
6
+ labtasker.submit_task(
7
+ args={
8
+ "args_a": {"a": i, "b": "boy"},
9
+ "args_b": {"foo": 2 * i, "bar": "baz"},
10
+ }
11
+ )
@@ -0,0 +1,38 @@
1
+ import time
2
+ from dataclasses import dataclass
3
+ from typing import Any, Dict
4
+
5
+ from typing_extensions import Annotated
6
+
7
+ import labtasker
8
+ from labtasker import Required
9
+
10
+
11
+ @dataclass
12
+ class ArgsGroupA:
13
+ a: int
14
+ b: str
15
+
16
+
17
+ @dataclass
18
+ class ArgsGroupB:
19
+ foo: int
20
+ bar: str
21
+
22
+
23
+ @labtasker.loop()
24
+ def main(
25
+ # use type annotation/default values to automatically resolve the required_fields
26
+ # use the self-defined resolver to convert the task args into custom types
27
+ args_a: Annotated[
28
+ Dict[str, Any], Required(resolver=lambda a: ArgsGroupA(**a))
29
+ ], # option1. use Annotated
30
+ args_b=Required(resolver=lambda b: ArgsGroupB(**b)), # option2. use default kwarg
31
+ ):
32
+ print(f"got args_a: {args_a}")
33
+ print(f"got args_b: {args_b}")
34
+ time.sleep(0.5)
35
+
36
+
37
+ if __name__ == "__main__":
38
+ main()
@@ -0,0 +1,29 @@
1
+ import time
2
+ from dataclasses import dataclass
3
+
4
+ import labtasker
5
+
6
+
7
+ @dataclass
8
+ class ArgsGroupA:
9
+ a: int
10
+ b: str
11
+
12
+
13
+ @dataclass
14
+ class ArgsGroupB:
15
+ foo: int
16
+ bar: str
17
+
18
+
19
+ @labtasker.loop(required_fields=["args_a", "args_b"], pass_args_dict=True)
20
+ def main(args):
21
+ args_a = ArgsGroupA(**args["args_a"])
22
+ args_b = ArgsGroupB(**args["args_b"])
23
+ print(f"got args_a: {args_a}")
24
+ print(f"got args_b: {args_b}")
25
+ time.sleep(0.5)
26
+
27
+
28
+ if __name__ == "__main__":
29
+ main()
@@ -0,0 +1,25 @@
1
+ import time
2
+
3
+ import labtasker
4
+ from labtasker import Required
5
+
6
+
7
+ def job(arg1: int, arg2: int):
8
+ """Simulate a long-running job"""
9
+ time.sleep(3) # simulate a long-running job
10
+ return arg1 + arg2
11
+
12
+
13
+ @labtasker.loop()
14
+ def main(arg1: int = Required(), arg2: int = Required()):
15
+ # The labtasker autofills the parameter specified by Required()
16
+ # or Annotated[Any, Required()]
17
+ # Alternatively, you can fill in the required fields
18
+ # in loop(required_fields=["arg1", "arg2"]
19
+ # and access it via labtasker.task_info().args
20
+ result = job(arg1, arg2)
21
+ print(f"The result is {result}")
22
+
23
+
24
+ if __name__ == "__main__":
25
+ main()
@@ -1,12 +1,14 @@
1
+ __version__ = "0.1.3"
2
+
1
3
  from labtasker.client.client_api import *
2
4
  from labtasker.client.core.config import get_client_config
3
5
  from labtasker.client.core.exceptions import *
4
6
  from labtasker.client.core.paths import get_labtasker_client_config_path
7
+ from labtasker.client.core.version_checker import check_pypi_status
5
8
  from labtasker.filtering import install_traceback_filter, set_traceback_filter_hook
6
9
 
7
- __version__ = "0.1.2"
8
-
9
10
  install_traceback_filter()
11
+ check_pypi_status()
10
12
 
11
13
  # by default, traceback filter is enabled.
12
14
  # you may disable it via client config
@@ -1,9 +1,12 @@
1
1
  from datetime import datetime
2
2
  from typing import Any, Dict, List, Optional, Union
3
3
 
4
- from pydantic import BaseModel, ConfigDict, Field, SecretStr
4
+ from packaging.version import Version
5
+ from pydantic import BaseModel, ConfigDict, Field, SecretStr, field_validator
5
6
 
7
+ from labtasker import __version__
6
8
  from labtasker.constants import Priority
9
+ from labtasker.utils import validate_dict_keys
7
10
 
8
11
 
9
12
  class BaseApiModel(BaseModel):
@@ -14,12 +17,61 @@ class BaseApiModel(BaseModel):
14
17
  model_config = ConfigDict(populate_by_name=True)
15
18
 
16
19
 
17
- class HealthCheckResponse(BaseApiModel):
20
+ class BaseRequestModel(BaseApiModel):
21
+ client_version: str = __version__
22
+
23
+ @field_validator("client_version")
24
+ def validate_client_version(cls, v, field):
25
+ # make sure it is a valid version
26
+ Version(v)
27
+ return v
28
+
29
+
30
+ class Notification(BaseModel):
31
+ """Server notification such as compatibility warning etc."""
32
+
33
+ type: str = Field(..., pattern=r"^(info|warning|error)$")
34
+ level: str = Field(..., pattern=r"^(low|medium|high)$")
35
+ details: Union[str, Dict[str, Any]]
36
+
37
+
38
+ class BaseResponseModel(BaseApiModel):
39
+ notification: Optional[List[Notification]] = None
40
+
41
+
42
+ class MetadataKeyValidateMixin:
43
+
44
+ @field_validator("metadata")
45
+ def validate_keys(cls, v, field):
46
+ if v:
47
+ validate_dict_keys(v)
48
+ return v
49
+
50
+
51
+ class ArgsKeyValidateMixin:
52
+
53
+ @field_validator("args")
54
+ def validate_keys(cls, v, field):
55
+ if v:
56
+ validate_dict_keys(v)
57
+ return v
58
+
59
+
60
+ class SummaryKeyValidateMixin:
61
+
62
+ @field_validator("summary")
63
+ def validate_keys(cls, v, field):
64
+ if v:
65
+ validate_dict_keys(v)
66
+ return v
67
+
68
+
69
+ class HealthCheckResponse(BaseResponseModel):
18
70
  status: str = Field(..., pattern=r"^(healthy|unhealthy)$")
19
71
  database: str
20
72
 
21
73
 
22
- class QueueCreateRequest(BaseApiModel):
74
+ class QueueCreateRequest(BaseRequestModel, MetadataKeyValidateMixin):
23
75
  queue_name: str = Field(
24
76
  ..., pattern=r"^[a-zA-Z0-9_-]+$", min_length=1, max_length=100
25
77
  )
@@ -35,11 +87,11 @@ class QueueCreateRequest(BaseApiModel):
35
87
  return result
36
88
 
37
89
 
38
- class QueueCreateResponse(BaseApiModel):
90
+ class QueueCreateResponse(BaseResponseModel):
39
91
  queue_id: str
40
92
 
41
93
 
42
- class QueueGetResponse(BaseApiModel):
94
+ class QueueGetResponse(BaseResponseModel, MetadataKeyValidateMixin):
43
95
  queue_id: str = Field(alias="_id")
44
96
  queue_name: str
45
97
  created_at: datetime
@@ -47,7 +99,12 @@ class QueueGetResponse(BaseApiModel):
47
99
  metadata: Dict[str, Any]
48
100
 
49
101
 
50
- class TaskSubmitRequest(BaseApiModel):
102
+ class TaskSubmitRequest(
103
+ BaseRequestModel,
104
+ ArgsKeyValidateMixin,
105
+ MetadataKeyValidateMixin,
106
+ SummaryKeyValidateMixin,
107
+ ):
51
108
  """Task submission request."""
52
109
 
53
110
  task_name: Optional[str] = Field(
@@ -62,16 +119,21 @@ class TaskSubmitRequest(BaseApiModel):
62
119
  priority: int = Priority.MEDIUM
63
120
 
64
121
 
65
- class TaskFetchRequest(BaseApiModel):
122
+ class TaskFetchRequest(BaseRequestModel):
66
123
  worker_id: Optional[str] = None
67
124
  eta_max: Optional[str] = None
68
125
  heartbeat_timeout: Optional[float] = None
69
126
  start_heartbeat: bool = True
70
- required_fields: Optional[Dict[str, Any]] = None
127
+ required_fields: Optional[List[str]] = None
71
128
  extra_filter: Optional[Dict[str, Any]] = None
72
129
 
73
130
 
74
- class Task(BaseApiModel):
131
+ class Task(
132
+ BaseApiModel,
133
+ ArgsKeyValidateMixin,
134
+ MetadataKeyValidateMixin,
135
+ SummaryKeyValidateMixin,
136
+ ):
75
137
  task_id: str = Field(alias="_id") # Accepts "_id" as an input field
76
138
  queue_id: str
77
139
  status: str
@@ -92,7 +154,12 @@ class Task(BaseApiModel):
92
154
  worker_id: Optional[str]
93
155
 
94
156
 
95
- class TaskUpdateRequest(BaseApiModel):
157
+ class TaskUpdateRequest(
158
+ BaseRequestModel,
159
+ ArgsKeyValidateMixin,
160
+ MetadataKeyValidateMixin,
161
+ SummaryKeyValidateMixin,
162
+ ):
96
163
  """This should be consistent with Task.
97
164
  Fields that disallow manual update are commented out.
98
165
  """
@@ -130,12 +197,12 @@ class TaskUpdateRequest(BaseApiModel):
130
197
  # worker_id: Optional[str]
131
198
 
132
199
 
133
- class TaskFetchResponse(BaseApiModel):
200
+ class TaskFetchResponse(BaseResponseModel):
134
201
  found: bool = False
135
202
  task: Optional[Task] = None
136
203
 
137
204
 
138
- class TaskLsRequest(BaseApiModel):
205
+ class TaskLsRequest(BaseRequestModel):
139
206
  offset: int = Field(0, ge=0)
140
207
  limit: int = Field(100, ge=0, le=1000)
141
208
  task_id: Optional[str] = None
@@ -143,36 +210,36 @@ class TaskLsRequest(BaseApiModel):
143
210
  extra_filter: Optional[Dict[str, Any]] = None
144
211
 
145
212
 
146
- class TaskLsResponse(BaseApiModel):
213
+ class TaskLsResponse(BaseResponseModel):
147
214
  found: bool = False
148
215
  content: List[Task] = Field(default_factory=list)
149
216
 
150
217
 
151
- class TaskSubmitResponse(BaseApiModel):
218
+ class TaskSubmitResponse(BaseResponseModel):
152
219
  task_id: str
153
220
 
154
221
 
155
- class TaskStatusUpdateRequest(BaseApiModel):
222
+ class TaskStatusUpdateRequest(BaseRequestModel):
156
223
  status: str = Field(..., pattern=r"^(success|failed|cancelled)$")
157
224
  worker_id: Optional[str] = None
158
225
  summary: Optional[Dict[str, Any]] = None
159
226
 
160
227
 
161
- class WorkerCreateRequest(BaseApiModel):
228
+ class WorkerCreateRequest(BaseRequestModel, MetadataKeyValidateMixin):
162
229
  worker_name: Optional[str] = None
163
230
  metadata: Optional[Dict[str, Any]] = None
164
231
  max_retries: Optional[int] = 3
165
232
 
166
233
 
167
- class WorkerCreateResponse(BaseApiModel):
234
+ class WorkerCreateResponse(BaseResponseModel):
168
235
  worker_id: str
169
236
 
170
237
 
171
- class WorkerStatusUpdateRequest(BaseApiModel):
238
+ class WorkerStatusUpdateRequest(BaseRequestModel):
172
239
  status: str = Field(..., pattern=r"^(active|suspended|failed)$")
173
240
 
174
241
 
175
- class WorkerLsRequest(BaseApiModel):
242
+ class WorkerLsRequest(BaseRequestModel):
176
243
  offset: int = Field(0, ge=0)
177
244
  limit: int = Field(100, ge=0, le=1000)
178
245
  worker_id: Optional[str] = None
@@ -180,7 +247,7 @@ class WorkerLsRequest(BaseApiModel):
180
247
  extra_filter: Optional[Dict[str, Any]] = None
181
248
 
182
249
 
183
- class Worker(BaseApiModel):
250
+ class Worker(BaseApiModel, MetadataKeyValidateMixin):
184
251
  worker_id: str = Field(alias="_id")
185
252
  queue_id: str
186
253
  status: str
@@ -194,12 +261,12 @@ class Worker(BaseApiModel):
194
261
  last_modified: datetime
195
262
 
196
263
 
197
- class WorkerLsResponse(BaseApiModel):
264
+ class WorkerLsResponse(BaseResponseModel):
198
265
  found: bool = False
199
266
  content: List[Worker] = Field(default_factory=list)
200
267
 
201
268
 
202
- class QueueUpdateRequest(BaseApiModel):
269
+ class QueueUpdateRequest(BaseRequestModel):
203
270
  new_queue_name: Optional[str] = Field(
204
271
  None, pattern=r"^[a-zA-Z0-9_-]+$", min_length=1, max_length=100
205
272
  )
@@ -8,7 +8,7 @@ import httpx
8
8
  import typer
9
9
  from typing_extensions import Annotated
10
10
 
11
- from labtasker import __version__
11
+ from labtasker import __version__, check_pypi_status
12
12
  from labtasker.client.core.api import health_check
13
13
  from labtasker.client.core.config import requires_client_config
14
14
  from labtasker.client.core.logging import stderr_console, stdout_console
@@ -19,6 +19,7 @@ app = typer.Typer(context_settings={"help_option_names": ["-h", "--help"]})
19
19
  def version_callback(value: bool):
20
20
  if value:
21
21
  stdout_console.print(f"Labtasker Version: {__version__}")
22
+ check_pypi_status(force_check=True, blocking=True)
22
23
  raise typer.Exit()
23
24
 
24
25
 
@@ -22,10 +22,8 @@ from labtasker.client.core.cli_utils import (
22
22
  from labtasker.client.core.cmd_parser import cmd_interpolate
23
23
  from labtasker.client.core.config import get_client_config
24
24
  from labtasker.client.core.exceptions import CmdParserError
25
- from labtasker.client.core.job_runner import finish
26
- from labtasker.client.core.job_runner import loop as loop_run
25
+ from labtasker.client.core.job_runner import finish, loop_run
27
26
  from labtasker.client.core.logging import logger, stderr_console, stdout_console
28
- from labtasker.utils import keys_to_query_dict
29
27
 
30
28
 
31
29
  class InfiniteDefaultDict(defaultdict):
@@ -59,6 +57,8 @@ def loop(
59
57
  ),
60
58
  extra_filter: Optional[str] = typer.Option(
61
59
  None,
60
+ "--extra-filter",
61
+ "-f",
62
62
  help='Optional mongodb filter as a dict string (e.g., \'{"key": "value"}\').',
63
63
  ),
64
64
  worker_id: Optional[str] = typer.Option(
@@ -102,7 +102,7 @@ def loop(
102
102
  except (CmdParserError, KeyError, TypeError) as e:
103
103
  raise typer.BadParameter(f"Command error with exception {e}")
104
104
 
105
- required_fields = keys_to_query_dict(list(queried_keys))
105
+ required_fields = list(queried_keys)
106
106
 
107
107
  logger.info(f"Got command: {cmd}")
108
108
 
@@ -74,6 +74,8 @@ def create(
74
74
  ),
75
75
  quiet: bool = typer.Option(
76
76
  False,
77
+ "--quiet",
78
+ "-q",
77
79
  help="Only show queue ID string, rather than full response. Useful when using in bash scripts.",
78
80
  ),
79
81
  ):
@@ -99,6 +101,8 @@ def create_from_config(
99
101
  ),
100
102
  quiet: bool = typer.Option(
101
103
  False,
104
+ "--quiet",
105
+ "-q",
102
106
  help="Only show queue ID string, rather than full response. Useful when using in bash scripts.",
103
107
  ),
104
108
  ):
@@ -118,7 +122,9 @@ def create_from_config(
118
122
  @app.command()
119
123
  @cli_utils_decorator
120
124
  def get(
121
- quiet: bool = typer.Option(False, help="Only show queue ID string."),
125
+ quiet: bool = typer.Option(
126
+ False, "--quiet", "-q", help="Only show queue ID string."
127
+ ),
122
128
  ):
123
129
  """Get current queue info."""
124
130
  resp = get_queue()
@@ -148,6 +154,8 @@ def update(
148
154
  ),
149
155
  quiet: bool = typer.Option(
150
156
  False,
157
+ "--quiet",
158
+ "-q",
151
159
  help="Suppress the output. Execution result is only available via status code.",
152
160
  ),
153
161
  ):
@@ -274,6 +274,8 @@ def ls(
274
274
  ),
275
275
  quiet: bool = typer.Option(
276
276
  False,
277
+ "--quiet",
278
+ "-q",
277
279
  help="Only show task IDs that match the query, rather than full entry. "
278
280
  "Useful when using in bash scripts.",
279
281
  ),
@@ -380,6 +382,8 @@ def update(
380
382
  ),
381
383
  quiet: bool = typer.Option(
382
384
  False,
385
+ "--quiet",
386
+ "-q",
383
387
  help="Disable interactive mode and confirmations. Set this to true if you are using this in a bash script.",
384
388
  ),
385
389
  editor: Optional[str] = typer.Option(
@@ -59,6 +59,8 @@ def create(
59
59
  ),
60
60
  quiet: bool = typer.Option(
61
61
  False,
62
+ "--quiet",
63
+ "-q",
62
64
  help="Only show worker ID string, rather than full response. Useful when using in bash scripts.",
63
65
  ),
64
66
  ):
@@ -95,10 +97,14 @@ def ls(
95
97
  ),
96
98
  extra_filter: Optional[str] = typer.Option(
97
99
  None,
100
+ "--extra-filter",
101
+ "-f",
98
102
  help='Optional mongodb filter as a dict string (e.g., \'{"key": "value"}\').',
99
103
  ),
100
104
  quiet: bool = typer.Option(
101
105
  False,
106
+ "--quiet",
107
+ "-q",
102
108
  help="Only show worker IDs that match the query, rather than full entry. Useful when using in bash scripts.",
103
109
  ),
104
110
  pager: bool = typer.Option(
@@ -0,0 +1,118 @@
1
+ from typing import Any, Dict, List, Optional, Union
2
+
3
+ from labtasker.client.core.api import *
4
+ from labtasker.client.core.context import current_task_id, current_worker_id, task_info
5
+ from labtasker.client.core.exceptions import LabtaskerTypeError, LabtaskerValueError
6
+ from labtasker.client.core.job_runner import finish, loop_run
7
+ from labtasker.client.core.resolver import (
8
+ Required,
9
+ get_params_from_function,
10
+ get_required_fields,
11
+ resolve_args_partial,
12
+ )
13
+ from labtasker.utils import validate_required_fields
14
+
15
+ __all__ = [
16
+ # python job runner api
17
+ "loop",
18
+ "finish",
19
+ "Required",
20
+ # context api
21
+ "task_info",
22
+ "current_task_id",
23
+ "current_worker_id",
24
+ # http api (you should be careful with these unless you know what you are doing)
25
+ "close_httpx_client",
26
+ "health_check",
27
+ "submit_task",
28
+ "delete_worker",
29
+ "create_queue",
30
+ "create_worker",
31
+ "delete_queue",
32
+ "delete_task",
33
+ "delete_worker",
34
+ "fetch_task",
35
+ "get_queue",
36
+ "health_check",
37
+ "ls_tasks",
38
+ "ls_worker",
39
+ "refresh_task_heartbeat",
40
+ "report_task_status",
41
+ ]
42
+
43
+
44
+ def loop(
45
+ required_fields: List[str] = None,
46
+ extra_filter: Optional[Dict[str, Any]] = None,
47
+ cmd: Optional[Union[str, List[str]]] = None,
48
+ worker_id: Optional[str] = None,
49
+ create_worker_kwargs: Optional[Dict[str, Any]] = None,
50
+ eta_max: Optional[str] = None,
51
+ heartbeat_timeout: Optional[float] = None,
52
+ pass_args_dict: bool = False,
53
+ ):
54
+ """Run the wrapped job function in loop.
55
+
56
+ Args:
57
+ required_fields: Fields (or extra fields other than specified using Required(...)) required for task execution in a dot-separated manner. E.g. ["arg1.arg11", "arg2.arg22"]
58
+ extra_filter: Additional filtering criteria for tasks
59
+ cmd: Command line arguments that runs current process. Default to sys.argv
60
+ worker_id: Specific worker ID to use
61
+ create_worker_kwargs: Arguments for default worker creation
62
+ eta_max: Maximum ETA for task execution.
63
+ heartbeat_timeout: Heartbeat timeout in seconds. Default to 3 times the send interval.
64
+ pass_args_dict: If True, passes task_info().args as first argument
65
+
66
+ Returns:
67
+ The decorated function
68
+
69
+ """
70
+ try:
71
+ if required_fields is not None:
72
+ validate_required_fields(required_fields)
73
+ except ValueError as e:
74
+ raise LabtaskerValueError(str(e)) from e
75
+ except TypeError as e:
76
+ raise LabtaskerTypeError(str(e)) from e
77
+
78
+ def decorator(func):
79
+ """
80
+ Steps:
81
+ 1. Try get required fields from type annotations.
82
+ 2. Wrap the job function with an args resolver wrapper
83
+ 3. Wrap the resolver-wrapped job function with loop_run
84
+ 4. Return the wrapped function
85
+
86
+ Args:
87
+ func:
88
+
89
+ Returns:
90
+
91
+ """
92
+ param_metas = get_params_from_function(func)
93
+
94
+ # if required_fields is provided, merge them with the ones specified in param_metas
95
+ all_required_fields = get_required_fields(
96
+ param_metas=param_metas, extra_required_fields=required_fields
97
+ )
98
+
99
+ # wrap the job function with args resolver
100
+ # that is, fill in the positional and keyword arguments for the required fields, type cast them
101
+ # into custom types (e.g. dataclasses) if the type_caster is provided
102
+ func = resolve_args_partial(
103
+ func, param_metas=param_metas, pass_args_dict=pass_args_dict
104
+ )
105
+
106
+ # return the decorated function
107
+ return loop_run(
108
+ required_fields=all_required_fields,
109
+ extra_filter=extra_filter,
110
+ cmd=cmd,
111
+ worker_id=worker_id,
112
+ create_worker_kwargs=create_worker_kwargs,
113
+ eta_max=eta_max,
114
+ heartbeat_timeout=heartbeat_timeout,
115
+ pass_args_dict=True,
116
+ )(func)
117
+
118
+ return decorator