automagix 4.0.0.dev1__tar.gz → 4.0.0.dev2__tar.gz

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 (31) hide show
  1. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/PKG-INFO +19 -11
  2. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/README.md +18 -10
  3. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/automagix.py +1 -1
  4. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/bash_completion.py +5 -6
  5. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/command.py +32 -10
  6. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/command_test.py +7 -2
  7. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/config.py +19 -34
  8. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/environment.py +31 -1
  9. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/helpers.py +24 -11
  10. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix.egg-info/PKG-INFO +19 -11
  11. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/setup.py +1 -1
  12. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/LICENSE +0 -0
  13. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/__init__.py +0 -0
  14. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/automagix_test.py +0 -0
  15. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/batch_runner.py +0 -0
  16. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/bundlewrap.py +0 -0
  17. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/colors.py +0 -0
  18. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/config_test.py +0 -0
  19. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/environment_test.py +0 -0
  20. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/logger.py +0 -0
  21. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/parallel.py +0 -0
  22. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/parallel_runner.py +0 -0
  23. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/parallel_ui.py +0 -0
  24. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/progress_bar.py +0 -0
  25. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix.egg-info/SOURCES.txt +0 -0
  26. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix.egg-info/dependency_links.txt +0 -0
  27. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix.egg-info/entry_points.txt +0 -0
  28. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix.egg-info/requires.txt +0 -0
  29. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix.egg-info/top_level.txt +0 -0
  30. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/setup.cfg +0 -0
  31. {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/tests/test_environment.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: automagix
3
- Version: 4.0.0.dev1
3
+ Version: 4.0.0.dev2
4
4
  Summary: Automation wrapper for bash and python commands
5
5
  Home-page: https://codeberg.org/vanadinit/automagix
6
6
  Author: Johannes Paul
@@ -83,10 +83,6 @@ Automagix requires Python ≥ 3.10.
83
83
  pip install automagix
84
84
  ```
85
85
 
86
- NOTICE: original `automagix` and `automagix_cmd` share the
87
- same main entrypoint. To avoid overwriting and confusion,
88
- you should have only installed **ONE** of them!
89
-
90
86
  # CONFIGURATION
91
87
 
92
88
  You can specify a path to a configuration YAML file via the
@@ -322,9 +318,14 @@ You can refer to these systems in the command pipeline in multiple ways:
322
318
  object (Bundlewrap nodes only, no groups)
323
319
 
324
320
  **vars** _(associative array)_
325
- : Define some vars. These are accessible in the command pipeline via
326
- {varname}. Note: Only valid Python variable names are allowed.
327
- In python actions you can access these variables directly via `VARS.varname`.
321
+ : Define some vars. Note: Only valid Python variable names are allowed.
322
+ To specify a variable type use `type|varname` where *type* is
323
+ one of *int, float, bool, str* and varname the variable name.
324
+ Automatix will try to convert an assigned value to the specified type.
325
+ **Note**: For the boolean type the string "False" and upper/lower-case
326
+ variations are converted to `False` deviating from Python's normal behavior.
327
+ Variables are accessible in the command pipeline via
328
+ {varname} (converted to *str*) and in python actions via `VARS.varname`.
328
329
 
329
330
  **secrets** _(associative array)_
330
331
  : Define teamvault secrets. Value has to be in this format:
@@ -387,8 +388,10 @@ Here you define the commands automagix shall execute.
387
388
  To do this prefix the desired variablename and = before the action
388
389
  key, e.g. `myvar=python: NODES.system.hostname`. Be careful when
389
390
  working with multiline statements. In **python** the first line is
390
- likely to set the variable. All variables will be converted to
391
- strings when used to build commands in following steps.
391
+ likely to set the variable.
392
+ Only for **local** and **remote** action: If a variable type was
393
+ specified in the **vars** section, Automatix will also try to
394
+ convert the value here.
392
395
 
393
396
  **CONDITIONS**: You can define the command only to be executed if
394
397
  your condition variable evaluates to "True" in Python. To achieve
@@ -398,6 +401,11 @@ Here you define the commands automagix shall execute.
398
401
  string which evaluates to "True" in Python, but empty output will
399
402
  evaluate to "False". Use `!?` instead of `?` to invert the condition.
400
403
 
404
+ **STATIC_FLAG**: If you do not want values to be replaced in your command,
405
+ you can suffix the command key with '-' to indicate a static command
406
+ value, e.g. `python-: f'{this} is not replaced'`. This is especially
407
+ useful for Python commands which use curly brackets themselves.
408
+
401
409
  **VALUE**: Your command. Variables will be replaced with Python
402
410
  format function. Therefore, use curly brackets to refer to variables,
403
411
  systems, secrets and constants.
@@ -413,7 +421,7 @@ In most cases its a good idea to define your command in quotes to
413
421
 
414
422
  #### Escaping in Pipeline
415
423
 
416
- Because automagix uses Python's format() function:
424
+ Because automagix uses Python's format() function (not for static commands):
417
425
  `{` -> `{{`
418
426
  `}` -> `}}`
419
427
 
@@ -47,10 +47,6 @@ Automagix requires Python ≥ 3.10.
47
47
  pip install automagix
48
48
  ```
49
49
 
50
- NOTICE: original `automagix` and `automagix_cmd` share the
51
- same main entrypoint. To avoid overwriting and confusion,
52
- you should have only installed **ONE** of them!
53
-
54
50
  # CONFIGURATION
55
51
 
56
52
  You can specify a path to a configuration YAML file via the
@@ -286,9 +282,14 @@ You can refer to these systems in the command pipeline in multiple ways:
286
282
  object (Bundlewrap nodes only, no groups)
287
283
 
288
284
  **vars** _(associative array)_
289
- : Define some vars. These are accessible in the command pipeline via
290
- {varname}. Note: Only valid Python variable names are allowed.
291
- In python actions you can access these variables directly via `VARS.varname`.
285
+ : Define some vars. Note: Only valid Python variable names are allowed.
286
+ To specify a variable type use `type|varname` where *type* is
287
+ one of *int, float, bool, str* and varname the variable name.
288
+ Automatix will try to convert an assigned value to the specified type.
289
+ **Note**: For the boolean type the string "False" and upper/lower-case
290
+ variations are converted to `False` deviating from Python's normal behavior.
291
+ Variables are accessible in the command pipeline via
292
+ {varname} (converted to *str*) and in python actions via `VARS.varname`.
292
293
 
293
294
  **secrets** _(associative array)_
294
295
  : Define teamvault secrets. Value has to be in this format:
@@ -351,8 +352,10 @@ Here you define the commands automagix shall execute.
351
352
  To do this prefix the desired variablename and = before the action
352
353
  key, e.g. `myvar=python: NODES.system.hostname`. Be careful when
353
354
  working with multiline statements. In **python** the first line is
354
- likely to set the variable. All variables will be converted to
355
- strings when used to build commands in following steps.
355
+ likely to set the variable.
356
+ Only for **local** and **remote** action: If a variable type was
357
+ specified in the **vars** section, Automatix will also try to
358
+ convert the value here.
356
359
 
357
360
  **CONDITIONS**: You can define the command only to be executed if
358
361
  your condition variable evaluates to "True" in Python. To achieve
@@ -362,6 +365,11 @@ Here you define the commands automagix shall execute.
362
365
  string which evaluates to "True" in Python, but empty output will
363
366
  evaluate to "False". Use `!?` instead of `?` to invert the condition.
364
367
 
368
+ **STATIC_FLAG**: If you do not want values to be replaced in your command,
369
+ you can suffix the command key with '-' to indicate a static command
370
+ value, e.g. `python-: f'{this} is not replaced'`. This is especially
371
+ useful for Python commands which use curly brackets themselves.
372
+
365
373
  **VALUE**: Your command. Variables will be replaced with Python
366
374
  format function. Therefore, use curly brackets to refer to variables,
367
375
  systems, secrets and constants.
@@ -377,7 +385,7 @@ In most cases its a good idea to define your command in quotes to
377
385
 
378
386
  #### Escaping in Pipeline
379
387
 
380
- Because automagix uses Python's format() function:
388
+ Because automagix uses Python's format() function (not for static commands):
381
389
  `{` -> `{{`
382
390
  `}` -> `}}`
383
391
 
@@ -78,7 +78,7 @@ class Automagix:
78
78
  return command_list
79
79
 
80
80
  def reload_script(self):
81
- self.script = get_script(args=self.env.cmd_args)
81
+ self.env.script = self.script = get_script(args=self.env.cmd_args)
82
82
  self._command_lists = {} # Clear cache
83
83
 
84
84
  def print_main_data(self):
@@ -4,7 +4,7 @@ from argparse import Action, Namespace
4
4
 
5
5
  from argcomplete import warn
6
6
 
7
- from .helpers import read_yaml, search_script
7
+ from .helpers import read_yaml, search_script, NoSuchScriptError, AmbiguousScriptError
8
8
 
9
9
 
10
10
  def _call(*args, **kwargs):
@@ -52,14 +52,13 @@ class ScriptFieldCompleter:
52
52
  return []
53
53
 
54
54
  s_file = search_script(name=parsed_args.scriptfile, script_dir=self.script_dir, non_interactive=True)
55
- if not s_file:
56
- warn('Script not found or multiple options. Cannot complete.')
57
- return []
58
-
59
55
  script = read_yaml(s_file)
60
- completion = [f'{key}=' for key in script.get(action.dest, {}).keys()]
56
+ completion = [f'{key.split("|", maxsplit=1)[-1]}=' for key in script.get(action.dest, {}).keys()]
61
57
 
62
58
  return completion
59
+ except (NoSuchScriptError, AmbiguousScriptError) as exc:
60
+ warn(f'{exc}. Cannot complete.')
61
+ return []
63
62
  except Exception as exc:
64
63
  warn(f'Shell completion failed: {repr(exc)}')
65
64
  return []
@@ -6,10 +6,9 @@ from dataclasses import dataclass
6
6
  from shlex import quote
7
7
  from time import time
8
8
 
9
- from .colors import italic, yellow
10
- from .environment import PipelineEnvironment, AttributedDict, AttributedDummyDict
9
+ from .colors import italic, yellow, green, red
10
+ from .environment import PipelineEnvironment, AttributedDict, AttributedDummyDict, ConversionError
11
11
  from .progress_bar import draw_progress_bar
12
- from .config import convert_boolean_from_input
13
12
 
14
13
  PERSISTENT_VARS = PVARS = AttributedDict()
15
14
 
@@ -81,6 +80,13 @@ class Command:
81
80
  for key, value in cmd.items():
82
81
  self.orig_key = key
83
82
  self.condition_var, self.assignment_var, self.key = parse_key(key=key)
83
+
84
+ if self.key.endswith('-'):
85
+ self.key = self.key[:-1]
86
+ self.static = True
87
+ else:
88
+ self.static = False
89
+
84
90
  if isinstance(value, dict):
85
91
  # We need this workaround because the yaml lib returns a dictionary instead of a string,
86
92
  # if there is nothing but a variable in the command. Alternative is to use quotes in the script yaml.
@@ -116,6 +122,9 @@ class Command:
116
122
  return 'localhost'
117
123
 
118
124
  def get_resolved_value(self, dummy: bool = False):
125
+ if self.static:
126
+ return self.value
127
+
119
128
  variables = self.env.vars.copy()
120
129
  variables['CONST'] = ConstantsWrapper(self.env.config['constants'])
121
130
  variables['SYSTEMS'] = SystemsWrapper(self.env.systems)
@@ -128,28 +137,41 @@ class Command:
128
137
 
129
138
  def show_and_change_variables(self):
130
139
  print()
131
- self.env.LOG.info('Variables:')
140
+ legend = f'definded type: {italic(green("match"))} - {italic(red("mismatch"))} - {italic(yellow("undefined"))}'
141
+ self.env.LOG.info(f'--- Variables ({legend}) ---')
142
+ print()
132
143
  keylen = max([len(key) for key in self.env.vars.keys()])
133
144
  typelen = len(italic(yellow(''))) + 4
134
145
  for key, value in self.env.vars.items():
135
- self.env.LOG.info(f" {key:<{keylen+1}}:{italic(yellow(type(value).__name__)):<{typelen}} {value}")
146
+ type_name = type(value).__name__
147
+ if key in self.env.script.get('_var_types'):
148
+ colored_type = green(type_name) if type_name == self.env.script['_var_types'][key] else red(type_name)
149
+ else:
150
+ colored_type = yellow(type_name)
151
+
152
+ self.env.LOG.info(f" {key:<{keylen + 1}}:{italic(colored_type):<{typelen}} {value}")
136
153
  print()
137
154
  self.env.LOG.info('To change/set variable write variable + "=" followed by value.')
138
155
  self.env.LOG.info('Example: var1=xyz')
139
156
  print()
140
157
  self.env.LOG.info('You can only change 1 variable at a time. Repeat if necessary.')
141
- self.env.LOG.info('Notice: All values are strings here! Use python debugging shell '
142
- 'and the VARS dictionary to assign values with other types.')
158
+ print()
159
+ self.env.LOG.info('Notice: All values are strings here by default!'
160
+ ' If specified in the vars section the value will be converted.'
161
+ ' Use Python debug console and the VARS dictionary to assign'
162
+ ' values with other types or more complex operations.')
143
163
  print()
144
164
  self.env.LOG.info('To not change anything just press "ENTER".')
145
165
  answer = self.env.interact('\n', progress_portion=self.progress_portion)
146
166
  try:
147
167
  key, value = answer.split('=', maxsplit=1)
148
- self.env.vars[key.strip()] = value.strip()
168
+ self.env.set_var(key=key.strip(), value=value.strip())
149
169
  self.env.LOG.info(f'Variable {key.strip()} = {value.strip()}')
150
170
  except ValueError:
151
171
  if answer:
152
172
  self.env.LOG.error('Input could not be parsed.')
173
+ except ConversionError as exc:
174
+ self.env.LOG.error(exc)
153
175
  self.print_command()
154
176
  print()
155
177
 
@@ -436,9 +458,9 @@ class Command:
436
458
  )
