experimaestro 1.4.3__py3-none-any.whl → 1.5.1__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.

@@ -1,7 +1,8 @@
1
1
  from dataclasses import dataclass
2
- from pathlib import Path, PurePosixPath
2
+ from pathlib import Path, _posix_flavour
3
3
  import io
4
4
  import os
5
+ import re
5
6
  from experimaestro.launcherfinder import LauncherRegistry, YAMLDataClass
6
7
  from fabric import Connection
7
8
  from invoke import Promise
@@ -21,7 +22,7 @@ from experimaestro.tokens import Token
21
22
  # Might be wise to switch to https://github.com/marian-code/ssh-utilities
22
23
 
23
24
 
24
- class SshPath(Path, PurePosixPath):
25
+ class SshPath(Path):
25
26
  """SSH path
26
27
 
27
28
  Absolute:
@@ -31,16 +32,31 @@ class SshPath(Path, PurePosixPath):
31
32
  ssh://[user@]host[:port]/relative/path
32
33
  """
33
34
 
35
+ _flavour = _posix_flavour
36
+
37
+ def __new__(cls, *args, **kwargs):
38
+ return object.__new__(cls)
39
+
40
+ def __init__(self, url: str):
41
+ parsed = urlparse(url)
42
+ assert parsed.scheme == "ssh"
43
+ self._host = parsed.hostname
44
+
45
+ self._parts = re.split(r"/+", parsed.path)
46
+ if parsed.path.startswith("//"):
47
+ self._parts[0] = "/"
48
+ else:
49
+ self._parts = self._parts[1:]
50
+
34
51
  @property
35
52
  def hostpath(self):
36
- # path = "/" if self._parts[:0] + "/".join(self._parts[1:])
37
53
  if self.is_absolute():
38
- return "/" + self._flavour.join(self._parts[1:])
39
- return self._flavour.join(self._parts)
54
+ return "/" + "/".join(self._parts[1:])
55
+ return "/".join(self._parts)
40
56
 
41
57
  @property
42
58
  def host(self):
43
- return self._drv
59
+ return self._host
44
60
 
45
61
  def is_absolute(self):
46
62
  return self._parts and self._parts[0] == "/"
@@ -66,11 +82,15 @@ class SshPath(Path, PurePosixPath):
66
82
 
67
83
  def _make_child(self, args):
68
84
  drv, root, parts = self._parse_args(args)
69
- assert self._drv == drv or drv == "", f"{self._drv} and {drv}"
85
+ assert self._host == drv or drv == "", f"{self._host} and {drv}"
70
86
  drv, root, parts = self._flavour.join_parsed_parts(
71
- "", self._root, self._parts, "", root, parts
87
+ "", "", self._parts, "", root, parts
72
88
  )
73
- return self._from_parsed_parts(self._drv, root, parts)
89
+
90
+ child = object.__new__(SshPath)
91
+ child._parts = parts
92
+ child._host = self._host
93
+ return child
74
94
 
75
95
  def open(self, mode="r", buffering=-1, encoding=None, errors=None, newline=None):
76
96
  # FIXME: should probably be wiser
@@ -105,10 +125,10 @@ class SshPath(Path, PurePosixPath):
105
125
  return obj
106
126
 
107
127
  def __repr__(self):
108
- return "SshPath(%s,%s)" % (self._drv, self._flavour.join(self._parts[1:]))
128
+ return "SshPath(%s,%s)" % (self._host, self._flavour.join(self._parts[1:]))
109
129
 
110
130
  def __str__(self):
111
- return "ssh://%s/%s" % (self._drv, self._flavour.join(self._parts[1:]))
131
+ return "ssh://%s/%s" % (self._host, self._flavour.join(self._parts[1:]))
112
132
 
113
133
 
114
134
  @dataclass
@@ -646,7 +646,7 @@ class ConfigInformation:
646
646
  "Cannot set non existing attribute %s in %s" % (k, self.xpmtype)
647
647
  )
648
648
  except Exception:
649
- logger.exception("Error while setting value %s" % k)
649
+ logger.error("Error while setting value %s in %s", k, self.xpmtype)
650
650
  raise
651
651
 
652
652
  def addtag(self, name, value):
@@ -1093,7 +1093,7 @@ class ConfigInformation:
1093
1093
  state_dict = {
1094
1094
  "id": id(self.pyobject),
1095
1095
  "module": self.xpmtype._module,
1096
- "type": self.xpmtype.objecttype.__qualname__,
1096
+ "type": self.xpmtype.basetype.__qualname__,
1097
1097
  "typename": self.xpmtype.name(),
1098
1098
  "identifier": self.identifier.state_dict(),
1099
1099
  }
@@ -1340,7 +1340,10 @@ class ConfigInformation:
1340
1340
  cls = getqualattr(mod, definition["type"])
1341
1341
 
1342
1342
  # Creates an object (or a config)
1343
- o = cls.__new__(cls, __xpmobject__=as_instance)
1343
+ if as_instance:
1344
+ o = cls.XPMValue.__new__(cls.XPMValue)
1345
+ else:
1346
+ o = cls.XPMConfig.__new__(cls.XPMConfig)
1344
1347
  assert definition["id"] not in objects, "Duplicate id %s" % definition["id"]
1345
1348
  objects[definition["id"]] = o
1346
1349
 
@@ -1377,6 +1380,7 @@ class ConfigInformation:
1377
1380
  o.__xpm_stdout__ = basepath / f"{name.lower()}.out"
1378
1381
  o.__xpm_stderr__ = basepath / f"{name.lower()}.err"
1379
1382
  else:
1383
+ o.__init__()
1380
1384
  xpminfo = o.__xpm__ # type: ConfigInformation
1381
1385
 
1382
1386
  meta = definition.get("meta", None)
@@ -1497,12 +1501,7 @@ class ConfigInformation:
1497
1501
 
1498
1502
  if o is None:
1499
1503
  # Creates an object (and not a config)
1500
- o = config.__xpmtype__.objecttype.__new__(
1501
- config.__xpmtype__.objecttype, __xpmobject__=True
1502
- )
1503
-
1504
- # And calls the parameter-less initialization
1505
- o.__init__()
1504
+ o = config.XPMValue()
1506
1505
 
1507
1506
  # Store in cache
1508
1507
  self.objects.add_stub(id(config), o)
@@ -1650,8 +1649,8 @@ class TypeConfig:
1650
1649
  [f"{key}={value}" for key, value in self.__xpm__.values.items()]
1651
1650
  )
1652
1651
  return (
1653
- f"{self.__xpmtype__.objecttype.__module__}."
1654
- f"{self.__xpmtype__.objecttype.__qualname__}({params})"
1652
+ f"{self.__xpmtype__.basetype.__module__}."
1653
+ f"{self.__xpmtype__.basetype.__qualname__}({params})"
1655
1654
  )
1656
1655
 
1657
1656
  def tag(self, name, value):
@@ -1770,6 +1769,11 @@ class TypeConfig:
1770
1769
  self.__xpm__.add_dependencies(*other.__xpm__.dependencies)
1771
1770
 
1772
1771
 
1772
+ class classproperty(property):
1773
+ def __get__(self, owner_self, owner_cls):
1774
+ return self.fget(owner_cls)
1775
+
1776
+
1773
1777
  class Config:
1774
1778
  """Base type for all objects in python interface"""
1775
1779
 
@@ -1781,6 +1785,42 @@ class Config:
1781
1785
  """The __xpm__ object contains all instance specific information about a
1782
1786
  configuration/task"""
1783
1787
 
