experimaestro 2.0.0a1__tar.gz → 2.0.0a4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/PKG-INFO +3 -2
  2. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/pyproject.toml +5 -2
  3. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/connectors/__init__.py +2 -2
  4. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/arguments.py +20 -1
  5. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/objects/config.py +81 -25
  6. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/objects/config_walk.py +3 -1
  7. experimaestro-2.0.0a4/src/experimaestro/scheduler/__init__.py +18 -0
  8. experimaestro-2.0.0a4/src/experimaestro/scheduler/base.py +310 -0
  9. experimaestro-2.0.0a4/src/experimaestro/scheduler/experiment.py +387 -0
  10. experimaestro-2.0.0a4/src/experimaestro/scheduler/jobs.py +475 -0
  11. experimaestro-2.0.0a4/src/experimaestro/scheduler/signal_handler.py +32 -0
  12. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/scheduler/state.py +1 -1
  13. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/__init__.py +36 -5
  14. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_dependencies.py +1 -1
  15. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_generators.py +41 -9
  16. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/typingutils.py +11 -2
  17. experimaestro-2.0.0a1/src/experimaestro/scheduler/__init__.py +0 -1
  18. experimaestro-2.0.0a1/src/experimaestro/scheduler/base.py +0 -1129
  19. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/LICENSE +0 -0
  20. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/README.md +0 -0
  21. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/__init__.py +0 -0
  22. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/__main__.py +0 -0
  23. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/annotations.py +0 -0
  24. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/checkers.py +0 -0
  25. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/cli/__init__.py +0 -0
  26. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/cli/filter.py +0 -0
  27. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/cli/jobs.py +0 -0
  28. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/cli/progress.py +0 -0
  29. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/click.py +0 -0
  30. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/commandline.py +0 -0
  31. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/compat.py +0 -0
  32. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/connectors/local.py +0 -0
  33. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/connectors/ssh.py +0 -0
  34. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/__init__.py +0 -0
  35. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/callbacks.py +0 -0
  36. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/context.py +0 -0
  37. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/identifier.py +0 -0
  38. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/objects/__init__.py +0 -0
  39. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/objects/config_utils.py +0 -0
  40. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/objects.pyi +0 -0
  41. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/serialization.py +0 -0
  42. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/serializers.py +0 -0
  43. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/types.py +0 -0
  44. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/utils.py +0 -0
  45. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/exceptions.py +0 -0
  46. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/experiments/__init__.py +0 -0
  47. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/experiments/cli.py +0 -0
  48. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/experiments/configuration.py +0 -0
  49. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/generators.py +0 -0
  50. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/huggingface.py +0 -0
  51. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/ipc.py +0 -0
  52. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/launcherfinder/__init__.py +0 -0
  53. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/launcherfinder/base.py +0 -0
  54. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/launcherfinder/parser.py +0 -0
  55. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/launcherfinder/registry.py +0 -0
  56. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/launcherfinder/specs.py +0 -0
  57. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/launchers/__init__.py +0 -0
  58. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/launchers/direct.py +0 -0
  59. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/launchers/oar.py +0 -0
  60. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/launchers/slurm/__init__.py +0 -0
  61. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/launchers/slurm/base.py +0 -0
  62. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/locking.py +0 -0
  63. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/mkdocs/__init__.py +0 -0
  64. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/mkdocs/annotations.py +0 -0
  65. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/mkdocs/base.py +0 -0
  66. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/mkdocs/metaloader.py +0 -0
  67. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/mkdocs/style.css +0 -0
  68. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/mypy.py +0 -0
  69. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/notifications.py +0 -0
  70. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/progress.py +0 -0
  71. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/py.typed +0 -0
  72. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/rpyc.py +0 -0
  73. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/run.py +0 -0
  74. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/scheduler/dependencies.py +0 -0
  75. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/scheduler/dynamic_outputs.py +0 -0
  76. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/scheduler/services.py +0 -0
  77. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/scheduler/workspace.py +0 -0
  78. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/scriptbuilder.py +0 -0
  79. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/0c35d18bf06992036b69.woff2 +0 -0
  80. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/1815e00441357e01619e.ttf +0 -0
  81. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/219aa9140e099e6c72ed.woff2 +0 -0
  82. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/2463b90d9a316e4e5294.woff2 +0 -0
  83. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/2582b0e4bcf85eceead0.ttf +0 -0
  84. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/3a4004a46a653d4b2166.woff +0 -0
  85. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/3baa5b8f3469222b822d.woff +0 -0
  86. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/4d73cb90e394b34b7670.woff +0 -0
  87. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/4ef4218c522f1eb6b5b1.woff2 +0 -0
  88. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/5d681e2edae8c60630db.woff +0 -0
  89. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/6f420cf17cc0d7676fad.woff2 +0 -0
  90. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/89999bdf5d835c012025.woff2 +0 -0
  91. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/914997e1bdfc990d0897.ttf +0 -0
  92. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/c210719e60948b211a12.woff2 +0 -0
  93. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/c380809fd3677d7d6903.woff2 +0 -0
  94. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/f882956fd323fd322f31.woff +0 -0
  95. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/favicon.ico +0 -0
  96. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/index.css +0 -0
  97. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/index.css.map +0 -0
  98. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/index.html +0 -0
  99. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/index.js +0 -0
  100. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/index.js.map +0 -0
  101. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/login.html +0 -0
  102. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/manifest.json +0 -0
  103. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/settings.py +0 -0
  104. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/sphinx/__init__.py +0 -0
  105. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/sphinx/static/experimaestro.css +0 -0
  106. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/taskglobals.py +0 -0
  107. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/__init__.py +0 -0
  108. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/conftest.py +0 -0
  109. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/connectors/bin/executable.py +0 -0
  110. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/connectors/test_local.py +0 -0
  111. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/connectors/utils.py +0 -0
  112. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/core/__init__.py +0 -0
  113. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/core/test_generics.py +0 -0
  114. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/definitions_types.py +0 -0
  115. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/launchers/__init__.py +0 -0
  116. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/launchers/bin/sacct +0 -0
  117. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/launchers/bin/sbatch +0 -0
  118. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/launchers/bin/srun +0 -0
  119. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/launchers/bin/test.py +0 -0
  120. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/launchers/common.py +0 -0
  121. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/launchers/config_slurm/__init__.py +0 -0
  122. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/launchers/config_slurm/launchers.py +0 -0
  123. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/launchers/test_local.py +0 -0
  124. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/launchers/test_slurm.py +0 -0
  125. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/restart.py +0 -0
  126. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/restart_main.py +0 -0
  127. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/scripts/notifyandwait.py +0 -0
  128. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/scripts/waitforfile.py +0 -0
  129. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/task_tokens.py +0 -0
  130. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/tasks/__init__.py +0 -0
  131. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/tasks/all.py +0 -0
  132. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/tasks/foreign.py +0 -0
  133. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_checkers.py +0 -0
  134. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_experiment.py +0 -0
  135. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_file_progress.py +0 -0
  136. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_file_progress_integration.py +0 -0
  137. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_findlauncher.py +0 -0
  138. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_forward.py +0 -0
  139. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_identifier.py +0 -0
  140. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_instance.py +0 -0
  141. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_objects.py +0 -0
  142. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_outputs.py +0 -0
  143. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_param.py +0 -0
  144. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_progress.py +0 -0
  145. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_serializers.py +0 -0
  146. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_snippets.py +0 -0
  147. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_ssh.py +0 -0
  148. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_tags.py +0 -0
  149. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_tasks.py +0 -0
  150. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_tokens.py +0 -0
  151. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_types.py +0 -0
  152. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_validation.py +0 -0
  153. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/token_reschedule.py +0 -0
  154. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/utils.py +0 -0
  155. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tokens.py +0 -0
  156. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tools/__init__.py +0 -0
  157. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tools/diff.py +0 -0
  158. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tools/documentation.py +0 -0
  159. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tools/jobs.py +0 -0
  160. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/utils/__init__.py +0 -0
  161. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/utils/asyncio.py +0 -0
  162. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/utils/jobs.py +0 -0
  163. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/utils/jupyter.py +0 -0
  164. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/utils/multiprocessing.py +0 -0
  165. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/utils/resources.py +0 -0
  166. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/utils/settings.py +0 -0
  167. {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/xpmutils.py +0 -0
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: experimaestro
3
- Version: 2.0.0a1
3
+ Version: 2.0.0a4
4
4
  Summary: "Experimaestro is a computer science experiment manager"
5
5
  License: GPL-3
6
+ License-File: LICENSE
6
7
  Keywords: experiment manager
7
8
  Author: Benjamin Piwowarski
8
9
  Author-email: benjamin@piwowarski.fr
@@ -48,7 +48,7 @@ dependencies = [
48
48
  "typing-extensions >=4.2; python_version < \"3.12\"",
49
49
  "watchdog >=2"
50
50
  ]
51
- version = "2.0.0-a1"
51
+ version = "2.0.0-a4"
52
52
 
53
53
  [tool.poetry-dynamic-versioning]
54
54
  enable = false
@@ -85,6 +85,9 @@ build-backend = "poetry_dynamic_versioning.backend"
85
85
 
86
86
  [dependency-groups]
87
87
  dev = [
88
+ "markdown-include>=0.8.1",
89
+ "mkdocstrings[python]>=0.30.1",
90
+ "pymdown-extensions>=10.16.1",
88
91
  "pytest>=8.4.1",
89
92
  "pytest-timeout>=2.4.0",
90
93
  ]
@@ -146,7 +149,7 @@ warn_unused_ignores = true
146
149
 
147
150
  [tool.commitizen]
148
151
  name = "cz_conventional_commits"
149
- version = "2.0.0a1"
152
+ version = "2.0.0a4"
150
153
  changelog_start_rev = "v1.0.0"
151
154
  tag_format = "v$major.$minor.$patch$prerelease"
152
155
  # update_changelog_on_bump = true
@@ -16,7 +16,7 @@ from experimaestro.utils import logger
16
16
  from experimaestro.locking import Lock
17
17
  from experimaestro.tokens import Token
18
18
  from experimaestro.utils.asyncio import asyncThreadcheck
19
- import pkg_resources
19
+ from importlib.metadata import entry_points
20
20
 
21
21
 
22
22
  class RedirectType(enum.Enum):
@@ -101,7 +101,7 @@ class Process:
101
101
  """Get a handler"""
102
102
  if Process.HANDLERS is None:
103
103
  Process.HANDLERS = {}
104
- for ep in pkg_resources.iter_entry_points(group="experimaestro.process"):
104
+ for ep in entry_points(group="experimaestro.process"):
105
105
  logging.debug("Adding process handler for type %s", ep.name)
106
106
  handler = ep.load()
107
107
  Process.HANDLERS[ep.name] = handler
@@ -80,10 +80,13 @@ class Argument:
80
80
 
81
81
  self.generator = generator
82
82
  self.default = None
83
+ self.ignore_generated = False
83
84
 
84
85
  if default is not None:
85
86
  assert self.generator is None, "generator and default are exclusive options"
86
87
  if isinstance(default, field):
88
+ self.ignore_generated = default.ignore_generated
89
+
87
90
  if default.default is not None:
88
91
  self.default = default.default
89
92
  elif default.default_factory is not None:
@@ -184,13 +187,29 @@ DataPath = Annotated[Path, dataHint]
184
187
  class field:
185
188
  """Extra information for a given experimaestro field (param or meta)"""
186
189
 
187
- def __init__(self, *, default: Any = None, default_factory: Callable = None):
190
+ def __init__(
191
+ self,
192
+ *,
193
+ default: Any = None,
194
+ default_factory: Callable = None,
195
+ ignore_generated=False,
196
+ ):
197
+ """Gives some extra per-field information
198
+
199
+ :param default: a default value, defaults to None
200
+ :param default_factory: a default factory for values, defaults to None
201
+ :param ignore_generated: True if the value is hidden – it won't be accessible in
202
+ tasks, defaults to False. The interest of hidden is to add a
203
+ configuration field that changes the identifier, but will not be
204
+ used.
205
+ """
188
206
  assert not (
189
207
  (default is not None) and (default_factory is not None)
190
208
  ), "default and default_factory are mutually exclusive options"
191
209
 
192
210
  self.default_factory = default_factory
193
211
  self.default = default
212
+ self.ignore_generated = ignore_generated
194
213
 
195
214
 
196
215
  class help(TypeAnnotation):
@@ -169,10 +169,33 @@ class ConfigInformation:
169
169
  self._sealed = False
170
170
  self._meta = None
171
171
 
172
- #: This flags is True when a value in this configuration,
173
- #: or any sub-configuration, is generated. This prevents problem
174
- #: when a configuration with generated values is re-used
175
- self._has_generated_value = False
172
+ # This contains the list of generated values (using context) in this
173
+ # configuration or any sub-configuration, is generated. This prevents
174
+ # problem when a configuration with generated values is re-used.
175
+ self._generated_values = []
176
+
177
+ def get_generated_paths(
178
+ self, path: list[str] = None, paths: list[str] = None
179
+ ) -> list[str]:
180
+ """Get the list of generated paths, useful to track down those
181
+
182
+ :param path: The current path
183
+ :param paths: The list of generated paths so far, defaults to None
184
+ :return: The full list of generated paths
185
+ """
186
+ paths = [] if paths is None else paths
187
+ path = [] if path is None else path
188
+
189
+ for key in self._generated_values:
190
+ value = self.values[key]
191
+ if isinstance(value, ConfigMixin) and value.__xpm__._generated_values:
192
+ path.append(key)
193
+ value.__xpm__.get_generated_paths(path, paths)
194
+ path.pop()
195
+ else:
196
+ paths.append(".".join(path + [key]))
197
+
198
+ return paths
176
199
 
177
200
  def set_meta(self, value: Optional[bool]):
178
201
  """Sets the meta flag"""
@@ -191,6 +214,31 @@ class ConfigInformation:
191
214
  # Not an argument, bypass
192
215
  return object.__getattribute__(self.pyobject, name)
193
216
 
217
+ @staticmethod
218
+ def is_generated_value(argument, value):
219
+ if argument.ignore_generated:
220
+ return False
221
+
222
+ if value is None:
223
+ return False
224
+
225
+ if isinstance(value, (int, str, float, bool, Path)):
226
+ return False
227
+
228
+ if isinstance(value, ConfigMixin):
229
+ return value.__xpm__._generated_values and value.__xpm__.task is None
230
+
231
+ if isinstance(value, list):
232
+ return any(ConfigInformation.is_generated_value(argument, x) for x in value)
233
+
234
+ if isinstance(value, dict):
235
+ return any(
236
+ ConfigInformation.is_generated_value(argument, x)
237
+ for x in value.values()
238
+ )
239
+
240
+ assert False, f"Cannot handle values of type {type(value)}"
241
+
194
242
  def set(self, k, v, bypass=False):
195
243
  from experimaestro.generators import Generator
196
244
 
@@ -207,18 +255,16 @@ class ConfigInformation:
207
255
  "Configuration (and not objects) should be used. Consider using .C(...)"
208
256
  )
