experimaestro 2.0.0a8__py3-none-any.whl → 2.0.0b4__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 +10 -11
- experimaestro/annotations.py +167 -206
- experimaestro/cli/__init__.py +130 -5
- experimaestro/cli/filter.py +42 -74
- experimaestro/cli/jobs.py +157 -106
- experimaestro/cli/refactor.py +249 -0
- experimaestro/click.py +0 -1
- experimaestro/commandline.py +19 -3
- experimaestro/connectors/__init__.py +20 -1
- experimaestro/connectors/local.py +12 -0
- experimaestro/core/arguments.py +182 -46
- experimaestro/core/identifier.py +107 -6
- experimaestro/core/objects/__init__.py +6 -0
- experimaestro/core/objects/config.py +542 -25
- experimaestro/core/objects/config_walk.py +20 -0
- experimaestro/core/serialization.py +91 -34
- experimaestro/core/subparameters.py +164 -0
- experimaestro/core/types.py +175 -38
- experimaestro/exceptions.py +26 -0
- experimaestro/experiments/cli.py +107 -25
- experimaestro/generators.py +50 -9
- experimaestro/huggingface.py +3 -1
- experimaestro/launcherfinder/parser.py +29 -0
- experimaestro/launchers/__init__.py +26 -1
- experimaestro/launchers/direct.py +12 -0
- experimaestro/launchers/slurm/base.py +154 -2
- experimaestro/mkdocs/metaloader.py +0 -1
- experimaestro/mypy.py +452 -7
- experimaestro/notifications.py +63 -13
- experimaestro/progress.py +0 -2
- experimaestro/rpyc.py +0 -1
- experimaestro/run.py +19 -6
- experimaestro/scheduler/base.py +489 -125
- experimaestro/scheduler/dependencies.py +43 -28
- experimaestro/scheduler/dynamic_outputs.py +259 -130
- experimaestro/scheduler/experiment.py +225 -30
- experimaestro/scheduler/interfaces.py +474 -0
- experimaestro/scheduler/jobs.py +216 -206
- experimaestro/scheduler/services.py +186 -12
- experimaestro/scheduler/state_db.py +388 -0
- experimaestro/scheduler/state_provider.py +2345 -0
- experimaestro/scheduler/state_sync.py +834 -0
- experimaestro/scheduler/workspace.py +52 -10
- experimaestro/scriptbuilder.py +7 -0
- experimaestro/server/__init__.py +147 -57
- experimaestro/server/data/index.css +0 -125
- experimaestro/server/data/index.css.map +1 -1
- experimaestro/server/data/index.js +194 -58
- experimaestro/server/data/index.js.map +1 -1
- experimaestro/settings.py +44 -5
- experimaestro/sphinx/__init__.py +3 -3
- experimaestro/taskglobals.py +20 -0
- experimaestro/tests/conftest.py +80 -0
- experimaestro/tests/core/test_generics.py +2 -2
- experimaestro/tests/identifier_stability.json +45 -0
- experimaestro/tests/launchers/bin/sacct +6 -2
- experimaestro/tests/launchers/bin/sbatch +4 -2
- experimaestro/tests/launchers/test_slurm.py +80 -0
- experimaestro/tests/tasks/test_dynamic.py +231 -0
- experimaestro/tests/test_cli_jobs.py +615 -0
- experimaestro/tests/test_deprecated.py +630 -0
- experimaestro/tests/test_environment.py +200 -0
- experimaestro/tests/test_file_progress_integration.py +1 -1
- experimaestro/tests/test_forward.py +3 -3
- experimaestro/tests/test_identifier.py +372 -41
- experimaestro/tests/test_identifier_stability.py +458 -0
- experimaestro/tests/test_instance.py +3 -3
- experimaestro/tests/test_multitoken.py +442 -0
- experimaestro/tests/test_mypy.py +433 -0
- experimaestro/tests/test_objects.py +312 -5
- experimaestro/tests/test_outputs.py +2 -2
- experimaestro/tests/test_param.py +8 -12
- experimaestro/tests/test_partial_paths.py +231 -0
- experimaestro/tests/test_progress.py +0 -48
- experimaestro/tests/test_resumable_task.py +480 -0
- experimaestro/tests/test_serializers.py +141 -1
- experimaestro/tests/test_state_db.py +434 -0
- experimaestro/tests/test_subparameters.py +160 -0
- experimaestro/tests/test_tags.py +136 -0
- experimaestro/tests/test_tasks.py +107 -121
- experimaestro/tests/test_token_locking.py +252 -0
- experimaestro/tests/test_tokens.py +17 -13
- experimaestro/tests/test_types.py +123 -1
- experimaestro/tests/test_workspace_triggers.py +158 -0
- experimaestro/tests/token_reschedule.py +4 -2
- experimaestro/tests/utils.py +2 -2
- experimaestro/tokens.py +154 -57
- experimaestro/tools/diff.py +1 -1
- experimaestro/tui/__init__.py +8 -0
- experimaestro/tui/app.py +2303 -0
- experimaestro/tui/app.tcss +353 -0
- experimaestro/tui/log_viewer.py +228 -0
- experimaestro/utils/__init__.py +23 -0
- experimaestro/utils/environment.py +148 -0
- experimaestro/utils/git.py +129 -0
- experimaestro/utils/resources.py +1 -1
- experimaestro/version.py +34 -0
- {experimaestro-2.0.0a8.dist-info → experimaestro-2.0.0b4.dist-info}/METADATA +68 -38
- experimaestro-2.0.0b4.dist-info/RECORD +181 -0
- {experimaestro-2.0.0a8.dist-info → experimaestro-2.0.0b4.dist-info}/WHEEL +1 -1
- experimaestro-2.0.0b4.dist-info/entry_points.txt +16 -0
- experimaestro/compat.py +0 -6
- experimaestro/core/objects.pyi +0 -221
- experimaestro/server/data/0c35d18bf06992036b69.woff2 +0 -0
- experimaestro/server/data/219aa9140e099e6c72ed.woff2 +0 -0
- experimaestro/server/data/3a4004a46a653d4b2166.woff +0 -0
- experimaestro/server/data/3baa5b8f3469222b822d.woff +0 -0
- experimaestro/server/data/4d73cb90e394b34b7670.woff +0 -0
- experimaestro/server/data/4ef4218c522f1eb6b5b1.woff2 +0 -0
- experimaestro/server/data/5d681e2edae8c60630db.woff +0 -0
- experimaestro/server/data/6f420cf17cc0d7676fad.woff2 +0 -0
- experimaestro/server/data/c380809fd3677d7d6903.woff2 +0 -0
- experimaestro/server/data/f882956fd323fd322f31.woff +0 -0
- experimaestro-2.0.0a8.dist-info/RECORD +0 -166
- experimaestro-2.0.0a8.dist-info/entry_points.txt +0 -17
- {experimaestro-2.0.0a8.dist-info → experimaestro-2.0.0b4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Refactoring commands for experimaestro codebase patterns"""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Iterator
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
from termcolor import cprint
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DefaultValueFinder(ast.NodeVisitor):
|
|
14
|
+
"""AST visitor to find class definitions with Param/Meta/Option annotations"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, source_lines: list[str]):
|
|
17
|
+
self.source_lines = source_lines
|
|
18
|
+
self.findings: list[dict] = []
|
|
19
|
+
self.current_class: str | None = None
|
|
20
|
+
|
|
21
|
+
def visit_ClassDef(self, node: ast.ClassDef):
|
|
22
|
+
old_class = self.current_class
|
|
23
|
+
self.current_class = node.name
|
|
24
|
+
|
|
25
|
+
# Check if this class might be a Config/Task (has annotations)
|
|
26
|
+
for item in node.body:
|
|
27
|
+
if isinstance(item, ast.AnnAssign):
|
|
28
|
+
self._check_annotation(item, node)
|
|
29
|
+
|
|
30
|
+
self.generic_visit(node)
|
|
31
|
+
self.current_class = old_class
|
|
32
|
+
|
|
33
|
+
def _check_annotation(self, node: ast.AnnAssign, class_node: ast.ClassDef):
|
|
34
|
+
"""Check if an annotated assignment uses Param/Meta/Option with bare default"""
|
|
35
|
+
if not isinstance(node.target, ast.Name):
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
param_name = node.target.id
|
|
39
|
+
|
|
40
|
+
# Check if annotation is Param[...], Meta[...], or Option[...]
|
|
41
|
+
annotation = node.annotation
|
|
42
|
+
is_param_type = False
|
|
43
|
+
|
|
44
|
+
if isinstance(annotation, ast.Subscript):
|
|
45
|
+
if isinstance(annotation.value, ast.Name):
|
|
46
|
+
if annotation.value.id in ("Param", "Meta", "Option"):
|
|
47
|
+
is_param_type = True
|
|
48
|
+
|
|
49
|
+
if not is_param_type:
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
# Check if there's a default value
|
|
53
|
+
if node.value is None:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
# Check if the default is already wrapped in field()
|
|
57
|
+
if isinstance(node.value, ast.Call):
|
|
58
|
+
if isinstance(node.value.func, ast.Name):
|
|
59
|
+
if node.value.func.id == "field":
|
|
60
|
+
return # Already using field()
|
|
61
|
+
|
|
62
|
+
# Found a bare default value
|
|
63
|
+
self.findings.append(
|
|
64
|
+
{
|
|
65
|
+
"class_name": self.current_class,
|
|
66
|
+
"param_name": param_name,
|
|
67
|
+
"line": node.lineno,
|
|
68
|
+
"col_offset": node.col_offset,
|
|
69
|
+
"end_line": node.end_lineno,
|
|
70
|
+
"end_col_offset": node.end_col_offset,
|
|
71
|
+
"value_line": node.value.lineno,
|
|
72
|
+
"value_col": node.value.col_offset,
|
|
73
|
+
"value_end_line": node.value.end_lineno,
|
|
74
|
+
"value_end_col": node.value.end_col_offset,
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def find_bare_defaults(file_path: Path) -> list[dict]:
|
|
80
|
+
"""Find all bare default values in a Python file"""
|
|
81
|
+
try:
|
|
82
|
+
source = file_path.read_text()
|
|
83
|
+
tree = ast.parse(source)
|
|
84
|
+
except (SyntaxError, UnicodeDecodeError):
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
source_lines = source.splitlines()
|
|
88
|
+
finder = DefaultValueFinder(source_lines)
|
|
89
|
+
finder.visit(tree)
|
|
90
|
+
return finder.findings
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def refactor_file(file_path: Path, perform: bool) -> int:
|
|
94
|
+
"""Refactor a single file, returns number of changes made/found"""
|
|
95
|
+
findings = find_bare_defaults(file_path)
|
|
96
|
+
if not findings:
|
|
97
|
+
return 0
|
|
98
|
+
|
|
99
|
+
source = file_path.read_text()
|
|
100
|
+
source_lines = source.splitlines(keepends=True)
|
|
101
|
+
|
|
102
|
+
# Sort findings by line number in reverse order (to not mess up offsets)
|
|
103
|
+
findings.sort(key=lambda f: (f["line"], f["col_offset"]), reverse=True)
|
|
104
|
+
|
|
105
|
+
changes_made = 0
|
|
106
|
+
for finding in findings:
|
|
107
|
+
class_name = finding["class_name"]
|
|
108
|
+
param_name = finding["param_name"]
|
|
109
|
+
line_num = finding["line"]
|
|
110
|
+
|
|
111
|
+
# Get the line content
|
|
112
|
+
line_idx = line_num - 1
|
|
113
|
+
if line_idx >= len(source_lines):
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
line = source_lines[line_idx]
|
|
117
|
+
|
|
118
|
+
# Try to find and replace the pattern on this line
|
|
119
|
+
# Pattern: `param_name: Param[...] = value` -> `param_name: Param[...] = field(ignore_default=value)`
|
|
120
|
+
# We need to be careful with multi-line values
|
|
121
|
+
|
|
122
|
+
# Simple case: value is on the same line
|
|
123
|
+
if finding["value_line"] == finding["value_end_line"] == line_num:
|
|
124
|
+
# Extract the value part
|
|
125
|
+
value_start = finding["value_col"]
|
|
126
|
+
value_end = finding["value_end_col"]
|
|
127
|
+
|
|
128
|
+
# Get the original value string
|
|
129
|
+
original_value = line[value_start:value_end]
|
|
130
|
+
|
|
131
|
+
# Create the replacement
|
|
132
|
+
new_value = f"field(ignore_default={original_value})"
|
|
133
|
+
|
|
134
|
+
# Replace in the line
|
|
135
|
+
new_line = line[:value_start] + new_value + line[value_end:]
|
|
136
|
+
source_lines[line_idx] = new_line
|
|
137
|
+
|
|
138
|
+
if perform:
|
|
139
|
+
cprint(
|
|
140
|
+
f" {file_path}:{line_num}: {class_name}.{param_name} = {original_value} "
|
|
141
|
+
f"-> field(ignore_default={original_value})",
|
|
142
|
+
"green",
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
cprint(
|
|
146
|
+
f" {file_path}:{line_num}: {class_name}.{param_name} = {original_value} "
|
|
147
|
+
f"-> field(ignore_default={original_value})",
|
|
148
|
+
"yellow",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
changes_made += 1
|
|
152
|
+
else:
|
|
153
|
+
# Multi-line value - more complex handling needed
|
|
154
|
+
# For now, just report it
|
|
155
|
+
cprint(
|
|
156
|
+
f" {file_path}:{line_num}: {class_name}.{param_name} has multi-line default "
|
|
157
|
+
f"(manual fix required)",
|
|
158
|
+
"red",
|
|
159
|
+
)
|
|
160
|
+
changes_made += 1
|
|
161
|
+
|
|
162
|
+
if perform and changes_made > 0:
|
|
163
|
+
# Check if we need to add 'field' import
|
|
164
|
+
new_source = "".join(source_lines)
|
|
165
|
+
|
|
166
|
+
# Simple check for field import
|
|
167
|
+
if "from experimaestro" in new_source or "import experimaestro" in new_source:
|
|
168
|
+
# Check if field is already imported
|
|
169
|
+
if not re.search(
|
|
170
|
+
r"from\s+experimaestro[^\n]*\bfield\b", new_source
|
|
171
|
+
) and not re.search(
|
|
172
|
+
r"from\s+experimaestro\.core\.arguments[^\n]*\bfield\b", new_source
|
|
173
|
+
):
|
|
174
|
+
# Try to add field to existing import
|
|
175
|
+
new_source = re.sub(
|
|
176
|
+
r"(from\s+experimaestro\s+import\s+)([^\n]+)",
|
|
177
|
+
r"\1field, \2",
|
|
178
|
+
new_source,
|
|
179
|
+
count=1,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
file_path.write_text(new_source)
|
|
183
|
+
|
|
184
|
+
return changes_made
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def find_python_files(path: Path) -> Iterator[Path]:
|
|
188
|
+
"""Find all Python files in a directory"""
|
|
189
|
+
if path.is_file():
|
|
190
|
+
if path.suffix == ".py":
|
|
191
|
+
yield path
|
|
192
|
+
else:
|
|
193
|
+
for py_file in path.rglob("*.py"):
|
|
194
|
+
# Skip common directories
|
|
195
|
+
parts = py_file.parts
|
|
196
|
+
if any(
|
|
197
|
+
p in parts
|
|
198
|
+
for p in ("__pycache__", ".git", ".venv", "venv", "node_modules")
|
|
199
|
+
):
|
|
200
|
+
continue
|
|
201
|
+
yield py_file
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@click.group()
|
|
205
|
+
def refactor():
|
|
206
|
+
"""Refactor codebase patterns"""
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@refactor.command(name="default-values")
|
|
211
|
+
@click.option(
|
|
212
|
+
"--perform",
|
|
213
|
+
is_flag=True,
|
|
214
|
+
help="Perform the refactoring (default is dry-run)",
|
|
215
|
+
)
|
|
216
|
+
@click.argument("path", type=click.Path(exists=True, path_type=Path), default=".")
|
|
217
|
+
def default_values(path: Path, perform: bool):
|
|
218
|
+
"""Fix ambiguous default values in configuration files.
|
|
219
|
+
|
|
220
|
+
Converts `x: Param[int] = 23` to `x: Param[int] = field(ignore_default=23)`
|
|
221
|
+
to make the behavior explicit.
|
|
222
|
+
|
|
223
|
+
By default runs in dry-run mode. Use --perform to apply changes.
|
|
224
|
+
"""
|
|
225
|
+
if not perform:
|
|
226
|
+
cprint("DRY RUN MODE: No changes will be written", "yellow")
|
|
227
|
+
cprint("Use --perform to apply changes\n", "yellow")
|
|
228
|
+
|
|
229
|
+
total_changes = 0
|
|
230
|
+
files_with_changes = 0
|
|
231
|
+
|
|
232
|
+
for py_file in find_python_files(path):
|
|
233
|
+
changes = refactor_file(py_file, perform)
|
|
234
|
+
if changes > 0:
|
|
235
|
+
total_changes += changes
|
|
236
|
+
files_with_changes += 1
|
|
237
|
+
|
|
238
|
+
if total_changes == 0:
|
|
239
|
+
cprint("\nNo bare default values found.", "green")
|
|
240
|
+
else:
|
|
241
|
+
action = "Fixed" if perform else "Found"
|
|
242
|
+
cprint(
|
|
243
|
+
f"\n{action} {total_changes} bare default value(s) in {files_with_changes} file(s).",
|
|
244
|
+
"green" if perform else "yellow",
|
|
245
|
+
)
|
|
246
|
+
if not perform:
|
|
247
|
+
cprint("Run with --perform to apply changes.", "yellow")
|
|
248
|
+
|
|
249
|
+
sys.exit(0 if perform or total_changes == 0 else 1)
|
experimaestro/click.py
CHANGED
|
@@ -37,7 +37,6 @@ class forwardoption(metaclass=forwardoptionMetaclass):
|
|
|
37
37
|
name = "--%s" % (option_name or argument.name.replace("_", "-"))
|
|
38
38
|
default = kwargs["default"] if "default" in kwargs else argument.default
|
|
39
39
|
|
|
40
|
-
# TODO: set the type of the option when not a simple type
|
|
41
40
|
return click.option(name, help=argument.help or "", default=default)
|
|
42
41
|
|
|
43
42
|
def __getattr__(self, key):
|
experimaestro/commandline.py
CHANGED
|
@@ -232,9 +232,14 @@ class CommandLineJob(Job):
|
|
|
232
232
|
workspace: Optional[Workspace] = None,
|
|
233
233
|
launcher=None,
|
|
234
234
|
run_mode: RunMode = None,
|
|
235
|
+
max_retries=None,
|
|
235
236
|
):
|
|
236
237
|
super().__init__(
|
|
237
|
-
parameters,
|
|
238
|
+
parameters,
|
|
239
|
+
workspace=workspace,
|
|
240
|
+
launcher=launcher,
|
|
241
|
+
run_mode=run_mode,
|
|
242
|
+
max_retries=max_retries,
|
|
238
243
|
)
|
|
239
244
|
self.commandline = commandline
|
|
240
245
|
|
|
@@ -300,7 +305,11 @@ class CommandLineJob(Job):
|
|
|
300
305
|
self._process = processbuilder.start(True)
|
|
301
306
|
|
|
302
307
|
with self.pidpath.open("w") as fp:
|
|
303
|
-
|
|
308
|
+
process_spec = self._process.tospec()
|
|
309
|
+
json.dump(process_spec, fp)
|
|
310
|
+
|
|
311
|
+
# Write process spec to metadata (contains launcher type, job ID, etc.)
|
|
312
|
+
self.write_metadata(process=process_spec)
|
|
304
313
|
|
|
305
314
|
self.state = JobState.RUNNING
|
|
306
315
|
logger.info("Process started (%s)", self._process)
|
|
@@ -312,7 +321,13 @@ class CommandLineTask:
|
|
|
312
321
|
self.commandline = commandline
|
|
313
322
|
|
|
314
323
|
def __call__(
|
|
315
|
-
self,
|
|
324
|
+
self,
|
|
325
|
+
pyobject,
|
|
326
|
+
*,
|
|
327
|
+
launcher=None,
|
|
328
|
+
workspace=None,
|
|
329
|
+
run_mode=None,
|
|
330
|
+
max_retries=None,
|
|
316
331
|
) -> Job:
|
|
317
332
|
return CommandLineJob(
|
|
318
333
|
self.commandline,
|
|
@@ -320,4 +335,5 @@ class CommandLineTask:
|
|
|
320
335
|
launcher=launcher,
|
|
321
336
|
workspace=workspace,
|
|
322
337
|
run_mode=run_mode,
|
|
338
|
+
max_retries=max_retries,
|
|
323
339
|
)
|
|
@@ -10,7 +10,7 @@ This module contains :
|
|
|
10
10
|
|
|
11
11
|
import enum
|
|
12
12
|
import logging
|
|
13
|
-
from typing import Any, Dict, Mapping, Type, Union, Optional
|
|
13
|
+
from typing import Any, Dict, Mapping, Type, Union, Optional, TYPE_CHECKING
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from experimaestro.utils import logger
|
|
16
16
|
from experimaestro.locking import Lock
|
|
@@ -18,6 +18,9 @@ from experimaestro.tokens import Token
|
|
|
18
18
|
from experimaestro.utils.asyncio import asyncThreadcheck
|
|
19
19
|
from importlib.metadata import entry_points
|
|
20
20
|
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from experimaestro.scheduler.jobs import JobState
|
|
23
|
+
|
|
21
24
|
|
|
22
25
|
class RedirectType(enum.Enum):
|
|
23
26
|
INHERIT = 0
|
|
@@ -135,6 +138,22 @@ class Process:
|
|
|
135
138
|
logger.debug("Got return code %s for %s", code, self)
|
|
136
139
|
return code
|
|
137
140
|
|
|
141
|
+
def get_job_state(self, code: int) -> "JobState":
|
|
142
|
+
"""Convert a process exit code to a JobState
|
|
143
|
+
|
|
144
|
+
This method allows process implementations to provide additional
|
|
145
|
+
information about failures (e.g., timeout detection for SLURM jobs).
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
code: The process exit code
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
JobState instance (JobState.DONE for success, JobStateError for failures)
|
|
152
|
+
"""
|
|
153
|
+
from experimaestro.scheduler.jobs import JobState
|
|
154
|
+
|
|
155
|
+
return JobState.DONE if code == 0 else JobState.ERROR
|
|
156
|
+
|
|
138
157
|
def kill(self):
|
|
139
158
|
raise NotImplementedError(f"Not implemented: {self.__class__}.kill")
|
|
140
159
|
|
|
@@ -163,6 +163,18 @@ class InterProcessLock(fasteners.InterProcessLock, Lock):
|
|
|
163
163
|
|
|
164
164
|
|
|
165
165
|
class LocalConnector(Connector):
|
|
166
|
+
"""Connector for executing tasks on the local machine.
|
|
167
|
+
|
|
168
|
+
This connector handles local file system operations and process execution.
|
|
169
|
+
It is the default connector used when no remote execution is needed.
|
|
170
|
+
|
|
171
|
+
Use :meth:`instance` to get a singleton instance of the local connector.
|
|
172
|
+
|
|
173
|
+
:param localpath: Base path for experimaestro data. Defaults to
|
|
174
|
+
``~/.local/share/experimaestro`` or the value of ``XPM_WORKDIR``
|
|
175
|
+
environment variable.
|
|
176
|
+
"""
|
|
177
|
+
|
|
166
178
|
INSTANCE: Connector = None
|
|
167
179
|
|
|
168
180
|
@staticmethod
|