437
459
  output = proc.stdout.decode(self.env.config["encoding"])
438
460
  assigned_value = output.rstrip('\r\n')
439
- self.env.vars[self.assignment_var] = convert_boolean_from_input(assigned_value)
461
+ self.env.set_var(key=self.assignment_var, value=assigned_value)
440
462
  hint = ' (trailing newline removed)' if (output.endswith('\n') or output.endswith('\r')) else ''
441
- self.env.LOG.info(f'Variable {self.assignment_var} = "{self.env.vars[self.assignment_var]}"{hint}')
463
+ self.env.LOG.info(f'Variable {self.assignment_var} = "{assigned_value}"{hint}')
442
464
  else:
443
465
  proc = subprocess.run(
444
466
  cmd,
@@ -142,18 +142,21 @@ def test__show_and_change_variables():
142
142
  'cond': '{cond}',
143
143
  'cond2': '{cond2}',
144
144
  'some_var': '{some_var}',
145
+ 'myvar2': 5.,
146
+ 'mybool': False,
145
147
  }
146
148
 
147
149
  with mock.patch.object(
148
150
  cmd.env,
149
151
  'interact',
150
- side_effect=['var1=xyz', 'myvar=hallo', ' cond2 = !dgkls=432 \n\t ']
152
+ side_effect=['var1=xyz', 'myvar=hallo', ' cond2 = !dgkls=432 \n\t ', 'mybool=a']
151
153
  ) as mock_interact:
