experimaestro 1.6.1__py3-none-any.whl → 1.15.2__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.
Files changed (98) hide show
  1. experimaestro/__init__.py +14 -3
  2. experimaestro/annotations.py +13 -3
  3. experimaestro/cli/filter.py +19 -5
  4. experimaestro/cli/jobs.py +12 -5
  5. experimaestro/commandline.py +3 -7
  6. experimaestro/connectors/__init__.py +27 -12
  7. experimaestro/connectors/local.py +19 -10
  8. experimaestro/connectors/ssh.py +1 -1
  9. experimaestro/core/arguments.py +35 -3
  10. experimaestro/core/callbacks.py +52 -0
  11. experimaestro/core/context.py +8 -9
  12. experimaestro/core/identifier.py +301 -0
  13. experimaestro/core/objects/__init__.py +44 -0
  14. experimaestro/core/{objects.py → objects/config.py} +364 -716
  15. experimaestro/core/objects/config_utils.py +58 -0
  16. experimaestro/core/objects/config_walk.py +151 -0
  17. experimaestro/core/objects.pyi +15 -45
  18. experimaestro/core/serialization.py +63 -9
  19. experimaestro/core/serializers.py +1 -8
  20. experimaestro/core/types.py +61 -6
  21. experimaestro/experiments/cli.py +79 -29
  22. experimaestro/experiments/configuration.py +3 -0
  23. experimaestro/generators.py +6 -1
  24. experimaestro/ipc.py +4 -1
  25. experimaestro/launcherfinder/parser.py +8 -3
  26. experimaestro/launcherfinder/registry.py +29 -10
  27. experimaestro/launcherfinder/specs.py +49 -10
  28. experimaestro/launchers/slurm/base.py +51 -13
  29. experimaestro/mkdocs/__init__.py +1 -1
  30. experimaestro/notifications.py +2 -1
  31. experimaestro/run.py +3 -1
  32. experimaestro/scheduler/base.py +114 -6
  33. experimaestro/scheduler/dynamic_outputs.py +184 -0
  34. experimaestro/scheduler/state.py +75 -0
  35. experimaestro/scheduler/workspace.py +2 -1
  36. experimaestro/scriptbuilder.py +13 -2
  37. experimaestro/server/data/0c35d18bf06992036b69.woff2 +0 -0
  38. experimaestro/server/data/1815e00441357e01619e.ttf +0 -0
  39. experimaestro/server/data/219aa9140e099e6c72ed.woff2 +0 -0
  40. experimaestro/server/data/2463b90d9a316e4e5294.woff2 +0 -0
  41. experimaestro/server/data/2582b0e4bcf85eceead0.ttf +0 -0
  42. experimaestro/server/data/3a4004a46a653d4b2166.woff +0 -0
  43. experimaestro/server/data/3baa5b8f3469222b822d.woff +0 -0
  44. experimaestro/server/data/4d73cb90e394b34b7670.woff +0 -0
  45. experimaestro/server/data/4ef4218c522f1eb6b5b1.woff2 +0 -0
  46. experimaestro/server/data/5d681e2edae8c60630db.woff +0 -0
  47. experimaestro/server/data/6f420cf17cc0d7676fad.woff2 +0 -0
  48. experimaestro/server/data/89999bdf5d835c012025.woff2 +0 -0
  49. experimaestro/server/data/914997e1bdfc990d0897.ttf +0 -0
  50. experimaestro/server/data/c210719e60948b211a12.woff2 +0 -0
  51. experimaestro/server/data/c380809fd3677d7d6903.woff2 +0 -0
  52. experimaestro/server/data/f882956fd323fd322f31.woff +0 -0
  53. experimaestro/server/data/favicon.ico +0 -0
  54. experimaestro/server/data/index.css +22963 -0
  55. experimaestro/server/data/index.css.map +1 -0
  56. experimaestro/server/data/index.html +27 -0
  57. experimaestro/server/data/index.js +101770 -0
  58. experimaestro/server/data/index.js.map +1 -0
  59. experimaestro/server/data/login.html +22 -0
  60. experimaestro/server/data/manifest.json +15 -0
  61. experimaestro/settings.py +2 -2
  62. experimaestro/sphinx/__init__.py +7 -17
  63. experimaestro/taskglobals.py +7 -2
  64. experimaestro/tests/core/__init__.py +0 -0
  65. experimaestro/tests/core/test_generics.py +206 -0
  66. experimaestro/tests/definitions_types.py +5 -3
  67. experimaestro/tests/launchers/bin/sbatch +34 -7
  68. experimaestro/tests/launchers/bin/srun +5 -0
  69. experimaestro/tests/launchers/common.py +16 -4
  70. experimaestro/tests/restart.py +9 -4
  71. experimaestro/tests/tasks/all.py +23 -10
  72. experimaestro/tests/tasks/foreign.py +2 -4
  73. experimaestro/tests/test_dependencies.py +0 -6
  74. experimaestro/tests/test_experiment.py +73 -0
  75. experimaestro/tests/test_findlauncher.py +11 -4
  76. experimaestro/tests/test_forward.py +5 -5
  77. experimaestro/tests/test_generators.py +93 -0
  78. experimaestro/tests/test_identifier.py +114 -99
  79. experimaestro/tests/test_instance.py +6 -21
  80. experimaestro/tests/test_objects.py +20 -4
  81. experimaestro/tests/test_param.py +60 -22
  82. experimaestro/tests/test_serializers.py +24 -64
  83. experimaestro/tests/test_tags.py +5 -11
  84. experimaestro/tests/test_tasks.py +10 -23
  85. experimaestro/tests/test_tokens.py +3 -2
  86. experimaestro/tests/test_types.py +20 -17
  87. experimaestro/tests/test_validation.py +48 -91
  88. experimaestro/tokens.py +16 -5
  89. experimaestro/typingutils.py +8 -8
  90. experimaestro/utils/asyncio.py +6 -2
  91. experimaestro/utils/multiprocessing.py +44 -0
  92. experimaestro/utils/resources.py +7 -3
  93. {experimaestro-1.6.1.dist-info → experimaestro-1.15.2.dist-info}/METADATA +27 -34
  94. experimaestro-1.15.2.dist-info/RECORD +159 -0
  95. {experimaestro-1.6.1.dist-info → experimaestro-1.15.2.dist-info}/WHEEL +1 -1
  96. experimaestro-1.6.1.dist-info/RECORD +0 -122
  97. {experimaestro-1.6.1.dist-info → experimaestro-1.15.2.dist-info}/entry_points.txt +0 -0
  98. {experimaestro-1.6.1.dist-info → experimaestro-1.15.2.dist-info/licenses}/LICENSE +0 -0
