experimaestro 1.5.4__py3-none-any.whl → 1.6.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 (62) hide show
  1. experimaestro/__init__.py +0 -1
  2. experimaestro/__main__.py +3 -423
  3. experimaestro/cli/__init__.py +312 -0
  4. experimaestro/{filter.py → cli/filter.py} +4 -4
  5. experimaestro/cli/jobs.py +261 -0
  6. experimaestro/click.py +0 -35
  7. experimaestro/connectors/ssh.py +26 -7
  8. experimaestro/core/objects.py +13 -6
  9. experimaestro/core/types.py +8 -3
  10. experimaestro/experiments/cli.py +97 -63
  11. experimaestro/experiments/configuration.py +7 -1
  12. experimaestro/launcherfinder/__init__.py +1 -1
  13. experimaestro/launcherfinder/base.py +2 -18
  14. experimaestro/launcherfinder/registry.py +22 -129
  15. experimaestro/launchers/direct.py +0 -47
  16. experimaestro/launchers/slurm/base.py +3 -1
  17. experimaestro/notifications.py +24 -8
  18. experimaestro/run.py +21 -3
  19. experimaestro/scheduler/base.py +26 -15
  20. experimaestro/scheduler/workspace.py +26 -8
  21. experimaestro/scriptbuilder.py +5 -1
  22. experimaestro/settings.py +43 -5
  23. experimaestro/tests/launchers/config_slurm/launchers.py +25 -0
  24. experimaestro/tests/test_findlauncher.py +1 -1
  25. experimaestro/tests/test_ssh.py +7 -0
  26. experimaestro/tests/test_tags.py +35 -0
  27. experimaestro/tokens.py +8 -8
  28. experimaestro/utils/resources.py +5 -1
  29. {experimaestro-1.5.4.dist-info → experimaestro-1.6.0.dist-info}/METADATA +4 -5
  30. {experimaestro-1.5.4.dist-info → experimaestro-1.6.0.dist-info}/RECORD +33 -59
  31. {experimaestro-1.5.4.dist-info → experimaestro-1.6.0.dist-info}/WHEEL +1 -1
  32. {experimaestro-1.5.4.dist-info → experimaestro-1.6.0.dist-info}/entry_points.txt +0 -4
  33. experimaestro/launchers/slurm/cli.py +0 -29
  34. experimaestro/launchers/slurm/configuration.py +0 -597
  35. experimaestro/scheduler/environment.py +0 -94
  36. experimaestro/server/data/016b4a6cdced82ab3aa1.ttf +0 -0
  37. experimaestro/server/data/0c35d18bf06992036b69.woff2 +0 -0
  38. experimaestro/server/data/219aa9140e099e6c72ed.woff2 +0 -0
  39. experimaestro/server/data/3a4004a46a653d4b2166.woff +0 -0
  40. experimaestro/server/data/3baa5b8f3469222b822d.woff +0 -0
  41. experimaestro/server/data/4d73cb90e394b34b7670.woff +0 -0
  42. experimaestro/server/data/4ef4218c522f1eb6b5b1.woff2 +0 -0
  43. experimaestro/server/data/50701fbb8177c2dde530.ttf +0 -0
  44. experimaestro/server/data/5d681e2edae8c60630db.woff +0 -0
  45. experimaestro/server/data/6f420cf17cc0d7676fad.woff2 +0 -0
  46. experimaestro/server/data/878f31251d960bd6266f.woff2 +0 -0
  47. experimaestro/server/data/b041b1fa4fe241b23445.woff2 +0 -0
  48. experimaestro/server/data/b6879d41b0852f01ed5b.woff2 +0 -0
  49. experimaestro/server/data/c380809fd3677d7d6903.woff2 +0 -0
  50. experimaestro/server/data/d75e3fd1eb12e9bd6655.ttf +0 -0
  51. experimaestro/server/data/f882956fd323fd322f31.woff +0 -0
  52. experimaestro/server/data/favicon.ico +0 -0
  53. experimaestro/server/data/index.css +0 -22844
  54. experimaestro/server/data/index.css.map +0 -1
  55. experimaestro/server/data/index.html +0 -27
  56. experimaestro/server/data/index.js +0 -100947
  57. experimaestro/server/data/index.js.map +0 -1
  58. experimaestro/server/data/login.html +0 -22
  59. experimaestro/server/data/manifest.json +0 -15
  60. experimaestro/tests/launchers/config_slurm/launchers.yaml +0 -134
  61. experimaestro/utils/yaml.py +0 -202
  62. {experimaestro-1.5.4.dist-info → experimaestro-1.6.0.dist-info}/LICENSE +0 -0
