experimaestro 1.11.1__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.

Files changed (133) hide show
  1. experimaestro/__init__.py +10 -11
  2. experimaestro/annotations.py +167 -206
  3. experimaestro/cli/__init__.py +140 -16
  4. experimaestro/cli/filter.py +42 -74
  5. experimaestro/cli/jobs.py +157 -106
  6. experimaestro/cli/progress.py +269 -0
  7. experimaestro/cli/refactor.py +249 -0
  8. experimaestro/click.py +0 -1
  9. experimaestro/commandline.py +19 -3
  10. experimaestro/connectors/__init__.py +22 -3
  11. experimaestro/connectors/local.py +12 -0
  12. experimaestro/core/arguments.py +192 -37
  13. experimaestro/core/identifier.py +127 -12
  14. experimaestro/core/objects/__init__.py +6 -0
  15. experimaestro/core/objects/config.py +702 -285
  16. experimaestro/core/objects/config_walk.py +24 -6
  17. experimaestro/core/serialization.py +91 -34
  18. experimaestro/core/serializers.py +1 -8
  19. experimaestro/core/subparameters.py +164 -0
  20. experimaestro/core/types.py +198 -83
  21. experimaestro/exceptions.py +26 -0
  22. experimaestro/experiments/cli.py +107 -25
  23. experimaestro/generators.py +50 -9
  24. experimaestro/huggingface.py +3 -1
  25. experimaestro/launcherfinder/parser.py +29 -0
  26. experimaestro/launcherfinder/registry.py +3 -3
  27. experimaestro/launchers/__init__.py +26 -1
  28. experimaestro/launchers/direct.py +12 -0
  29. experimaestro/launchers/slurm/base.py +154 -2
  30. experimaestro/mkdocs/base.py +6 -8
  31. experimaestro/mkdocs/metaloader.py +0 -1
  32. experimaestro/mypy.py +452 -7
  33. experimaestro/notifications.py +75 -16
  34. experimaestro/progress.py +404 -0
  35. experimaestro/rpyc.py +0 -1
  36. experimaestro/run.py +19 -6
  37. experimaestro/scheduler/__init__.py +18 -1
  38. experimaestro/scheduler/base.py +504 -959
  39. experimaestro/scheduler/dependencies.py +43 -28
  40. experimaestro/scheduler/dynamic_outputs.py +259 -130
  41. experimaestro/scheduler/experiment.py +582 -0
  42. experimaestro/scheduler/interfaces.py +474 -0
  43. experimaestro/scheduler/jobs.py +485 -0
  44. experimaestro/scheduler/services.py +186 -12
  45. experimaestro/scheduler/signal_handler.py +32 -0
  46. experimaestro/scheduler/state.py +1 -1
  47. experimaestro/scheduler/state_db.py +388 -0
  48. experimaestro/scheduler/state_provider.py +2345 -0
  49. experimaestro/scheduler/state_sync.py +834 -0
  50. experimaestro/scheduler/workspace.py +52 -10
  51. experimaestro/scriptbuilder.py +7 -0
  52. experimaestro/server/__init__.py +153 -32
  53. experimaestro/server/data/index.css +0 -125
  54. experimaestro/server/data/index.css.map +1 -1
  55. experimaestro/server/data/index.js +194 -58
  56. experimaestro/server/data/index.js.map +1 -1
  57. experimaestro/settings.py +47 -6
  58. experimaestro/sphinx/__init__.py +3 -3
  59. experimaestro/taskglobals.py +20 -0
  60. experimaestro/tests/conftest.py +80 -0
  61. experimaestro/tests/core/test_generics.py +2 -2
  62. experimaestro/tests/identifier_stability.json +45 -0
  63. experimaestro/tests/launchers/bin/sacct +6 -2
  64. experimaestro/tests/launchers/bin/sbatch +4 -2
  65. experimaestro/tests/launchers/common.py +2 -2
  66. experimaestro/tests/launchers/test_slurm.py +80 -0
  67. experimaestro/tests/restart.py +1 -1
  68. experimaestro/tests/tasks/all.py +7 -0
  69. experimaestro/tests/tasks/test_dynamic.py +231 -0
  70. experimaestro/tests/test_checkers.py +2 -2
  71. experimaestro/tests/test_cli_jobs.py +615 -0
  72. experimaestro/tests/test_dependencies.py +11 -17
  73. experimaestro/tests/test_deprecated.py +630 -0
  74. experimaestro/tests/test_environment.py +200 -0
  75. experimaestro/tests/test_experiment.py +3 -3
  76. experimaestro/tests/test_file_progress.py +425 -0
  77. experimaestro/tests/test_file_progress_integration.py +477 -0
  78. experimaestro/tests/test_forward.py +3 -3
  79. experimaestro/tests/test_generators.py +93 -0
  80. experimaestro/tests/test_identifier.py +520 -169
  81. experimaestro/tests/test_identifier_stability.py +458 -0
  82. experimaestro/tests/test_instance.py +16 -21
  83. experimaestro/tests/test_multitoken.py +442 -0
  84. experimaestro/tests/test_mypy.py +433 -0
  85. experimaestro/tests/test_objects.py +314 -30
  86. experimaestro/tests/test_outputs.py +8 -8
  87. experimaestro/tests/test_param.py +22 -26
  88. experimaestro/tests/test_partial_paths.py +231 -0
  89. experimaestro/tests/test_progress.py +2 -50
  90. experimaestro/tests/test_resumable_task.py +480 -0
  91. experimaestro/tests/test_serializers.py +141 -60
  92. experimaestro/tests/test_state_db.py +434 -0
  93. experimaestro/tests/test_subparameters.py +160 -0
  94. experimaestro/tests/test_tags.py +151 -15
  95. experimaestro/tests/test_tasks.py +137 -160
  96. experimaestro/tests/test_token_locking.py +252 -0
  97. experimaestro/tests/test_tokens.py +25 -19
  98. experimaestro/tests/test_types.py +133 -11
  99. experimaestro/tests/test_validation.py +19 -19
  100. experimaestro/tests/test_workspace_triggers.py +158 -0
  101. experimaestro/tests/token_reschedule.py +5 -3
  102. experimaestro/tests/utils.py +2 -2
  103. experimaestro/tokens.py +154 -57
  104. experimaestro/tools/diff.py +8 -1
  105. experimaestro/tui/__init__.py +8 -0
  106. experimaestro/tui/app.py +2303 -0
  107. experimaestro/tui/app.tcss +353 -0
  108. experimaestro/tui/log_viewer.py +228 -0
  109. experimaestro/typingutils.py +11 -2
  110. experimaestro/utils/__init__.py +23 -0
  111. experimaestro/utils/environment.py +148 -0
  112. experimaestro/utils/git.py +129 -0
  113. experimaestro/utils/resources.py +1 -1
  114. experimaestro/version.py +34 -0
  115. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info}/METADATA +70 -39
  116. experimaestro-2.0.0b4.dist-info/RECORD +181 -0
  117. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info}/WHEEL +1 -1
  118. experimaestro-2.0.0b4.dist-info/entry_points.txt +16 -0
  119. experimaestro/compat.py +0 -6
  120. experimaestro/core/objects.pyi +0 -225
  121. experimaestro/server/data/0c35d18bf06992036b69.woff2 +0 -0
  122. experimaestro/server/data/219aa9140e099e6c72ed.woff2 +0 -0
  123. experimaestro/server/data/3a4004a46a653d4b2166.woff +0 -0
  124. experimaestro/server/data/3baa5b8f3469222b822d.woff +0 -0
  125. experimaestro/server/data/4d73cb90e394b34b7670.woff +0 -0
  126. experimaestro/server/data/4ef4218c522f1eb6b5b1.woff2 +0 -0
  127. experimaestro/server/data/5d681e2edae8c60630db.woff +0 -0
  128. experimaestro/server/data/6f420cf17cc0d7676fad.woff2 +0 -0
  129. experimaestro/server/data/c380809fd3677d7d6903.woff2 +0 -0
  130. experimaestro/server/data/f882956fd323fd322f31.woff +0 -0
  131. experimaestro-1.11.1.dist-info/RECORD +0 -158
  132. experimaestro-1.11.1.dist-info/entry_points.txt +0 -17
  133. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info/licenses}/LICENSE +0 -0