@@ -2,18 +2,9 @@
2
2
 
3
3
  import pytest
4
4
  from pathlib import Path
5
- from experimaestro import (
6
- config,
7
- Task,
8
- Identifier,
9
- argument,
10
- pathoption,
11
- ConstantParam,
12
- Param,
13
- Config,
14
- )
5
+ from experimaestro import Task, field, Identifier, Constant, Param, Config, Meta
15
6
  from enum import Enum
16
- import experimaestro.core.types as types
7
+ from experimaestro.generators import PathGenerator
17
8
  from experimaestro.scheduler import Job, JobContext
18
9
  from experimaestro.scheduler.workspace import RunMode
19
10
  from .utils import TemporaryExperiment
@@ -31,57 +22,50 @@ def expect_notvalidate(value):
31
22
  value.__xpm__.validate()
32
23
 
33
24
 
34
- @argument("value", type=int)
35
- @config()
36
- class A:
37
- pass
25
+ class A(Config):
26
+ value: Param[int]
38
27
 
39
28
 
40
- @argument("a", type=A)
41
- @config()
42
- class B:
43
- pass
29
+ class B(Config):
30
+ a: Param[A]
44
31
 
45
32
 
46
- @pathoption("path", "outdir")
47
- @config()
48
- class C:
33
+ class C(Config):
34
+ path: Meta[Path] = field(default_factory=PathGenerator("outdir"))
49
35
  pass
50
36
 
51
37
 
52
- def test_simple():
38
+ def test_validation_simple():
53
39
  expect_validate(A(value=1))
54
40
 
55
41
 
56
- def test_missing():
42
+ def test_validation_missing():
57
43
  expect_notvalidate(A())
58
44
 
59
45
 
60
- def test_simple_nested():
46
+ def test_validation_simple_nested():
61
47
  b = B()
62
48
  b.a = A(value=1)
63
49
  expect_validate(b)
64
50
 
65
51
 
66
- def test_missing_nested():
52
+ def test_validation_missing_nested():
67
53
  b = B()
68
54
  b.a = A()
69
55
  expect_notvalidate(b)
70
56
 
71
57
 
