experimaestro 2.0.0a8__py3-none-any.whl → 2.0.0b8__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 +278 -7
- 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 +111 -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 +510 -125
- experimaestro/scheduler/dependencies.py +43 -28
- experimaestro/scheduler/dynamic_outputs.py +259 -130
- experimaestro/scheduler/experiment.py +256 -31
- experimaestro/scheduler/interfaces.py +501 -0
- experimaestro/scheduler/jobs.py +216 -206
- experimaestro/scheduler/remote/__init__.py +31 -0
- experimaestro/scheduler/remote/client.py +874 -0
- experimaestro/scheduler/remote/protocol.py +467 -0
- experimaestro/scheduler/remote/server.py +423 -0
- experimaestro/scheduler/remote/sync.py +144 -0
- experimaestro/scheduler/services.py +323 -23
- experimaestro/scheduler/state_db.py +437 -0
- experimaestro/scheduler/state_provider.py +2766 -0
- experimaestro/scheduler/state_sync.py +891 -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_remote_state.py +671 -0
- 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 +2395 -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.0b8.dist-info}/METADATA +68 -38
- experimaestro-2.0.0b8.dist-info/RECORD +187 -0
- {experimaestro-2.0.0a8.dist-info → experimaestro-2.0.0b8.dist-info}/WHEEL +1 -1
- experimaestro-2.0.0b8.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.0b8.dist-info}/licenses/LICENSE +0 -0
experimaestro/cli/__init__.py
CHANGED
|
@@ -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,11 +9,12 @@ 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)
|
|
@@ -257,13 +257,13 @@ def find_launchers(config: Optional[Path], spec: str):
|
|
|
257
257
|
print(launcher_registry.find_launcher(spec))
|
|
258
258
|
|
|
259
259
|
|
|
260
|
-
class Launchers(click.
|
|
261
|
-
"""
|
|
260
|
+
class Launchers(click.Group):
|
|
261
|
+
"""Dynamic command group for entry point discovery"""
|
|
262
262
|
|
|
263
263
|
@cached_property
|
|
264
264
|
def commands(self):
|
|
265
265
|
map = {}
|
|
266
|
-
for ep in
|
|
266
|
+
for ep in entry_points(group=f"experimaestro.{self.name}"):
|
|
267
267
|
if get_cli := getattr(ep.load(), "get_cli", None):
|
|
268
268
|
map[ep.name] = get_cli()
|
|
269
269
|
return map
|
|
@@ -289,6 +289,11 @@ from .jobs import jobs as jobs_cli
|
|
|
289
289
|
|
|
290
290
|
cli.add_command(jobs_cli)
|
|
291
291
|
|
|
292
|
+
# Import and add refactor commands
|
|
293
|
+
from .refactor import refactor as refactor_cli
|
|
294
|
+
|
|
295
|
+
cli.add_command(refactor_cli)
|
|
296
|
+
|
|
292
297
|
|
|
293
298
|
@cli.group()
|
|
294
299
|
@click.option("--workdir", type=Path, default=None)
|
|
@@ -304,8 +309,274 @@ def experiments(ctx, workdir, workspace):
|
|
|
304
309
|
@experiments.command()
|
|
305
310
|
@pass_cfg
|
|
306
311
|
def list(workdir: Path):
|
|
312
|
+
"""List experiments in the workspace"""
|
|
313
|
+
from experimaestro.scheduler.state_provider import WorkspaceStateProvider
|
|
314
|
+
|
|
315
|
+
# Get experiments from state provider for detailed info
|
|
316
|
+
state_provider = WorkspaceStateProvider.get_instance(
|
|
317
|
+
workdir, read_only=True, sync_on_start=True
|
|
318
|
+
)
|
|
319
|
+
experiments_list = state_provider.get_experiments()
|
|
320
|
+
|
|
321
|
+
# Build lookup by experiment_id
|
|
322
|
+
exp_info = {exp.experiment_id: exp for exp in experiments_list}
|
|
323
|
+
|
|
307
324
|
for p in (workdir / "xp").iterdir():
|
|
325
|
+
exp_id = p.name
|
|
326
|
+
exp = exp_info.get(exp_id)
|
|
327
|
+
|
|
328
|
+
# Build display string
|
|
329
|
+
display_parts = []
|
|
330
|
+
|
|
308
331
|
if (p / "jobs.bak").exists():
|
|
309
|
-
|
|
332
|
+
display_parts.append("[unfinished]")
|
|
333
|
+
|
|
334
|
+
display_parts.append(exp_id)
|
|
335
|
+
|
|
336
|
+
# Add hostname if available
|
|
337
|
+
if exp and getattr(exp, "hostname", None):
|
|
338
|
+
display_parts.append(f"[{exp.hostname}]")
|
|
339
|
+
|
|
340
|
+
# Add job stats if available
|
|
341
|
+
if exp:
|
|
342
|
+
display_parts.append(f"({exp.finished_jobs}/{exp.total_jobs} jobs)")
|
|
343
|
+
|
|
344
|
+
display_str = " ".join(display_parts)
|
|
345
|
+
|
|
346
|
+
if (p / "jobs.bak").exists():
|
|
347
|
+
cprint(display_str, "yellow")
|
|
348
|
+
else:
|
|
349
|
+
cprint(display_str, "cyan")
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _run_monitor_ui(
|
|
353
|
+
state_provider, workdir: Path, console: bool, port: int, title: str = ""
|
|
354
|
+
):
|
|
355
|
+
"""Shared code for running monitor UI (TUI or web)
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
state_provider: StateProvider instance (local or remote)
|
|
359
|
+
workdir: Local workspace/cache directory
|
|
360
|
+
console: If True, use TUI; otherwise use web UI
|
|
361
|
+
port: Port for web server
|
|
362
|
+
title: Optional title for status messages
|
|
363
|
+
"""
|
|
364
|
+
try:
|
|
365
|
+
if console:
|
|
366
|
+
# Use Textual TUI
|
|
367
|
+
from experimaestro.tui import ExperimentTUI
|
|
368
|
+
|
|
369
|
+
app = ExperimentTUI(
|
|
370
|
+
workdir, state_provider=state_provider, watch=True, show_logs=True
|
|
371
|
+
)
|
|
372
|
+
app.run()
|
|
310
373
|
else:
|
|
311
|
-
|
|
374
|
+
# Use React web server
|
|
375
|
+
from experimaestro.server import Server
|
|
376
|
+
|
|
377
|
+
if title:
|
|
378
|
+
cprint(
|
|
379
|
+
f"Starting experiment monitor for {title} on http://localhost:{port}",
|
|
380
|
+
"green",
|
|
381
|
+
)
|
|
382
|
+
else:
|
|
383
|
+
cprint(
|
|
384
|
+
f"Starting experiment monitor on http://localhost:{port}", "green"
|
|
385
|
+
)
|
|
386
|
+
cprint("Press Ctrl+C to stop", "yellow")
|
|
387
|
+
|
|
388
|
+
settings = ServerSettings()
|
|
389
|
+
settings.port = port
|
|
390
|
+
server = Server.instance(settings, state_provider=state_provider)
|
|
391
|
+
server.start()
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
import time
|
|
395
|
+
|
|
396
|
+
while True:
|
|
397
|
+
time.sleep(1)
|
|
398
|
+
except KeyboardInterrupt:
|
|
399
|
+
pass
|
|
400
|
+
finally:
|
|
401
|
+
cprint("\nShutting down...", "yellow")
|
|
402
|
+
if state_provider:
|
|
403
|
+
state_provider.close()
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@experiments.command()
|
|
407
|
+
@click.option("--console", is_flag=True, help="Use console TUI instead of web UI")
|
|
408
|
+
@click.option(
|
|
409
|
+
"--port", type=int, default=12345, help="Port for web server (default: 12345)"
|
|
410
|
+
)
|
|
411
|
+
@click.option(
|
|
412
|
+
"--sync", is_flag=True, help="Force sync from disk before starting monitor"
|
|
413
|
+
)
|
|
414
|
+
@pass_cfg
|
|
415
|
+
def monitor(workdir: Path, console: bool, port: int, sync: bool):
|
|
416
|
+
"""Monitor local experiments with web UI or console TUI"""
|
|
417
|
+
# Force sync from disk if requested
|
|
418
|
+
if sync:
|
|
419
|
+
from experimaestro.scheduler.state_sync import sync_workspace_from_disk
|
|
420
|
+
|
|
421
|
+
cprint("Syncing workspace from disk...", "yellow")
|
|
422
|
+
sync_workspace_from_disk(workdir, write_mode=True, force=True)
|
|
423
|
+
cprint("Sync complete", "green")
|
|
424
|
+
|
|
425
|
+
from experimaestro.scheduler.state_provider import WorkspaceStateProvider
|
|
426
|
+
|
|
427
|
+
state_provider = WorkspaceStateProvider.get_instance(
|
|
428
|
+
workdir,
|
|
429
|
+
sync_on_start=not sync, # Skip auto-sync if we just did a forced one
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
_run_monitor_ui(state_provider, workdir, console, port)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@experiments.command("ssh-monitor")
|
|
436
|
+
@click.argument("host", type=str)
|
|
437
|
+
@click.argument("remote_workdir", type=str)
|
|
438
|
+
@click.option("--console", is_flag=True, help="Use console TUI instead of web UI")
|
|
439
|
+
@click.option(
|
|
440
|
+
"--port", type=int, default=12345, help="Port for web server (default: 12345)"
|
|
441
|
+
)
|
|
442
|
+
@click.option(
|
|
443
|
+
"--remote-xpm",
|
|
444
|
+
type=str,
|
|
445
|
+
default=None,
|
|
446
|
+
help="Path to experimaestro on remote host (default: use 'uv tool run')",
|
|
447
|
+
)
|
|
448
|
+
@click.option(
|
|
449
|
+
"--ssh-option",
|
|
450
|
+
"-o",
|
|
451
|
+
multiple=True,
|
|
452
|
+
help="Additional SSH options (can be repeated, e.g., -o '-p 2222')",
|
|
453
|
+
)
|
|
454
|
+
def ssh_monitor(
|
|
455
|
+
host: str,
|
|
456
|
+
remote_workdir: str,
|
|
457
|
+
console: bool,
|
|
458
|
+
port: int,
|
|
459
|
+
remote_xpm: str,
|
|
460
|
+
ssh_option: tuple,
|
|
461
|
+
):
|
|
462
|
+
"""Monitor experiments on a remote server via SSH
|
|
463
|
+
|
|
464
|
+
HOST is the SSH host (e.g., user@server)
|
|
465
|
+
REMOTE_WORKDIR is the workspace path on the remote server
|
|
466
|
+
|
|
467
|
+
Examples:
|
|
468
|
+
experimaestro experiments ssh-monitor myserver /path/to/workspace
|
|
469
|
+
experimaestro experiments ssh-monitor user@host /workspace --console
|
|
470
|
+
experimaestro experiments ssh-monitor host /workspace --remote-xpm /opt/xpm/bin/experimaestro
|
|
471
|
+
"""
|
|
472
|
+
from experimaestro.scheduler.remote.client import SSHStateProviderClient
|
|
473
|
+
|
|
474
|
+
cprint(f"Connecting to {host}...", "yellow")
|
|
475
|
+
state_provider = SSHStateProviderClient(
|
|
476
|
+
host=host,
|
|
477
|
+
remote_workspace=remote_workdir,
|
|
478
|
+
ssh_options=list(ssh_option) if ssh_option else None,
|
|
479
|
+
remote_xpm_path=remote_xpm,
|
|
480
|
+
)
|
|
481
|
+
try:
|
|
482
|
+
state_provider.connect()
|
|
483
|
+
cprint(f"Connected to {host}", "green")
|
|
484
|
+
except Exception as e:
|
|
485
|
+
cprint(f"Failed to connect: {e}", "red")
|
|
486
|
+
raise click.Abort()
|
|
487
|
+
|
|
488
|
+
_run_monitor_ui(
|
|
489
|
+
state_provider,
|
|
490
|
+
state_provider.local_cache_dir,
|
|
491
|
+
console,
|
|
492
|
+
port,
|
|
493
|
+
title=host,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
@experiments.command("monitor-server")
|
|
498
|
+
@pass_cfg
|
|
499
|
+
def monitor_server(workdir: Path):
|
|
500
|
+
"""Start monitoring server for SSH connections (JSON-RPC over stdio)
|
|
501
|
+
|
|
502
|
+
This command is intended to be run over SSH to provide remote monitoring.
|
|
503
|
+
Communication is via JSON-RPC over stdin/stdout.
|
|
504
|
+
|
|
505
|
+
Example:
|
|
506
|
+
ssh host 'experimaestro experiments --workdir /path monitor-server'
|
|
507
|
+
"""
|
|
508
|
+
from experimaestro.scheduler.remote.server import SSHStateProviderServer
|
|
509
|
+
|
|
510
|
+
server = SSHStateProviderServer(workdir)
|
|
511
|
+
try:
|
|
512
|
+
server.start()
|
|
513
|
+
except KeyboardInterrupt:
|
|
514
|
+
server.stop()
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
@experiments.command()
|
|
518
|
+
@click.option(
|
|
519
|
+
"--dry-run",
|
|
520
|
+
is_flag=True,
|
|
521
|
+
help="Don't write to database, only show what would be synced",
|
|
522
|
+
)
|
|
523
|
+
@click.option(
|
|
524
|
+
"--force",
|
|
525
|
+
is_flag=True,
|
|
526
|
+
help="Force sync even if recently synced (bypasses time throttling)",
|
|
527
|
+
)
|
|
528
|
+
@click.option(
|
|
529
|
+
"--no-wait",
|
|
530
|
+
is_flag=True,
|
|
531
|
+
help="Don't wait for lock, fail immediately if unavailable",
|
|
532
|
+
)
|
|
533
|
+
@pass_cfg
|
|
534
|
+
def sync(workdir: Path, dry_run: bool, force: bool, no_wait: bool):
|
|
535
|
+
"""Synchronize workspace database from disk state
|
|
536
|
+
|
|
537
|
+
Scans experiment directories and job marker files to update the workspace
|
|
538
|
+
database. Uses exclusive locking to prevent conflicts with running experiments.
|
|
539
|
+
"""
|
|
540
|
+
from experimaestro.scheduler.state_sync import sync_workspace_from_disk
|
|
541
|
+
from experimaestro.scheduler.workspace import Workspace
|
|
542
|
+
from experimaestro.settings import Settings
|
|
543
|
+
|
|
544
|
+
# Get settings and workspace settings
|
|
545
|
+
settings = Settings.instance()
|
|
546
|
+
ws_settings = find_workspace(workdir=workdir)
|
|
547
|
+
|
|
548
|
+
# Create workspace instance (manages database lifecycle)
|
|
549
|
+
workspace = Workspace(
|
|
550
|
+
settings=settings,
|
|
551
|
+
workspace_settings=ws_settings,
|
|
552
|
+
sync_on_init=False, # Don't sync on init since we're explicitly syncing
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
try:
|
|
556
|
+
# Enter workspace context to initialize database
|
|
557
|
+
with workspace:
|
|
558
|
+
cprint(f"Syncing workspace: {workspace.path}", "cyan")
|
|
559
|
+
if dry_run:
|
|
560
|
+
cprint("DRY RUN MODE: No changes will be written", "yellow")
|
|
561
|
+
if force:
|
|
562
|
+
cprint("FORCE MODE: Bypassing time throttling", "yellow")
|
|
563
|
+
|
|
564
|
+
# Run sync
|
|
565
|
+
sync_workspace_from_disk(
|
|
566
|
+
workspace=workspace,
|
|
567
|
+
write_mode=not dry_run,
|
|
568
|
+
force=force,
|
|
569
|
+
blocking=not no_wait,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
cprint("Sync completed successfully", "green")
|
|
573
|
+
|
|
574
|
+
except RuntimeError as e:
|
|
575
|
+
cprint(f"Sync failed: {e}", "red")
|
|
576
|
+
sys.exit(1)
|
|
577
|
+
except Exception as e:
|
|
578
|
+
cprint(f"Unexpected error during sync: {e}", "red")
|
|
579
|
+
import traceback
|
|
580
|
+
|
|
581
|
+
traceback.print_exc()
|
|
582
|
+
sys.exit(1)
|
experimaestro/cli/filter.py
CHANGED
|
@@ -1,57 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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,
|
|
22
|
+
def get(self, job: "MockJob"):
|
|
65
23
|
if self.varname == "@state":
|
|
66
|
-
return
|
|
24
|
+
return job.state.name if job.state else None
|
|
67
25
|
|
|
68
26
|
if self.varname == "@name":
|
|
69
|
-
return str(
|
|
27
|
+
return str(job.path.parent.name)
|
|
70
28
|
|
|
71
|
-
return
|
|
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,
|
|
85
|
-
value = self.var.get(
|
|
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,
|
|
94
|
-
value = self.var.get(
|
|
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,
|
|
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,
|
|
116
|
-
value = self.var.get(
|
|
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.
|
|
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,
|
|
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,
|
|
142
|
-
return self.var1.get(
|
|
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,
|
|
110
|
+
def filter(self, job: "MockJob"):
|
|
153
111
|
if self.operator == "and":
|
|
154
|
-
return self.y.filter(
|
|
112
|
+
return self.y.filter(job) and self.x.filter(job)
|
|
155
113
|
|
|
156
|
-
return self.y.filter(
|
|
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
|
-
|
|
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[[
|
|
224
|
-
"""Returns a filter
|
|
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
|