@@ -583,6 +583,9 @@ class ConfigInformation:
583
583
  # State information
584
584
  self.job = None
585
585
 
586
+ #: True when this configuration was loaded from disk
587
+ self.loaded = False
588
+
586
589
  # Explicitely added dependencies
587
590
  self.dependencies = []
588
591
 
@@ -846,8 +849,8 @@ class ConfigInformation:
846
849
  dependencies, path + ["__init_tasks__"], taskids
847
850
  )
848
851
 
849
- # Check for an associated task
850
- if self.task:
852
+ # Check for an associated task (and not loaded)
853
+ if self.task and not self.loaded:
851
854
  if id(self.task) not in taskids:
852
855
  taskids.add(id(self.task))
853
856
  dependencies.add(self.task.__xpm__.dependency())
@@ -916,14 +919,14 @@ class ConfigInformation:
916
919
 
917
920
  # --- Submit the job
918
921
 
922
+ # Sets the init tasks
923
+ self.init_tasks = init_tasks
924
+
919
925
  # Creates a new job
920
926
  self.job = self.xpmtype.task(
921
927
  self.pyobject, launcher=launcher, workspace=workspace, run_mode=run_mode
922
928
  )
923
929
 
924
- # Sets the init tasks
925
- self.init_tasks = init_tasks
926
-
927
930
  # Validate the object
928
931
  job_context = JobContext(self.job)
929
932
  self.validate_and_seal(job_context)
@@ -979,6 +982,9 @@ class ConfigInformation:
979
982
  elif self.job.failedpath.is_file():
980
983
  color = "light_red"
981
984
  cprint(f"[failed] {s}", color, file=sys.stderr)
985
+ elif self.job.pidpath.is_file():
986
+ color = "blue"
987
+ cprint(f"[running] {s}", color, file=sys.stderr)
982
988
  else:
983
989
  color = "light_blue"
984
990
  cprint(f"[not run] {s}", color, file=sys.stderr)
@@ -1382,12 +1388,13 @@ class ConfigInformation:
1382
1388
  else:
1383
1389
  o.__init__()
1384
1390
  xpminfo = o.__xpm__ # type: ConfigInformation
1391
+ xpminfo.loaded = True
1385
1392
 
1386
1393
  meta = definition.get("meta", None)
1387
1394
  if meta:
1388
1395
  xpminfo._meta = meta
1389
1396
  if xpminfo.xpmtype.task is not None:
1390
- o.__xpm__.job = object()
1397
+ xpminfo.job = object()
1391
1398
 
1392
1399
  # Set the fields
1393
1400
  for name, value in definition["fields"].items():
@@ -325,12 +325,17 @@ class ObjectType(Type):
325
325
 
326
326
  # Get the module
327
327
  module = inspect.getmodule(self.originaltype)
328
- if getattr(module, "__file__", None) is None:
328
+ self._module = module.__name__
329
+ self._package = module.__package__
330
+
331
+ if self._module and self._package:
329
332
  self._file = None
330
333
  else:
331
334
  self._file = Path(inspect.getfile(self.originaltype)).absolute()
332
- self._module = module.__name__
333
- self._package = module.__package__
335
+
336
+ assert (
337
+ self._module and self._package
338
+ ) or self._file, f"Could not detect module/file for {self.originaltype}"
334
339
 
335
340
  # The class of the object
336
341
 
@@ -1,27 +1,29 @@
1
+ import imp
2
+ import importlib
1
3
  import inspect