72
- def test_type():
73
- @config(valns.type.a)
74
- class A:
58
+ def test_validation_type():
59
+ class A(Config):
60
+ __xpmid__ = valns.type.a
75
61
  pass
76
62
 
77
- @config(valns.type.b)
78
- class B:
79
- pass
63
+ class B(Config):
64
+ __xpmid__ = valns.type.b
80
65
 
81
- @argument("a", A)
82
- @config(valns.type.c)
83
- class C:
84
- pass
66
+ class C(Config):
67
+ a: Param[A]
68
+ __xpmid__ = valns.type.c
85
69
 
86
70
  with pytest.raises(ValueError):
87
71
  C(a=B())
@@ -91,30 +75,26 @@ def test_type():
91
75
  c.a = B()
92
76
 
93
77
 
94
- def test_subtype():
95
- @config(valns.subtype.a)
96
- class A:
97
- pass
78
+ def test_validation_subtype():
79
+ class A(Config):
80
+ __xpmid__ = valns.subtype.a
98
81
 
99
- @config(valns.subtype.a1)
100
82
  class A1(A):
101
- pass
83
+ __xpmid__ = valns.subtype.a1
102
84
 
103
- @argument("a", A)
104
- @config(valns.subtype.b)
105
- class B:
106
- pass
85
+ class B(Config):
86
+ __xpmid__ = valns.subtype.b
87
+ a: Param[A]
107
88
 
108
89
  expect_validate(B(a=A1()))
109
90
 
110
91
 
111
- def test_path():
112
- """Test of @pathoption"""
92
+ def test_validation_path_generator():
93
+ """Test of path generator"""
113
94
 
114
- @pathoption("value", "file.txt")
115
- @config(valns.path.a)
116
- class A:
117
- pass
95
+ class A(Config):
96
+ __xpmid__ = valns.path.a
97
+ value: Meta[Path] = field(default_factory=PathGenerator("file.txt"))
118
98
 
119
99
  a = A()
120
100
  a.__xpm__.validate()
@@ -129,13 +109,12 @@ def test_path():
129
109
  assert a.value.parents[3] == xp.workspace.path
130
110
 
131
111
 
132
- def test_constant():
133
- """Test of @ConstantParam"""
112
+ def test_validation_constant():
113
+ """Test of constant"""
134
114
 
135
- @ConstantParam("value", 1)
136
- @config(valns.constant.a)
137
- class A:
138
- pass
115
+ class A(Config):
116
+ __xpmid__ = valns.constant.a
117
+ value: Constant[int] = 1
139
118
 
140
119
  a = A()
141
120
  a.__xpm__.validate()
@@ -145,31 +124,26 @@ def test_constant():
145
124
  assert a.value == 1
146
125
 
147
126
 
148
- @argument("x", type=int)
149
- @config()
150
- class Parent:
151
- pass
127
+ class Parent(Config):
128
+ x: Param[int]
152
129
 
153
130
 
154
- @config()
155
131
  class Child(Parent):
156
132
  pass
157
133
 
158
134
 
159
- def test_child():
135
+ def test_validation_child():
160
136
  expect_validate(Child(x=1))
161
137
 
162
138
 
163
139
  # --- Path argument checks
164
140
 
165
141
 
166
- @pathoption("x", "x")
167
- @config()
168
- class PathParent:
169
- pass
142
+ class PathParent(Config):
143
+ x: Meta[Path] = field(default_factory=PathGenerator("x"))
170
144
 
171
145
 
172
- def test_path_option():
146
+ def test_validation_path_option():
173
147
  c = PathParent()
174
148
  expect_validate(c)
175
149
 
@@ -177,28 +151,11 @@ def test_path_option():
177
151
  # --- Default value
178
152
 
179
153
 
180
- @pytest.mark.parametrize(
181
- "value,apitype",
182
- [(1.5, types.FloatType), (1, types.IntType), (False, types.BoolType)],
183
- )
184
- def test_default(value, apitype):
185
- @argument("default", default=value)
186
- @config(valns.default[str(type(value))])
187
- class Default:
188
- pass
189
-
190
- value = Default()
191
- expect_validate(value)
192
- assert Default.__xpmtype__.arguments["default"].type.__class__ == apitype
193
-
194
-
195
- def test_seal():
154
+ def test_validation_seal():
196
155
  """Test value sealing"""
197
156
 
198
- @argument("a", int)
199
- @config()
200
- class A:
201
- pass
157
+ class A(Config):
158
+ a: Param[int]
202
159
 