152
154
  cmd.show_and_change_variables()
153
155
  cmd.show_and_change_variables()
154
156
  cmd.show_and_change_variables()
157
+ cmd.show_and_change_variables()
155
158
 
156
- assert mock_interact.call_count == 3
159
+ assert mock_interact.call_count == 4
157
160
  assert cmd.env.vars == {
158
161
  'a': '{a}',
159
162
  'myvar': 'hallo',
@@ -161,4 +164,6 @@ def test__show_and_change_variables():
161
164
  'cond2': '!dgkls=432',
162
165
  'var1': 'xyz',
163
166
  'some_var': '{some_var}',
167
+ 'myvar2': 5.,
168
+ 'mybool': True,
164
169
  }
@@ -166,11 +166,7 @@ def _overwrite(script: dict, key: str, data: list[str]):
166
166
  script.setdefault(key, {})
167
167
  for item in data:
168
168
  k, v = item.split('=')
169
- # Only convert booleans in vars, not in systems or secrets
170
- if key == 'vars':
171
- script[key][k] = convert_boolean_from_input(v)
172
- else:
173
- script[key][k] = v
169
+ script[key][k] = v
174
170
 
175
171
 
176
172
  def _tupelize(string) -> tuple:
@@ -183,30 +179,13 @@ def _tupelize(string) -> tuple:
183
179
  return tuple(result)