1788
+ @classproperty
1789
+ def XPMConfig(cls):
1790
+ if issubclass(cls, TypeConfig):
1791
+ return cls
1792
+ return cls.__getxpmtype__().configtype
1793
+
1794
+ @classproperty
1795
+ def C(cls):
1796
+ return cls.XPMConfig
1797
+
1798
+ @classproperty
1799
+ def XPMValue(cls):
1800
+ """Returns the value object for this configuration"""
1801
+ if issubclass(cls, TypeConfig):
1802
+ return cls.__xpmtype__.objecttype
1803
+
1804
+ if value_cls := cls.__dict__.get("__XPMValue__", None):
1805
+ pass
1806
+ else:
1807
+ from .types import XPMValue
1808
+
1809
+ __objectbases__ = tuple(
1810
+ s.XPMValue
1811
+ for s in cls.__bases__
1812
+ if issubclass(s, Config) and (s is not Config)
1813
+ ) or (XPMValue,)
1814
+
1815
+ *tp_qual, tp_name = cls.__qualname__.split(".")
1816
+ value_cls = type(f"{tp_name}.XPMValue", (cls,) + __objectbases__, {})
1817
+ value_cls.__qualname__ = ".".join(tp_qual + [value_cls.__name__])
1818
+ value_cls.__module__ = cls.__module__
1819
+
1820
+ setattr(cls, "__XPMValue__", value_cls)
1821
+
1822
+ return value_cls
1823
+
1784
1824
  @classmethod
1785
1825
  def __getxpmtype__(cls) -> "ObjectType":
1786
1826
  """Get (and create if necessary) the Object type of this"""
@@ -1796,27 +1836,32 @@ class Config:
1796
1836
  raise
1797
1837
  return xpmtype
1798
1838
 
1799
- def __getnewargs_ex__(self):
1800
- # __new__ will be called with those arguments when unserializing
1801
- return ((), {"__xpmobject__": True})
1839
+ def __new__(cls: Type[T], *args, **kwargs) -> T:
1840
+ """Returns an instance of a TypeConfig (for compatibility, use XPMConfig
1841
+ or C if possible)"""
1802
1842
 
1803
- @classmethod
1804
- def c(cls: Type[T], **kwargs) -> T:
1805
- """Allows typing to process easily"""
1806
- return cls.__new__(cls, **kwargs)
1843
+ # If this is an XPMValue, just return a new instance
1844
+ from experimaestro.core.types import XPMValue
1807
1845
 
1808
- def __new__(cls: Type[T], *args, __xpmobject__=False, **kwargs) -> T:
1809
- """Returns an instance of a TypeConfig when called __xpmobject__ is False,
1810
- and otherwise the real object
1811
- """
1846
+ if issubclass(cls, XPMValue):
1847
+ return object.__new__(cls)
1812
1848
 
1813
- if __xpmobject__:
1814
- # __init__ is called directly
1849
+ # If this is the XPMConfig, just return a new instance
1850
+ # __init__ will be called
1851
+ if issubclass(cls, TypeConfig):
1815
1852
  return object.__new__(cls)
1816
1853
 
1817
- # We use the configuration type
1818
- o = object.__new__(cls.__getxpmtype__().configtype)
1819
- o.__init__(*args, **kwargs)
1854
+ # otherwise, we use the configuration type
1855
+ o: TypeConfig = object.__new__(cls.__getxpmtype__().configtype)
1856
+ try:
1857
+ o.__init__(*args, **kwargs)
1858
+ except Exception:
1859
+ caller = inspect.getframeinfo(inspect.stack()[1][0])
1860
+ logger.error(
1861
+ "Init error in %s:%s"
1862
+ % (str(Path(caller.filename).absolute()), caller.lineno)
1863
+ )
1864
+ raise
1820
1865
  return o
1821
1866
 
1822
1867
  def __validate__(self):
@@ -27,13 +27,16 @@ from typing import (
27
27
  Callable,
28
28
  ClassVar,
29
29
  Dict,
30
+ Generic,
30
31
  List,
31
32
  Optional,
32
33
  Set,
34
+ Type,
33
35
  TypeVar,
34
36
  Union,
35
37
  overload,
36
38
  )
39
+ from typing_extensions import Self
37
40
 
38
41
  TConfig = TypeVar("TConfig", bound="Config")
39
42
 
@@ -205,12 +208,15 @@ class TypeConfig:
205
208
  class Config:
206
209
  __xpmtype__: ClassVar[ObjectType]
207
210
  __xpm__: ConfigInformation
211
+ __use_xpmobject__: ClassVar[bool]
212
+
213
+ XPMValue: Type[Self]
214
+ XPMConfig: Union[Type[Self], Type[TypeConfig[Self]]]
215
+ C: Union[Type[Self], Type[TypeConfig[Self]]]
216
+
208
217
  @classmethod
209
218
  def __getxpmtype__(cls) -> ObjectType: ...
210
- def __getnewargs_ex__(self): ...
211
- @classmethod
212
- def c(cls, **kwargs) -> T: ...
213
- def __new__(cls, *args, __xpmobject__: bool = ..., **kwargs) -> T: ...
219
+ def __new__(cls, *args, **kwargs) -> Self: ...
214
220
  def __validate__(self) -> None: ...
215
221
  def __post_init__(self) -> None: ...
216
222
  def __json__(self): ...
@@ -240,6 +246,6 @@ class Task(LightweightTask):
240
246
  def copyconfig(config_or_output: TConfig, **kwargs) -> TConfig: ...
241
247
  def setmeta(config: TConfig, flag: bool) -> TConfig: ...
242
248
 
243
- class TypeConfig:
249
+ class TypeConfig(Generic[T]):
244
250
  def __validate__(self):
245
251
  pass
@@ -1,7 +1,7 @@
1
1
  from abc import ABC, abstractmethod
2
2
  import inspect
3
3
  import sys
4
- from typing import Set, Union, Dict, Iterator, List
4
+ from typing import Set, Union, Dict, Iterator, List, get_args, get_origin
5
5
  from collections import ChainMap
6
6
  from pathlib import Path
7
7
  import typing
@@ -126,6 +126,10 @@ class Type:
126
126
  if t:
127
127
  return DictType(Type.fromType(t[0]), Type.fromType(t[1]))
128
128
 
129
+ # Takes care of generics
130
+ if get_origin(key):
131
+ return GenericType(key)
132
+
129
133
  raise Exception("No type found for %s", key)
130
134
 
131
135
 
@@ -179,6 +183,12 @@ class SubmitHook(ABC):
179
183
  return hash((self.__class__, self.spec()))
180
184
 
181
185
 
186
+ class XPMValue:
187
+ """Jut marks a XPMValue"""
188
+
189
+ pass
190
+
191
+
182
192
  class ObjectType(Type):
183
193
  submit_hooks: Set[SubmitHook]
184
194
  """Hooks associated with this configuration"""
@@ -211,6 +221,8 @@ class ObjectType(Type):
211
221
  # Get the identifier
212
222
  if identifier is None and hasattr(tp, "__xpmid__"):
213
223
  __xpmid__ = getattr(tp, "__xpmid__")
224
+ if isinstance(__xpmid__, Identifier):
225
+ identifier = __xpmid__
214
226
  if inspect.ismethod(__xpmid__):
215
227
  identifier = Identifier(__xpmid__())
216
228
  elif "__xpmid__" in tp.__dict__:
@@ -247,7 +259,7 @@ class ObjectType(Type):
247
259
  else:
248
260
  self.basetype = tp
249
261
 
