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.
- experimaestro/__init__.py +10 -11
- experimaestro/annotations.py +167 -206
- experimaestro/cli/__init__.py +140 -16
- experimaestro/cli/filter.py +42 -74
- experimaestro/cli/jobs.py +157 -106
- experimaestro/cli/progress.py +269 -0
- experimaestro/cli/refactor.py +249 -0
- experimaestro/click.py +0 -1
- experimaestro/commandline.py +19 -3
- experimaestro/connectors/__init__.py +22 -3
- experimaestro/connectors/local.py +12 -0
- experimaestro/core/arguments.py +192 -37
- experimaestro/core/identifier.py +127 -12
- experimaestro/core/objects/__init__.py +6 -0
- experimaestro/core/objects/config.py +702 -285
- experimaestro/core/objects/config_walk.py +24 -6
- experimaestro/core/serialization.py +91 -34
- experimaestro/core/serializers.py +1 -8
- experimaestro/core/subparameters.py +164 -0
- experimaestro/core/types.py +198 -83
- 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/launcherfinder/registry.py +3 -3
- experimaestro/launchers/__init__.py +26 -1
- experimaestro/launchers/direct.py +12 -0
- experimaestro/launchers/slurm/base.py +154 -2
- experimaestro/mkdocs/base.py +6 -8
- experimaestro/mkdocs/metaloader.py +0 -1
- experimaestro/mypy.py +452 -7
- experimaestro/notifications.py +75 -16
- experimaestro/progress.py +404 -0
- experimaestro/rpyc.py +0 -1
- experimaestro/run.py +19 -6
- experimaestro/scheduler/__init__.py +18 -1
- experimaestro/scheduler/base.py +504 -959
- experimaestro/scheduler/dependencies.py +43 -28
- experimaestro/scheduler/dynamic_outputs.py +259 -130
- experimaestro/scheduler/experiment.py +582 -0
- experimaestro/scheduler/interfaces.py +474 -0
- experimaestro/scheduler/jobs.py +485 -0
- experimaestro/scheduler/services.py +186 -12
- experimaestro/scheduler/signal_handler.py +32 -0
- experimaestro/scheduler/state.py +1 -1
- 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 +153 -32
- 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 +47 -6
- 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/common.py +2 -2
- experimaestro/tests/launchers/test_slurm.py +80 -0
- experimaestro/tests/restart.py +1 -1
- experimaestro/tests/tasks/all.py +7 -0
- experimaestro/tests/tasks/test_dynamic.py +231 -0
- experimaestro/tests/test_checkers.py +2 -2
- experimaestro/tests/test_cli_jobs.py +615 -0
- experimaestro/tests/test_dependencies.py +11 -17
- experimaestro/tests/test_deprecated.py +630 -0
- experimaestro/tests/test_environment.py +200 -0
- experimaestro/tests/test_experiment.py +3 -3
- experimaestro/tests/test_file_progress.py +425 -0
- experimaestro/tests/test_file_progress_integration.py +477 -0
- experimaestro/tests/test_forward.py +3 -3
- experimaestro/tests/test_generators.py +93 -0
- experimaestro/tests/test_identifier.py +520 -169
- experimaestro/tests/test_identifier_stability.py +458 -0
- experimaestro/tests/test_instance.py +16 -21
- experimaestro/tests/test_multitoken.py +442 -0
- experimaestro/tests/test_mypy.py +433 -0
- experimaestro/tests/test_objects.py +314 -30
- experimaestro/tests/test_outputs.py +8 -8
- experimaestro/tests/test_param.py +22 -26
- experimaestro/tests/test_partial_paths.py +231 -0
- experimaestro/tests/test_progress.py +2 -50
- experimaestro/tests/test_resumable_task.py +480 -0
- experimaestro/tests/test_serializers.py +141 -60
- experimaestro/tests/test_state_db.py +434 -0
- experimaestro/tests/test_subparameters.py +160 -0
- experimaestro/tests/test_tags.py +151 -15
- experimaestro/tests/test_tasks.py +137 -160
- experimaestro/tests/test_token_locking.py +252 -0
- experimaestro/tests/test_tokens.py +25 -19
- experimaestro/tests/test_types.py +133 -11
- experimaestro/tests/test_validation.py +19 -19
- experimaestro/tests/test_workspace_triggers.py +158 -0
- experimaestro/tests/token_reschedule.py +5 -3
- experimaestro/tests/utils.py +2 -2
- experimaestro/tokens.py +154 -57
- experimaestro/tools/diff.py +8 -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/typingutils.py +11 -2
- 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-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info}/METADATA +70 -39
- experimaestro-2.0.0b4.dist-info/RECORD +181 -0
- {experimaestro-1.11.1.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 -225
- 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-1.11.1.dist-info/RECORD +0 -158
- experimaestro-1.11.1.dist-info/entry_points.txt +0 -17
- {experimaestro-1.11.1.dist-info → experimaestro-2.0.0b4.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,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.
|
|
272
|
-
"""
|
|
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
|
|
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)
|
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
|