184
180
 
185
181
 
186
- def get_script_path(name: str):
187
- s_file = name
188
- LOG.debug(f'Scriptfile input: {s_file}')
189
- # First try relative path
190
- if not os.path.isfile(s_file):
191
- LOG.debug('Script not found at relative path')
192
- s_file = f'{SCRIPT_DIR}/{name}'
193
- # Second try relative path from SCRIPT_DIR
194
- if not os.path.isfile(s_file):
195
- LOG.debug('Script not found at relative path from SCRIPT_DIR')
196
- # Third search and offer selection
197
- s_file = search_script(name=name, script_dir=SCRIPT_DIR)
198
- if not s_file:
199
- LOG.debug('Script not found at all.')
200
- raise ValueError('Script not found')
201
- LOG.notice(f'Script found at {s_file}.')
202
- return s_file
203
-
204
-
205
182
  def get_script(args: argparse.Namespace) -> dict:
206
- s_file = get_script_path(name=args.scriptfile)
183
+ s_file = search_script(name=args.scriptfile, script_dir=SCRIPT_DIR)
184
+ LOG.notice(f'Script found at {s_file}.')
207
185
 
208
186
  try:
209
187
  script = read_yaml(s_file)
188
+ collect_var_types(script)
210
189
  validate_script(script)