250
- # Create the type-specific configuration class
262
+ # --- Create the type-specific configuration class (XPMConfig)
251
263
  __configbases__ = tuple(
252
264
  s.__getxpmtype__().configtype
253
265
  for s in tp.__bases__
@@ -256,23 +268,18 @@ class ObjectType(Type):
256
268
 
257
269
  *tp_qual, tp_name = self.basetype.__qualname__.split(".")
258
270
  self.configtype = type(
259
- f"{tp_name}_XPMConfig", __configbases__ + (self.basetype,), {}
271
+ f"{tp_name}.XPMConfig", __configbases__ + (self.basetype,), {}
260
272
  )
261
273
  self.configtype.__qualname__ = ".".join(tp_qual + [self.configtype.__name__])
262
274
  self.configtype.__module__ = tp.__module__
263
275
 
264
- # Create the type-specific object class
265
- # (now, the same as basetype - but in the future, remove references)
266
- self.objecttype = self.basetype # type: type
267
- self.basetype._ = self.configtype
268
-
269
276
  # Return type is used by tasks to change the output
270
277
  if hasattr(self.basetype, "task_outputs") or False:
271
278
  self.returntype = get_type_hints(
272
279
  getattr(self.basetype, "task_outputs")
273
280
  ).get("return", typing.Any)
274
281
  else:
275
- self.returntype = self.objecttype
282
+ self.returntype = self.basetype
276
283
 
277
284
  # Registers ourselves
278
285
  self.basetype.__xpmtype__ = self
@@ -284,6 +291,11 @@ class ObjectType(Type):
284
291
  self.annotations = []
285
292
  self._deprecated = False
286
293
 
294
+ @property
295
+ def objecttype(self):
296
+ """Returns the object type"""
297
+ return self.basetype.XPMValue
298
+
287
299
  def addAnnotation(self, annotation):
288
300
  assert not self.__initialized__
289
301
  self.annotations.append(annotation)
@@ -636,3 +648,31 @@ class DictType(Type):
636
648
 
637
649
  def __repr__(self):
638
650
  return str(self)
651
+
652
+
653
+ class GenericType(Type):
654
+ def __init__(self, type: typing.Type):
655
+ self.type = type
656
+ self.origin = get_origin(type)
657
+
658
+ self.args = get_args(type)
659
+
660
+ def name(self):
661
+ return str(self.type)
662
+
663
+ def __repr__(self):
664
+ return repr(self.type)
665
+
666
+ def validate(self, value):
667
+ # Now, let's check generics...
668
+ mros = typingutils.generic_mro(type(value))
669
+ matching = next(
670
+ (mro for mro in mros if (get_origin(mro) or mro) is self.origin), None
671
+ )
672
+ target = get_origin(self.type) or self.type
673
+ if matching is None:
674
+ raise ValueError(
675
+ f"{type(value)} is not of type {target} ({type(value).__mro__})"
676
+ )
677
+
678
+ return value
@@ -0,0 +1,2 @@
1
+ class HandledException(Exception):
2
+ pass
@@ -1,4 +1,5 @@
1
1
  import inspect
2
+ import itertools
2
3
  import json
3
4
  import logging
4
5
  import sys
@@ -10,7 +11,8 @@ import omegaconf
10
11
  import yaml
11
12
  from experimaestro import LauncherRegistry, RunMode, experiment
12
13
  from experimaestro.experiments.configuration import ConfigurationBase
13
- from experimaestro.settings import get_workspace
14
+ from experimaestro.exceptions import HandledException
15
+ from experimaestro.settings import get_settings, get_workspace
14
16
  from omegaconf import OmegaConf, SCMode
15
17
  from termcolor import cprint
16
18
 
@@ -63,7 +65,7 @@ def load(yaml_file: Path):
63
65
  if parent := _data.get("parent", None):
64
66
  data.extend(load(yaml_file.parent / parent))
65
67
 
66
- return data
68
+ return data[::-1]
67
69
 
68
70
 
69
71
  @click.option("--debug", is_flag=True, help="Print debug information")
@@ -110,10 +112,16 @@ def load(yaml_file: Path):
110
112
  help="Working directory - if None, uses the default XPM " "working directory",
111
113
  )
112
114
  @click.option("--conf", "-c", "extra_conf", type=str, multiple=True)
115
+ @click.option(
116
+ "--pre-yaml", type=str, multiple=True, help="Add YAML file after the main one"
117
+ )
118
+ @click.option(
119
+ "--post-yaml", type=str, multiple=True, help="Add YAML file before the main one"
120
+ )
113
121
  @click.argument("args", nargs=-1, type=click.UNPROCESSED)
114
122
  @click.argument("yaml_file", metavar="YAML file", type=str)
115
123
  @click.command()