2
- import itertools
3
4
  import json
4
5
  import logging
5
6
  import sys
6
7
  from pathlib import Path
7
- from typing import Any, List, Optional, Protocol, Tuple, Dict
8
+ from typing import Any, List, Optional, Protocol, Tuple
8
9
 
9
10
  import click
10
11
  import omegaconf
11
12
  import yaml
12
- from experimaestro import LauncherRegistry, RunMode, experiment
13
- from experimaestro.experiments.configuration import ConfigurationBase
14
- from experimaestro.exceptions import HandledException
15
- from experimaestro.settings import get_settings, get_workspace
16
13
  from omegaconf import OmegaConf, SCMode
17
14
  from termcolor import cprint
18
15
 
16
+ from experimaestro import LauncherRegistry, RunMode, experiment
17
+ from experimaestro.exceptions import HandledException
18
+ from experimaestro.experiments.configuration import ConfigurationBase
19
+ from experimaestro.settings import find_workspace
20
+
19
21
 
20
22
  class ExperimentHelper:
21
23
  """Helper for experiments"""
22
24
 
23
- # The experiment
24
25
  xp: experiment
26
+ """The experiment object"""
25
27
 
26
28
  #: Run function
27
29
  callable: "ExperimentCallable"
@@ -54,18 +56,29 @@ class ExperimentCallable(Protocol):
54
56
  ...
55
57
 
56
58
 
57
- def load(yaml_file: Path):
58
- """Loads a YAML file, and parents one if they exist"""
59
- if not yaml_file.exists() and yaml_file.suffix != ".yaml":
60
- yaml_file = yaml_file.with_suffix(".yaml")
59
+ class ConfigurationLoader:
60
+ def __init__(self):
61
+ self.yamls = []
62
+ self.pythonpath = set()
61
63
 
62
- with yaml_file.open("rt") as fp:
63
- _data = yaml.full_load(fp)
64
- data = [_data]
65
- if parent := _data.get("parent", None):
66
- data.extend(load(yaml_file.parent / parent))
64
+ def load(self, yaml_file: Path):
65
+ """Loads a YAML file, and parents one if they exist"""
66
+ if not yaml_file.exists() and yaml_file.suffix != ".yaml":
67
+ yaml_file = yaml_file.with_suffix(".yaml")
67
68
 
68
- return data[::-1]
69
+ with yaml_file.open("rt") as fp:
70
+ _data = yaml.full_load(fp)
71
+ if parent := _data.get("parent", None):
72
+ self.load(yaml_file.parent / parent)
73
+
74
+ self.yamls.append(_data)
75
+
76
+ for path in _data.get("pythonpath", []):
77
+ path = Path(path)
78
+ if path.is_absolute():
79
+ self.pythonpath.add(path.resolve())
80
+ else:
81
+ self.pythonpath.add((yaml_file.parent / path).resolve())
69
82
 
70
83
 
71
84
  @click.option("--debug", is_flag=True, help="Print debug information")
@@ -105,11 +118,20 @@ def load(yaml_file: Path):
105
118
  @click.option(
106
119
  "--file", "xp_file", help="The file containing the main experimental code"
107
120
  )
121
+ @click.option(
122
+ "--module-name", "module_name", help="Module containing the experimental code"
123
+ )
124
+ @click.option(
125
+ "--workspace",
126
+ type=str,
127
+ default=None,
128
+ help="Workspace ID (reads from settings.yaml in experimaestro config)",
129
+ )
108
130
  @click.option(
109
131
  "--workdir",
110
132
  type=str,
111
133
  default=None,
112
- help="Working directory - if None, uses the default XPM " "working directory",
134
+ help="Working environment",
113
135
  )
114
136
  @click.option("--conf", "-c", "extra_conf", type=str, multiple=True)
