dyngle 0.4.3__py3-none-any.whl → 0.6.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.

Potentially problematic release.


This version of dyngle might be problematic. Click here for more details.

dyngle/__init__.py CHANGED
@@ -1,11 +1,14 @@
1
1
  from functools import cached_property
2
+ from pathlib import Path
2
3
  from wizlib.app import WizApp
3
4
  from wizlib.stream_handler import StreamHandler
4
5
  from wizlib.config_handler import ConfigHandler
5
6
  from wizlib.ui_handler import UIHandler
6
7
 
7
8
  from dyngle.command import DyngleCommand
9
+ from dyngle.error import DyngleError
8
10
  from dyngle.expression import expression
11
+ from dyngle.operation import Operation
9
12
 
10
13
 
11
14
  class DyngleApp(WizApp):
@@ -14,10 +17,59 @@ class DyngleApp(WizApp):
14
17
  name = 'dyngle'
15
18
  handlers = [StreamHandler, ConfigHandler, UIHandler]
16
19
 
20
+ # For possible upstreaming to WizLib, a mechanism to "import" configuration
21
+ # settings from external files.
22
+
23
+ @property
24
+ def _imported_configrations(self):
25
+ if not hasattr(self, '__imported_configurations'):
26
+ imports = self.config.get('dyngle-imports')
27
+ confs = []
28
+ if imports:
29
+ for filename in imports:
30
+ full_filename = Path(filename).expanduser()
31
+ confs.append(ConfigHandler(full_filename))
32
+ self.__imported_configurations = confs
33
+ return self.__imported_configurations
34
+
35
+ def _get_configuration_details(self, type: str):
36
+ label = f'dyngle-{type}'
37
+ details = {}
38
+ for conf in self._imported_configrations:
39
+ if (imported_details := conf.get(label)):
40
+ details |= imported_details
41
+ configured_details = self.config.get(label)
42
+ if configured_details:
43
+ details |= configured_details
44
+ return details
45
+
17
46
  @cached_property
18
- def expressions(self):
19
- expr_texts = self.config.get('dyngle-expressions')
20
- if expr_texts:
21
- return {k: expression(t) for k, t in expr_texts.items()}
22
- else:
23
- return {}
47
+ def operations(self):
48
+ operations_configs = self._get_configuration_details('operations')
49
+ if not operations_configs:
50
+ raise DyngleError("No operations defined in configuration")
51
+ operations = {}
52
+ for key, config in operations_configs.items():
53
+ if isinstance(config, list):
54
+ operation = Operation({}, config)
55
+ elif isinstance(config, dict):
56
+ local_expr_texts = config.get('expressions') or {}
57
+ local_expressions = _expressions_from_texts(local_expr_texts)
58
+ steps = config.get('steps') or []
59
+ operation = Operation(local_expressions, steps)
60
+ else:
61
+ raise DyngleError(f"Invalid operation configuration for {key}")
62
+ operations[key] = operation
63
+ return operations
64
+
65
+ @cached_property
66
+ def global_expressions(self):
67
+ expr_texts = self._get_configuration_details('expressions')
68
+ return _expressions_from_texts(expr_texts)
69
+
70
+
71
+ def _expressions_from_texts(expr_texts):
72
+ if expr_texts:
73
+ return {k: expression(t) for k, t in expr_texts.items()}
74
+ else:
75
+ return {}
@@ -29,15 +29,17 @@ class RunCommand(DyngleCommand):
29
29
  if self.operation not in operations:
30
30
  available_operations = ', '.join(operations.keys())
31
31
  raise DyngleError(
32
- f'Operation "{self.operation}" not found. " + \
33
- f"Available operations: {available_operations}')
32
+ f"Operation '{self.operation}' not found. " +
33
+ f"Available operations: {available_operations}")
34
34
 
35
35
  @DyngleCommand.wrap
36
36
  def execute(self):
37
- expressions = self.app.expressions
38
- operations = self.app.config.get('dyngle-operations')
37
+ operations = self.app.operations
39
38
  self._validate_operation_exists(operations)
40
- steps = operations[self.operation]
39
+ operation = operations[self.operation]
40
+ steps = operation.steps
41
+ expressions = self.app.global_expressions | \
42
+ operation.local_expressions
41
43
  data_string = self.app.stream.text
42
44
  data = safe_load(data_string) or {}
43
45
  data['args'] = self.args
dyngle/operation.py ADDED
@@ -0,0 +1,9 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class Operation:
6
+
7
+ local_expressions: dict
8
+
9
+ steps: list
dyngle/template.py CHANGED
@@ -2,6 +2,8 @@ from dataclasses import dataclass
2
2
  from functools import partial
3
3
  import re
4
4
 
5
+ from dyngle.error import DyngleError
6
+
5
7
 
6
8
  PATTERN = re.compile(r'\{\{\s*([^}]+)\s*\}\}')
7
9
 
@@ -44,6 +46,9 @@ class Template:
44
46
  parts = key.split('.')
45
47
  current = live_data
46
48
  for part in parts:
49
+ if part not in current:
50
+ raise DyngleError(
51
+ f"Invalid expression or data reference '{key}")
47
52
  current = current[part]
48
53
  if callable(current):
49
54
  return current(live_data)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dyngle
3
- Version: 0.4.3
3
+ Version: 0.6.0
4
4
  Summary: Run lightweight local workflows
5
5
  License: MIT
6
6
  Author: Steampunk Wizard