@@ -1,7 +1,6 @@
1
1
  # flake8: noqa: T201
2
2
  import sys
3
3
  from typing import Set, Optional
4
- import pkg_resources
5
4
  from itertools import chain
6
5
  from shutil import rmtree
7
6
  import click
@@ -10,26 +9,17 @@ from functools import cached_property, update_wrapper
10
9
  from pathlib import Path
11
10
  import subprocess
12
11
  from termcolor import cprint
12
+ from importlib.metadata import entry_points
13
13
 
14
14
  import experimaestro
15
15
  from experimaestro.experiments.cli import experiments_cli
16
16
  import experimaestro.launcherfinder.registry as launcher_registry
17
- from experimaestro.settings import find_workspace
17
+ from experimaestro.settings import ServerSettings, find_workspace
18
18
 
19
19
  # --- Command line main options
20
20
  logging.basicConfig(level=logging.INFO)
21
21
 
22
22
 
23
- def pass_cfg(f):
24
- """Pass configuration information"""
25
-
26
- @click.pass_context
27
- def new_func(ctx, *args, **kwargs):
28
- return ctx.invoke(f, ctx.obj, *args, **kwargs)
29
-
30
- return update_wrapper(new_func, f)
31
-
32
-
33
23
  def check_xp_path(ctx, self, path: Path):