115
137
  @click.option(
@@ -128,36 +150,55 @@ def experiments_cli( # noqa: C901
128
150
  port: int,
129
151
  xpm_config_dir: Path,
130
152
  workdir: Optional[Path],
153
+ workspace: Optional[str],
131
154
  env: List[Tuple[str, str]],
132
155
  run_mode: RunMode,
133
156
  extra_conf: List[str],
134
157
  pre_yaml: List[str],
135
158
  post_yaml: List[str],
159
+ module_name: Optional[str],
136
160
  args: List[str],
137
161
  show: bool,
138
162
  debug: bool,
139
163
  ):
140
164
  """Run an experiment"""
165
+
141
166
  # --- Set the logger
142
167
  logging.getLogger().setLevel(logging.DEBUG if debug else logging.INFO)
143
168
  logging.getLogger("xpm.hash").setLevel(logging.INFO)
144
169
 
145
170
  # --- Loads the YAML
146
- yamls = []
171
+ conf_loader = ConfigurationLoader()
147
172
  for y in pre_yaml:
148
- yamls.extend(load(Path(y)))
149
- yamls.extend(load(Path(yaml_file)))
173
+ conf_loader.load(Path(y))
174
+ conf_loader.load(Path(yaml_file))
150
175
  for y in post_yaml:
151
- yamls.extend(load(Path(y)))
176
+ conf_loader.load(Path(y))
177
+
178
+ # --- Merge the YAMLs
179
+ configuration = OmegaConf.merge(*conf_loader.yamls)
180
+ if extra_conf:
181
+ configuration.merge_with(OmegaConf.from_dotlist(extra_conf))
152
182
 
153
183
  # --- Get the XP file
154
- if xp_file is None:
155
- for data in yamls[::-1]:
156
- if xp_file := data.get("file", None):
157
- break
184
+ pythonpath = list(conf_loader.pythonpath)
185
+ if module_name is None:
186
+ module_name = configuration.get("module", None)
158
187
 
159
- if xp_file is None:
160
- raise ValueError("No experiment file given")
188
+ if xp_file is None:
189
+ xp_file = configuration.get("file", None)
190
+ if xp_file:
191
+ assert (
192
+ not module_name
193
+ ), "Module name and experiment file are mutually exclusive options"
194
+ xp_file = Path(xp_file)
195
+ if not pythonpath:
196
+ pythonpath.append(xp_file.parent)
197
+ logging.info("Using python path: %s", ", ".join(str(s) for s in pythonpath))
198
+
199
+ assert (
200
+ module_name or xp_file
201
+ ), "Either the module name or experiment file should be given"
161
202
 
162
203
  # --- Set some options
163
204
 
@@ -165,32 +206,35 @@ def experiments_cli( # noqa: C901
165
206
  assert xpm_config_dir.is_dir()
166
207
  LauncherRegistry.set_config_dir(xpm_config_dir)
167
208
 
168
- # --- Loads the XP file
169
- xp_file = Path(xp_file)
170
- if not xp_file.exists() and xp_file.suffix != ".py":
171
- xp_file = xp_file.with_suffix(".py")
172
- xp_file = Path(yaml_file).parent / xp_file
173
-
174
- with open(xp_file, "r") as f:
175
- source = f.read()
176
- if sys.version_info < (3, 9):
177
- the__file__ = str(xp_file)
178
- else:
179
- the__file__ = str(xp_file.absolute())
209
+ # --- Finds the "run" function
180
210
 
181
- code = compile(source, filename=the__file__, mode="exec")
182
- _locals: Dict[str, Any] = {}
183
-
184
- sys.path.append(str(xp_file.parent.absolute()))
185
211
  try:
186
- exec(code, _locals, _locals)
212
+ for path in pythonpath:
213
+ sys.path.append(str(path))
214
+
215
+ if xp_file:
216
+ if not xp_file.exists() and xp_file.suffix != ".py":
217
+ xp_file = xp_file.with_suffix(".py")
218
+ xp_file: Path = Path(yaml_file).parent / xp_file
219
+ with open(xp_file) as src:
220
+ module_name = xp_file.with_suffix("").name
221
+ mod = imp.load_module(
222
+ module_name,
223
+ src,
224
+ str(xp_file.absolute()),
225
+ (".py", "r", imp.PY_SOURCE),
226
+ )
227
+ else:
228
+ # Module
229
+ mod = importlib.import_module(module_name)
230
+
231
+ helper = getattr(mod, "run", None)
187
232
  finally:
188
- sys.path.pop()
233
+ pass
189
234
 
190
235
  # --- ... and runs it
191
- helper = _locals.get("run", None)
192
236
  if helper is None:
193
- raise ValueError(f"Could not find run function in {the__file__}")
237
+ raise ValueError(f"Could not find run function in {xp_file}")
194
238
 
195
239
  if not isinstance(helper, ExperimentHelper):
196
240
  helper = ExperimentHelper(helper)
@@ -205,9 +249,6 @@ def experiments_cli( # noqa: C901
205
249
  schema = list_parameters[1].annotation
206
250
  omegaconf_schema = OmegaConf.structured(schema())
207
251
 
208
- configuration = OmegaConf.merge(*yamls)
209
- if extra_conf:
210
- configuration.merge_with(OmegaConf.from_dotlist(extra_conf))
211
252
  if omegaconf_schema is not None:
212
253
  try:
213
254
  configuration = OmegaConf.merge(omegaconf_schema, configuration)
@@ -224,26 +265,19 @@ def experiments_cli( # noqa: C901
224
265
  configuration, structured_config_mode=SCMode.INSTANTIATE
225
266
  )
226
267
 
227
- # Get the working directory
228
- settings = get_settings()
229
- ws_env = {}
230
- workdir = Path(workdir) if workdir else None
231
- if (workdir is None) or (not workdir.is_dir()):
232
- logging.info("Searching for workspace %s", workdir)
233
- ws_settings = get_workspace(str(workdir))
234
- workdir = ws_settings.path.expanduser()
235
- ws_env = ws_settings.env
268
+ # Define the workspace
269
+ ws_env = find_workspace(workdir=workdir, workspace=workspace)
270
+ workdir = ws_env.path
236
271
 
237
272
  logging.info("Using working directory %s", str(workdir.resolve()))
238
273
 
239
274
  # --- Runs the experiment
240
275
  with experiment(
241
- workdir, configuration.id, host=host, port=port, run_mode=run_mode
276
+ ws_env, configuration.id, host=host, port=port, run_mode=run_mode
242
277
  ) as xp:
243
278
  # Set up the environment
244
279
  # (1) global settings (2) workspace settings and (3) command line settings
245
- for key, value in itertools.chain(settings.env.items(), ws_env.items(), env):
246
- logging.info("Setting environment: %s=%s", key, value)
280
+ for key, value in env:
247
281
  xp.setenv(key, value)
248
282
 
249
283
  try:
@@ -1,5 +1,5 @@
1
1
  from omegaconf import MISSING
2
- from typing import Optional
2
+ from typing import Optional, List
3
3
  import attr
4
4
 
5
5
  try:
@@ -31,6 +31,12 @@ class ConfigurationBase:
31
31
  file: str = "experiment"
32
32
  """Relative path of the file containing a run function"""
33
33
 
34
+ module: Optional[str] = None
35
+ """Relative path of the file containing a run function"""
36
+
37
+ pythonpath: Optional[List[str]] = None
38
+ """Python path relative to the parent directory of the YAML file"""
39
+
34
40
  parent: Optional[str] = None
35
41
  """Relative path of a YAML file that should be merged"""
36
42
 
@@ -10,5 +10,5 @@ from .specs import (
10
10
  HostRequirement,
11
11
  MatchRequirement,
12
12
  )
13
- from .registry import find_launcher, LauncherRegistry, YAMLDataClass
13
+ from .registry import find_launcher, LauncherRegistry
14
14
  from .parser import parse
@@ -1,33 +1,17 @@
1
- from dataclasses import dataclass, field
2
- from typing import TYPE_CHECKING, List, Optional
1
+ from typing import TYPE_CHECKING
3
2
 
4
- from experimaestro.utils.yaml import YAMLDataClass
5
- from .specs import HostRequirement
6
3
 
7
4
  if TYPE_CHECKING:
8
- from experimaestro.launchers import Launcher
9
5
  from experimaestro.connectors import Connector
10
6
  from experimaestro.tokens import Token
11
7
  from .registry import LauncherRegistry
12
8
 
13
9
 
14
- class LauncherConfiguration:
15
- tags: List[str]
16
- weight: int
17
-
18
- """Generic class for a launcher configuration"""
19
-
20
- def get(
21
- self, registry: "LauncherRegistry", requirement: HostRequirement
22
- ) -> Optional["Launcher"]:
23
- raise NotImplementedError(f"For {self.__class__}")
24
-
25
-
26
10
  class ConnectorConfiguration:
27
11
  def create(self, registry: "LauncherRegistry") -> "Connector":
28
12
  raise NotImplementedError(f"For {self.__class__}")
29
13
 
30
14
 
31
- class TokenConfiguration(YAMLDataClass):
15
+ class TokenConfiguration:
32
16
  def create(self, registry: "LauncherRegistry", identifier: str) -> "Token":
33
17
  raise NotImplementedError(f"For {self.__class__}")
@@ -1,27 +1,15 @@
1
- # Launcher registry
1
+ # Configuration registers
2
+
3
+ from typing import ClassVar, Dict, Optional, Set, Type, Union
2
4
 
3
- from dataclasses import dataclass
4
- import itertools
5
- from types import new_class
6
- from typing import ClassVar, Dict, List, Optional, Set, Type, Union
7
- from experimaestro import Annotated
8
5
  from pathlib import Path
9
6
  import typing
7
+ from omegaconf import DictConfig, OmegaConf, SCMode
10
8
  import pkg_resources
11
- import humanfriendly
12
- import yaml
13
- from yaml import Loader, Dumper
14
9
  from experimaestro.utils import logger
15
- from experimaestro.utils.yaml import (
16
- Initialize,
17
- YAMLDataClass,
18
- YAMLException,
19
- YAMLList,
20
- add_path_resolvers,
21
- )
22
10
 
23
- from .base import LauncherConfiguration, ConnectorConfiguration, TokenConfiguration
24
- from .specs import CPUSpecification, CudaSpecification, HostRequirement
11
+ from .base import ConnectorConfiguration, TokenConfiguration
12
+ from .specs import HostRequirement
25
13
 
26
14
  if typing.TYPE_CHECKING:
27
15
  from experimaestro.launchers import Launcher
@@ -32,80 +20,20 @@ class LauncherNotFoundError(Exception):
32
20
  pass
33
21
 
34
22
 
35
- @dataclass
36
- class GPU(YAMLDataClass):
37
- """Represents a GPU"""
38
-
39
- model: str
40
- count: int
41
- memory: Annotated[int, Initialize(humanfriendly.parse_size)]
42
-
43
- def to_spec(self):
44
- return [CudaSpecification(self.memory, self.model) for _ in range(self.count)]
45
-
46
-
47
- class GPUList(YAMLList[GPU]):
48
- """Represents a list of GPUs"""
49
-
50
- def __repr__(self):
51
- return f"GPUs({super().__repr__()})"
52
-
53
- def to_spec(self) -> List[CudaSpecification]:
54
- return list(itertools.chain(*[gpu.to_spec() for gpu in self]))
55
-
56
-
57
- @dataclass
58
- class CPU(YAMLDataClass):
59
- """Represents a CPU"""
60
-
61
- memory: Annotated[int, Initialize(humanfriendly.parse_size)] = 0
62
- cores: int = 1
63
-
64
- def to_spec(self):
65
- return CPUSpecification(self.memory, self.cores)
66
-
67
-
68
- @dataclass
69
- class Host(YAMLDataClass):
70
- name: str
71
- gpus: List[GPU]
72
- launchers: List[str]
73
-
74
-
75
- Launchers = Dict[str, List[LauncherConfiguration]]
76
23
  Connectors = Dict[str, Dict[str, ConnectorConfiguration]]
77
24
  Tokens = Dict[str, Dict[str, TokenConfiguration]]
78
25
 
79
26
 
80
- def new_loader(name: str) -> Type[Loader]:
81
- return new_class("LauncherLoader", (yaml.FullLoader,)) # type: ignore
82
-
83
-
84
- def load_yaml(loader_cls: Type[Loader], path: Path):
27
+ def load_yaml(schema, path: Path):
85
28
  if not path.is_file():
86
- return None
29
+ return {}
87
30
 
88
- logger.warning(
89
- "Using YAML file to configure launchers is deprecated. Please remove %s using launchers.py",
90
- path,
91
- )
92
31
  logger.debug("Loading %s", path)
93
32
  with path.open("rt") as fp:
94
- loader = loader_cls(fp)
95
- try:
96
- return loader.get_single_data()
97
- finally:
98
- loader.dispose()
99
-
100
-
101
- def unknown_error(loader: Loader, node):
102
- raise YAMLException(
103
- "",
104
- node.start_mark.name,
105
- node.start_mark.line,
106
- node.start_mark.column,
107
- f"No handler defined for key {node}",
108
- )
33
+ cfg = OmegaConf.load(fp)
34
+ return OmegaConf.to_container(
35
+ OmegaConf.merge(cfg, schema), structured_config_mode=SCMode.INSTANTIATE
36
+ )
109
37
 
110
38
 
111
39
  class LauncherRegistry:
@@ -132,27 +60,14 @@ class LauncherRegistry:
132
60
  LauncherRegistry.CURRENT_CONFIG_DIR = config_dir
133
61
 
134
62
  def __init__(self, basepath: Path):
135
- self.LauncherLoader: Type[Loader] = new_loader("LauncherLoader")
136
- self.ConnectorLoader: Type[Loader] = new_loader("ConnectorLoader")
137
- self.TokenLoader: Type[Loader] = new_loader("TokenLoader")
138
- self.Dumper: Type[Dumper] = new_class("CustomDumper", (Dumper,), {})
63
+ self.connectors_schema = DictConfig({})
64
+ self.tokens_schema = DictConfig({})
139
65
  self.find_launcher_fn = None
140
66
 
141
- # Add safeguards
142
- add_path_resolvers(
143
- self.LauncherLoader,
144
- [],
145
- Dict[str, LauncherConfiguration],
146
- dumper=self.Dumper,
147
- )
148
-
149
67
  # Use entry points for connectors and launchers
150
68
  for entry_point in pkg_resources.iter_entry_points("experimaestro.connectors"):
151
69
  entry_point.load().init_registry(self)
152
70
 
153
- for entry_point in pkg_resources.iter_entry_points("experimaestro.launchers"):
154
- entry_point.load().init_registry(self)
155
-
156
71
  for entry_point in pkg_resources.iter_entry_points("experimaestro.tokens"):
157
72
  entry_point.load().init_registry(self)
158
73
 
@@ -172,32 +87,16 @@ class LauncherRegistry:
172
87
  logger.warn("No find_launcher() function was found in %s", launchers_py)
173
88
 
174
89
  # Read the configuration file
175
- launchers: Launchers = (
176
- load_yaml(self.LauncherLoader, basepath / "launchers.yaml") or {}
177
- )
178
- self.launchers = sorted(
179
- itertools.chain(*launchers.values()), key=lambda launcher: -launcher.weight
180
- )
181
-
182
- self.connectors: Connectors = (
183
- load_yaml(self.ConnectorLoader, basepath / "connectors.yaml") or {}
184
- )
185
- self.tokens: Tokens = (
186
- load_yaml(self.TokenLoader, basepath / "tokens.yaml") or {}
187
- )
188
-
189
- def register_launcher(self, identifier: str, cls: Type[YAMLDataClass]):
190
- add_path_resolvers(
191
- self.LauncherLoader, [identifier, None], cls, dumper=self.Dumper
90
+ self.connectors = load_yaml(
91
+ self.connectors_schema, basepath / "connectors.yaml"
192
92
  )
93
+ self.tokens = load_yaml(self.tokens_schema, basepath / "tokens.yaml")
193
94
 
194
- def register_connector(self, identifier: str, cls: Type[YAMLDataClass]):
195
- add_path_resolvers(
196
- self.ConnectorLoader, [identifier, None], cls, dumper=self.Dumper
197
- )
95
+ def register_connector(self, identifier: str, cls: Type):
96
+ self.connectors_schema.merge_with({identifier: cls})
198
97
 
199
- def register_token(self, identifier: str, cls: Type[YAMLDataClass]):
200
- add_path_resolvers(self.TokenLoader, [identifier], cls, dumper=self.Dumper)
98
+ def register_token(self, identifier: str, cls: Type):
99
+ self.tokens_schema.merge_with({identifier: cls})
201
100
 
202
101
  def getToken(self, identifier: str) -> "Token":
203
102
  for tokens in self.tokens.values():
@@ -227,7 +126,7 @@ class LauncherRegistry:
227
126
  tags: Restrict the launchers to those containing one of the specified tags
228
127
  """
229
128
 
230
- if len(self.launchers) == 0 and self.find_launcher_fn is None:
129
+ if self.find_launcher_fn is None:
231
130
  logger.info("No launchers.yaml file: using local host ")
232
131
  from experimaestro.launchers.direct import DirectLauncher
233
132
  from experimaestro.connectors.local import LocalConnector
@@ -250,12 +149,6 @@ class LauncherRegistry:
250
149
  if launcher := self.find_launcher_fn(spec, tags):
251
150
  return launcher
252
151
 
253
- # We have registered launchers
254
- for spec in specs:
255
- for handler in self.launchers:
256
- if (not tags) or any((tag in tags) for tag in handler.tags):
257
- if launcher := handler.get(self, spec):
258
- return launcher
259
152
  return None
260
153
 
261
154
 
@@ -1,15 +1,3 @@
1
- from dataclasses import dataclass, field
2
- from functools import cached_property
3
- from typing import Dict, List, Optional
4
- from experimaestro.launcherfinder import (
5
- LauncherConfiguration,
6
- LauncherRegistry,
7
- HostRequirement,
8
- )
9
- from experimaestro.launcherfinder.registry import CPU, GPUList, YAMLDataClass
10
- from experimaestro.launcherfinder.specs import (
11
- HostSpecification,
12
- )
13
1
  from experimaestro.scriptbuilder import PythonScriptBuilder
14
2
  from . import Launcher
15
3
 
@@ -18,40 +6,5 @@ class DirectLauncher(Launcher):
18
6
  def scriptbuilder(self):
19
7
  return PythonScriptBuilder()
20
8
 
21
- @staticmethod
22
- def init_registry(registry: LauncherRegistry):
23
- registry.register_launcher("local", DirectLauncherConfiguration)
24
-
25
9
  def __str__(self):
26
10
  return f"DirectLauncher({self.connector})"
27
-
28
-
29
- @dataclass
30
- class DirectLauncherConfiguration(YAMLDataClass, LauncherConfiguration):
31
- connector: str = "connector"
32
- cpu: CPU = field(default_factory=CPU)
33
- gpus: GPUList = field(default_factory=GPUList)
34
- tokens: Optional[Dict[str, int]] = None
35
- tags: List[str] = field(default_factory=lambda: [])
36
- weight: int = 0
37
- disable: bool = False
38
-
39
- @cached_property
40
- def spec(self) -> HostSpecification:
41
- return HostSpecification(cpu=self.cpu.to_spec(), cuda=self.gpus.to_spec())
42
-
43
- def get(
44
- self, registry: LauncherRegistry, requirement: "HostRequirement"
45
- ) -> Optional[Launcher]:
46
- if requirement.match(self.spec):
47
- launcher = DirectLauncher(connector=registry.getConnector(self.connector))
48
- if self.tokens:
49
- for token_identifier, count in self.tokens.items():
50
- token = registry.getToken(token_identifier)
51
- # TODO: handle the case where this is not a CounterToken
52
- launcher.addListener(
53
- lambda job: job.dependencies.add(token.dependency(count))
54
- )
55
- return launcher
56
-
57
- return None