203
160
  a = A(a=2)
204
161
  a.__xpm__.seal(EmptyContext())
@@ -207,7 +164,7 @@ def test_seal():
207
164
  a.a = 1
208
165
 
209
166
 
210
- def test_validation_enum():
167
+ def test_validation_validation_enum():
211
168
  """Path arguments should be ignored"""
212
169
 
213
170
  class EnumParam(Enum):
@@ -241,7 +198,7 @@ class TaskConfigConsumer(Config):
241
198
  x: Param[TaskParentConfig]
242
199
 
243
200
 
244
- def test_taskargument():
201
+ def test_validation_taskargument():
245
202
  x = taskconfig()
246
203
  with TemporaryExperiment("fake"):
247
204
  x.submit(run_mode=RunMode.DRY_RUN)
experimaestro/tokens.py CHANGED
@@ -59,6 +59,8 @@ class CounterTokenLock(Lock):
59
59
 
60
60
 
61
61
  class CounterTokenDependency(Dependency):
62
+ """A dependency onto a token"""
63
+
62
64
  def __init__(self, token: "CounterToken", count: int):
63
65
  super().__init__(token)
64
66
  self._token = token
@@ -66,6 +68,7 @@ class CounterTokenDependency(Dependency):
66
68
 
67
69
  @property
68
70
  def name(self):
71
+ """The (file) name for this dependency, when taken"""
69
72
  return f"{self.target.identifier}.token"
70
73
 
71
74
  def status(self) -> DependencyStatus:
@@ -165,7 +168,7 @@ class CounterToken(Token, FileSystemEventHandler):
165
168
  - TIMESTAMP.token contains (1) the number of tokens (2) the job URI
166
169
  """
167
170
 
168
- """Maps paths to instances"""
171
+ """Maps token keys to CounterToken instances"""
169
172
  TOKENS: Dict[str, "CounterToken"] = {}
170
173
 
171
174
  @staticmethod
@@ -193,7 +196,7 @@ class CounterToken(Token, FileSystemEventHandler):
193
196
  """[summary]
194
197
 
195
198
  Arguments:
196
- path {Path} -- The file path of the token file
199
+ path {Path} -- The file path of the token directory
197
200
  count {int} -- Number of tokens (overrides previous definitions)
198
201
  force -- If the token has already been created, force to write the maximum
199
202
  number of tokens
@@ -298,6 +301,9 @@ class CounterToken(Token, FileSystemEventHandler):
298
301
  tokenfile = TokenFile(path)
299
302
  tokenfile.watch()
300
303
  self.cache[path.name] = tokenfile
304
+ except FileNotFoundError:
305
+ # We did not find the token file... just ignore
306
+ pass
301
307
  except Exception:
302
308
  logger.exception("Uncaught exception in on_modified handler")
303
309
  raise
@@ -336,13 +342,18 @@ class CounterToken(Token, FileSystemEventHandler):
336
342
  for dependency in dependents:
337
343
  dependency.check()
338
344
 
345
+ # A modified dependency not in cache
339
346
  elif path.name.endswith(".token") and path.name not in self.cache:
340
347
  with self.lock:
341
348
  if path.name not in self.cache:
342
349
  logger.debug("Token file not in cache %s", path.name)
343
- tokenfile = TokenFile(path)
344
- tokenfile.watch()
345
- self.cache[path.name] = tokenfile
350
+ try:
351
+ tokenfile = TokenFile(path)
352
+ tokenfile.watch()
353
+ self.cache[path.name] = tokenfile
354
+ except FileNotFoundError:
355
+ # Well, the file did not exist anymore...
356
+ pass
346
357
  except Exception:
347
358
  logger.exception("Uncaught exception in on_modified handler")
348
359
  raise
@@ -8,14 +8,7 @@ if sys.version_info.major == 3:
8
8
  else:
9
9
  from typing import _collect_parameters
10
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
11
+ from typing import _AnnotatedAlias as AnnotatedAlias, get_args, get_origin
19
12
 
20
13
  GenericAlias = typing._GenericAlias
21
14
 
@@ -26,6 +19,13 @@ def isgenericalias(typehint):
26
19
  return isinstance(typehint, GenericAlias)
27
20
 
28
21
 