@@ -54,6 +54,18 @@ Dyngle reads configuration from YAML files. You can specify the config file loca
54
54
 
55
55
  The configuration has 2 parts: `operations:` and `expressions`.
56
56
 
57
+ Configuration files can import other configuration files, by providing an entry `imports:` with an array of filepaths. The most obvious example is a Dyngle config in a local directory which imports the user-level configuration.
58
+
59
+ ```yaml
60
+ dyngle:
61
+ imports:
62
+ - ~/.dyngle.yml
63
+ expressions:
64
+ operations:
65
+ ```
66
+
67
+ In the event of item name conflicts, expressions and operations are loaded from imports in the order specified, so imports lower in the array will override those higher up. The expressions and operations defined in the main file override the imports. Imports are not recursive.
68
+
57
69
  ## Data
58
70
 
59
71
  Dyngle maintains a block of data throughout operations, which is parsed from YAML in stdin.
@@ -143,6 +155,22 @@ dyngle:
143
155
  'Hello ' + author_name()
144
156
  ```
145
157
 
158
+ ## Local Expressions
159
+
160
+ Expressions can also be defined in a way that applies only to one operation - especially useful for command-line arguments.
161
+
162
+ In this case, the operation definition has a different structure. See the example below.
163
+
164
+ ```yaml
165
+ dyngle:
166
+ operations:
167
+ name_from_arg:
168
+ expressions:
169
+ local_name: "args[0]"
170
+ steps:
171
+ - echo "Hello {{local_name}}"
172
+ ```
173
+
146
174
  ## Security
147
175
 
148
176
  Commands are executed using Python's `subprocess.run()` with arguments split in a shell-like fashion. The shell is not used, which reduces the likelihood of shell injection attacks. However, note that Dyngle is not robust to malicious configuration. Use with caution.
@@ -0,0 +1,13 @@
1
+ dyngle/__init__.py,sha256=kCFjx6sIuf0hiuH2C4QyHIaC9DY6YKudvkiiziFybgk,2716
2
+ dyngle/__main__.py,sha256=pYRIwzix_AL8CdJaDDis_8yMBBWO2N72NNwkroo1dQo,95
3
+ dyngle/command/__init__.py,sha256=1S86gbef8MYvG-TWD5JRIWzFg7qV5xKhp9QXx9zEx5c,94
4
+ dyngle/command/run_command.py,sha256=s6hR4PHBXbZ9pfQ-thVmpcvSW5DP5rwSpz3aginaPJ8,1914
5
+ dyngle/error.py,sha256=CGcTa8L4O1qsHEYnzp_JBbkvntJTv2Qz46wj_TI8NLk,39
6
+ dyngle/expression.py,sha256=-uLVbrO8ovNZGGLNqMZWIy_StCK-0laZqcQ1gOPhU6w,3476
7
+ dyngle/operation.py,sha256=QCAUH2YAYq9dtCO49F_irWgIWxBDaxyDv4X7Xen04Jo,110
8
+ dyngle/safe_path.py,sha256=Hk2AhP6e3yKGh3kKrLLwhvAlMNx-j2jObBYJL-_doAU,3339
9
+ dyngle/template.py,sha256=AbgGCVi9B0K_SY_0BO0Q8vbFNvFM0aYAVBJoOE4N_pE,1612
10
+ dyngle-0.6.0.dist-info/METADATA,sha256=r511S0EBtCDb5SdfP6V8kuhcGYaAF1sxKfRSewmyt14,5524
11
+ dyngle-0.6.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
12
+ dyngle-0.6.0.dist-info/entry_points.txt,sha256=rekiGhtweiHKm9g1jdGb3FhzqDrk1kigJDeSNollZSA,48
13
+ dyngle-0.6.0.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- dyngle/__init__.py,sha256=drqd18F3T7LMTNqAU38WYjanczl1kANAA1vVBjbPEyU,653
2
- dyngle/__main__.py,sha256=pYRIwzix_AL8CdJaDDis_8yMBBWO2N72NNwkroo1dQo,95
3
- dyngle/command/__init__.py,sha256=1S86gbef8MYvG-TWD5JRIWzFg7qV5xKhp9QXx9zEx5c,94
4
- dyngle/command/run_command.py,sha256=fgso_ZadAkA0f1ZjSGAUHs32hCHnX3QE_abnAmvg77g,1854
5
- dyngle/error.py,sha256=CGcTa8L4O1qsHEYnzp_JBbkvntJTv2Qz46wj_TI8NLk,39
6
- dyngle/expression.py,sha256=-uLVbrO8ovNZGGLNqMZWIy_StCK-0laZqcQ1gOPhU6w,3476
7
- dyngle/safe_path.py,sha256=Hk2AhP6e3yKGh3kKrLLwhvAlMNx-j2jObBYJL-_doAU,3339
8
- dyngle/template.py,sha256=XM5VtOiuMVzNjMTsUdMMfugiygcOuAh-8MedfhEbGn0,1435
9
- dyngle-0.4.3.dist-info/METADATA,sha256=RpCvS1bRIN-Xcdyw2VIJ7j-zm-BAEVokemfUh52bpr0,4544
10
- dyngle-0.4.3.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
11
- dyngle-0.4.3.dist-info/entry_points.txt,sha256=rekiGhtweiHKm9g1jdGb3FhzqDrk1kigJDeSNollZSA,48
12
- dyngle-0.4.3.dist-info/RECORD,,
File without changes