experimaestro 1.6.1__py3-none-any.whl → 1.7.0__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 (81) hide show
  1. experimaestro/__init__.py +3 -1
  2. experimaestro/annotations.py +13 -3
  3. experimaestro/cli/filter.py +3 -3
  4. experimaestro/cli/jobs.py +1 -1
  5. experimaestro/commandline.py +3 -7
  6. experimaestro/connectors/__init__.py +22 -10
  7. experimaestro/connectors/local.py +17 -8
  8. experimaestro/connectors/ssh.py +1 -1
  9. experimaestro/core/arguments.py +26 -3
  10. experimaestro/core/objects.py +90 -6
  11. experimaestro/core/objects.pyi +7 -1
  12. experimaestro/core/types.py +33 -2
  13. experimaestro/experiments/cli.py +21 -9
  14. experimaestro/generators.py +6 -1
  15. experimaestro/ipc.py +4 -1
  16. experimaestro/launcherfinder/registry.py +23 -5
  17. experimaestro/launchers/slurm/base.py +47 -9
  18. experimaestro/notifications.py +1 -1
  19. experimaestro/run.py +1 -1
  20. experimaestro/scheduler/base.py +102 -6
  21. experimaestro/scheduler/dynamic_outputs.py +184 -0
  22. experimaestro/scheduler/workspace.py +2 -1
  23. experimaestro/scriptbuilder.py +13 -2
  24. experimaestro/server/data/016b4a6cdced82ab3aa1.ttf +0 -0
  25. experimaestro/server/data/0c35d18bf06992036b69.woff2 +0 -0
  26. experimaestro/server/data/1815e00441357e01619e.ttf +0 -0
  27. experimaestro/server/data/219aa9140e099e6c72ed.woff2 +0 -0
  28. experimaestro/server/data/2463b90d9a316e4e5294.woff2 +0 -0
  29. experimaestro/server/data/2582b0e4bcf85eceead0.ttf +0 -0
  30. experimaestro/server/data/3a4004a46a653d4b2166.woff +0 -0
  31. experimaestro/server/data/3baa5b8f3469222b822d.woff +0 -0
  32. experimaestro/server/data/4d73cb90e394b34b7670.woff +0 -0
  33. experimaestro/server/data/4ef4218c522f1eb6b5b1.woff2 +0 -0
  34. experimaestro/server/data/50701fbb8177c2dde530.ttf +0 -0
  35. experimaestro/server/data/5d681e2edae8c60630db.woff +0 -0
  36. experimaestro/server/data/6f420cf17cc0d7676fad.woff2 +0 -0
  37. experimaestro/server/data/878f31251d960bd6266f.woff2 +0 -0
  38. experimaestro/server/data/89999bdf5d835c012025.woff2 +0 -0
  39. experimaestro/server/data/914997e1bdfc990d0897.ttf +0 -0
  40. experimaestro/server/data/b041b1fa4fe241b23445.woff2 +0 -0
  41. experimaestro/server/data/b6879d41b0852f01ed5b.woff2 +0 -0
  42. experimaestro/server/data/c210719e60948b211a12.woff2 +0 -0
  43. experimaestro/server/data/c380809fd3677d7d6903.woff2 +0 -0
  44. experimaestro/server/data/d75e3fd1eb12e9bd6655.ttf +0 -0
  45. experimaestro/server/data/f882956fd323fd322f31.woff +0 -0
  46. experimaestro/server/data/favicon.ico +0 -0
  47. experimaestro/server/data/index.css +22963 -0
  48. experimaestro/server/data/index.css.map +1 -0
  49. experimaestro/server/data/index.html +27 -0
  50. experimaestro/server/data/index.js +101770 -0
  51. experimaestro/server/data/index.js.map +1 -0
  52. experimaestro/server/data/login.html +22 -0
  53. experimaestro/server/data/manifest.json +15 -0
  54. experimaestro/settings.py +2 -2
  55. experimaestro/sphinx/__init__.py +7 -17
  56. experimaestro/taskglobals.py +7 -2
  57. experimaestro/tests/definitions_types.py +5 -3
  58. experimaestro/tests/launchers/bin/sbatch +34 -7
  59. experimaestro/tests/launchers/bin/srun +5 -0
  60. experimaestro/tests/launchers/common.py +16 -4
  61. experimaestro/tests/restart.py +6 -3
  62. experimaestro/tests/tasks/all.py +16 -10
  63. experimaestro/tests/tasks/foreign.py +2 -4
  64. experimaestro/tests/test_forward.py +5 -5
  65. experimaestro/tests/test_identifier.py +61 -66
  66. experimaestro/tests/test_instance.py +3 -6
  67. experimaestro/tests/test_param.py +40 -22
  68. experimaestro/tests/test_tags.py +5 -11
  69. experimaestro/tests/test_tokens.py +3 -2
  70. experimaestro/tests/test_types.py +17 -14
  71. experimaestro/tests/test_validation.py +48 -91
  72. experimaestro/tokens.py +16 -5
  73. experimaestro/typingutils.py +7 -0
  74. experimaestro/utils/asyncio.py +6 -2
  75. experimaestro/utils/resources.py +7 -3
  76. {experimaestro-1.6.1.dist-info → experimaestro-1.7.0.dist-info}/METADATA +3 -4
  77. experimaestro-1.7.0.dist-info/RECORD +154 -0
  78. {experimaestro-1.6.1.dist-info → experimaestro-1.7.0.dist-info}/WHEEL +1 -1
  79. experimaestro-1.6.1.dist-info/RECORD +0 -122
  80. {experimaestro-1.6.1.dist-info → experimaestro-1.7.0.dist-info}/LICENSE +0 -0
  81. {experimaestro-1.6.1.dist-info → experimaestro-1.7.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,184 @@
1
+ """Handles dynamic task outputs"""
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ import queue
7
+ import threading
8
+ from collections import defaultdict
9
+ from functools import cached_property
10
+ from pathlib import Path
11
+ from typing import Callable, TYPE_CHECKING
12
+
13
+ from watchdog.events import FileSystemEventHandler
14
+
15
+ from experimaestro.ipc import ipcom
16
+ from experimaestro.utils import logger
17
+
18
+ from .base import Job, experiment
19
+
20
+ if TYPE_CHECKING:
21
+ from experimaestro.core.objects import WatchedOutput
22
+
23
+
24
+ class TaskOutputCallbackHandler:
25
+ def __init__(self, converter: Callable):
26
+ pass
27
+
28
+
29
+ class TaskOutputs(FileSystemEventHandler):
30
+ """Represent and monitors dynamic outputs generated by one task"""
31
+
32
+ #: Global dictionary for handles
33
+ HANDLERS: dict[Path, "TaskOutputs"] = {}
34
+
35
+ #: Global lock to access current HANDLERS
36
+ LOCK = threading.Lock()
37
+
38
+ def create(job: Job):
39
+ with TaskOutputs.LOCK:
40
+ if instance := TaskOutputs.get(job.task_outputs_path, None):
41
+ return instance
42
+
43
+ instance = TaskOutputs(job.task_outputs_path)
44
+ TaskOutputs[job.task_outputs_path] = instance
45
+ return instance
46
+
47
+ def __init__(self, path: Path):
48
+ """Monitors an event path"""
49
+ logger.debug("Watching dynamic task outputs in %s", path)
50
+ self.path = path
51
+ self.handle = None
52
+ self.count = 0
53
+ self.lock = threading.Lock()
54
+ self.listeners: dict[str, dict[Callable, set[Callable]]] = defaultdict(
55
+ lambda: defaultdict(set)
56
+ )
57
+
58
+ #: The events registered so far
59
+ self.events = []
60
+
61
+ def __enter__(self):
62
+ """Starts monitoring task outputs"""
63
+ self.job.task_outputs_path.parent.mkdir(parents=True, exist_ok=True)
64
+ with self.lock:
65
+ if self.handle is None:
66
+ assert self.count == 0
67
+ self.handle = ipcom().fswatch(self, self.path.parent, False)
68
+ self.count += 1
69
+ return self
70
+
71
+ def __exit__(self, *args):
72
+ """Stops monitoring task outputs"""
73
+ with self.lock:
74
+ self.count -= 1
75
+ if self.count == 0:
76
+ ipcom().fsunwatch(self.handle)
77
+ self.fh.close()
78
+
79
+ self.handle = None
80
+ self._fh = None
81
+
82
+ def watch_output(self, watched: "WatchedOutput"):
83
+ """Add a new listener"""
84
+ key = f"{watched.config.__identifier__}/{watched.method_name}"
85
+ with self.lock:
86
+ # Process events so far
87
+ listener = self.listeners[key].get(watched.method, None)
88
+ if listener is None:
89
+ listener = TaskOutputCallbackHandler(watched.method)
90
+
91
+ # Register
92
+ self.listeners[key][watched.method].add(watched.callback)
93
+
94
+ #
95
+ # --- Events
96
+ #
97
+
98
+ @cached_property
99
+ def fh(self):
100
+ if self._fh is None:
101
+ self._fh = self.path.open("rt")
102
+ return self._fh
103
+
104
+ def on_modified(self, event):
105
+ self.handle(Path(event.src_path))
106
+
107
+ def on_created(self, event):
108
+ self.handle(Path(event.src_path))
109
+
110
+ def handle(self, path: Path):
111
+ if path != self.path:
112
+ return
113
+
114
+ with self.lock:
115
+ logger.debug("[TASK OUTPUT] Handling task output for %s", self.path)
116
+
117
+ while json_line := self.fh.readline():
118
+ # Read the event
119
+ event = json.loads(json_line)
120
+ logger.debug("Event: %s", event)
121
+
122
+ # FIXME: move elsewhere
123
+ # # Process the event
124
+ # event = self.config_method(
125
+ # self.job.config.__xpm__.mark_output,
126
+ # *event["args"],
127
+ # **event["kwargs"],
128
+ # )
129
+
130
+ self.events.append(event)
131
+ # self.job.scheduler.xp.taskOutputsWorker.add(self, event)
132
+
133
+
134
+ class TaskOutputsWorker(threading.Thread):
135
+ """This worker process dynamic output queue for one experiment"""
136
+
137
+ def __init__(self, xp: experiment):
138
+ super().__init__(name="task outputs worker", daemon=True)
139
+ self.queue = queue.Queue()
140
+ self.xp = xp
141
+
142
+ def watch_output(self, watched: "WatchedOutput"):
143
+ """Watch an output
144
+
145
+ :param watched: The watched output specification
146
+ """
147
+ logger.debug("Registering task output listener %s", watched)
148
+
149
+ # path = watched.job.tasks_output_path
150
+ TaskOutputs.create(watched.job).watch_output(watched)
151
+
152
+ def add(self, watcher, event):
153
+ asyncio.run_coroutine_threadsafe(
154
+ self.xp.update_task_output_count(1),
155
+ self.xp.scheduler.loop,
156
+ ).result()
157
+ self.queue.put((watcher, event))
158
+
159
+ def run(self):
160
+ logging.debug("Starting output listener queue")
161
+ while True:
162
+ # Get the next element in the queue
163
+ element = self.queue.get()
164
+ if element is None:
165
+ # end of processing
166
+ break
167
+
168
+ # Call all the listeners
169
+ logging.debug("Got one event: %s", element)
170
+ watcher, event = element
171
+ for listener in watcher.listeners:
172
+ try:
173
+ logger.debug("Calling listener [%s] with %s", listener, event)
174
+ listener(event)
175
+ logger.debug(
176
+ "[done] Calling listener [%s] with %s", listener, event
177
+ )
178
+ except Exception:
179
+ logging.exception("Exception while calling the listener")
180
+ self.queue.task_done()
181
+
182
+ asyncio.run_coroutine_threadsafe(
183
+ self.xp.update_task_output_count(-1), self.xp.scheduler.loop
184
+ ).result()
@@ -2,7 +2,7 @@ from collections import ChainMap
2
2
  from enum import Enum
3
3
  from functools import cached_property
4
4
  from pathlib import Path
5
- from typing import Optional
5
+ from typing import Iterator, Optional
6
6
  from experimaestro.settings import WorkspaceSettings, Settings
7
7
 
8
8
 
@@ -46,6 +46,7 @@ class Workspace:
46
46
  path = path.absolute()
47
47
  self.path = path
48
48
  self.run_mode = run_mode
49
+ self.python_path = []
49
50
  from ..launchers import Launcher
50
51
 
51
52
  self.launcher = launcher or Launcher.get(path)
@@ -51,6 +51,8 @@ class PythonScriptBuilder:
51
51
  self.lockfiles: List[Path] = []
52
52
  self.notificationURL: Optional[str] = None
53
53
  self.command: Optional[AbstractCommand] = None
54
+
55
+ # This is used to serialize the full process identifier on disk
54
56
  self.processtype = "local"
55
57
 
56
58
  def write(self, job: CommandLineJob):
@@ -63,7 +65,7 @@ class PythonScriptBuilder:
63
65
  job {CommandLineJob} -- [description]
64
66
 
65
67
  Returns:
66
- [type] -- [description]
68
+ str -- The script path on disk
67
69
  """
68
70
  assert isinstance(
69
71
  job, CommandLineJob
@@ -94,6 +96,7 @@ class PythonScriptBuilder:
94
96
  out.write("# Experimaestro generated task\n\n")
95
97
  out.write(
96
98
  """import logging\n"""
99
+ """import sys\n"""
97
100
  """logging.basicConfig(level=logging.INFO, """
98
101
  """format='%(levelname)s:%(process)d:%(asctime)s [%(name)s] %(message)s', datefmt='%y-%m-%d %H:%M:%S')\n\n"""
99
102
  )
@@ -112,9 +115,17 @@ class PythonScriptBuilder:
112
115
  out.write(" ]\n")
113
116
 
114
117
  for name, value in job.environ.items():
115
- out.write(f""" os.environ["{name}"] = "{shquote(value)}"\n""")
118
+ if name == "PYTHONPATH":
119
+ # Handles properly python path
120
+ for path in value.split(":"):
121
+ out.write(f""" sys.path.insert(0, "{shquote(path)}")\n""")
122
+ else:
123
+ out.write(f""" os.environ["{name}"] = "{shquote(value)}"\n""")
116
124
  out.write("\n")
117
125
 
126
+ for path in job.python_path:
127
+ out.write(f""" sys.path.insert(0, "{shquote(str(path))}")\n""")
128
+
118
129
  out.write(
119
130
  f""" TaskRunner("{shquote(connector.resolve(scriptpath))}","""
120
131
  """ lockfiles).run()\n"""
Binary file