22
+ def get_union(typehint):
23
+ """Return the list of types of a union (or the type itself if it is not an union)"""
24
+ if isgenericalias(typehint) and typehint.__origin__ == typing.Union:
25
+ return typehint.__args__
26
+ return None
27
+
28
+
29
29
  def get_optional(typehint):
30
30
  if isgenericalias(typehint) and typehint.__origin__ == typing.Union:
31
31
  if len(typehint.__args__) == 2:
@@ -10,8 +10,12 @@ def asyncThreadcheck(name, func, *args, **kwargs) -> asyncio.Future:
10
10
 
11
11
  def dowait():
12
12
  logging.debug("Running %s", func)
13
- result = func(*args, **kwargs)
14
- logging.debug("Got result from %s", func)
13
+ try:
14
+ result = func(*args, **kwargs)
15
+ logging.debug("Got result from %s", func)
16
+ except Exception:
17
+ logging.exception("Got an error in the thread")
18
+ raise
15
19
  loop.call_soon_threadsafe(future.set_result, result)
16
20
 
17
21
  # Start thread
@@ -0,0 +1,44 @@
1
+ import logging
2
+ import multiprocessing as mp
3
+ import time
4
+ import os
5
+ import signal
6
+ import threading
7
+
8
+
9
+ def delayed_shutdown(delay=60, *, exit_code=1, grace_period=5):
10
+ """After *delay*'s try a graceful stop, then SIGKILL anything left.
11
+
12
+ :param delay: Delay in seconds before killing
13
+ :param grace_period: Delay in seconds before force-killing a child process
14
+ :param exit_code: The exit code to use
15
+ """
16
+
17
+ def _killer():
18
+ time.sleep(delay)
19
+
20
+ logging.info("Stall dectected – killing all subprocesses")
21
+
22
+ # 1️⃣ Try graceful termination
23
+ for p in mp.active_children():
24
+ # sends SIGTERM / TerminateProcess
25
+ p.terminate()
26
+
27
+ alive = mp.active_children()
28
+ deadline = time.time() + grace_period
29
+ while alive and time.time() < deadline:
30
+ alive = [p for p in alive if p.is_alive()]
31
+ time.sleep(0.1)
32
+
33
+ # 2️⃣ Anything still alive? Nuke it.
34
+ for p in alive:
35
+ try:
36
+ os.kill(p.pid, signal.SIGKILL)
37
+ except OSError:
38
+ pass
39
+
40
+ # 3️⃣ Finally kill the parent
41
+ os.kill(os.getpid(), signal.SIGKILL)
42
+
43
+ # Start the thread (non blocking)
44
+ threading.Thread(target=_killer, daemon=True).start()
@@ -24,15 +24,19 @@ class ResourcePathWrapper(PathLike):
24
24
  parents = [s.name for s in reversed(self.path.parents)][1:]
25
25
  return ".".join(parents)
26
26
 
27
- @property
27
+ @cached_property
28
28
  def name(self):
29
29
  return self.path.name
30
30
 
31
31
  def is_file(self):
32
- return resources.is_resource(self.package, self.name)
32
+ return any(
33
+ traversable.name == self.name and traversable.is_file()
34
+ for traversable in resources.files(self.package).iterdir()
35
+ )
33
36
 
34
37
  def __fspath__(self):
35
- return resources.path(self.package, self.name).__fspath__()
38
+ """Return the file system path representation of the object"""
39
+ return resources.as_file(resources.files(self.package) / self.name)
36
40
 
37
41
  @contextmanager
38
42
  def open(self, *args, **kwargs):
@@ -1,53 +1,46 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: experimaestro
3
- Version: 1.6.1
3
+ Version: 1.15.2
4
4
  Summary: "Experimaestro is a computer science experiment manager"
5
5
  License: GPL-3
6
+ License-File: LICENSE
6
7
  Keywords: experiment manager
7
8
  Author: Benjamin Piwowarski
8
9
  Author-email: benjamin@piwowarski.fr
9
- Requires-Python: >=3.8,<4.0
10
+ Requires-Python: >=3.10
10
11
  Classifier: Development Status :: 4 - Beta
11
12
  Classifier: Intended Audience :: Science/Research
12
13
  Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
13
- Classifier: License :: Other/Proprietary License
14
14
  Classifier: Operating System :: OS Independent
15
15
  Classifier: Programming Language :: Python
16
16
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.8
18
- Classifier: Programming Language :: Python :: 3.9
19
- Classifier: Programming Language :: Python :: 3.10
20
- Classifier: Programming Language :: Python :: 3.11
21
- Classifier: Programming Language :: Python :: 3.12
22
- Classifier: Programming Language :: Python :: 3.13
23
17
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
18
  Requires-Dist: arpeggio (>=2,<3)