211
190
  except Exception:
212
191
  LOG.exception('Script validation failed! Please fix syntax before retrying!')
@@ -227,6 +206,20 @@ def get_script(args: argparse.Namespace) -> dict:
227
206
  return script
228
207
 
229
208
 
209
+ def collect_var_types(script: dict):
210
+ script['_var_types'] = {}
211
+ for variable_name in list(script.get('vars')):
212
+ if match := re.match(r'(\w+)\|(.*)', variable_name):
213
+ v_name = match.group(2)
214
+ v_type = match.group(1)
215
+ if v_type not in ['int', 'float', 'bool', 'str']:
216
+ raise ValidationError(f'[vars:{v_name}] Unknown type "{v_type}" not allowed.'
217
+ f' Known types are "int", "float", "bool", "str".')
218
+ script['_var_types'][v_name] = v_type
219
+ script['vars'][v_name] = script['vars'][variable_name]
220
+ del script['vars'][variable_name]
221
+
222
+
230
223
  def check_deprecated_syntax(ckey: str, entry: str, script: dict, prefix: str) -> int:
231
224
  warn = 0
232
225
  if isinstance(entry, dict):
@@ -389,15 +382,7 @@ def update_script_from_row(row: dict, script: dict, index: int):
389
382
  raise ValidationError(
390
383
  f'First row in CSV: Field name is \'{key_type}\','
391
384
  f' but has to be one of {list(SCRIPT_FIELDS.keys())}.')
392
-
393
- script[key_type][key_name] = convert_boolean_from_input(value=value)
394
-
395
-
396
- def convert_boolean_from_input(value: str) -> bool | str:
397
- """Convert "true"/"false" strings to boolean, leave everything else as is."""
398
- if isinstance(value, str) and value.lower() in ('false', 'true'):
399
- return value.lower() == 'true'
400
- return value
385
+ script[key_type][key_name] = value
401
386
 
402
387
 
403
388
  class UnknownSecretTypeException(Exception):
@@ -1,6 +1,7 @@
1
1
  from argparse import Namespace
2
2
  from logging import getLogger
3
3
  from pathlib import Path