116
- def experiments_cli(
124
+ def experiments_cli( # noqa: C901
117
125
  yaml_file: str,
118
126
  xp_file: str,
119
127
  host: str,
@@ -123,6 +131,8 @@ def experiments_cli(
123
131
  env: List[Tuple[str, str]],
124
132
  run_mode: RunMode,
125
133
  extra_conf: List[str],
134
+ pre_yaml: List[str],
135
+ post_yaml: List[str],
126
136
  args: List[str],
127
137
  show: bool,
128
138
  debug: bool,
@@ -133,13 +143,17 @@ def experiments_cli(
133
143
  logging.getLogger("xpm.hash").setLevel(logging.INFO)
134
144
 
135
145
  # --- Loads the YAML
136
- yamls = load(Path(yaml_file))
146
+ yamls = []
147
+ for y in pre_yaml:
148
+ yamls.extend(load(Path(y)))
149
+ yamls.extend(load(Path(yaml_file)))
150
+ for y in post_yaml:
151
+ yamls.extend(load(Path(y)))
137
152
 
138
153
  # --- Get the XP file
139
154
  if xp_file is None:
140
- for data in yamls:
141
- if xp_file := data.get("file"):
142
- del data["file"]
155
+ for data in yamls[::-1]:
156
+ if xp_file := data.get("file", None):
143
157
  break
144
158
 
145
159
  if xp_file is None:
@@ -201,31 +215,44 @@ def experiments_cli(
201
215
  cprint(f"Error in configuration:\n\n{e}", "red", file=sys.stderr)
202
216
  sys.exit(1)
203
217
 
204
- # Move to an object container
205
- configuration: schema = OmegaConf.to_container(
206
- configuration, structured_config_mode=SCMode.INSTANTIATE
207
- )
208
-
209
218
  if show:
210
219
  print(json.dumps(OmegaConf.to_container(configuration))) # noqa: T201
211
220
  sys.exit(0)
212
221
 
222
+ # Move to an object container
223
+ configuration = OmegaConf.to_container(
224
+ configuration, structured_config_mode=SCMode.INSTANTIATE
225
+ )
226
+
213
227
  # Get the working directory
214
- if workdir is None or not Path(workdir).is_dir():
215
- workdir = get_workspace(workdir).path.expanduser().resolve()
216
- logging.info("Using working directory %s", workdir)
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
236
+
237
+ logging.info("Using working directory %s", str(workdir.resolve()))
217
238
 
218
239
  # --- Runs the experiment
219
240
  with experiment(
220
241
  workdir, configuration.id, host=host, port=port, run_mode=run_mode
221
242
  ) as xp:
222
243
  # Set up the environment
223
- for key, value in env:
244
+ # (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)
224
247
  xp.setenv(key, value)
225
248
 
226
- # Run the experiment
227
- helper.xp = xp
228
- helper.run(list(args), configuration)
249
+ try:
250
+ # Run the experiment
251
+ helper.xp = xp
252
+ helper.run(list(args), configuration)
253
+
254
+ # ... and wait
255
+ xp.wait()
229
256
 
230
- # ... and wait
231
- xp.wait()
257
+ except HandledException:
258
+ sys.exit(1)
@@ -20,14 +20,28 @@ def configuration(*args, **kwargs):
20
20
 
21
21
  @configuration()
22
22
  class ConfigurationBase:
23
+ """Base configuration for any experiment"""
24
+
23
25
  id: str = MISSING
24
- """ID of the experiment"""
26
+ """ID of the experiment
25
27
 
26
- description: str = ""
27
- """Description of the experiment"""
28
+ This ID is used by experimaestro when running as the experiment.
29
+ """
28
30
 
29
31
  file: str = "experiment"
30
- """qualified name (relative to the module) for the file containing a run function"""
32
+ """Relative path of the file containing a run function"""
31
33
 
32
34
  parent: Optional[str] = None
33
35
  """Relative path of a YAML file that should be merged"""
36
+
37
+ title: str = ""
38
+ """Short description of the experiment"""
39
+
40
+ subtitle: str = ""
41
+ """Allows to give some more details about the experiment"""
42
+
43
+ paper: str = ""
44
+ """Source paper for this experiment"""
45
+
46
+ description: str = ""
47
+ """Description of the experiment"""
@@ -54,8 +54,12 @@ def duration():
54
54
  return "duration", "=", RegExMatch(r"\d+"), RegExMatch(r"h(ours)?|d(ays)?")
55
55
 
56
56
 
57
+ def one_spec():
58
+ return OneOrMore(OrderedChoice([duration, cuda, cpu]), sep="&")
59
+
60
+
57
61
  def grammar():
58
- return OneOrMore(OrderedChoice([duration, cuda, cpu]), sep="&"), EndOfFile()
62
+ return OneOrMore(one_spec, sep="|"), EndOfFile()
59
63
 
60
64
 
61
65
  # ---- Visitor
@@ -63,6 +67,9 @@ def grammar():
63
67
 
64
68
  class Visitor(PTNodeVisitor):
65
69
  def visit_grammar(self, node, children):
70
+ return [child for child in children]
71
+
72
+ def visit_one_spec(self, node, children):
66
73
  return reduce(lambda x, el: x & el, children)
67
74
 
68
75
  def visit_duration(self, node, children):
@@ -85,6 +85,10 @@ def load_yaml(loader_cls: Type[Loader], path: Path):
85
85
  if not path.is_file():
86
86
  return None
87
87
 
88
+ logger.warning(
89
+ "Using YAML file to configure launchers is deprecated. Please remove %s using launchers.py",
90
+ path,
91
+ )
88
92
  logger.debug("Loading %s", path)
89
93
  with path.open("rt") as fp:
90
94
  loader = loader_cls(fp)
@@ -215,7 +219,7 @@ class LauncherRegistry:
215
219
  raise AssertionError(f"No connector with identifier {identifier}")
216
220
 
217
221
  def find(
218
- self, *specs: Union[HostRequirement, str], tags: Set[str] = set()
222
+ self, *input_specs: Union[HostRequirement, str], tags: Set[str] = set()
219
223
  ) -> Optional["Launcher"]:
220
224
  """ "
221
225
  Arguments:
@@ -233,7 +237,12 @@ class LauncherRegistry:
233
237
  # Parse specs
234
238
  from .parser import parse
235
239
 
236
- specs = [parse(spec) if isinstance(spec, str) else spec for spec in specs]
240
+ specs = []
241
+ for spec in input_specs:
242
+ if isinstance(spec, str):
243
+ specs.extend(parse(spec))
244
+ else:
245
+ specs.append(spec)
237
246
 
238
247
  # Use launcher function
239
248
  if self.find_launcher_fn is not None:
@@ -243,8 +252,6 @@ class LauncherRegistry:
243
252
 
244
253
  # We have registered launchers
245
254
  for spec in specs:
246
- if isinstance(spec, str):
247
- spec = parse(spec)
248
255
  for handler in self.launchers:
249
256
  if (not tags) or any((tag in tags) for tag in handler.tags):
250
257
  if launcher := handler.get(self, spec):
@@ -51,6 +51,13 @@ class CPUSpecification:
51
51
  def __lt__(self, other: "CPUSpecification"):
52
52
  return self.memory < other.memory and self.cores < other.cores
53
53
 
54
+ def total_memory(self, gpus: int = 0):
55
+ return max(
56
+ self.memory,
57
+ self.mem_per_cpu * self.cores,
58
+ self.cpu_per_gpu * self.mem_per_cpu * gpus,
59
+ )
60
+
54
61
 
55
62
  @define(kw_only=True)
56
63
  class HostSpecification:
@@ -92,6 +92,8 @@ class SlurmProcessWatcher(threading.Thread):
92
92
  self.jobs: Dict[str, SlurmJobState] = {}
93
93
 
94
94
  self.cv = ThreadingCondition()
95
+ self.fetched_event = threading.Event()
96
+ self.updating_jobs = threading.Lock()
95
97
  self.start()
96
98
 
97
99
  @staticmethod
@@ -109,10 +111,18 @@ class SlurmProcessWatcher(threading.Thread):
109
111
  with watcher.cv:
110
112
  watcher.cv.notify()
111
113
 
112
- def getjob(self, jobid):
114
+ def getjob(self, jobid, timeout=None):
113
115
  """Allows to share the calls to sacct"""
116
+
117
+ # Ensures that we have fetched at least once
118
+ self.fetched_event.wait()
119
+
120
+ # Waits that jobs are refreshed (with a timeout)
114
121
  with self.cv:
115
- self.cv.wait()
122
+ self.cv.wait(timeout=timeout)
123
+
124
+ # Ensures jobs are not updated right now
125
+ with self.updating_jobs:
116
126
  return self.jobs.get(jobid)
117
127
 
118
128
  def run(self):
@@ -129,9 +139,9 @@ class SlurmProcessWatcher(threading.Thread):
129
139
  builder.stdout = Redirect.pipe(handler)
130
140
  builder.environ = self.launcher.launcherenv
131
141
  logger.debug("Checking SLURM state with sacct")
132
- builder.start()
142
+ process = builder.start()
133
143
 
134
- with self.cv:
144
+ with self.updating_jobs:
135
145
  self.jobs = {}
136
146
  output = handler.output.decode("utf-8")
137
147
  for line in output.split("\n"):
@@ -143,7 +153,11 @@ class SlurmProcessWatcher(threading.Thread):
143
153
  logger.debug("Parsed line: %s", line)
144
154
  except ValueError:
145
155
  logger.error("Could not parse line %s", line)
156
+ process.kill()
157
+
158
+ with self.cv:
146
159
  logger.debug("Jobs %s", self.jobs)
160
+ self.fetched_event.set()
147
161
  self.cv.notify_all()
148
162
 
149
163
  self.cv.wait_for(
@@ -193,7 +207,18 @@ class BatchSlurmProcess(Process):
193
207
  def fromspec(cls, connector: Connector, spec: Dict[str, Any]):
194
208
  options = {k: v for k, v in spec.get("options", ())}
195
209
  launcher = SlurmLauncher(connector=connector, **options)
196
- return BatchSlurmProcess(launcher, spec["pid"])
210
+ process = BatchSlurmProcess(launcher, spec["pid"])
211
+
212
+ # Checks that the process is running
213
+ with SlurmProcessWatcher.get(launcher) as watcher:
214
+ logger.info("Checking SLURM job %s", process.jobid)
215
+ jobinfo = watcher.getjob(process.jobid, timeout=0.1)
216
+ if jobinfo and jobinfo.state.running:
217
+ logger.debug(
218
+ "SLURM job is running (%s), returning process", process.jobid
219
+ )
220
+ return process
221
+ return None
197
222
 
198
223
 
199
224
  def addstream(command: List[str], option: str, redirect: Redirect):
@@ -9,6 +9,7 @@ from typing import Any, List, Optional, Set, TypeVar, Union, TYPE_CHECKING
9
9
  import enum
10
10
  import signal
11
11
  import asyncio
12
+ from experimaestro.exceptions import HandledException
12
13
  from experimaestro.notifications import LevelInformation, Reporter
13
14
  from typing import Dict
14
15
  from experimaestro.scheduler.services import Service
@@ -29,7 +30,7 @@ if TYPE_CHECKING:
29
30
  from experimaestro.launchers import Launcher
30
31
 
31
32
 
32
- class FailedExperiment(RuntimeError):
33
+ class FailedExperiment(HandledException):
33
34
  """Raised when an experiment failed"""
34
35
 
35
36
  pass
@@ -70,6 +71,17 @@ class JobState(enum.Enum):
70
71
  return self.value >= JobState.DONE.value
71
72
 
72
73
 
74
+ class JobFailureStatus(enum.Enum):
75
+ #: Job failed
76
+ DEPENDENCY = 0
77
+
78
+ #: Job dependency failed
79
+ FAILED = 1
80
+
81
+ #: Memory
82
+ MEMORY = 2
83
+
84
+
73
85
  class JobLock(Lock):
74
86
  def __init__(self, job):
75
87
  super().__init__()
@@ -130,6 +142,9 @@ class Job(Resource):
130
142
  self.config = config
131
143
  self.state: JobState = JobState.UNSCHEDULED
132
144
 
145
+ #: If a job has failed, indicates the failure status
146
+ self.failure_status: JobFailureStatus = None
147
+
133
148
  # Dependencies
134
149
  self.dependencies: Set[Dependency] = set() # as target
135
150
 
@@ -294,6 +309,7 @@ class Job(Resource):
294
309
  # Job completed
295
310
  if not self.state.finished():
296
311
  self.state = JobState.ERROR
312
+ self.failure_status = JobFailureStatus.DEPENDENCY
297
313
  self._readyEvent.set()
298
314
 
299
315
  if self.unsatisfied == 0:
@@ -846,13 +862,16 @@ class experiment:
846
862
 
847
863
  if self.failedJobs:
848
864
  # Show some more information
865
+ count = 0
849
866
  for job in self.failedJobs.values():
850
- logger.error(
851
- "Job %s failed, check the log file %s",
852
- job.relpath,
853
- job.stderr,
854
- )
855
- raise FailedExperiment(f"{len(self.failedJobs)} failed jobs")
867
+ if job.failure_status != JobFailureStatus.DEPENDENCY:
868
+ count += 1
869
+ logger.error(
870
+ "Job %s failed, check the log file %s",
871
+ job.relpath,
872
+ job.stderr,
873
+ )
874
+ raise FailedExperiment(f"{count} failed jobs")
856
875
 
857
876
  future = asyncio.run_coroutine_threadsafe(awaitcompletion(), self.loop)
858
877
  return future.result()
@@ -922,7 +941,7 @@ class experiment:
922
941
  if exc_type:
923
942
  # import faulthandler
924
943
  # faulthandler.dump_traceback()
925
- logger.exception(
944
+ logger.error(
926
945
  "Not waiting since an exception was thrown"
927
946
  " (some jobs may be running)"
928
947
  )
@@ -91,24 +91,28 @@ class PythonScriptBuilder:
91
91
  logger.debug("Writing script %s", scriptpath)
92
92
  with scriptpath.open("wt") as out:
93
93
  out.write("#!{}\n".format(self.pythonpath))
94
- out.write("# Experimaestro generated task\n")
94
+ out.write("# Experimaestro generated task\n\n")
95
+ out.write("""import logging\nlogging.basicConfig(level=logging.INFO)\n\n""")
96
+
97
+ out.write("\nif __name__ == '__main__':\n\n" "")
95
98
 
96
99
  # --- Checks locks right away
97
100
 
98
- out.write("""import logging\nlogging.basicConfig(level=logging.INFO)\n\n""")
99
- out.write("""from experimaestro.run import TaskRunner\nimport os\n\n""")
101
+ out.write(
102
+ """ from experimaestro.run import TaskRunner\n import os\n\n"""
103
+ )
100
104
 
101
- out.write("lockfiles = [\n")
105
+ out.write(" lockfiles = [\n")
102
106
  for path in self.lockfiles:
103
- out.write(f" '''{relpath(path)}''',\n")
104
- out.write("]\n")
107
+ out.write(f" '''{relpath(path)}''',\n")
108
+ out.write(" ]\n")
105
109
 
106
110
  for name, value in job.environ.items():
107
- out.write(f"""os.environ["{name}"] = "{shquote(value)}"\n""")
111
+ out.write(f""" os.environ["{name}"] = "{shquote(value)}"\n""")
108
112
  out.write("\n")
109
113
 
110
114
  out.write(
111
- f"""TaskRunner("{shquote(connector.resolve(scriptpath))}","""
115
+ f""" TaskRunner("{shquote(connector.resolve(scriptpath))}","""
112
116
  """ lockfiles).run()\n"""
113
117
  )
114
118
 
experimaestro/settings.py CHANGED
@@ -1,9 +1,9 @@
1
1
  import os
2
2
  from omegaconf import OmegaConf
3
- from dataclasses import dataclass, field
3
+ from dataclasses import field, dataclass
4
4
  from functools import lru_cache
5
5
  from pathlib import Path
6
- from typing import Optional, List
6
+ from typing import Dict, Optional, List
7
7
 
8
8
 
9
9
  @dataclass
@@ -29,12 +29,18 @@ class WorkspaceSettings:
29
29
  path: Path
30
30
  """The workspace path"""
31
31
 
32
+ env: Dict[str, str] = field(default_factory=dict)
33
+ """Workspace specific environment variables"""
34
+
32
35
 
33
36
  @dataclass
34
37
  class Settings:
35
38
  server: ServerSettings = field(default_factory=ServerSettings)
36
39
  workspaces: List[WorkspaceSettings] = field(default_factory=list)
37
40
 
41
+ env: Dict[str, str] = field(default_factory=dict)
42
+ """Default environment variables"""
43
+
38
44
 
39
45
  @lru_cache()
40
46
  def get_settings(path: Optional[Path] = None) -> Settings:
@@ -60,7 +60,7 @@ def test_findlauncher_specs_gpu_mem():
60
60
 
61
61
 
62
62
  def test_findlauncher_parse():
63
- r = parse("""duration=4 d & cuda(mem=4G) * 2 & cpu(mem=400M, cores=4)""")
63
+ (r,) = parse("""duration=4 d & cuda(mem=4G) * 2 & cpu(mem=400M, cores=4)""")
64
64
  assert isinstance(r, HostSimpleRequirement)
65
65
 
66
66
  assert len(r.cuda_gpus) == 2
@@ -4,6 +4,7 @@ import pytest
4
4
  from experimaestro import Config, Task, Annotated, copyconfig, default
5
5
  from experimaestro.core.arguments import Param
6
6
  from experimaestro.core.objects import TypeConfig
7
+ from experimaestro.core.types import XPMValue
7
8
  from experimaestro.generators import pathgenerator
8
9
  from experimaestro.scheduler.workspace import RunMode
9
10
  from experimaestro.tests.utils import TemporaryExperiment
@@ -33,6 +34,10 @@ class C(B):
33
34
  pass
34
35
 
35
36
 
37
+ class D(B, A):
38
+ pass
39
+
40
+
36
41
  class DefaultAnnotationConfig(Config):
37
42
  a: Annotated[A, default(A(x=3))]
38
43
 
@@ -51,9 +56,10 @@ def test_object_config_default():
51
56
 
52
57
  def test_hierarchy():
53
58
  """Test if the object hierarchy is OK"""
54
- OA = A.__xpmtype__.objecttype
55
- OB = B.__xpmtype__.objecttype
56
- OC = C.__xpmtype__.objecttype
59
+ OA = A.__getxpmtype__().objecttype
60
+ OB = B.__getxpmtype__().objecttype
61
+ OC = C.__getxpmtype__().objecttype
62
+ OD = D.__getxpmtype__().objecttype
57
63
 
58
64
  assert issubclass(A, Config)
59
65
  assert issubclass(B, Config)
@@ -65,9 +71,10 @@ def test_hierarchy():
65
71
 
66
72
  assert issubclass(C, B)
67
73
 
68
- assert OA.__bases__ == (Config,)
69
- assert OB.__bases__ == (Config,)
70
- assert OC.__bases__ == (B,)
74
+ assert OA.__bases__ == (A, XPMValue)
75
+ assert OB.__bases__ == (B, XPMValue)
76
+ assert OC.__bases__ == (C, B.XPMValue)
77
+ assert OD.__bases__ == (D, B.XPMValue, A.XPMValue)
71
78
 
72
79
 
73
80
  class CopyConfig(Task):
@@ -232,8 +232,8 @@ def test_redefined_param():
232
232
  class B:
233
233
  x: Param[int] = 3
234
234
 
235
- atx = A._.__xpmtype__.getArgument("x")
236
- btx = B._.__xpmtype__.getArgument("x")
235
+ atx = A.C.__xpmtype__.getArgument("x")
236
+ btx = B.C.__xpmtype__.getArgument("x")
237
237
 
238
238
  assert atx.required
239
239
 
@@ -1,10 +1,21 @@
1
1
  import sys
2
2
  import typing
3
-
4
- if sys.version_info.major == 3 and sys.version_info.minor < 9:
5
- from typing_extensions import _AnnotatedAlias as AnnotatedAlias, get_args
6
- else:
7
- from typing import _AnnotatedAlias as AnnotatedAlias, get_args
3
+ from typing import Generic, Protocol
4
+
5
+ if sys.version_info.major == 3:
6
+ if sys.version_info.minor < 11:
7
+ from typing import _collect_type_vars as _collect_parameters
8
+ else:
9
+ from typing import _collect_parameters
10
+
11
+ if sys.version_info.minor < 9:
12
+ from typing_extensions import (
13
+ _AnnotatedAlias as AnnotatedAlias,
14
+ get_args,
15
+ get_origin,
16
+ )
17
+ else:
18
+ from typing import _AnnotatedAlias as AnnotatedAlias, get_args, get_origin
8
19
 
9
20
  GenericAlias = typing._GenericAlias
10
21
 
@@ -19,7 +30,9 @@ def get_optional(typehint):
19
30
  if isgenericalias(typehint) and typehint.__origin__ == typing.Union:
20
31
  if len(typehint.__args__) == 2:
21
32
  for ix in (0, 1):
22
- if typehint.__args__[ix] == type(None):
33
+ argtype = typehint.__args__[ix]
34
+ origin = get_origin(argtype) or argtype
35
+ if issubclass(origin, type(None)):
23
36
  return typehint.__args__[1 - ix]
24
37
  return None
25
38
 
@@ -63,3 +76,35 @@ def get_dict(typehint):
63
76
  assert len(typehint.__args__) == 2
64
77
  return typehint.__args__
65
78
  return None
79
+
80
+
81
+ # From https://github.com/python/typing/issues/777
82
+
83
+
84
+ def _generic_mro(result, tp):
85
+ origin = typing.get_origin(tp) or tp
86
+
87
+ result[origin] = tp
88
+ if hasattr(origin, "__orig_bases__"):
89
+ parameters = _collect_parameters(origin.__orig_bases__)
90
+ substitution = dict(zip(parameters, get_args(tp)))
91
+ for base in origin.__orig_bases__:
92
+ if get_origin(base) in result:
93
+ continue
94
+ base_parameters = getattr(base, "__parameters__", ())
95
+ if base_parameters:
96
+ base = base[tuple(substitution.get(p, p) for p in base_parameters)]
97
+ _generic_mro(result, base)
98
+
99
+
100
+ def generic_mro(tp):
101
+ origin = get_origin(tp)
102
+ if origin is None and not hasattr(tp, "__orig_bases__"):
103
+ if not isinstance(tp, type):
104
+ raise TypeError(f"{tp!r} is not a type or a generic alias")
105
+ return tp.__mro__
106
+ # sentinel value to avoid to subscript Generic and Protocol
107
+ result = {Generic: Generic, Protocol: Protocol}
108
+ _generic_mro(result, tp)
109
+ cls = origin if origin is not None else tp
110
+ return tuple(result.get(sub_cls, sub_cls) for sub_cls in cls.__mro__)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: experimaestro
3
- Version: 1.4.3
3
+ Version: 1.5.1
4
4
  Summary: "Experimaestro is a computer science experiment manager"
5
5
  Home-page: https://github.com/experimaestro/experimaestro-python
6
6
  License: GPL-3
@@ -23,7 +23,6 @@ Classifier: Programming Language :: Python :: 3.12
23
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
24
  Requires-Dist: arpeggio (>=2,<3)
25
25
  Requires-Dist: attrs (>=23.1.0,<24.0.0)
26
- Requires-Dist: cached-property (>=1.5.2,<2.0.0) ; python_version < "3.9"
27
26
  Requires-Dist: click (>=8)
28
27
  Requires-Dist: decorator (>=5,<6)
29
28
  Requires-Dist: docstring-parser (>=0.15,<0.16)
@@ -46,7 +45,7 @@ Requires-Dist: rpyc (>=5,<6)
46
45
  Requires-Dist: sortedcontainers (>=2.4,<3.0)
47
46
  Requires-Dist: termcolor (>=2.3)
48
47
  Requires-Dist: tqdm (>=4.66.1,<5.0.0)
49
- Requires-Dist: typing-extensions (>=4.2) ; python_version < "3.11"
48
+ Requires-Dist: typing-extensions (>=4.2) ; python_version < "3.12"
50
49
  Requires-Dist: watchdog (>=2,<3)
51
50
  Project-URL: Documentation, https://experimaestro-python.readthedocs.io/
52
51
  Project-URL: Repository, https://github.com/experimaestro/experimaestro-python
@@ -7,33 +7,34 @@ experimaestro/commandline.py,sha256=NS1ubme8DTJtDS2uWwdHLQiZsl6TSK1LkNxu39c3-cw,
7
7
  experimaestro/compat.py,sha256=dQqE2ZNHLM2wtdfp7fBRYMfC33qNehVf9J6FGRBUQhs,171
8
8
  experimaestro/connectors/__init__.py,sha256=hxcBSeVLk_7oyiIlS3l-9dGg1NGtShwvRD1tS7f8D2M,5461
9
9
  experimaestro/connectors/local.py,sha256=6tlaZb0tvNS2mjsapiVbfY7kIfLICJad137VXBMz-xo,5816
10
- experimaestro/connectors/ssh.py,sha256=A6qObY_phynZVQFdAsyymP9c0fiUHwn04nShROzMrec,7896
10
+ experimaestro/connectors/ssh.py,sha256=hmvU6bCq6ZsB1Zjz273mzb9pdZdTinUhUqZFJTZl8Fg,8290
11
11
  experimaestro/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  experimaestro/core/arguments.py,sha256=dW32opqNEsULYr6nR7Zk8PqHsSCbLPclfXofw27GTpI,5620
13
13
  experimaestro/core/context.py,sha256=Q8_ngiHRBZ0laavXRJNiDvdCprrnROVTWaHfrwMdlG4,2638
14
- experimaestro/core/objects.py,sha256=UeQJmJ4597T89mjxNauGbnnfeiPkG4ifmoZuKDoi2Ds,62627
15
- experimaestro/core/objects.pyi,sha256=D8-kd-NU-pRl9-_ktXUne_k1YH6TG3VGqRaKV8mrZwk,7003
14
+ experimaestro/core/objects.py,sha256=s9dt7U8LDT9XNVY7mowdt1DvuBH19EO4obips1ZBSHg,63989
15
+ experimaestro/core/objects.pyi,sha256=Adi2OKCW0-B9GROlEUgxxBDK6SHUqccTBHTofRRJE9M,7131
16
16
  experimaestro/core/serialization.py,sha256=9tg5ebLF3YeZ_zG9DiTHPLthppvo7io710ohD_dcLTo,3836
17
17
  experimaestro/core/serializers.py,sha256=R_CAMyjjfU1oi-eHU6VlEUixJpFayGqEPaYu7VsD9xA,1197
18
- experimaestro/core/types.py,sha256=jQGlyC1xnsZ5NKct3FELIcXcXQMvNQvauCEuumyfBz8,19253
18
+ experimaestro/core/types.py,sha256=oTXD4UjMVoYn_Usxn2C4h6IGhYDTtekKB3O3hfeOynQ,20176
19
19
  experimaestro/core/utils.py,sha256=JfC3qGUS9b6FUHc2VxIYUI9ysNpXSQ1LjOBkjfZ8n7o,495
20
+ experimaestro/exceptions.py,sha256=cUy83WHM3GeynxmMk6QRr5xsnpqUAdAoc-m3KQVrE2o,44
20
21
  experimaestro/experiments/__init__.py,sha256=GcpDUIbCvhnv6rxFdAp4wTffCVNTv-InY6fbQAlTy-o,159
21
- experimaestro/experiments/cli.py,sha256=ftAo8Km0WM_CuooBvFMZXhNgTtisGkLajEPDWlTF3cg,6543
22
- experimaestro/experiments/configuration.py,sha256=tGaI7VJdXlD4VcBIpuJxnL1uXjrDaz7M4yUXyxwZlP0,834
22
+ experimaestro/experiments/cli.py,sha256=MXJNRCvSuespStK8BSR82q-3NSPPXm6jq0Rsui4V2BU,7535
23
+ experimaestro/experiments/configuration.py,sha256=8GRqyLG1leF_NbvbFzqpm0yM24O0WjSNmQzvnuLnxxw,1150
23
24
  experimaestro/filter.py,sha256=DN1PrmS9yXoOa5Xnv001zbxzpdzvcVZFI9xZFKZ1-6g,5794
24
25
  experimaestro/generators.py,sha256=9NQ_TfDfASkArLnO4PF7s5Yoo9KWjlna2DCPzk5gJOI,1230
25
26
  experimaestro/huggingface.py,sha256=gnVlr6SZnbutYz4PLH0Q77n1TRF-uk-dR-3UFzFqAY0,2956
26
27
  experimaestro/ipc.py,sha256=ltYqybPm_XfcQC3yiskMfhfI_1dREs-XRu0F83YsNws,1490
27
28
  experimaestro/launcherfinder/__init__.py,sha256=jIeT9uRKsIjUv-oyKt0MhFzXJJrSdpJKwM2vL9Sk5YY,294
28
29
  experimaestro/launcherfinder/base.py,sha256=NptPJ0e8CktdhOPejocSfI_B4mloeH_EmJrbXruUCSA,1020
29
- experimaestro/launcherfinder/parser.py,sha256=0qDXgdPce_PsWDy-hKTfxxjXjTAu4FA8moKtyllB2-Q,2129
30
- experimaestro/launcherfinder/registry.py,sha256=KdxQgUn56Iui3vGNXt8D-LiWz0CrxhiY09LYTxYj8PA,8751
31
- experimaestro/launcherfinder/specs.py,sha256=DHX7-07ffe_nrv-HSU8oCl_nd3519zhXkh3ymKBukIA,6250
30
+ experimaestro/launcherfinder/parser.py,sha256=pYbfEJw7osnqZWm7fkVhQawhpNU8dLU_6vEjtXdc8E8,2279
31
+ experimaestro/launcherfinder/registry.py,sha256=FKoacw7sIFxfYTRvaJpQs6o67qtBIDJobX0nmEUBU1Y,8927
32
+ experimaestro/launcherfinder/specs.py,sha256=G8za6mEmkVxuZY_ab3OhWJIpONpcBMO_iXeB30sUbhI,6448
32
33
  experimaestro/launchers/__init__.py,sha256=lXn544sgJExr6uirILWzAXu_IfmfyqFZOt4OzRnjHXg,2525
33
34
  experimaestro/launchers/direct.py,sha256=VJzQNrUGnh-1Ovt6uw4yYIjXNu45QpR-_6V45lcZAfQ,1967
34
35
  experimaestro/launchers/oar.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
36
  experimaestro/launchers/slurm/__init__.py,sha256=R1Zwd4phZaXV8FwCYhzfB44n0V4cf-hBQzOc6NkFQ0s,41
36
- experimaestro/launchers/slurm/base.py,sha256=NoOO4Dj_KP9A0wjVthmDT6PoOquc21JbVsh_hKsmGiU,13200
37
+ experimaestro/launchers/slurm/base.py,sha256=nMoSBMbA901OcneSHVXj8PXGqfv4mVN5G-NPDZ0HXO0,14135
37
38
  experimaestro/launchers/slurm/cli.py,sha256=c-S0TImvhZ-ZxFs5-5T2GRDm5manRYRiYudpQLHwsrQ,818
38
39
  experimaestro/launchers/slurm/configuration.py,sha256=mtozeuvIZmEfHlvEylwCgBrlVRFHT_jWNAKVxR4Tz1E,19357
39
40
  experimaestro/locking.py,sha256=hPT-LuDGZTijpbme8O0kEoB9a3WjdVzI2h31OT44UxE,1477
@@ -48,12 +49,12 @@ experimaestro/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
49
  experimaestro/rpyc.py,sha256=ZRKol-3tVoeoUITLNFenLF4dhWBLW_FvSV_GvsypmeI,3605
49
50
  experimaestro/run.py,sha256=tH9oG7o8_c7cp0cyBLIzWN7CVYjtz72SChz2X-FjYhs,4455
50
51
  experimaestro/scheduler/__init__.py,sha256=ERmmOxz_9mUkIuccNbzUa5Y6gVLLVDdyc4cCxbCCUbY,20
51
- experimaestro/scheduler/base.py,sha256=l-DPZJQMNHr-pbUwfLJbGS7Wp9jX1m9I9cLwdS5xbZ8,30914
52
+ experimaestro/scheduler/base.py,sha256=1SkwjSbOKTHs_pAB44z63xmcsgYrXsxxluO2MhJW5uU,31450
52
53
  experimaestro/scheduler/dependencies.py,sha256=n9XegwrmjayOIxt3xhuTEBVEBGSq4oeVdzz-FviDGXo,1994
53
54
  experimaestro/scheduler/environment.py,sha256=ZaSHSgAcZBmIj7b_eS1OvNQOuVLFvuw-qvqtYrc3Vms,2393
54
55
  experimaestro/scheduler/services.py,sha256=aCKkNZMULlceabqf-kOs_-C7KPINnjU3Q-I00o5x6iY,2189
55
56
  experimaestro/scheduler/workspace.py,sha256=xATJi6-GJcpdwB4alliJnmAuvwt-URUiUKUfq5scUac,1731
56
- experimaestro/scriptbuilder.py,sha256=-wq4jgH8eKpjKWyU_7SxU7MNJLfPqZ0VdERv6rrkJ88,4235
57
+ experimaestro/scriptbuilder.py,sha256=AYh2LdO__mrCWuFa1JWvdfKm_wnk5hHDr_OjHo1voIE,4357
57
58
  experimaestro/server/__init__.py,sha256=F2bzLf2q29Haj2OIbPA26r5WVbaipBNylIozg-As758,10854
58
59
  experimaestro/server/data/016b4a6cdced82ab3aa1.ttf,sha256=AD8RVBhWpkmmyCNcYmbIk2IkxdYJ5RRC2iTcVVbRT78,189684
59
60
  experimaestro/server/data/0c35d18bf06992036b69.woff2,sha256=gmX2R4Y5fWuDLRygqv3xSa2E5ydZ__qfcnLpGg-wFdE,128352
@@ -79,7 +80,7 @@ experimaestro/server/data/index.js,sha256=f0GvRsfsQ4ayP4en7Q-raZ6buwRXLCswCbzVax
79
80
  experimaestro/server/data/index.js.map,sha256=za3MUIjzyyGRI6F5KuBFMTgrFU55xgt0LBrw-4YPHag,3904832
80
81
  experimaestro/server/data/login.html,sha256=4dvhSOn6DHp_tbmzqIKrqq2uAo0sAUbgLVD0lTnPp4s,511
81
82
  experimaestro/server/data/manifest.json,sha256=EpzHQZzrGh9c1Kf63nrqvI33H1cm0nLYfdh5lDm8ijI,318
82
- experimaestro/settings.py,sha256=YTUHZROgrGuSAhZIke74bhBrF2CBH7eSj98ZNjmnMsU,1600
83
+ experimaestro/settings.py,sha256=nZO-667XgM-aC6A-RHw1C4gglDlDeIIIJAxs83IgRyI,1807
83
84
  experimaestro/sphinx/__init__.py,sha256=heovvtwbYToZM-b6HNi4pJdBoo_97usdEawhMGSK3bk,9560
84
85
  experimaestro/sphinx/static/experimaestro.css,sha256=0rEgt1LoDdD-a_R5rVfWZ19zD1gR-1L7q3f4UibIB58,294
85
86
  experimaestro/taskglobals.py,sha256=aBjPpo4HQp6E6M3GQ8L6PR4rK2Lu0kD5dS1WjnaGgDc,499
@@ -108,13 +109,13 @@ experimaestro/tests/tasks/all.py,sha256=hrI2CDyeaYrp2IPzXWif-Uu1Uirkndmuih3Jj09C
108
109
  experimaestro/tests/tasks/foreign.py,sha256=7IAF525mmMORxSPKQmU1z1B84XPmwsO8PGdxBvYknwU,153
109
110
  experimaestro/tests/test_checkers.py,sha256=Kg5frDNRE3pvWVmmYzyk0tJFNO885KOrK48lSu-NlYA,403
110
111
  experimaestro/tests/test_dependencies.py,sha256=xfWrSkvjT45G4FSCL535m1huLT2ghmyW7kvP_XvvCJQ,2005
111
- experimaestro/tests/test_findlauncher.py,sha256=twliDFFI75KPuUJE6EgJwDMf8u3ic1LAOyNOwufFtTw,2964
112
+ experimaestro/tests/test_findlauncher.py,sha256=r34dci01FK9YpjtTHR9fIQ7AMHaMXQOrWtarytcf_Us,2967
112
113
  experimaestro/tests/test_forward.py,sha256=XkZ2iOPETVj-kbTyniOQU9gyHXdfvn89GTwpMq9J6qc,780
113
114
  experimaestro/tests/test_identifier.py,sha256=fnl7jCUAg86npRrS3yeXtb9JysSKhs5czgFzW9yJ9Q8,13397
114
115
  experimaestro/tests/test_instance.py,sha256=awIIMnhiec_qDO6jZBqWDR13ReTzh3arK_60QDY6TLQ,1540
115
- experimaestro/tests/test_objects.py,sha256=wDUW7FXQ9rmB8EXJriJrO--E90Xq2CDIksPdEp8fEoM,1837
116
+ experimaestro/tests/test_objects.py,sha256=6PSG5FtqkiLg2OK26ZzvBYbJYPbFpyDYZtKS-DbiXIs,2037
116
117
  experimaestro/tests/test_outputs.py,sha256=DYzPk5TT_yLumy8SsQbl-S66ivVxJ-ERFrZ68KQZ4KU,781
117
- experimaestro/tests/test_param.py,sha256=YoJI2hhtyrTPEUHgEGZyu1C1pabW0oBTHcaFLKkr-qs,6405
118
+ experimaestro/tests/test_param.py,sha256=FcRF8HbjoW96SR6cTW3fqracLM4BivAsTq0iZvl14Ow,6405
118
119
  experimaestro/tests/test_progress.py,sha256=wtIGQzlV3ldd_wMng11LinVESchW-1J954mCJNlG28E,7580
119
120
  experimaestro/tests/test_serializers.py,sha256=xSCezAM9yH_Ix1wr7j0au9SyBv9DtZ7b0zs2-Ynt-VM,2338
120
121
  experimaestro/tests/test_snippets.py,sha256=rojnyDjtmAMnSuDUj6Bv9XEgdP8oQf2nVc132JF8vsM,3081
@@ -131,7 +132,7 @@ experimaestro/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
131
132
  experimaestro/tools/diff.py,sha256=FBCwupPrSwkEOcqYu0NWe7LTkuOW6r-_btoCel2_k_I,3436
132
133
  experimaestro/tools/documentation.py,sha256=O2UzjzodPqGot3YSe6NYlK7S42XpplakUdqxFpvjqHQ,9184
133
134
  experimaestro/tools/jobs.py,sha256=63bXhJ7RGdczLU_nxu2skGn-9dwgr4r5pD23qH4WeBA,3516
134
- experimaestro/typingutils.py,sha256=gkEhJ9Fir05xh5v45pYKmrGYiLCykGtlU8sL-ydFXS8,1785
135
+ experimaestro/typingutils.py,sha256=Gtsz_nU-t2CBVZP-EeUn-uAlxcbcS9Hp03Xy-TLDY-Q,3334
135
136
  experimaestro/utils/__init__.py,sha256=BdYguxAbR1jOQPV36OgGA31itaMvBJ6WVPV6b4Jn4xw,2434
136
137
  experimaestro/utils/asyncio.py,sha256=zEQQqZW6uHGnFknp_udt9WjjtqLNNMWun9TPL6FOF64,601
137
138
  experimaestro/utils/jobs.py,sha256=42FAdKcn_v_-M6hcQZPUBr9kbDt1eVsk3a4E8Gc4eu8,2394
@@ -140,8 +141,8 @@ experimaestro/utils/resources.py,sha256=gDjkrRjo7GULWyXmNXm_u1uqzEIAoAvJydICk56n
140
141
  experimaestro/utils/settings.py,sha256=jpFMqF0DLL4_P1xGal0zVR5cOrdD8O0Y2IOYvnRgN3k,793
141
142
  experimaestro/utils/yaml.py,sha256=jEjqXqUtJ333wNUdIc0o3LGvdsTQ9AKW9a9CCd-bmGU,6766
142
143
  experimaestro/xpmutils.py,sha256=S21eMbDYsHfvmZ1HmKpq5Pz5O-1HnCLYxKbyTBbASyQ,638
143
- experimaestro-1.4.3.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
144
- experimaestro-1.4.3.dist-info/METADATA,sha256=jFUznTSnjqfDBRU98s8mFP3sNBRzX5W9rJPOu3p8nMc,6338
145
- experimaestro-1.4.3.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
146
- experimaestro-1.4.3.dist-info/entry_points.txt,sha256=PhaEili_fDgn5q7rBJGip_uhGkRBq5l3Yuhg91zkcbk,574
147
- experimaestro-1.4.3.dist-info/RECORD,,
144
+ experimaestro-1.5.1.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
145
+ experimaestro-1.5.1.dist-info/METADATA,sha256=A79XLTCvr5By9ybZMel3_0FZduD20ww4kOamF-4xeQA,6265
146
+ experimaestro-1.5.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
147
+ experimaestro-1.5.1.dist-info/entry_points.txt,sha256=PhaEili_fDgn5q7rBJGip_uhGkRBq5l3Yuhg91zkcbk,574
148
+ experimaestro-1.5.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.8.1
2
+ Generator: poetry-core 1.9.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any