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.
@@ -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")
@@ -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