25
- Requires-Dist: attrs (>=23.1.0,<24.0.0)
19
+ Requires-Dist: attrs (>=23.1.0,<24)
26
20
  Requires-Dist: click (>=8)
27
21
  Requires-Dist: decorator (>=5,<6)
28
- Requires-Dist: docstring-parser (>=0.15,<0.16)
29
- Requires-Dist: fasteners (>=0.19,<0.20)
30
- Requires-Dist: flask (>=2.3,<3.0)
31
- Requires-Dist: flask-socketio (>=5.3,<6.0)
32
- Requires-Dist: gevent (>=23.9,<24.0)
33
- Requires-Dist: gevent-websocket (>=0.10,<0.11)
22
+ Requires-Dist: docstring-parser (>=0.15,<1)
23
+ Requires-Dist: fasteners (>=0.19,<1)
24
+ Requires-Dist: flask (>=2.3)
25
+ Requires-Dist: flask-socketio (>=5.3)
26
+ Requires-Dist: gevent (>=25)
27
+ Requires-Dist: gevent-websocket (>=0.10)
34
28
  Requires-Dist: huggingface-hub (>0.17)
35
- Requires-Dist: humanfriendly (>=10,<11)
36
- Requires-Dist: marshmallow (>=3.20,<4.0)
37
- Requires-Dist: omegaconf (>=2.3,<3.0)
38
- Requires-Dist: psutil (>=5.9.5,<6.0.0)
39
- Requires-Dist: pyparsing (>=3.1,<4.0)
40
- Requires-Dist: pytools (>=2023.1.1,<2024.0.0)
41
- Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
42
- Requires-Dist: requests (>=2.31,<3.0)
29
+ Requires-Dist: humanfriendly (>=10)
30
+ Requires-Dist: marshmallow (>=3.20,<4)
31
+ Requires-Dist: mkdocs (>=1.5,<2)
32
+ Requires-Dist: omegaconf (>=2.3,<3)
33
+ Requires-Dist: psutil (>=7,<8)
34
+ Requires-Dist: pyparsing (>=3.1,<4)
35
+ Requires-Dist: pytools (>=2023.1.1,<2024)
36
+ Requires-Dist: pyyaml (>=6.0.1,<7)
37
+ Requires-Dist: requests (>=2.31,<3)
43
38
  Requires-Dist: rpyc (>=5,<7)
44
- Requires-Dist: sortedcontainers (>=2.4,<3.0)
45
- Requires-Dist: termcolor (>=2.3)
46
- Requires-Dist: tqdm (>=4.66.1,<5.0.0)
39
+ Requires-Dist: sortedcontainers (>=2.4,<3)
40
+ Requires-Dist: termcolor (>=2.3,<3)
41
+ Requires-Dist: tqdm (>=4.66.1,<5)
47
42
  Requires-Dist: typing-extensions (>=4.2) ; python_version < "3.12"
48
- Requires-Dist: watchdog (>=2,<3)
49
- Project-URL: Documentation, https://experimaestro-python.readthedocs.io/
50
- Project-URL: Repository, https://github.com/experimaestro/experimaestro-python
43
+ Requires-Dist: watchdog (>=2)
51
44
  Description-Content-Type: text/markdown
52
45
 
53
46
  [![PyPI version](https://badge.fury.io/py/experimaestro.svg)](https://badge.fury.io/py/experimaestro)
@@ -145,11 +138,11 @@ def cli(port, workdir, sleeptime):
145
138
  # Sets the working directory and the name of the xp
146
139
  with experiment(workdir, "helloworld", port=port) as xp:
147
140
  # Submit the tasks
148
- hello = Say(word="hello", sleeptime=sleeptime).submit()
149
- world = Say(word="world", sleeptime=sleeptime).submit()
141
+ hello = Say.C(word="hello", sleeptime=sleeptime).submit()
142
+ world = Say.C(word="world", sleeptime=sleeptime).submit()
150
143
 
151
144
  # Concat will depend on the two first tasks
152
- Concat(strings=[hello, world], sleeptime=sleeptime).tag("y", 1).submit()
145
+ Concat.C(strings=[hello, world], sleeptime=sleeptime).tag("y", 1).submit()
153
146
 
154
147
 
155
148
  if __name__ == "__main__":