34
24
  if not (path / ".__experimaestro__").is_file():
35
25
  cprint(f"{path} is not an experimaestro working directory", "red")
@@ -142,7 +132,6 @@ def diff(path: Path):
142
132
  """Show the reason of the identifier change for a job"""
143
133
  from experimaestro.tools.jobs import load_job
144
134
  from experimaestro import Config
145
- from experimaestro.core.objects import ConfigWalkContext
146
135
 
147
136
  _, job = load_job(path / "params.json", discard_id=False)
148
137
  _, new_job = load_job(path / "params.json")
@@ -268,13 +257,13 @@ def find_launchers(config: Optional[Path], spec: str):
268
257
  print(launcher_registry.find_launcher(spec))
269
258
 
270
259
 
271
- class Launchers(click.MultiCommand):
272
- """Connectors commands"""
260
+ class Launchers(click.Group):
261
+ """Dynamic command group for entry point discovery"""
273
262
 
274
263
  @cached_property
275
264
  def commands(self):
276
265
  map = {}
277
- for ep in pkg_resources.iter_entry_points(f"experimaestro.{self.name}"):
266
+ for ep in entry_points(group=f"experimaestro.{self.name}"):
278
267
  if get_cli := getattr(ep.load(), "get_cli", None):
279
268
  map[ep.name] = get_cli()
280
269
  return map
@@ -290,6 +279,21 @@ cli.add_command(Launchers("launchers", help="Launcher specific commands"))
290
279
  cli.add_command(Launchers("connectors", help="Connector specific commands"))
291
280
  cli.add_command(Launchers("tokens", help="Token specific commands"))
292
281
 
282
+ # Import and add progress commands
283
+ from .progress import progress as progress_cli
284
+
285
+ cli.add_command(progress_cli)
286
+
287
+ # Import and add jobs commands
288
+ from .jobs import jobs as jobs_cli
289
+
290
+ cli.add_command(jobs_cli)
291
+
292
+ # Import and add refactor commands
293
+ from .refactor import refactor as refactor_cli
294
+
295
+ cli.add_command(refactor_cli)
296
+
293
297
 
294
298
  @cli.group()
295
299
  @click.option("--workdir", type=Path, default=None)
@@ -310,3 +314,123 @@ def list(workdir: Path):
310
314
  cprint(f"[unfinished] {p.name}", "yellow")
311
315
  else:
312
316
  cprint(p.name, "cyan")
317
+
318
+
319
+ @experiments.command()
320
+ @click.option("--console", is_flag=True, help="Use console TUI instead of web UI")
321
+ @click.option(
322
+ "--port", type=int, default=12345, help="Port for web server (default: 12345)"
323
+ )
324
+ @click.option(
325
+ "--sync", is_flag=True, help="Force sync from disk before starting monitor"
326
+ )
327
+ @pass_cfg
328
+ def monitor(workdir: Path, console: bool, port: int, sync: bool):
329
+ """Monitor experiments with web UI or console TUI"""
330
+ # Force sync from disk if requested
331
+ if sync:
332
+ from experimaestro.scheduler.state_sync import sync_workspace_from_disk
333
+
334
+ cprint("Syncing workspace from disk...", "yellow")
335
+ sync_workspace_from_disk(workdir, write_mode=True, force=True)
336
+ cprint("Sync complete", "green")
337
+
338
+ if console:
339
+ # Use Textual TUI
340
+ from experimaestro.tui import ExperimentTUI
341
+
342
+ app = ExperimentTUI(workdir, watch=True)
343
+ app.run()
344
+ else:
345
+ # Use React web server
346
+ from experimaestro.scheduler.state_provider import WorkspaceStateProvider
347
+ from experimaestro.server import Server
348
+
349
+ cprint(f"Starting experiment monitor on http://localhost:{port}", "green")
350
+ cprint("Press Ctrl+C to stop", "yellow")
351
+
352
+ state_provider = WorkspaceStateProvider.get_instance(
353
+ workdir,
354
+ sync_on_start=not sync, # Skip auto-sync if we just did a forced one
355
+ )
356
+ settings = ServerSettings()
357
+ settings.port = port
358
+ server = Server.instance(settings, state_provider=state_provider)
359
+ server.start()
360
+
361
+ try:
362
+ import time
363
+
364
+ while True:
365
+ time.sleep(1)
366
+ except KeyboardInterrupt:
367
+ cprint("\nShutting down...", "yellow")
368
+ state_provider.close()
369
+
370
+
371
+ @experiments.command()
372
+ @click.option(
373
+ "--dry-run",
374
+ is_flag=True,
375
+ help="Don't write to database, only show what would be synced",
376
+ )
377
+ @click.option(
378
+ "--force",
379
+ is_flag=True,
380
+ help="Force sync even if recently synced (bypasses time throttling)",
381
+ )
382
+ @click.option(
383
+ "--no-wait",
384
+ is_flag=True,
385
+ help="Don't wait for lock, fail immediately if unavailable",
386
+ )
387
+ @pass_cfg
388
+ def sync(workdir: Path, dry_run: bool, force: bool, no_wait: bool):
389
+ """Synchronize workspace database from disk state
390
+
391
+ Scans experiment directories and job marker files to update the workspace
392
+ database. Uses exclusive locking to prevent conflicts with running experiments.
393
+ """
394
+ from experimaestro.scheduler.state_sync import sync_workspace_from_disk
395
+ from experimaestro.scheduler.workspace import Workspace
396
+ from experimaestro.settings import Settings
397
+
398
+ # Get settings and workspace settings
399
+ settings = Settings.instance()
400
+ ws_settings = find_workspace(workdir=workdir)
401
+
402
+ # Create workspace instance (manages database lifecycle)
403
+ workspace = Workspace(
404
+ settings=settings,
405
+ workspace_settings=ws_settings,
406
+ sync_on_init=False, # Don't sync on init since we're explicitly syncing
407
+ )
408
+
409
+ try:
410
+ # Enter workspace context to initialize database
411
+ with workspace:
412
+ cprint(f"Syncing workspace: {workspace.path}", "cyan")
413
+ if dry_run:
414
+ cprint("DRY RUN MODE: No changes will be written", "yellow")
415
+ if force:
416
+ cprint("FORCE MODE: Bypassing time throttling", "yellow")
417
+
418
+ # Run sync
419
+ sync_workspace_from_disk(
420
+ workspace=workspace,
421
+ write_mode=not dry_run,
422
+ force=force,
423
+ blocking=not no_wait,
424
+ )
425
+
426
+ cprint("Sync completed successfully", "green")
427
+
428
+ except RuntimeError as e:
429
+ cprint(f"Sync failed: {e}", "red")
430
+ sys.exit(1)
431
+ except Exception as e:
432
+ cprint(f"Unexpected error during sync: {e}", "red")
433
+ import traceback
434
+
435
+ traceback.print_exc()
436
+ sys.exit(1)
@@ -1,57 +1,15 @@
1
- import asyncio
2
- import logging
3
- from typing import Any, Callable, Dict, List, Optional
4
- import pyparsing as pp
5
- from pathlib import Path
6
- import json
7
- from experimaestro.compat import cached_property
1
+ """Filter expressions for job queries
2
+
3
+ This module provides a filter expression parser for querying jobs by state,
4
+ tags, and other attributes.
5
+ """
6
+
8
7
  import re
