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.
- experimaestro/__init__.py +12 -5
- experimaestro/cli/__init__.py +239 -126
- experimaestro/cli/filter.py +48 -23
- experimaestro/cli/jobs.py +253 -71
- experimaestro/cli/refactor.py +1 -2
- experimaestro/commandline.py +7 -4
- experimaestro/connectors/__init__.py +9 -1
- experimaestro/connectors/local.py +43 -3
- experimaestro/core/arguments.py +18 -18
- experimaestro/core/identifier.py +11 -11
- experimaestro/core/objects/config.py +96 -39
- experimaestro/core/objects/config_walk.py +3 -3
- experimaestro/core/{subparameters.py → partial.py} +16 -16
- experimaestro/core/partial_lock.py +394 -0
- experimaestro/core/types.py +12 -15
- experimaestro/dynamic.py +290 -0
- experimaestro/experiments/__init__.py +6 -2
- experimaestro/experiments/cli.py +217 -50
- experimaestro/experiments/configuration.py +24 -0
- experimaestro/generators.py +5 -5
- experimaestro/ipc.py +118 -1
- experimaestro/launcherfinder/__init__.py +2 -2
- experimaestro/launcherfinder/registry.py +6 -7
- experimaestro/launcherfinder/specs.py +2 -9
- experimaestro/launchers/slurm/__init__.py +2 -2
- experimaestro/launchers/slurm/base.py +62 -0
- experimaestro/locking.py +957 -1
- experimaestro/notifications.py +89 -201
- experimaestro/progress.py +63 -366
- experimaestro/rpyc.py +0 -2
- experimaestro/run.py +29 -2
- experimaestro/scheduler/__init__.py +8 -1
- experimaestro/scheduler/base.py +629 -53
- experimaestro/scheduler/dependencies.py +20 -16
- experimaestro/scheduler/experiment.py +732 -167
- experimaestro/scheduler/interfaces.py +316 -101
- experimaestro/scheduler/jobs.py +58 -20
- experimaestro/scheduler/remote/adaptive_sync.py +265 -0
- experimaestro/scheduler/remote/client.py +171 -117
- experimaestro/scheduler/remote/protocol.py +8 -193
- experimaestro/scheduler/remote/server.py +95 -71
- experimaestro/scheduler/services.py +53 -28
- experimaestro/scheduler/state_provider.py +663 -2430
- experimaestro/scheduler/state_status.py +1247 -0
- experimaestro/scheduler/transient.py +31 -0
- experimaestro/scheduler/workspace.py +1 -1
- experimaestro/scheduler/workspace_state_provider.py +1273 -0
- experimaestro/scriptbuilder.py +4 -4
- experimaestro/settings.py +36 -0
- experimaestro/tests/conftest.py +33 -5
- experimaestro/tests/connectors/bin/executable.py +1 -1
- experimaestro/tests/fixtures/pre_experiment/experiment_check_env.py +16 -0
- experimaestro/tests/fixtures/pre_experiment/experiment_check_mock.py +14 -0
- experimaestro/tests/fixtures/pre_experiment/experiment_simple.py +12 -0
- experimaestro/tests/fixtures/pre_experiment/pre_setup_env.py +5 -0
- experimaestro/tests/fixtures/pre_experiment/pre_setup_error.py +3 -0
- experimaestro/tests/fixtures/pre_experiment/pre_setup_mock.py +8 -0
- experimaestro/tests/launchers/bin/test.py +1 -0
- experimaestro/tests/launchers/test_slurm.py +9 -9
- experimaestro/tests/partial_reschedule.py +46 -0
- experimaestro/tests/restart.py +3 -3
- experimaestro/tests/restart_main.py +1 -0
- experimaestro/tests/scripts/notifyandwait.py +1 -0
- experimaestro/tests/task_partial.py +38 -0
- experimaestro/tests/task_tokens.py +2 -2
- experimaestro/tests/tasks/test_dynamic.py +6 -6
- experimaestro/tests/test_dependencies.py +3 -3
- experimaestro/tests/test_deprecated.py +15 -15
- experimaestro/tests/test_dynamic_locking.py +317 -0
- experimaestro/tests/test_environment.py +24 -14
- experimaestro/tests/test_experiment.py +171 -36
- experimaestro/tests/test_identifier.py +25 -25
- experimaestro/tests/test_identifier_stability.py +3 -5
- experimaestro/tests/test_multitoken.py +2 -4
- experimaestro/tests/{test_subparameters.py → test_partial.py} +25 -25
- experimaestro/tests/test_partial_paths.py +81 -138
- experimaestro/tests/test_pre_experiment.py +219 -0
- experimaestro/tests/test_progress.py +2 -8
- experimaestro/tests/test_remote_state.py +560 -99
- experimaestro/tests/test_stray_jobs.py +261 -0
- experimaestro/tests/test_tasks.py +1 -2
- experimaestro/tests/test_token_locking.py +52 -67
- experimaestro/tests/test_tokens.py +5 -6
- experimaestro/tests/test_transient.py +225 -0
- experimaestro/tests/test_workspace_state_provider.py +768 -0
- experimaestro/tests/token_reschedule.py +1 -3
- experimaestro/tests/utils.py +2 -7
- experimaestro/tokens.py +227 -372
- experimaestro/tools/diff.py +1 -0
- experimaestro/tools/documentation.py +4 -5
- experimaestro/tools/jobs.py +1 -2
- experimaestro/tui/app.py +438 -1966
- experimaestro/tui/app.tcss +162 -0
- experimaestro/tui/dialogs.py +172 -0
- experimaestro/tui/log_viewer.py +253 -3
- experimaestro/tui/messages.py +137 -0
- experimaestro/tui/utils.py +54 -0
- experimaestro/tui/widgets/__init__.py +23 -0
- experimaestro/tui/widgets/experiments.py +468 -0
- experimaestro/tui/widgets/global_services.py +238 -0
- experimaestro/tui/widgets/jobs.py +972 -0
- experimaestro/tui/widgets/log.py +156 -0
- experimaestro/tui/widgets/orphans.py +363 -0
- experimaestro/tui/widgets/runs.py +185 -0
- experimaestro/tui/widgets/services.py +314 -0
- experimaestro/tui/widgets/stray_jobs.py +528 -0
- experimaestro/utils/__init__.py +1 -1
- experimaestro/utils/environment.py +105 -22
- experimaestro/utils/fswatcher.py +124 -0
- experimaestro/utils/jobs.py +1 -2
- experimaestro/utils/jupyter.py +1 -2
- experimaestro/utils/logging.py +72 -0
- experimaestro/version.py +2 -2
- experimaestro/webui/__init__.py +9 -0
- experimaestro/webui/app.py +117 -0
- experimaestro/{server → webui}/data/index.css +66 -11
- experimaestro/webui/data/index.css.map +1 -0
- experimaestro/{server → webui}/data/index.js +82763 -87217
- experimaestro/webui/data/index.js.map +1 -0
- experimaestro/webui/routes/__init__.py +5 -0
- experimaestro/webui/routes/auth.py +53 -0
- experimaestro/webui/routes/proxy.py +117 -0
- experimaestro/webui/server.py +200 -0
- experimaestro/webui/state_bridge.py +152 -0
- experimaestro/webui/websocket.py +413 -0
- {experimaestro-2.0.0b8.dist-info → experimaestro-2.0.0b17.dist-info}/METADATA +5 -6
- experimaestro-2.0.0b17.dist-info/RECORD +219 -0
- experimaestro/cli/progress.py +0 -269
- experimaestro/scheduler/state.py +0 -75
- experimaestro/scheduler/state_db.py +0 -437
- experimaestro/scheduler/state_sync.py +0 -891
- experimaestro/server/__init__.py +0 -467
- experimaestro/server/data/index.css.map +0 -1
- experimaestro/server/data/index.js.map +0 -1
- experimaestro/tests/test_cli_jobs.py +0 -615
- experimaestro/tests/test_file_progress.py +0 -425
- experimaestro/tests/test_file_progress_integration.py +0 -477
- experimaestro/tests/test_state_db.py +0 -434
- experimaestro-2.0.0b8.dist-info/RECORD +0 -187
- /experimaestro/{server → webui}/data/1815e00441357e01619e.ttf +0 -0
- /experimaestro/{server → webui}/data/2463b90d9a316e4e5294.woff2 +0 -0
- /experimaestro/{server → webui}/data/2582b0e4bcf85eceead0.ttf +0 -0
- /experimaestro/{server → webui}/data/89999bdf5d835c012025.woff2 +0 -0
- /experimaestro/{server → webui}/data/914997e1bdfc990d0897.ttf +0 -0
- /experimaestro/{server → webui}/data/c210719e60948b211a12.woff2 +0 -0
- /experimaestro/{server → webui}/data/favicon.ico +0 -0
- /experimaestro/{server → webui}/data/index.html +0 -0
- /experimaestro/{server → webui}/data/login.html +0 -0
- /experimaestro/{server → webui}/data/manifest.json +0 -0
- {experimaestro-2.0.0b8.dist-info → experimaestro-2.0.0b17.dist-info}/WHEEL +0 -0
- {experimaestro-2.0.0b8.dist-info → experimaestro-2.0.0b17.dist-info}/entry_points.txt +0 -0
- {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
|
+
)
|