209
257
 
210
- if (
211
- isinstance(v, ConfigMixin)
212
- and v.__xpm__._has_generated_value
213
- and v.__xpm__.task is None
214
- ):
215
- raise AttributeError(
216
- f"Cannot set {k} to a configuration with generated values"
217
- )
218
-
219
258
  try:
220
259
  argument = self.xpmtype.arguments.get(k, None)
221
260
  if argument:
261
+ if ConfigInformation.is_generated_value(argument, v):
262
+ raise AttributeError(
263
+ f"Cannot set {k} to a configuration with generated values. "
264
+ "Here is the list of paths to help you: "
265
+ f"""{', '.join(v.__xpm__.get_generated_paths([k]))}"""
266
+ )
267
+
222
268
  if not bypass and (
223
269
  (isinstance(argument.generator, Generator)) or argument.constant
224
270
  ):
@@ -344,14 +390,15 @@ class ConfigInformation:
344
390
  Arguments:
345
391
  - context: the generation context
346
392
  """
347
- subconfigs = [
348
- v.__xpm__
349
- for v in self.values.values()
350
- if isinstance(v, Config) and v.__xpm__.task is None
351
- ]
352
-
353
- if any(v._has_generated_value for v in subconfigs):
354
- raise AttributeError("Cannot seal a configuration with generated values")
393
+ if generated_keys := [
394
+ k
395
+ for k, v in self.values.items()
396
+ if ConfigInformation.is_generated_value(self.xpmtype.arguments[k], v)
397
+ ]:
398
+ raise AttributeError(
399
+ "Cannot seal a configuration with generated values:"
400
+ f"""{",".join(generated_keys)} in {context.currentpath}"""
401
+ )
355
402
 
356
403
  class Sealer(ConfigWalk):
357
404
  def preprocess(self, config: ConfigMixin):
@@ -375,13 +422,18 @@ class ConfigInformation:
375
422
  if len(sig.parameters) == 0:
376
423
  value = argument.generator()
377
424
  elif len(sig.parameters) == 2:
425
+ # Only in that case do we need to flag this configuration
426
+ # as containing generated values
427
+ if not argument.ignore_generated:
428
+ config.__xpm__._generated_values.append(k)
429
+ else:
430
+ logging.warning("Ignoring %s", k)
378
431
  value = argument.generator(self.context, config)
379
432
  else:
380
433
  assert (
381
434
  False
382
435
  ), "generator has either two parameters (context and config), or none"
383
436
  config.__xpm__.set(k, value, bypass=True)
384
- config.__xpm__._has_generated_value = True
385
437
  else:
386
438
  value = config.__xpm__.values.get(k)
387
439
  except Exception:
@@ -392,11 +444,14 @@ class ConfigInformation:
392
444
 
393
445
  # Propagate the generated value flag
394
446
  if (
395
- (value is not None)
447
+ value is not None
396
448
  and isinstance(value, ConfigMixin)
397
- and value.__xpm__._has_generated_value
449
+ and value.__xpm__._generated_values
398
450
  ):
399
- self._has_generated_value = True
451
+ if not argument.ignore_generated:
452
+ config.__xpm__._generated_values.append(k)
453
+ else:
454
+ logging.warning("Ignoring %s", k)
400
455
 
401
456
  config.__xpm__._sealed = True
402
457
 
@@ -889,6 +944,7 @@ class ConfigInformation:
889
944
  "workspace": str(context.workspace.path.absolute()),
890
945
  "tags": {key: value for key, value in self.tags().items()},
891
946
  "version": 2,
947
+ "experimaestro": experimaestro.__version__,
892
948
  "objects": self.__get_objects__([], context),
893
949
  },
894
950
  out,
@@ -71,6 +71,7 @@ class ConfigWalk:
71
71
  return self.context.push(str(i))
72
72
 
73
73
  def map(self, k: str):
74
+ """Provides a path context when processing a tree"""
74
75
  return self.context.push(k)
75
76
 
76
77
  def stub(self, config):
@@ -123,7 +124,8 @@ class ConfigWalk:
123
124
  and self.recurse_task
124
125
  and x.__xpm__.task is not x
125
126
  ):
126
- self(x.__xpm__.task)
127
+ with self.map("__task__"):
128
+ self(x.__xpm__.task)
127
129
 
128
130
  processed = self.postprocess(stub, x, result)
129
131
  self.visited[xid] = processed
@@ -0,0 +1,18 @@
1
+ from .base import Scheduler, Listener
2
+ from .workspace import Workspace, RunMode
3
+ from .experiment import experiment, FailedExperiment
4
+ from .jobs import Job, JobState, JobFailureStatus, JobDependency, JobContext
5
+
6
+ __all__ = [
7
+ "Scheduler",
8
+ "Listener",
9
+ "Workspace",
10
+ "RunMode",
11
+ "experiment",
12
+ "FailedExperiment",
13
+ "Job",
14
+ "JobState",
15
+ "JobFailureStatus",
16
+ "JobDependency",
17
+ "JobContext",
18
+ ]
@@ -0,0 +1,310 @@
1
+ import logging
2
+ import threading
3
+ import time
4
+ from typing import (
5
+ Optional,
6
+ Set,
7
+ )
8
+ import asyncio
9
+ from typing import Dict
10
+
11
+ from experimaestro.scheduler import experiment
12
+ from experimaestro.scheduler.jobs import Job, JobState
13
+ from experimaestro.scheduler.services import Service
14
+
15
+
16
+ from experimaestro.utils import logger
17
+ from experimaestro.utils.asyncio import asyncThreadcheck
18
+ import concurrent.futures
19
+
20
+
21
+ class Listener:
22
+ def job_submitted(self, job):
23
+ pass
24
+
25
+ def job_state(self, job):
26
+ pass
27
+
28
+ def service_add(self, service: Service):
29
+ """Notify when a new service is added"""
30
+ pass
31
+
32
+
33
+ class Scheduler(threading.Thread):
34
+ """A job scheduler
35
+
36
+ The scheduler is based on asyncio for easy concurrency handling
37
+ """
38
+
39
+ def __init__(self, xp: "experiment", name: str):
40
+ super().__init__(name=f"Scheduler ({name})", daemon=True)
41
+ self._ready = threading.Event()
42
+
43
+ # Name of the experiment
44
+ self.name = name
45
+ self.xp = xp
46
+
47
+ # Exit mode activated
48
+ self.exitmode = False
49
+
50
+ # List of all jobs
51
+ self.jobs: Dict[str, "Job"] = {}
52
+
53
+ # List of jobs
54
+ self.waitingjobs: Set[Job] = set()
55
+
56
+ # Listeners
57
+ self.listeners: Set[Listener] = set()
58
+
59
+ @staticmethod
60
+ def create(xp: "experiment", name: str):
61
+ instance = Scheduler(xp, name)
62
+ instance.start()
63
+ instance._ready.wait()
64
+ return instance
65
+
66
+ def run(self):
67
+ """Run the event loop forever"""
68
+ logger.debug("Starting event loop thread")
69
+ # Ported from SchedulerCentral
70
+ self.loop = asyncio.new_event_loop()
71
+ asyncio.set_event_loop(self.loop)
72
+ # Set loop-dependent variables
73
+ self.exitCondition = asyncio.Condition()
74
+ self.dependencyLock = asyncio.Lock()
75
+ self._ready.set()
76
+ self.loop.run_forever()
77
+
78
+ def start_scheduler(self):
79
+ """Start the scheduler event loop in a thread"""
80
+ if not self.is_alive():
81
+ self.start()
82
+ self._ready.wait()
83
+ else:
84
+ logger.warning("Scheduler already started")
85
+
86
+ def addlistener(self, listener: Listener):
87
+ self.listeners.add(listener)
88
+
89
+ def removelistener(self, listener: Listener):
90
+ self.listeners.remove(listener)
91
+
92
+ def getJobState(self, job: Job) -> "concurrent.futures.Future[JobState]":
93
+ # Check if the job belongs to this scheduler
94
+ if job.identifier not in self.jobs:
95
+ # If job is not in this scheduler, return its current state directly
96
+ future = concurrent.futures.Future()
97
+ future.set_result(job.state)
98
+ return future
99
+
100
+ return asyncio.run_coroutine_threadsafe(self.aio_getjobstate(job), self.loop)
101
+
102
+ async def aio_getjobstate(self, job: Job):
103
+ return job.state
104
+
105
+ def submit(self, job: Job) -> Optional[Job]:
106
+ # Wait for the future containing the submitted job
107
+ logger.debug("Registering the job %s within the scheduler", job)
108
+ otherFuture = asyncio.run_coroutine_threadsafe(
109
+ self.aio_registerJob(job), self.loop
110
+ )
111
+ other = otherFuture.result()
112
+ logger.debug("Job already submitted" if other else "First submission")
113
+ if other:
114
+ return other
115
+
116
+ job._future = asyncio.run_coroutine_threadsafe(self.aio_submit(job), self.loop)
117
+ return None
118
+
119
+ def prepare(self, job: Job):
120
+ """Prepares the job for running"""
121
+ logger.info("Preparing job %s", job.path)
122
+ job.prepare(overwrite=True)
123
+
124
+ async def aio_registerJob(self, job: Job):
125
+ """Register a job by adding it to the list, and checks
126
+ whether the job has already been submitted
127
+ """
128
+ logger.debug("Registering job %s", job)
129
+
130
+ if self.exitmode:
131
+ logger.warning("Exit mode: not submitting")
132
+
133
+ elif job.identifier in self.jobs:
134
+ other = self.jobs[job.identifier]
135
+ assert job.type == other.type
136
+ if other.state == JobState.ERROR:
137
+ logger.info("Re-submitting job")
138
+ else:
139
+ logger.warning("Job %s already submitted", job.identifier)
140
+ return other
141
+
142
+ else:
143
+ # Register this job
144
+ self.xp.unfinishedJobs += 1
145
+ self.jobs[job.identifier] = job
146
+
147
+ return None
148
+
149
+ def notify_job_submitted(self, job: Job):
150
+ """Notify the listeners that a job has been submitted"""
151
+ for listener in self.listeners:
152
+ try:
153
+ listener.job_submitted(job)
154
+ except Exception:
155
+ logger.exception("Got an error with listener %s", listener)
156
+
157
+ def notify_job_state(self, job: Job):
158
+ """Notify the listeners that a job has changed state"""
159
+ for listener in self.listeners:
160
+ try:
161
+ listener.job_state(job)
162
+ except Exception:
163
+ logger.exception("Got an error with listener %s", listener)
164
+
165
+ async def aio_submit(self, job: Job) -> JobState: # noqa: C901
166
+ """Main scheduler function: submit a job, run it (if needed), and returns
167
+ the status code
168
+ """
169
+ logger.info("Submitting job %s", job)
170
+ job._readyEvent = asyncio.Event()
171
+ job.submittime = time.time()
172
+ job.scheduler = self
173
+ self.waitingjobs.add(job)
174
+
175
+ # Check that we don't have a completed job in
176
+ # alternate directories
177
+ for jobspath in experiment.current().alt_jobspaths:
178
+ # FIXME: check if done
179
+ pass
180
+
181
+ # Creates a link into the experiment folder
182
+ path = experiment.current().jobspath / job.relpath
183
+ path.parent.mkdir(parents=True, exist_ok=True)
184
+ if path.is_symlink():
185
+ path.unlink()
186
+ path.symlink_to(job.path)
187
+
188
+ job.state = JobState.WAITING
189
+
190
+ self.notify_job_submitted(job)
191
+
192
+ # Add dependencies, and add to blocking resources
193
+ if job.dependencies:
194
+ job.unsatisfied = len(job.dependencies)
195
+
196
+ for dependency in job.dependencies:
197
+ dependency.target = job
198
+ dependency.loop = self.loop
199
+ dependency.origin.dependents.add(dependency)
200
+ dependency.check()
201
+ else:
202
+ job._readyEvent.set()
203
+ job.state = JobState.READY
204
+
205
+ if job.donepath.exists():
206
+ job.state = JobState.DONE
207
+
208
+ # Check if we have a running process
209
+ process = await job.aio_process()
210
+ if process is not None:
211
+ # Yep! First we notify the listeners
212
+ job.state = JobState.RUNNING
213
+ # Notify the listeners
214
+ self.notify_job_state(job)
215
+
216
+ # Adds to the listeners
217
+ if self.xp.server is not None:
218
+ job.add_notification_server(self.xp.server)
219
+
220
+ # And now, we wait...
221
+ logger.info("Got a process for job %s - waiting to complete", job)
222
+ code = await process.aio_code()
223
+ logger.info("Job %s completed with code %s", job, code)
224
+ job.state = JobState.DONE if code == 0 else JobState.ERROR
225
+
226
+ # Check if done
227
+ if job.donepath.exists():
228
+ job.state = JobState.DONE
229
+
230
+ # OK, not done; let's start the job for real
231
+ while not job.state.finished():
232
+ # Wait that the job is ready
233
+ await job._readyEvent.wait()
234
+ job._readyEvent.clear()
235
+
236
+ if job.state == JobState.READY:
237
+ try:
238
+ state = await self.aio_start(job)
239
+ except Exception:
240
+ logger.exception("Got an exception while starting the job")
241
+ raise
242
+
243
+ if state is None:
244
+ # State is None if this is not the main thread
245
+ return JobState.ERROR
246
+
247
+ job.state = state
248
+
249
+ self.notify_job_state(job)
250
+
251
+ # Job is finished
252
+ if job.state != JobState.DONE:
253
+ self.xp.failedJobs[job.identifier] = job
254
+
255
+ # Process all remaining tasks outputs
256
+ await asyncThreadcheck("End of job processing", job.done_handler)
257
+
258
+ # Decrement the number of unfinished jobs and notify
259
+ self.xp.unfinishedJobs -= 1
260
+ async with self.exitCondition:
261
+ logging.debug("Updated number of unfinished jobs")
262
+ self.exitCondition.notify_all()
263
+
264
+ job.endtime = time.time()
265
+ if job in self.waitingjobs:
266
+ self.waitingjobs.remove(job)
267
+
268
+ with job.dependents as dependents:
269
+ logger.info("Processing %d dependent jobs", len(dependents))
270
+ for dependency in dependents:
271
+ logger.debug("Checking dependency %s", dependency)
272
+ self.loop.call_soon(dependency.check)
273
+
274
+ return job.state
275
+
276
+ async def aio_start(self, job: Job) -> Optional[JobState]:
277
+ """Start a job (scheduler coordination layer)
278
+
279
+ This method serves as a coordination layer that delegates the actual
280
+ job starting logic to the job itself while handling scheduler-specific
281
+ concerns like state notifications and providing coordination context.
282
+
283
+ :param job: The job to start
284
+ :return: JobState.WAITING if dependencies could not be locked, JobState.DONE
285
+ if job completed successfully, JobState.ERROR if job failed during execution,
286
+ or None (should not occur in normal operation)
287
+ :raises Exception: Various exceptions during scheduler coordination
288
+ """
289
+
290
+ # Assert preconditions
291
+ assert job.launcher is not None
292
+
293
+ try:
294
+ # Call job's start method with scheduler context
295
+ state = await job.aio_start(
296
+ sched_dependency_lock=self.dependencyLock,
297
+ notification_server=self.xp.server if self.xp else None,
298
+ )
299
+
300
+ if state is None:
301
+ # Dependencies couldn't be locked, return WAITING state
302
+ return JobState.WAITING
303
+
304
+ # Notify scheduler listeners of job state after successful start
305
+ self.notify_job_state(job)
306
+ return state
307
+
308
+ except Exception:
309
+ logger.warning("Error in scheduler job coordination", exc_info=True)
310
+ return JobState.ERROR