9
- from experimaestro.scheduler import JobState
10
-
11
-
12
- class JobInformation:
13
- def __init__(self, path: Path, scriptname: str, check: bool = False):
14
- self.path = path
15
- self.scriptname = scriptname
16
- self.check = check
17
-
18
- @cached_property
19
- def params(self):
20
- try:
21
- return json.loads((self.path / "params.json").read_text())
22
- except Exception:
23
- logging.warning("Could not load params.json in %s", self.path)
24
- return {"tags": {}}
25
-
26
- @cached_property
27
- def tags(self) -> List[str]:
28
- return self.params["tags"]
29
-
30
- @cached_property
31
- def state(self) -> Optional[JobState]:
32
- if (self.path / f"{self.scriptname}.done").is_file():
33
- return JobState.DONE
34
- if (self.path / f"{self.scriptname}.failed").is_file():
35
- return JobState.ERROR
36
- if (self.path / f"{self.scriptname}.pid").is_file():
37
- if self.check:
38
- if process := self.getprocess():
39
- state = asyncio.run(process.aio_state(0))
40
- if state is None or state.finished:
41
- return JobState.ERROR
42
- else:
43
- return JobState.ERROR
44
- return JobState.RUNNING
45
- else:
46
- return None
47
-
48
- def getprocess(self):
49
- from experimaestro.connectors import Process
50
- from experimaestro.connectors.local import LocalConnector
51
-
52
- connector = LocalConnector.instance()
53
- pinfo = json.loads((self.path / f"{self.scriptname}.pid").read_text())
54
- return Process.fromDefinition(connector, pinfo)
8
+ from typing import Callable, TYPE_CHECKING
9
+ import pyparsing as pp
10
+
11
+ if TYPE_CHECKING:
12
+ from experimaestro.scheduler.state_provider import MockJob
55
13
 
56
14
 
57
15
  # --- classes for processing
@@ -61,14 +19,14 @@ class VarExpr:
61
19
  def __init__(self, values):
62
20
  (self.varname,) = values
63
21
 
64
- def get(self, info: JobInformation):
22
+ def get(self, job: "MockJob"):
65
23
  if self.varname == "@state":
66
- return info.state.name if info.state else None
24
+ return job.state.name if job.state else None
67
25
 
68
26
  if self.varname == "@name":
69
- return str(info.path.parent.name)
27
+ return str(job.path.parent.name)
70
28
 
71
- return info.tags.get(self.varname, None)
29
+ return job.tags.get(self.varname, None)
72
30
 
73
31
  def __repr__(self):
74
32
  return f"""VAR<{self.varname}>"""
@@ -81,8 +39,8 @@ class BaseInExpr:
81
39
 
82
40
 
83
41
  class InExpr(BaseInExpr):
84
- def filter(self, information: JobInformation):
85
- value = self.var.get(information)
42
+ def filter(self, job: "MockJob"):
43
+ value = self.var.get(job)
86
44
  return value in self.values
87
45
 
88
46
  def __repr__(self):
