experimaestro 2.0.0b8__py3-none-any.whl → 2.0.0b17__py3-none-any.whl

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.

Potentially problematic release.


This version of experimaestro might be problematic. Click here for more details.

Files changed (152) hide show
  1. experimaestro/__init__.py +12 -5
  2. experimaestro/cli/__init__.py +239 -126
  3. experimaestro/cli/filter.py +48 -23
  4. experimaestro/cli/jobs.py +253 -71
  5. experimaestro/cli/refactor.py +1 -2
  6. experimaestro/commandline.py +7 -4
  7. experimaestro/connectors/__init__.py +9 -1
  8. experimaestro/connectors/local.py +43 -3
  9. experimaestro/core/arguments.py +18 -18
  10. experimaestro/core/identifier.py +11 -11
  11. experimaestro/core/objects/config.py +96 -39
  12. experimaestro/core/objects/config_walk.py +3 -3
  13. experimaestro/core/{subparameters.py → partial.py} +16 -16
  14. experimaestro/core/partial_lock.py +394 -0
  15. experimaestro/core/types.py +12 -15
  16. experimaestro/dynamic.py +290 -0
  17. experimaestro/experiments/__init__.py +6 -2
  18. experimaestro/experiments/cli.py +217 -50
  19. experimaestro/experiments/configuration.py +24 -0
  20. experimaestro/generators.py +5 -5
  21. experimaestro/ipc.py +118 -1
  22. experimaestro/launcherfinder/__init__.py +2 -2
  23. experimaestro/launcherfinder/registry.py +6 -7
  24. experimaestro/launcherfinder/specs.py +2 -9
  25. experimaestro/launchers/slurm/__init__.py +2 -2
  26. experimaestro/launchers/slurm/base.py +62 -0
  27. experimaestro/locking.py +957 -1
  28. experimaestro/notifications.py +89 -201
  29. experimaestro/progress.py +63 -366
  30. experimaestro/rpyc.py +0 -2
  31. experimaestro/run.py +29 -2
  32. experimaestro/scheduler/__init__.py +8 -1
  33. experimaestro/scheduler/base.py +629 -53
  34. experimaestro/scheduler/dependencies.py +20 -16
  35. experimaestro/scheduler/experiment.py +732 -167
  36. experimaestro/scheduler/interfaces.py +316 -101
  37. experimaestro/scheduler/jobs.py +58 -20
  38. experimaestro/scheduler/remote/adaptive_sync.py +265 -0
  39. experimaestro/scheduler/remote/client.py +171 -117
  40. experimaestro/scheduler/remote/protocol.py +8 -193
  41. experimaestro/scheduler/remote/server.py +95 -71
  42. experimaestro/scheduler/services.py +53 -28
  43. experimaestro/scheduler/state_provider.py +663 -2430
  44. experimaestro/scheduler/state_status.py +1247 -0
  45. experimaestro/scheduler/transient.py +31 -0
  46. experimaestro/scheduler/workspace.py +1 -1
  47. experimaestro/scheduler/workspace_state_provider.py +1273 -0
  48. experimaestro/scriptbuilder.py +4 -4
  49. experimaestro/settings.py +36 -0
  50. experimaestro/tests/conftest.py +33 -5
  51. experimaestro/tests/connectors/bin/executable.py +1 -1
  52. experimaestro/tests/fixtures/pre_experiment/experiment_check_env.py +16 -0
  53. experimaestro/tests/fixtures/pre_experiment/experiment_check_mock.py +14 -0
  54. experimaestro/tests/fixtures/pre_experiment/experiment_simple.py +12 -0
  55. experimaestro/tests/fixtures/pre_experiment/pre_setup_env.py +5 -0
  56. experimaestro/tests/fixtures/pre_experiment/pre_setup_error.py +3 -0
  57. experimaestro/tests/fixtures/pre_experiment/pre_setup_mock.py +8 -0
  58. experimaestro/tests/launchers/bin/test.py +1 -0
  59. experimaestro/tests/launchers/test_slurm.py +9 -9
  60. experimaestro/tests/partial_reschedule.py +46 -0
  61. experimaestro/tests/restart.py +3 -3
  62. experimaestro/tests/restart_main.py +1 -0
  63. experimaestro/tests/scripts/notifyandwait.py +1 -0
  64. experimaestro/tests/task_partial.py +38 -0
  65. experimaestro/tests/task_tokens.py +2 -2
  66. experimaestro/tests/tasks/test_dynamic.py +6 -6
  67. experimaestro/tests/test_dependencies.py +3 -3
  68. experimaestro/tests/test_deprecated.py +15 -15
  69. experimaestro/tests/test_dynamic_locking.py +317 -0
  70. experimaestro/tests/test_environment.py +24 -14
  71. experimaestro/tests/test_experiment.py +171 -36
  72. experimaestro/tests/test_identifier.py +25 -25
  73. experimaestro/tests/test_identifier_stability.py +3 -5
  74. experimaestro/tests/test_multitoken.py +2 -4
  75. experimaestro/tests/{test_subparameters.py → test_partial.py} +25 -25
  76. experimaestro/tests/test_partial_paths.py +81 -138
  77. experimaestro/tests/test_pre_experiment.py +219 -0
  78. experimaestro/tests/test_progress.py +2 -8
  79. experimaestro/tests/test_remote_state.py +560 -99
  80. experimaestro/tests/test_stray_jobs.py +261 -0
  81. experimaestro/tests/test_tasks.py +1 -2
  82. experimaestro/tests/test_token_locking.py +52 -67
  83. experimaestro/tests/test_tokens.py +5 -6
  84. experimaestro/tests/test_transient.py +225 -0
  85. experimaestro/tests/test_workspace_state_provider.py +768 -0
  86. experimaestro/tests/token_reschedule.py +1 -3
  87. experimaestro/tests/utils.py +2 -7
  88. experimaestro/tokens.py +227 -372
  89. experimaestro/tools/diff.py +1 -0
  90. experimaestro/tools/documentation.py +4 -5
  91. experimaestro/tools/jobs.py +1 -2
  92. experimaestro/tui/app.py +438 -1966
  93. experimaestro/tui/app.tcss +162 -0
  94. experimaestro/tui/dialogs.py +172 -0
  95. experimaestro/tui/log_viewer.py +253 -3
  96. experimaestro/tui/messages.py +137 -0
  97. experimaestro/tui/utils.py +54 -0
  98. experimaestro/tui/widgets/__init__.py +23 -0
  99. experimaestro/tui/widgets/experiments.py +468 -0
  100. experimaestro/tui/widgets/global_services.py +238 -0
  101. experimaestro/tui/widgets/jobs.py +972 -0
  102. experimaestro/tui/widgets/log.py +156 -0
  103. experimaestro/tui/widgets/orphans.py +363 -0
  104. experimaestro/tui/widgets/runs.py +185 -0
  105. experimaestro/tui/widgets/services.py +314 -0
  106. experimaestro/tui/widgets/stray_jobs.py +528 -0
  107. experimaestro/utils/__init__.py +1 -1
  108. experimaestro/utils/environment.py +105 -22
  109. experimaestro/utils/fswatcher.py +124 -0
  110. experimaestro/utils/jobs.py +1 -2
  111. experimaestro/utils/jupyter.py +1 -2
  112. experimaestro/utils/logging.py +72 -0
  113. experimaestro/version.py +2 -2
  114. experimaestro/webui/__init__.py +9 -0
  115. experimaestro/webui/app.py +117 -0
  116. experimaestro/{server → webui}/data/index.css +66 -11
  117. experimaestro/webui/data/index.css.map +1 -0
  118. experimaestro/{server → webui}/data/index.js +82763 -87217
  119. experimaestro/webui/data/index.js.map +1 -0
  120. experimaestro/webui/routes/__init__.py +5 -0
  121. experimaestro/webui/routes/auth.py +53 -0
  122. experimaestro/webui/routes/proxy.py +117 -0
  123. experimaestro/webui/server.py +200 -0
  124. experimaestro/webui/state_bridge.py +152 -0
  125. experimaestro/webui/websocket.py +413 -0
  126. {experimaestro-2.0.0b8.dist-info → experimaestro-2.0.0b17.dist-info}/METADATA +5 -6
  127. experimaestro-2.0.0b17.dist-info/RECORD +219 -0
  128. experimaestro/cli/progress.py +0 -269
  129. experimaestro/scheduler/state.py +0 -75
  130. experimaestro/scheduler/state_db.py +0 -437
  131. experimaestro/scheduler/state_sync.py +0 -891
  132. experimaestro/server/__init__.py +0 -467
  133. experimaestro/server/data/index.css.map +0 -1
  134. experimaestro/server/data/index.js.map +0 -1
  135. experimaestro/tests/test_cli_jobs.py +0 -615
  136. experimaestro/tests/test_file_progress.py +0 -425
  137. experimaestro/tests/test_file_progress_integration.py +0 -477
  138. experimaestro/tests/test_state_db.py +0 -434
  139. experimaestro-2.0.0b8.dist-info/RECORD +0 -187
  140. /experimaestro/{server → webui}/data/1815e00441357e01619e.ttf +0 -0
  141. /experimaestro/{server → webui}/data/2463b90d9a316e4e5294.woff2 +0 -0
  142. /experimaestro/{server → webui}/data/2582b0e4bcf85eceead0.ttf +0 -0
  143. /experimaestro/{server → webui}/data/89999bdf5d835c012025.woff2 +0 -0
  144. /experimaestro/{server → webui}/data/914997e1bdfc990d0897.ttf +0 -0
  145. /experimaestro/{server → webui}/data/c210719e60948b211a12.woff2 +0 -0
  146. /experimaestro/{server → webui}/data/favicon.ico +0 -0
  147. /experimaestro/{server → webui}/data/index.html +0 -0
  148. /experimaestro/{server → webui}/data/login.html +0 -0
  149. /experimaestro/{server → webui}/data/manifest.json +0 -0
  150. {experimaestro-2.0.0b8.dist-info → experimaestro-2.0.0b17.dist-info}/WHEEL +0 -0
  151. {experimaestro-2.0.0b8.dist-info → experimaestro-2.0.0b17.dist-info}/entry_points.txt +0 -0
  152. {experimaestro-2.0.0b8.dist-info → experimaestro-2.0.0b17.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,225 @@
1
+ """Tests for transient task functionality"""
2
+
3
+ from tempfile import TemporaryDirectory
4
+ from typing import List, Optional
5
+ from experimaestro import Config, Task, Param, TransientMode
6
+ from experimaestro.scheduler import JobState
7
+ from experimaestro.scheduler.base import Scheduler
8
+ from .utils import TemporaryExperiment
9
+
10
+
11
+ class TransientTask(Task):
12
+ """A simple task that can be marked as transient"""
13
+
14
+ x: Param[int]
15
+
16
+ def execute(self):
17
+ print(f"TransientTask x={self.x}") # noqa: T201
18
+
19
+
20
+ class DependentTask(Task):
21
+ """A task that depends on TransientTask"""
22
+
23
+ deps: Param[List[TransientTask]]
24
+
25
+ def execute(self):
26
+ print(f"DependentTask with {len(self.deps)} deps") # noqa: T201
27
+
28
+
29
+ class SingleDependentTask(Task):
30
+ """A task that depends on a single TransientTask"""
31
+
32
+ dep: Param[TransientTask]
33
+
34
+ def execute(self):
35
+ print(f"SingleDependentTask with dep x={self.dep.x}") # noqa: T201
36
+
37
+
38
+ class ChainableTask(Task):
39
+ """A task that can depend on any Config (for chain testing)"""
40
+
41
+ dep: Param[Optional[Config]]
42
+
43
+ def execute(self):
44
+ print("ChainableTask executed") # noqa: T201
45
+
46
+
47
+ def test_transient_with_dependents():
48
+ """Transient task should run when it has dependents"""
49
+ with TemporaryExperiment("transient_with_deps", maxwait=30):
50
+ # Submit transient task
51
+ a = TransientTask.C(x=1).submit(transient=TransientMode.TRANSIENT)
52
+
53
+ # Submit dependent tasks
54
+ b1 = SingleDependentTask.C(dep=a).submit()
55
+ b2 = SingleDependentTask.C(dep=a).submit()
56
+
57
+ # Transient task should have run because b1 and b2 depend on it
58
+ assert a.__xpm__.job.state == JobState.DONE
59
+ assert b1.__xpm__.job.state == JobState.DONE
60
+ assert b2.__xpm__.job.state == JobState.DONE
61
+
62
+
63
+ def test_transient_without_dependents():
64
+ """Transient task should be skipped when it has no dependents"""
65
+ with TemporaryExperiment("transient_no_deps", maxwait=30):
66
+ # Submit transient task with no dependents
67
+ a = TransientTask.C(x=1).submit(transient=TransientMode.TRANSIENT)
68
+
69
+ # Transient task should remain UNSCHEDULED since it was skipped
70
+ assert a.__xpm__.job.state == JobState.UNSCHEDULED
71
+
72
+
73
+ def test_transient_remove_mode():
74
+ """Transient task with REMOVE mode should have its directory removed"""
75
+ with TemporaryExperiment("transient_remove", maxwait=30):
76
+ # Submit transient task with REMOVE mode
77
+ a = TransientTask.C(x=1).submit(transient=TransientMode.REMOVE)
78
+
79
+ # Submit a dependent task
80
+ b = SingleDependentTask.C(dep=a).submit()
81
+
82
+ # Store the job path before the experiment ends
83
+ job_path = a.__xpm__.job.path
84
+
85
+ # Both tasks should complete
86
+ assert a.__xpm__.job.state == JobState.DONE
87
+ assert b.__xpm__.job.state == JobState.DONE
88
+
89
+ # The transient task's directory should be removed
90
+ assert not job_path.exists(), f"Job path {job_path} should have been removed"
91
+
92
+
93
+ def test_transient_remove_without_dependents():
94
+ """Transient task with REMOVE mode and no dependents should be skipped"""
95
+ with TemporaryExperiment("transient_remove_no_deps", maxwait=30):
96
+ # Submit transient task with REMOVE mode and no dependents
97
+ a = TransientTask.C(x=1).submit(transient=TransientMode.REMOVE)
98
+
99
+ # Task should remain UNSCHEDULED since it was skipped
100
+ assert a.__xpm__.job.state == JobState.UNSCHEDULED
101
+
102
+
103
+ def test_transient_mode_merge_none_wins():
104
+ """When resubmitting, NONE mode should win over transient modes"""
105
+ with TemporaryExperiment("transient_merge_none", maxwait=30):
106
+ # Submit with TRANSIENT mode first
107
+ a1 = TransientTask.C(x=1).submit(transient=TransientMode.TRANSIENT)
108
+
109
+ # Resubmit same task with NONE mode
110
+ a2 = TransientTask.C(x=1).submit(transient=TransientMode.NONE)
111
+
112
+ # They should be the same job
113
+ assert a1.__xpm__.job is a2.__xpm__.job
114
+
115
+ # The job should have run (NONE mode takes precedence)
116
+ assert a1.__xpm__.job.state == JobState.DONE
117
+ assert a1.__xpm__.job.transient == TransientMode.NONE
118
+
119
+
120
+ def test_transient_mode_merge_transient_wins_over_remove():
121
+ """When resubmitting, TRANSIENT mode should win over REMOVE mode"""
122
+ with TemporaryExperiment("transient_merge_transient", maxwait=30):
123
+ # Submit with REMOVE mode first
124
+ a1 = TransientTask.C(x=2).submit(transient=TransientMode.REMOVE)
125
+
126
+ # Resubmit same task with TRANSIENT mode
127
+ a2 = TransientTask.C(x=2).submit(transient=TransientMode.TRANSIENT)
128
+
129
+ # They should be the same job
130
+ assert a1.__xpm__.job is a2.__xpm__.job
131
+
132
+ # The transient mode should be merged to TRANSIENT (more conservative)
133
+ assert a1.__xpm__.job.transient == TransientMode.TRANSIENT
134
+
135
+ # Job should be UNSCHEDULED since no non-transient dependent exists
136
+ assert a1.__xpm__.job.state == JobState.UNSCHEDULED
137
+
138
+
139
+ def test_transient_chain():
140
+ """Chain of transient tasks should work correctly"""
141
+ with TemporaryExperiment("transient_chain", maxwait=30):
142
+ # Create a chain: a -> b -> c where a and b are transient
143
+ a = TransientTask.C(x=1).submit(transient=TransientMode.TRANSIENT)
144
+ b = ChainableTask.C(dep=a).submit(transient=TransientMode.TRANSIENT)
145
+ c = ChainableTask.C(dep=b).submit() # Non-transient
146
+
147
+ # All tasks should run because c needs b which needs a
148
+ assert a.__xpm__.job.state == JobState.DONE
149
+ assert b.__xpm__.job.state == JobState.DONE
150
+ assert c.__xpm__.job.state == JobState.DONE
151
+
152
+
153
+ def test_transient_chain_all_transient():
154
+ """Chain of all transient tasks - all should be skipped"""
155
+ with TemporaryExperiment("transient_chain_all", maxwait=30):
156
+ # Create a chain: a -> b where both are transient
157
+ a = TransientTask.C(x=1).submit(transient=TransientMode.TRANSIENT)
158
+ b = ChainableTask.C(dep=a).submit(transient=TransientMode.TRANSIENT)
159
+
160
+ # Both should be skipped since there's no non-transient job at the end
161
+ # b has no dependents → UNSCHEDULED
162
+ # a is transient, and b (its only dependent) is also transient and never runs
163
+ # so a is never started via ensure_started() → UNSCHEDULED
164
+ assert a.__xpm__.job.state == JobState.UNSCHEDULED
165
+ assert b.__xpm__.job.state == JobState.UNSCHEDULED
166
+
167
+
168
+ def test_transient_resubmit_within_experiment():
169
+ """When resubmitting transient job within same experiment, state should reflect final mode"""
170
+ with TemporaryExperiment("transient_resubmit", maxwait=30):
171
+ # Submit as transient first (will be skipped)
172
+ a1 = TransientTask.C(x=3).submit(transient=TransientMode.TRANSIENT)
173
+
174
+ # Resubmit as non-transient - should trigger run
175
+ a2 = TransientTask.C(x=3).submit(transient=TransientMode.NONE)
176
+
177
+ # They should be the same job
178
+ assert a1.__xpm__.job is a2.__xpm__.job
179
+
180
+ # Job should be done (NONE mode triggers run)
181
+ assert a1.__xpm__.job.state == JobState.DONE
182
+ assert a1.__xpm__.job.transient == TransientMode.NONE
183
+
184
+
185
+ def test_transient_remove_then_transient_across_experiments():
186
+ """Test transient behavior across experiments with shared workspace.
187
+
188
+ First experiment: A(REMOVE) -> B, both run, A's directory removed at end.
189
+ Second experiment: A(TRANSIENT) -> B, B already done, A should not run.
190
+ """
191
+ with TemporaryDirectory(prefix="xpm_transient_") as workdir:
192
+ # First experiment: A with REMOVE, B depends on A
193
+ with TemporaryExperiment("transient_across", maxwait=30, workdir=workdir):
194
+ a1 = TransientTask.C(x=10).submit(transient=TransientMode.REMOVE)
195
+ b1 = SingleDependentTask.C(dep=a1).submit()
196
+
197
+ # Store paths for later checks
198
+ a_job_path = a1.__xpm__.job.path
199
+
200
+ # Both should complete
201
+ assert a1.__xpm__.job.state == JobState.DONE
202
+ assert b1.__xpm__.job.state == JobState.DONE
203
+
204
+ # A's directory should be removed (REMOVE mode)
205
+ assert not a_job_path.exists(), "A's directory should be removed after REMOVE"
206
+
207
+ # Clear the scheduler to simulate a fresh experiment
208
+ Scheduler.instance().jobs.clear()
209
+
210
+ # Second experiment: same workspace, A with TRANSIENT, B depends on A
211
+ with TemporaryExperiment("transient_across", maxwait=30, workdir=workdir):
212
+ a2 = TransientTask.C(x=10).submit(transient=TransientMode.TRANSIENT)
213
+ b2 = SingleDependentTask.C(dep=a2).submit()
214
+
215
+ # B should be DONE (already completed from previous run - donepath exists)
216
+ assert b2.__xpm__.job.state == JobState.DONE
217
+
218
+ # A should be UNSCHEDULED (transient with no need to run since B is done)
219
+ assert a2.__xpm__.job.state == JobState.UNSCHEDULED
220
+ # Verify A was never started (aio_start never called)
221
+ assert a2.__xpm__.job.starttime is None, "A should not have been started"
222
+ # Verify no job folder was created (check for .experimaestro subdir)
223
+ assert not a2.__xpm__.job.path.exists(), (
224
+ "A's job folder should not have been created"
225
+ )