stepup 1.0.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.
- stepup/core/__init__.py +30 -0
- stepup/core/_version.py +16 -0
- stepup/core/api.py +633 -0
- stepup/core/assoc.py +235 -0
- stepup/core/cascade.py +498 -0
- stepup/core/deferred_glob.py +86 -0
- stepup/core/director.py +455 -0
- stepup/core/exceptions.py +32 -0
- stepup/core/file.py +203 -0
- stepup/core/hash.py +392 -0
- stepup/core/interact.py +62 -0
- stepup/core/job.py +124 -0
- stepup/core/nglob.py +560 -0
- stepup/core/pytest.py +127 -0
- stepup/core/reporter.py +185 -0
- stepup/core/rpc.py +596 -0
- stepup/core/runner.py +292 -0
- stepup/core/scheduler.py +154 -0
- stepup/core/script.py +295 -0
- stepup/core/step.py +690 -0
- stepup/core/tui.py +255 -0
- stepup/core/utils.py +260 -0
- stepup/core/watcher.py +75 -0
- stepup/core/worker.py +518 -0
- stepup/core/workflow.py +759 -0
- stepup-1.0.0.dist-info/LICENSE +674 -0
- stepup-1.0.0.dist-info/METADATA +46 -0
- stepup-1.0.0.dist-info/RECORD +31 -0
- stepup-1.0.0.dist-info/WHEEL +5 -0
- stepup-1.0.0.dist-info/entry_points.txt +2 -0
- stepup-1.0.0.dist-info/top_level.txt +1 -0
stepup/core/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# StepUp Core provides the basic framework for the StepUp build tool.
|
|
2
|
+
# Copyright (C) 2024 Toon Verstraelen
|
|
3
|
+
#
|
|
4
|
+
# This file is part of StepUp Core.
|
|
5
|
+
#
|
|
6
|
+
# StepUp Core is free software; you can redistribute it and/or
|
|
7
|
+
# modify it under the terms of the GNU General Public License
|
|
8
|
+
# as published by the Free Software Foundation; either version 3
|
|
9
|
+
# of the License, or (at your option) any later version.
|
|
10
|
+
#
|
|
11
|
+
# StepUp Core is distributed in the hope that it will be useful,
|
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
# GNU General Public License for more details.
|
|
15
|
+
#
|
|
16
|
+
# You should have received a copy of the GNU General Public License
|
|
17
|
+
# along with this program; if not, see <http://www.gnu.org/licenses/>
|
|
18
|
+
#
|
|
19
|
+
# --
|
|
20
|
+
"""StepUp Core package."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__all__ = ("__version__", "__version_tuple__")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
from ._version import __version__, __version_tuple__
|
|
28
|
+
except ImportError:
|
|
29
|
+
__version__ = "0.0.0a-dev"
|
|
30
|
+
__version_tuple__ = (0, 0, 0, "a-dev")
|
stepup/core/_version.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# file generated by setuptools_scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
TYPE_CHECKING = False
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from typing import Tuple, Union
|
|
6
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
|
+
else:
|
|
8
|
+
VERSION_TUPLE = object
|
|
9
|
+
|
|
10
|
+
version: str
|
|
11
|
+
__version__: str
|
|
12
|
+
__version_tuple__: VERSION_TUPLE
|
|
13
|
+
version_tuple: VERSION_TUPLE
|
|
14
|
+
|
|
15
|
+
__version__ = version = '1.0.0'
|
|
16
|
+
__version_tuple__ = version_tuple = (1, 0, 0)
|
stepup/core/api.py
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
# StepUp Core provides the basic framework for the StepUp build tool.
|
|
2
|
+
# Copyright (C) 2024 Toon Verstraelen
|
|
3
|
+
#
|
|
4
|
+
# This file is part of StepUp Core.
|
|
5
|
+
#
|
|
6
|
+
# StepUp Core is free software; you can redistribute it and/or
|
|
7
|
+
# modify it under the terms of the GNU General Public License
|
|
8
|
+
# as published by the Free Software Foundation; either version 3
|
|
9
|
+
# of the License, or (at your option) any later version.
|
|
10
|
+
#
|
|
11
|
+
# StepUp Core is distributed in the hope that it will be useful,
|
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
# GNU General Public License for more details.
|
|
15
|
+
#
|
|
16
|
+
# You should have received a copy of the GNU General Public License
|
|
17
|
+
# along with this program; if not, see <http://www.gnu.org/licenses/>
|
|
18
|
+
#
|
|
19
|
+
# --
|
|
20
|
+
"""Application programming interface to the director.
|
|
21
|
+
|
|
22
|
+
To keep things simple, it is assumed that one Python process only communicates with one director.
|
|
23
|
+
|
|
24
|
+
This module should not be imported by other stepup.core modules, except for stepup.interact.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import contextlib
|
|
28
|
+
import os
|
|
29
|
+
from time import sleep
|
|
30
|
+
from typing import Collection, Iterable, Iterator, Callable
|
|
31
|
+
|
|
32
|
+
from path import Path
|
|
33
|
+
|
|
34
|
+
from .nglob import NGlobMulti
|
|
35
|
+
from .utils import (
|
|
36
|
+
myrelpath,
|
|
37
|
+
CaseSensitiveTemplate,
|
|
38
|
+
mynormpath,
|
|
39
|
+
make_path_out,
|
|
40
|
+
check_inp_path,
|
|
41
|
+
lookupdict,
|
|
42
|
+
)
|
|
43
|
+
from .rpc import SocketSyncRPCClient, DummySyncRPCClient
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
__all__ = (
|
|
47
|
+
# Basic API
|
|
48
|
+
"static",
|
|
49
|
+
"glob",
|
|
50
|
+
"step",
|
|
51
|
+
"pool",
|
|
52
|
+
"amend",
|
|
53
|
+
# Composite API
|
|
54
|
+
"plan",
|
|
55
|
+
"copy",
|
|
56
|
+
"mkdir",
|
|
57
|
+
"getenv",
|
|
58
|
+
"script",
|
|
59
|
+
# Utilities for API development
|
|
60
|
+
"subs_env_vars",
|
|
61
|
+
"translate",
|
|
62
|
+
# For stepup.core.interact
|
|
63
|
+
"RPC_CLIENT",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
#
|
|
68
|
+
# Basic API
|
|
69
|
+
#
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def static(*paths: str | Iterable[str]):
|
|
73
|
+
"""Declare static paths.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
*paths
|
|
78
|
+
One or more static paths (files or directories),
|
|
79
|
+
relative to the current working directory.
|
|
80
|
+
Arguments may also be lists of strings.
|
|
81
|
+
|
|
82
|
+
Raises
|
|
83
|
+
------
|
|
84
|
+
ValueError
|
|
85
|
+
When a file does not exist or there is an error with the trailing separator.
|
|
86
|
+
|
|
87
|
+
Notes
|
|
88
|
+
-----
|
|
89
|
+
Environment variables in the `paths` will be
|
|
90
|
+
substituted directly and amend the current step's env_vars list, if needed.
|
|
91
|
+
These substitutions will ignore changes to `os.environ` made in the calling script.
|
|
92
|
+
"""
|
|
93
|
+
# Turn paths into one big list.
|
|
94
|
+
_paths = paths
|
|
95
|
+
paths = []
|
|
96
|
+
for path in _paths:
|
|
97
|
+
if isinstance(path, str):
|
|
98
|
+
paths.append(path)
|
|
99
|
+
elif isinstance(path, Iterable):
|
|
100
|
+
paths.extend(path)
|
|
101
|
+
del _paths
|
|
102
|
+
|
|
103
|
+
# Avoid empty RPC calls.
|
|
104
|
+
if len(paths) > 0:
|
|
105
|
+
# Perform env var substitutions.
|
|
106
|
+
with subs_env_vars() as subs:
|
|
107
|
+
su_paths = [subs(path) for path in paths]
|
|
108
|
+
# Sanity checks
|
|
109
|
+
check_inp_paths(su_paths)
|
|
110
|
+
# Translate paths to directory working dir and make RPC call
|
|
111
|
+
tr_paths = sorted(translate(path) for path in su_paths)
|
|
112
|
+
RPC_CLIENT.call.static(_get_step_key(), tr_paths)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def glob(
|
|
116
|
+
*patterns: str, _required: bool = False, _defer: bool = False, **subs: str
|
|
117
|
+
) -> NGlobMulti | None:
|
|
118
|
+
"""Declare static paths through pattern matching.
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
*patterns
|
|
123
|
+
One or more patterns for static files or directories,
|
|
124
|
+
relative to the current working directory.
|
|
125
|
+
The patterns may contain (named) wildcards and one
|
|
126
|
+
may specify the pattern for each named wildcard with
|
|
127
|
+
the keyword arguments.
|
|
128
|
+
_required
|
|
129
|
+
When True, an error will be raised when there are no matches.
|
|
130
|
+
_defer
|
|
131
|
+
When True, static files are not added yet.
|
|
132
|
+
Instead, the glob is installed in the workflow as a deferred glob.
|
|
133
|
+
As soon as any file is needed as input and matches the pattern,
|
|
134
|
+
it will be made static.
|
|
135
|
+
This is not compatible with `_required=True`.
|
|
136
|
+
Named wildcards are not supported in deferred globs.
|
|
137
|
+
**subs
|
|
138
|
+
When using named wildcards, they will match the pattern ``*`` by default.
|
|
139
|
+
Through the subs argument each name can be associated with another glob pattern.
|
|
140
|
+
Names starting with underscores are not allowed.
|
|
141
|
+
|
|
142
|
+
Raises
|
|
143
|
+
------
|
|
144
|
+
FileNotFoundError
|
|
145
|
+
when no matches were found and _required is True.
|
|
146
|
+
|
|
147
|
+
Returns
|
|
148
|
+
-------
|
|
149
|
+
ngm
|
|
150
|
+
An `NGlobMulti` instance holding all the matched (combinations of) paths.
|
|
151
|
+
This object acts as an iterator.
|
|
152
|
+
When named wildcards are used, it iterates over `NGlobMatch` instances.
|
|
153
|
+
When using only anonymous wildcards, it iterates over unique paths.
|
|
154
|
+
It also features `ngm.matches()` and `ngm.files()` iterators,
|
|
155
|
+
with which the type of iterator can be overruled.
|
|
156
|
+
Finally, one may also use ngm in conditional expressions:
|
|
157
|
+
It evaluates to True if and only if it contains some matches.
|
|
158
|
+
|
|
159
|
+
`None` is returned when `_defer=True`.
|
|
160
|
+
|
|
161
|
+
Notes
|
|
162
|
+
-----
|
|
163
|
+
The combinatorics allow one to construct nested loops easily in one call.
|
|
164
|
+
For unrelated patterns, it may be more efficient to use separate `glob` calls.
|
|
165
|
+
|
|
166
|
+
Environment variables in the `patterns` will be
|
|
167
|
+
substituted directly and amend the current step's env_vars list, if needed.
|
|
168
|
+
These substitutions will ignore changes to `os.environ` made in the calling script.
|
|
169
|
+
"""
|
|
170
|
+
if len(patterns) == 0:
|
|
171
|
+
raise ValueError("At least one path is required for glob.")
|
|
172
|
+
if any(name.startswith("_") for name in subs):
|
|
173
|
+
raise ValueError("Substitutions cannot have names starting with underscores.")
|
|
174
|
+
|
|
175
|
+
# Substitute environment variables
|
|
176
|
+
with subs_env_vars() as subs_path:
|
|
177
|
+
su_patterns = [subs_path(pattern) for pattern in patterns]
|
|
178
|
+
|
|
179
|
+
if _defer:
|
|
180
|
+
if _required:
|
|
181
|
+
raise ValueError("Combination of options not supported: _defer=True, _required=True")
|
|
182
|
+
if len(subs) > 0:
|
|
183
|
+
raise ValueError("Named wildcards are not supported in deferred globs.")
|
|
184
|
+
tr_patterns = [translate(su_pattern) for su_pattern in su_patterns]
|
|
185
|
+
RPC_CLIENT.call.defer(_get_step_key(), tr_patterns)
|
|
186
|
+
else:
|
|
187
|
+
# Collect all matches
|
|
188
|
+
nglob_multi = NGlobMulti.from_patterns(su_patterns, subs)
|
|
189
|
+
nglob_multi.glob()
|
|
190
|
+
if _required and len(nglob_multi.results) == 0:
|
|
191
|
+
raise FileNotFoundError("Could not find any matching paths on the filesystem.")
|
|
192
|
+
|
|
193
|
+
# Send static paths
|
|
194
|
+
static_paths = nglob_multi.files()
|
|
195
|
+
if len(static_paths) > 0:
|
|
196
|
+
check_inp_paths(static_paths)
|
|
197
|
+
tr_static_paths = [translate(static_path) for static_path in static_paths]
|
|
198
|
+
RPC_CLIENT.call.static(_get_step_key(), tr_static_paths)
|
|
199
|
+
|
|
200
|
+
# Unstructure the nglob_multi and translate all paths before sending it to the director.
|
|
201
|
+
lookup = lookupdict()
|
|
202
|
+
ngm_data = nglob_multi.unstructure(lookup)
|
|
203
|
+
tr_strings = [str(translate(path)) for path in lookup.get_list()]
|
|
204
|
+
RPC_CLIENT.call.nglob(_get_step_key(), ngm_data, tr_strings)
|
|
205
|
+
return nglob_multi
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _str_to_list(arg: Collection[str] | str) -> list[str]:
|
|
209
|
+
return [arg] if isinstance(arg, str) else arg
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def step(
|
|
213
|
+
command: str,
|
|
214
|
+
*,
|
|
215
|
+
inp: Collection[str] | str = (),
|
|
216
|
+
env: Collection[str] | str = (),
|
|
217
|
+
out: Collection[str] | str = (),
|
|
218
|
+
vol: Collection[str] | str = (),
|
|
219
|
+
workdir: str = "./",
|
|
220
|
+
optional: bool = False,
|
|
221
|
+
pool: str | None = None,
|
|
222
|
+
block: bool = False,
|
|
223
|
+
) -> str:
|
|
224
|
+
"""Add a step to the build graph.
|
|
225
|
+
|
|
226
|
+
Parameters
|
|
227
|
+
----------
|
|
228
|
+
command
|
|
229
|
+
Command to execute (in the working directory of the director).
|
|
230
|
+
inp
|
|
231
|
+
File(s) required by the step.
|
|
232
|
+
Can be files or directories (trailing slash).
|
|
233
|
+
env
|
|
234
|
+
Environment variable(s) to which the step is sensitive.
|
|
235
|
+
If they change, or when they are (un)defined, the step digest will change,
|
|
236
|
+
such that the step cannot be skipped.
|
|
237
|
+
out
|
|
238
|
+
File(s) created by the step.
|
|
239
|
+
These can be files or directories (trailing slash).
|
|
240
|
+
vol
|
|
241
|
+
Volatile file(s) created by the step.
|
|
242
|
+
These can be files only.
|
|
243
|
+
workdir
|
|
244
|
+
The directory where the command must be executed.
|
|
245
|
+
(The default is the current directory.)
|
|
246
|
+
optional
|
|
247
|
+
When set to True, the step is only executed when required by other mandatory steps.
|
|
248
|
+
pool
|
|
249
|
+
Restricts execution to a pool, optional.
|
|
250
|
+
block
|
|
251
|
+
When set to True, the step will always remain pending.
|
|
252
|
+
This can be used to temporarily prevent part of the workflow from executing.
|
|
253
|
+
|
|
254
|
+
Returns
|
|
255
|
+
-------
|
|
256
|
+
step_key
|
|
257
|
+
The key of the newly created step
|
|
258
|
+
|
|
259
|
+
Notes
|
|
260
|
+
-----
|
|
261
|
+
Environment variables in the `workdir`, `inp`, `out` and `vol` paths and workdir will be
|
|
262
|
+
substituted directly and amend the current step's env_vars list, if needed.
|
|
263
|
+
These substitutions will ignore changes to `os.environ` made in the calling script.
|
|
264
|
+
|
|
265
|
+
Before sending the step to the director the variables `${inp}`, `${out}` and `${vol}`
|
|
266
|
+
in the command are substituted by white-space concatenated list of `inp`, `out` and
|
|
267
|
+
`vol`, respectively.
|
|
268
|
+
Relative paths in `inp`, `out` and `env` are interpreted in the current working directory.
|
|
269
|
+
Before substitution, they are rewritten as paths relative to the workdir.
|
|
270
|
+
(Amended inputs and outputs are never substituted this way because they are yet unknown.)
|
|
271
|
+
"""
|
|
272
|
+
inp_paths = _str_to_list(inp)
|
|
273
|
+
env_vars = _str_to_list(env)
|
|
274
|
+
out_paths = _str_to_list(out)
|
|
275
|
+
vol_paths = _str_to_list(vol)
|
|
276
|
+
amended_env_vars = set()
|
|
277
|
+
with subs_env_vars() as subs:
|
|
278
|
+
inp_paths = [translate(subs(inp_path)) for inp_path in inp_paths]
|
|
279
|
+
out_paths = [translate(subs(out_path)) for out_path in out_paths]
|
|
280
|
+
vol_paths = [translate(subs(vol_path)) for vol_path in vol_paths]
|
|
281
|
+
workdir = translate(subs(workdir))
|
|
282
|
+
amend(env=sorted(amended_env_vars))
|
|
283
|
+
command = CaseSensitiveTemplate(command).safe_substitute(
|
|
284
|
+
inp=" ".join(myrelpath(inp_path, workdir) for inp_path in inp_paths),
|
|
285
|
+
out=" ".join(myrelpath(out_path, workdir) for out_path in out_paths),
|
|
286
|
+
vol=" ".join(myrelpath(vol_path, workdir) for vol_path in vol_paths),
|
|
287
|
+
)
|
|
288
|
+
return RPC_CLIENT(
|
|
289
|
+
"step",
|
|
290
|
+
_get_step_key(),
|
|
291
|
+
command,
|
|
292
|
+
inp_paths,
|
|
293
|
+
env_vars,
|
|
294
|
+
out_paths,
|
|
295
|
+
vol_paths,
|
|
296
|
+
workdir,
|
|
297
|
+
optional,
|
|
298
|
+
pool,
|
|
299
|
+
block,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def pool(name: str, size: int):
|
|
304
|
+
"""Define a pool with given size or change an existing pool size.
|
|
305
|
+
|
|
306
|
+
Parameters
|
|
307
|
+
----------
|
|
308
|
+
name
|
|
309
|
+
The name of the pool.
|
|
310
|
+
size
|
|
311
|
+
The pool size.
|
|
312
|
+
"""
|
|
313
|
+
RPC_CLIENT.call.pool(name, size)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def amend(
|
|
317
|
+
*,
|
|
318
|
+
inp: Collection[str] | str = (),
|
|
319
|
+
env: Collection[str] | str = (),
|
|
320
|
+
out: Collection[str] | str = (),
|
|
321
|
+
vol: Collection[str] | str = (),
|
|
322
|
+
) -> bool:
|
|
323
|
+
"""Specify additional inputs and outputs from within a running step.
|
|
324
|
+
|
|
325
|
+
Parameters
|
|
326
|
+
----------
|
|
327
|
+
inp
|
|
328
|
+
Files required by the step.
|
|
329
|
+
Can be files or directories (trailing slash).
|
|
330
|
+
env
|
|
331
|
+
Environment variables to which the step is sensitive.
|
|
332
|
+
If the change, or when they are (un)defined, the step digest will change,
|
|
333
|
+
such that the step is not skipped when these variables change.
|
|
334
|
+
out
|
|
335
|
+
Files created by the step.
|
|
336
|
+
Can be files or directories (trailing slash).
|
|
337
|
+
vol
|
|
338
|
+
Volatile files created by the step.
|
|
339
|
+
Can be files or directories (trailing slash).
|
|
340
|
+
|
|
341
|
+
Returns
|
|
342
|
+
-------
|
|
343
|
+
keep_going
|
|
344
|
+
True when the additional inputs are available and the step can safely use them.
|
|
345
|
+
False otherwise, meaning the step can exit early and will be rescheduled later.
|
|
346
|
+
|
|
347
|
+
Notes
|
|
348
|
+
-----
|
|
349
|
+
Environment variables in the `inp`, `out` and `vol` paths are substituted in the same way
|
|
350
|
+
as in the `step()` function. The used variables are added to the env_vars argument.
|
|
351
|
+
|
|
352
|
+
"""
|
|
353
|
+
inp_paths = _str_to_list(inp)
|
|
354
|
+
env_vars = _str_to_list(env)
|
|
355
|
+
out_paths = _str_to_list(out)
|
|
356
|
+
vol_paths = _str_to_list(vol)
|
|
357
|
+
if all(len(collection) == 0 for collection in [inp_paths, env_vars, out_paths, vol_paths]):
|
|
358
|
+
return True
|
|
359
|
+
env_vars = set(env_vars)
|
|
360
|
+
with subs_env_vars() as subs:
|
|
361
|
+
su_inp_paths = [subs(inp_path) for inp_path in inp_paths]
|
|
362
|
+
tr_inp_paths = [translate(inp_path) for inp_path in su_inp_paths]
|
|
363
|
+
tr_out_paths = [translate(subs(out_path)) for out_path in out_paths]
|
|
364
|
+
tr_vol_paths = [translate(subs(vol_path)) for vol_path in vol_paths]
|
|
365
|
+
keep_going = RPC_CLIENT(
|
|
366
|
+
"amend",
|
|
367
|
+
_get_step_key(),
|
|
368
|
+
tr_inp_paths,
|
|
369
|
+
sorted(env_vars),
|
|
370
|
+
tr_out_paths,
|
|
371
|
+
tr_vol_paths,
|
|
372
|
+
)
|
|
373
|
+
if keep_going:
|
|
374
|
+
check_inp_paths(su_inp_paths)
|
|
375
|
+
return keep_going
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
#
|
|
379
|
+
# Composite functions, created with the functions above.
|
|
380
|
+
#
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def plan(subdir: str, block: bool = False):
|
|
384
|
+
"""Run a `plan.py` script in a subdirectory.
|
|
385
|
+
|
|
386
|
+
Parameters
|
|
387
|
+
----------
|
|
388
|
+
subdir
|
|
389
|
+
The subdirectory in which another ``plan.py`` script can be found.
|
|
390
|
+
The file must be executable and have `#!/usr/bin/env python` as its first line.
|
|
391
|
+
block
|
|
392
|
+
When True, the step will always remain pending.
|
|
393
|
+
"""
|
|
394
|
+
with subs_env_vars() as subs:
|
|
395
|
+
subdir = subs(subdir)
|
|
396
|
+
path_subdir = Path(subdir)
|
|
397
|
+
path_plan = path_subdir / "plan.py"
|
|
398
|
+
step("./plan.py", inp=path_plan, workdir=subdir, block=block)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def copy(src: str, dst: str, optional: bool = False, block: bool = False):
|
|
402
|
+
"""Add a step that copies a file.
|
|
403
|
+
|
|
404
|
+
Parameters
|
|
405
|
+
----------
|
|
406
|
+
src
|
|
407
|
+
This must be a file. Environment variables are substituted.
|
|
408
|
+
dst
|
|
409
|
+
This can be a file or a directory. Environment variables are substituted.
|
|
410
|
+
If it is a directory, it must have a trailing slash.
|
|
411
|
+
optional
|
|
412
|
+
When True, the file is only copied when needed as input for another step.
|
|
413
|
+
block
|
|
414
|
+
When True, the step will always remain pending.
|
|
415
|
+
"""
|
|
416
|
+
amended_env_vars = set()
|
|
417
|
+
with subs_env_vars() as subs:
|
|
418
|
+
src = subs(src)
|
|
419
|
+
dst = subs(dst)
|
|
420
|
+
path_src = myrelpath(src)
|
|
421
|
+
path_dst = make_path_out(src, dst, None)
|
|
422
|
+
amend(env=amended_env_vars)
|
|
423
|
+
step("cp -aT ${inp} ${out}", inp=path_src, out=path_dst, optional=optional, block=block)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def mkdir(dirname: str, optional: bool = False, block: bool = False):
|
|
427
|
+
"""Make a directory.
|
|
428
|
+
|
|
429
|
+
Parameters
|
|
430
|
+
----------
|
|
431
|
+
dirname
|
|
432
|
+
The director to create. (Trailing slash is added if missing.)
|
|
433
|
+
Environment variables are substituted.
|
|
434
|
+
optional
|
|
435
|
+
When True, the directory is only created when needed by other steps.
|
|
436
|
+
block
|
|
437
|
+
When True, the step will always remain pending.
|
|
438
|
+
"""
|
|
439
|
+
amended_env_vars = set()
|
|
440
|
+
with subs_env_vars() as subs:
|
|
441
|
+
dirname = subs(dirname)
|
|
442
|
+
if not dirname.endswith("/"):
|
|
443
|
+
dirname += "/"
|
|
444
|
+
dirname = myrelpath(dirname)
|
|
445
|
+
amend(env=amended_env_vars)
|
|
446
|
+
step(f"mkdir -p {dirname}", out=dirname, optional=optional, block=block)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def getenv(name: str, default: str | None = None, is_path: bool = False) -> str | Path:
|
|
450
|
+
"""Get an environment variable and amend the current step with the variable name.
|
|
451
|
+
|
|
452
|
+
Parameters
|
|
453
|
+
----------
|
|
454
|
+
name
|
|
455
|
+
The name of the environment variable, which is retrieved with `os.getenv`.
|
|
456
|
+
default
|
|
457
|
+
The value to return when the environment variable is unset.
|
|
458
|
+
is_path
|
|
459
|
+
Set to True if the variable taken from the environment is assumed to be a path.
|
|
460
|
+
Shell variables are substituted (once) in such paths.
|
|
461
|
+
If the path is relative, it is assumed to be relative to the StepUp's working directory.
|
|
462
|
+
In this case, translated to become usable from the working directory of the caller.
|
|
463
|
+
|
|
464
|
+
Returns
|
|
465
|
+
-------
|
|
466
|
+
value
|
|
467
|
+
The value of the environment variable.
|
|
468
|
+
If `is_path` is set to `True`, this is a `Path` instance.
|
|
469
|
+
Otherwise, the result is a string.
|
|
470
|
+
"""
|
|
471
|
+
value = os.getenv(name, default)
|
|
472
|
+
names = [name]
|
|
473
|
+
if is_path:
|
|
474
|
+
value = Path(value)
|
|
475
|
+
if not value.isabs():
|
|
476
|
+
value = mynormpath(os.getenv("ROOT", ".") / Path(value))
|
|
477
|
+
names.append("ROOT")
|
|
478
|
+
with subs_env_vars() as subs:
|
|
479
|
+
value = subs(value)
|
|
480
|
+
amend(env=names)
|
|
481
|
+
return value
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def script(executable: str, workdir: str = "./", optional: bool = False, block: bool = False):
|
|
485
|
+
"""Run the executable with a single argument `plan` in a working directory.
|
|
486
|
+
|
|
487
|
+
Parameters
|
|
488
|
+
----------
|
|
489
|
+
executable
|
|
490
|
+
The path of a local executable that will be called with the argument `plan`.
|
|
491
|
+
The file must be executable.
|
|
492
|
+
workdir
|
|
493
|
+
The subdirectory in which the script is to be executed.
|
|
494
|
+
The path of the executable is assumed to be relative to this directory.
|
|
495
|
+
optional
|
|
496
|
+
When True, the steps planned by the executable are made optional.
|
|
497
|
+
The planing itself is never optional.
|
|
498
|
+
block
|
|
499
|
+
When True, the planning will always remain pending.
|
|
500
|
+
"""
|
|
501
|
+
with subs_env_vars() as subs:
|
|
502
|
+
executable = subs(executable)
|
|
503
|
+
workdir = subs(workdir)
|
|
504
|
+
path_workdir = Path(workdir)
|
|
505
|
+
path_script = path_workdir / executable
|
|
506
|
+
command = f"./{executable} plan"
|
|
507
|
+
if optional:
|
|
508
|
+
command += " --optional"
|
|
509
|
+
step(command, inp=[path_script], workdir=path_workdir, block=block)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
#
|
|
513
|
+
# API development utilities
|
|
514
|
+
#
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
@contextlib.contextmanager
|
|
518
|
+
def subs_env_vars() -> Iterator[Callable]:
|
|
519
|
+
"""A context manager for substituting environment variables and tracking the used variables.
|
|
520
|
+
|
|
521
|
+
The context manager yields a function, `subs`, which takes a string with variables and
|
|
522
|
+
returns the substituted form.
|
|
523
|
+
All used variables are recorded and sent to the director with `amend(env=...)`.
|
|
524
|
+
For example:
|
|
525
|
+
|
|
526
|
+
```python
|
|
527
|
+
with subs_env_vars() as subs:
|
|
528
|
+
path_inp = subs(path_inp)
|
|
529
|
+
path_out = subs(path_out)
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
This function may be used in other API functions to substitute environment variables in
|
|
533
|
+
all relevant paths.
|
|
534
|
+
"""
|
|
535
|
+
env_vars = set()
|
|
536
|
+
|
|
537
|
+
def subs(path: str | None) -> Path | None:
|
|
538
|
+
if path is None:
|
|
539
|
+
return None
|
|
540
|
+
template = CaseSensitiveTemplate(path)
|
|
541
|
+
if not template.is_valid():
|
|
542
|
+
raise ValueError("The path contains invalid shell variable identifiers.")
|
|
543
|
+
mapping = {}
|
|
544
|
+
for name in template.get_identifiers():
|
|
545
|
+
if name.startswith("*"):
|
|
546
|
+
mapping[name] = f"${{{name}}}"
|
|
547
|
+
else:
|
|
548
|
+
value = os.getenv(name)
|
|
549
|
+
if value is None:
|
|
550
|
+
raise ValueError(f"Undefined shell variable: {name}")
|
|
551
|
+
mapping[name] = value
|
|
552
|
+
env_vars.add(name)
|
|
553
|
+
result = path if len(mapping) == 0 else template.substitute(mapping)
|
|
554
|
+
return Path(result)
|
|
555
|
+
|
|
556
|
+
yield subs
|
|
557
|
+
amend(env=env_vars)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def translate(path: str) -> Path:
|
|
561
|
+
"""Normalize the path and, if relative, make it relative to `ROOT` by prepending `HERE`.
|
|
562
|
+
|
|
563
|
+
Parameters
|
|
564
|
+
----------
|
|
565
|
+
path
|
|
566
|
+
The path to translate.
|
|
567
|
+
|
|
568
|
+
Returns
|
|
569
|
+
-------
|
|
570
|
+
translated_path
|
|
571
|
+
A path that can be interpreted in the working directory of the StepUp director.
|
|
572
|
+
"""
|
|
573
|
+
path = mynormpath(path)
|
|
574
|
+
if not path.isabs():
|
|
575
|
+
here = os.getenv("HERE")
|
|
576
|
+
if here is not None:
|
|
577
|
+
path = mynormpath(here / path)
|
|
578
|
+
return path
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def check_inp_paths(inp_paths: Collection[Path]):
|
|
582
|
+
"""Check the validity of the input paths."""
|
|
583
|
+
for inp_path in inp_paths:
|
|
584
|
+
message = check_inp_path(inp_path)
|
|
585
|
+
if message is not None:
|
|
586
|
+
raise ValueError(f"{message}: {inp_path}")
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
#
|
|
590
|
+
# Internal stuff
|
|
591
|
+
#
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _get_rpc_client():
|
|
595
|
+
"""Try setting up a Synchronous RPC client or fall back to the dummy client if that fails."""
|
|
596
|
+
STEPUP_DIRECTOR_SOCKET = os.getenv("STEPUP_DIRECTOR_SOCKET")
|
|
597
|
+
if STEPUP_DIRECTOR_SOCKET is None:
|
|
598
|
+
STEPUP_ROOT = Path(os.getenv("STEPUP_ROOT", "./"))
|
|
599
|
+
path_tmpsock = STEPUP_ROOT / ".stepup/tmpsock.txt"
|
|
600
|
+
time = 0.0
|
|
601
|
+
for itry in range(5):
|
|
602
|
+
if time > 0:
|
|
603
|
+
print(f"WARNING: waiting {time} seconds for {path_tmpsock}")
|
|
604
|
+
sleep(time)
|
|
605
|
+
time *= 2
|
|
606
|
+
else:
|
|
607
|
+
time = 0.1
|
|
608
|
+
if path_tmpsock.is_file():
|
|
609
|
+
with open(path_tmpsock) as fh:
|
|
610
|
+
dir_sockets = Path(fh.read().strip())
|
|
611
|
+
if dir_sockets != "":
|
|
612
|
+
path_socket = dir_sockets / "director"
|
|
613
|
+
if path_socket.exists():
|
|
614
|
+
STEPUP_DIRECTOR_SOCKET = path_socket
|
|
615
|
+
break
|
|
616
|
+
if STEPUP_DIRECTOR_SOCKET is None:
|
|
617
|
+
print("STEPUP_DIRECTOR_SOCKET not set and .stepup/tmpsock.txt not valid.")
|
|
618
|
+
print("RPC calls are printed and have no effect.")
|
|
619
|
+
return DummySyncRPCClient()
|
|
620
|
+
return SocketSyncRPCClient(STEPUP_DIRECTOR_SOCKET)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
RPC_CLIENT = _get_rpc_client()
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _get_step_key():
|
|
627
|
+
stepup_step_key = os.getenv("STEPUP_STEP_KEY")
|
|
628
|
+
if stepup_step_key is None:
|
|
629
|
+
if isinstance(RPC_CLIENT, DummySyncRPCClient):
|
|
630
|
+
return "dummy:"
|
|
631
|
+
else:
|
|
632
|
+
raise RuntimeError("The STEPUP_STEP_KEY environment variable is not defined.")
|
|
633
|
+
return stepup_step_key
|