4
+ from typing import Any
4
5
 
5
6
  from .config import init_logger
6
7
  from .helpers import empty_queued_input_data
@@ -46,7 +47,10 @@ class PipelineEnvironment:
46
47
  ):
47
48
  self.config = config
48
49
  self.script = script
49
- self.vars = AttributedDict(variables)
50
+ self.vars = AttributedDict()
51
+ for k, v in variables.items():
52
+ self.set_var(key=k, value=v)
53
+
50
54
  self.batch_index = batch_index
51
55
  self.cmd_args = cmd_args
52
56
 
@@ -83,3 +87,29 @@ class PipelineEnvironment:
83
87
  if progress_portion is not None and self.config['progress_bar']:
84
88
  draw_progress_bar(progress_portion)
85
89
  return answer
90
+
91
+ def set_var(self, key: str, value: Any) -> None:
92
+ if key not in self.script.get('_var_types', {}):
93
+ self.vars[key] = value
94
+ return
95
+
96
+ var_type = self.script['_var_types'][key]
97
+ try:
98
+ match var_type:
99
+ case 'int':
100
+ self.vars[key] = int(value)
101
+ case 'float':
102
+ self.vars[key] = float(value)
103
+ case 'bool':
104
+ if isinstance(value, str) and value.lower() == 'false':
105
+ self.vars[key] = False
106
+ return
107
+ self.vars[key] = bool(value)
108
+ case 'str':
109
+ self.vars[key] = str(value)
110
+ except ValueError as exc:
111
+ raise ConversionError(f'Variable conversion failed: Could not convert {key}:"{value}" to {var_type}') from exc
112
+
113
+
114
+ class ConversionError(Exception):
115
+ pass
@@ -1,10 +1,11 @@
1
- import os
1
+ from os import walk, mkdir, rmdir
2
+ from os.path import abspath, join, isfile
2
3
  from sys import stdin
4
+ from termios import tcflush, TCIFLUSH
3
5
  from time import sleep
4
6
  from typing import List
5
7
 
6
8
  import yaml
7
- from termios import tcflush, TCIFLUSH
8
9
 
9
10
  yaml.warnings({'YAMLLoadWarning': False})
10
11
 