@@ -90,8 +48,8 @@ class InExpr(BaseInExpr):
90
48
 
91
49
 
92
50
  class NotInExpr(BaseInExpr):
93
- def filter(self, information: JobInformation):
94
- value = self.var.get(information)
51
+ def filter(self, job: "MockJob"):
52
+ value = self.var.get(job)
95
53
  return value not in self.values
96
54
 
97
55
  def __repr__(self):
@@ -106,25 +64,25 @@ class RegexExpr:
106
64
  def __repr__(self):
107
65
  return f"""REGEX[{self.varname}, {self.value}]"""
108
66
 
109
- def matches(self, manager, publication):
67
+ def matches(self, _manager, publication):
110
68
  if self.varname == "tag":
111
69
  return self.value in publication.tags
112
70
 
113
71
  raise AssertionError()
114
72
 
115
- def filter(self, information: JobInformation):
116
- value = self.var.get(information)
73
+ def filter(self, job: "MockJob"):
74
+ value = self.var.get(job)
117
75
  if not value:
118
76
  return False
119
77
 
120
- return self.re.match(value)
78
+ return self.regex.match(value)
121
79
 
122
80
 
123
81
  class ConstantString:
124
82
  def __init__(self, tokens):
125
83
  (self.value,) = tokens
126
84
 
127
- def get(self, information: JobInformation):
85
+ def get(self, _job: "MockJob"):
128
86
  return self.value
129
87
 
130
88
  def __repr__(self):
@@ -138,8 +96,8 @@ class EqExpr:
138
96
  def __repr__(self):
139
97
  return f"""EQ[{self.var1}, {self.var2}]"""
140
98
 
141
- def filter(self, information: JobInformation):
142
- return self.var1.get(information) == self.var2.get(information)
99
+ def filter(self, job: "MockJob"):
100
+ return self.var1.get(job) == self.var2.get(job)
143
101
 
144
102
 
145
103
  class LogicExpr:
@@ -149,11 +107,11 @@ class LogicExpr:
149
107
  self.operator, self.y = tokens
150
108
  self.x = None
151
109
 
152
- def filter(self, information: JobInformation):
110
+ def filter(self, job: "MockJob"):
153
111
  if self.operator == "and":
154
- return self.y.filter(information) and self.x.filter(information)
112
+ return self.y.filter(job) and self.x.filter(job)
155
113
 
156
- return self.y.filter(information) or self.x.filter(information)
114
+ return self.y.filter(job) or self.x.filter(job)
157
115
 
158
116
  @staticmethod
159
117
  def summary(tokens):
@@ -187,7 +145,10 @@ quotedString = pp.QuotedString('"', unquoteResults=True) | pp.QuotedString(
187
145
  "'", unquoteResults=True
188
146
  )
189
147
 
190
- var = l("@state") | l("@name") | pp.Word(pp.alphas)
148
+ # Tag names can contain letters, digits, underscores, and hyphens
149
+ # First character must be a letter, rest can include digits, underscores, hyphens
150
+ tag_name = pp.Word(pp.alphas, pp.alphanums + "_-")
151
+ var = l("@state") | l("@name") | tag_name
191
152
  var.setParseAction(VarExpr)
192
153
 
193
154
  regexExpr = var + tilde + quotedString
@@ -220,7 +181,14 @@ filterExpr = (
220
181
  expr = (matchExpr + pp.Optional(pipe + filterExpr)).setParseAction(LogicExpr.generator)
221
182
 
222
183
 
223
- def createFilter(query: str) -> Callable[[Dict[str, Any]], bool]:
224
- """Returns a filter object given a query"""
184
+ def createFilter(query: str) -> Callable[["MockJob"], bool]:
185
+ """Returns a filter function given a query string
186
+
187
+ Args:
188
+ query: Filter expression (e.g., '@state = "DONE" and model = "bm25"')
189
+
190
+ Returns:
191
+ A callable that takes a MockJob and returns True if it matches
192
+ """
225
193
  (r,) = logicExpr.parseString(query, parseAll=True)
226
194
  return r.filter