@@ -42,15 +43,27 @@ def selector(entries: List[tuple], message: str = 'Found multiple entries, pleas
42
43
 
43
44
 
44
45
  def search_script(name: str, script_dir: str, non_interactive: bool = False) -> str | None:
45
- paths = []
46
- for dirpath, dirnames, filenames in os.walk(script_dir):
46
+ paths: set[tuple[str, str]] = set() # Second str is the label for the selector
47
+ if isfile(name):
48
+ return abspath(name)
49
+ for dirpath, dirnames, filenames in walk(script_dir):
47
50
  for filename in filenames:
48
51
  if filename == name:
49
- path = str(os.path.join(dirpath, filename))
50
- paths.append((path, path)) # Second one is the label for the selector
51
- if non_interactive and len(paths) != 1:
52
- return None
53
- return selector(entries=paths, message='Script found at multiple locations. Please choose:')
52
+ path = str(abspath(join(dirpath, filename)))
53
+ paths.add((path, path))
54
+ if not paths:
55
+ raise NoSuchScriptError('Script not found')
56
+ if non_interactive and len(paths) > 1:
57
+ raise AmbiguousScriptError('Multiple scripts found')
58
+ return selector(entries=list(paths), message='Script found at multiple locations. Please choose:')
59
+
60
+
61
+ class NoSuchScriptError(Exception):
62
+ pass
63
+
64
+
65
+ class AmbiguousScriptError(Exception):
66
+ pass
54
67
 
55
68
 
56
69
  class FileWithLock:
@@ -72,7 +85,7 @@ class FileWithLock:
72
85
  def get_lock(file_path: str):
73
86
  while True:
74
87
  try:
75
- os.mkdir(f'{file_path}.lock')
88
+ mkdir(f'{file_path}.lock')
76
89
  except FileExistsError:
77
90
  sleep(1)
78
91
  continue
@@ -80,4 +93,4 @@ def get_lock(file_path: str):
80
93
 
81
94
 
82
95
  def release_lock(file_path: str):
83
- os.rmdir(f'{file_path}.lock')
96
+ rmdir(f'{file_path}.lock')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: automagix
3
- Version: 4.0.0.dev1
3
+ Version: 4.0.0.dev2
4
4
  Summary: Automation wrapper for bash and python commands
5
5
  Home-page: https://codeberg.org/vanadinit/automagix
6
6
  Author: Johannes Paul
@@ -83,10 +83,6 @@ Automagix requires Python &ge; 3.10.
83
83
  pip install automagix
84
84
  ```
85
85
 
86
- NOTICE: original `automagix` and `automagix_cmd` share the
87
- same main entrypoint. To avoid overwriting and confusion,
88
- you should have only installed **ONE** of them!
89
-
90
86
  # CONFIGURATION
91
87
 
92
88
  You can specify a path to a configuration YAML file via the
@@ -322,9 +318,14 @@ You can refer to these systems in the command pipeline in multiple ways:
322
318
  object (Bundlewrap nodes only, no groups)
323
319
 
324
320
  **vars** _(associative array)_
325
- : Define some vars. These are accessible in the command pipeline via
326
- {varname}. Note: Only valid Python variable names are allowed.
327
- In python actions you can access these variables directly via `VARS.varname`.
321
+ : Define some vars. Note: Only valid Python variable names are allowed.
322
+ To specify a variable type use `type|varname` where *type* is
323
+ one of *int, float, bool, str* and varname the variable name.
324
+ Automatix will try to convert an assigned value to the specified type.
325
+ **Note**: For the boolean type the string "False" and upper/lower-case
326
+ variations are converted to `False` deviating from Python's normal behavior.
327
+ Variables are accessible in the command pipeline via
328
+ {varname} (converted to *str*) and in python actions via `VARS.varname`.
328
329
 
329
330
  **secrets** _(associative array)_
330
331
  : Define teamvault secrets. Value has to be in this format:
@@ -387,8 +388,10 @@ Here you define the commands automagix shall execute.
387
388
  To do this prefix the desired variablename and = before the action
388
389
  key, e.g. `myvar=python: NODES.system.hostname`. Be careful when
389
390
  working with multiline statements. In **python** the first line is
390
- likely to set the variable. All variables will be converted to
391
- strings when used to build commands in following steps.
391
+ likely to set the variable.
392
+ Only for **local** and **remote** action: If a variable type was
393
+ specified in the **vars** section, Automatix will also try to
394
+ convert the value here.
392
395
 
393
396
  **CONDITIONS**: You can define the command only to be executed if
394
397
  your condition variable evaluates to "True" in Python. To achieve
@@ -398,6 +401,11 @@ Here you define the commands automagix shall execute.
398
401
  string which evaluates to "True" in Python, but empty output will
399
402
  evaluate to "False". Use `!?` instead of `?` to invert the condition.
400
403
 
404
+ **STATIC_FLAG**: If you do not want values to be replaced in your command,
405
+ you can suffix the command key with '-' to indicate a static command
406
+ value, e.g. `python-: f'{this} is not replaced'`. This is especially
407
+ useful for Python commands which use curly brackets themselves.
408
+
401
409
  **VALUE**: Your command. Variables will be replaced with Python
402
410
  format function. Therefore, use curly brackets to refer to variables,
403
411
  systems, secrets and constants.
@@ -413,7 +421,7 @@ In most cases its a good idea to define your command in quotes to
413
421
 
414
422
  #### Escaping in Pipeline
415
423
 
416
- Because automagix uses Python's format() function:
424
+ Because automagix uses Python's format() function (not for static commands):
417
425
  `{` -> `{{`
418
426
  `}` -> `}}`
419
427
 
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setup(
7
7
  name='automagix',
8
- version='4.0.0.dev1',
8
+ version='4.0.0.dev2',
9
9
  description='Automation wrapper for bash and python commands',
10
10
  keywords=['bash', 'shell', 'command', 'automation', 'process', 'wrapper', 'devops', 'system administration'],
11
11
  long_description=long_description,
File without changes
File without changes