dyngle 0.6.0__py3-none-any.whl → 0.7.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/command/run_command.py +6 -13
- dyngle/operation.py +51 -0
- dyngle/template.py +1 -1
- dyngle-0.7.0.dist-info/METADATA +276 -0
- {dyngle-0.6.0.dist-info → dyngle-0.7.0.dist-info}/RECORD +7 -7
- dyngle-0.6.0.dist-info/METADATA +0 -185
- {dyngle-0.6.0.dist-info → dyngle-0.7.0.dist-info}/WHEEL +0 -0
- {dyngle-0.6.0.dist-info → dyngle-0.7.0.dist-info}/entry_points.txt +0 -0
dyngle/command/run_command.py
CHANGED
|
@@ -34,21 +34,14 @@ class RunCommand(DyngleCommand):
|
|
|
34
34
|
|
|
35
35
|
@DyngleCommand.wrap
|
|
36
36
|
def execute(self):
|
|
37
|
-
operations = self.app.operations
|
|
38
|
-
self._validate_operation_exists(operations)
|
|
39
|
-
operation = operations[self.operation]
|
|
40
|
-
steps = operation.steps
|
|
41
|
-
expressions = self.app.global_expressions | \
|
|
42
|
-
operation.local_expressions
|
|
43
37
|
data_string = self.app.stream.text
|
|
44
38
|
data = safe_load(data_string) or {}
|
|
45
39
|
data['args'] = self.args
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
f'Task failed with code {result.returncode}: {step}')
|
|
40
|
+
|
|
41
|
+
operations = self.app.operations
|
|
42
|
+
self._validate_operation_exists(operations)
|
|
43
|
+
operation = operations[self.operation]
|
|
44
|
+
|
|
45
|
+
operation.run(data, self.app.global_expressions)
|
|
53
46
|
|
|
54
47
|
return f'Operation "{self.operation}" completed successfully'
|
dyngle/operation.py
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
+
from functools import cached_property
|
|
3
|
+
import re
|
|
4
|
+
import shlex
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
from dyngle.error import DyngleError
|
|
8
|
+
from dyngle.template import Template
|
|
2
9
|
|
|
3
10
|
|
|
4
11
|
@dataclass
|
|
@@ -7,3 +14,47 @@ class Operation:
|
|
|
7
14
|
local_expressions: dict
|
|
8
15
|
|
|
9
16
|
steps: list
|
|
17
|
+
|
|
18
|
+
def run(self, data: dict, global_expressions: dict):
|
|
19
|
+
# The data dict is mutable
|
|
20
|
+
steps = self.steps
|
|
21
|
+
expressions = global_expressions | self.local_expressions
|
|
22
|
+
for markup in steps:
|
|
23
|
+
step = Step(markup)
|
|
24
|
+
step.run(data, expressions)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
STEP_PATTERN = re.compile(
|
|
28
|
+
r'^\s*(?:([\w.-]+)\s+->\s+)?(.+?)(?:\s+=>\s+([\w.-]+))?\s*$')
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_step(markup):
|
|
32
|
+
if match := STEP_PATTERN.match(markup):
|
|
33
|
+
input, command_text, output = match.groups()
|
|
34
|
+
command_template = shlex.split(command_text.strip())
|
|
35
|
+
return input, command_template, output
|
|
36
|
+
else:
|
|
37
|
+
raise DyngleError(f"Invalid step markup {{markup}}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class Step:
|
|
42
|
+
|
|
43
|
+
markup: str
|
|
44
|
+
|
|
45
|
+
def __post_init__(self):
|
|
46
|
+
self.input, self.command_template, self.output = \
|
|
47
|
+
parse_step(self.markup)
|
|
48
|
+
|
|
49
|
+
def run(self, data, expressions):
|
|
50
|
+
command = [Template(word).render(data, expressions)
|
|
51
|
+
for word in self.command_template]
|
|
52
|
+
pipes = {}
|
|
53
|
+
if self.output:
|
|
54
|
+
pipes['stdout'] = subprocess.PIPE
|
|
55
|
+
result = subprocess.run(command, text=True, **pipes)
|
|
56
|
+
if result.returncode != 0:
|
|
57
|
+
raise DyngleError(
|
|
58
|
+
f'Step failed with code {result.returncode}: {self.markup}')
|
|
59
|
+
if self.output:
|
|
60
|
+
data[self.output] = result.stdout
|
dyngle/template.py
CHANGED
|
@@ -48,7 +48,7 @@ class Template:
|
|
|
48
48
|
for part in parts:
|
|
49
49
|
if part not in current:
|
|
50
50
|
raise DyngleError(
|
|
51
|
-
f"Invalid expression or data reference '{key}")
|
|
51
|
+
f"Invalid expression or data reference '{key}'")
|
|
52
52
|
current = current[part]
|
|
53
53
|
if callable(current):
|
|
54
54
|
return current(live_data)
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: dyngle
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Run lightweight local workflows
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Steampunk Wizard
|
|
7
|
+
Author-email: dyngle@steamwiz.io
|
|
8
|
+
Requires-Python: >=3.11,<3.12
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
13
|
+
Requires-Dist: wizlib (>=3.1.4,<4.0.0)
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# Dyngle
|
|
17
|
+
|
|
18
|
+
Use cases
|
|
19
|
+
|
|
20
|
+
- A task runner
|
|
21
|
+
- A lightweight workflow engine
|
|
22
|
+
- A replacement for Make in Python projects
|
|
23
|
+
- A replacement for short functions in RC files
|
|
24
|
+
- Freedom from quirky Bash syntax
|
|
25
|
+
|
|
26
|
+
Technical foundations
|
|
27
|
+
|
|
28
|
+
- Configuration, task definition, and flow control in YAML
|
|
29
|
+
- Operations as system commands using a familiar shell-like syntax
|
|
30
|
+
- Expressions and logic in pure Python
|
|
31
|
+
|
|
32
|
+
## Quick installation (MacOS)
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
brew install python@3.11
|
|
36
|
+
python3.11 -m pip install pipx
|
|
37
|
+
pipx install dyngle
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Getting started
|
|
41
|
+
|
|
42
|
+
Create a file `.dyngle.yml`:
|
|
43
|
+
|
|
44
|
+
```yaml
|
|
45
|
+
dyngle:
|
|
46
|
+
operations:
|
|
47
|
+
hello:
|
|
48
|
+
- echo "Hello world"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Run an operation:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
dyngle run hello
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Configuration
|
|
58
|
+
|
|
59
|
+
Dyngle reads configuration from YAML files. You can specify the config file location using:
|
|
60
|
+
|
|
61
|
+
- `--config` command line option
|
|
62
|
+
- `DYNGLE_CONFIG` environment variable
|
|
63
|
+
- `.dyngle.yml` in current directory
|
|
64
|
+
- `~/.dyngle.yml` in home directory
|
|
65
|
+
|
|
66
|
+
## Operations
|
|
67
|
+
|
|
68
|
+
Operations are defined under `dyngle:` in the configuration. In its simplest form, an Operation takes the form of a YAML array defining the steps, as a system command with space-separated arguments. In that sense, a Dyngle operation looks something akin to a "PHONY" Make target, a short Bash script, or a CI/CD job. As a serious example, consider the `init` operation from the Dyngle configuration delivered with the project's source code.
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
dyngle:
|
|
72
|
+
operations:
|
|
73
|
+
init:
|
|
74
|
+
- rm -rf .venv
|
|
75
|
+
- python3.11 -m venv .venv
|
|
76
|
+
- .venv/bin/pip install --upgrade pip poetry
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The elements of the YAML array _look_ like lines of Bash, but Dyngle replaces the shell with YAML-based flow control and Python-based expressions (described below). So shell-specific operators such as `|`, `>`, and `$VARIABLES` won't work.
|
|
80
|
+
|
|
81
|
+
## Data and Templates
|
|
82
|
+
|
|
83
|
+
Dyngle maintains a block of Data throughout an operation, which is a set of named values (Python `dict`, YAML "mapping"). The values are usually strings but can also be other data types that are valid in both YAML and Python.
|
|
84
|
+
|
|
85
|
+
The `dyngle run` command feeds the contents of stdin to the Operation as Data, by converting a YAML mapping to named Python values. The values may be substituted into commands or arguments in Steps using double-curly-bracket syntax (`{{` and `}}`) similar to Jinja2.
|
|
86
|
+
|
|
87
|
+
For example, consider the following configuration:
|
|
88
|
+
|
|
89
|
+
``` yaml
|
|
90
|
+
dyngle:
|
|
91
|
+
operations:
|
|
92
|
+
hello:
|
|
93
|
+
- echo "Hello {{name}}!"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Cram some YAML into stdin to try it in your shell:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
echo "name: Francis" | dyngle run hello
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The output will say:
|
|
103
|
+
|
|
104
|
+
```text
|
|
105
|
+
Hello Francis!
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Expressions
|
|
109
|
+
|
|
110
|
+
Operations may contain Expressions, written in Python, that can be referenced in operation steps using the same syntax as for Data. In the case of a naming conflict, an Expression takes precedence over Data with the same name. Expressions can reference names in the Data directly.
|
|
111
|
+
|
|
112
|
+
Expressions may be defined in either of two ways in the configuration:
|
|
113
|
+
|
|
114
|
+
1. Global Expressions, under the `dyngle:` mapping, using the `expressions:` key.
|
|
115
|
+
2. Local Expressions, within a single Operation, in which case the Steps of the operation require a `steps:` key.
|
|
116
|
+
|
|
117
|
+
Here's an example of a global Expression
|
|
118
|
+
|
|
119
|
+
```yaml
|
|
120
|
+
dyngle:
|
|
121
|
+
expressions:
|
|
122
|
+
count: len(name)
|
|
123
|
+
operations:
|
|
124
|
+
say-hello:
|
|
125
|
+
- echo "Hello {{name}}! Your name has {{count}} characters."
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
For completeness, consider the following example using a local Expression for the same purpose.
|
|
129
|
+
|
|
130
|
+
```yaml
|
|
131
|
+
dyngle:
|
|
132
|
+
operations:
|
|
133
|
+
say-hello:
|
|
134
|
+
expressions:
|
|
135
|
+
count: len(name)
|
|
136
|
+
steps:
|
|
137
|
+
- echo "Hello {{name}}! Your name has {{count}} characters."
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Expressions can use a controlled subset of the Python standard library, including:
|
|
141
|
+
|
|
142
|
+
- Built-in data types such as `str()`
|
|
143
|
+
- Essential built-in functions such as `len()`
|
|
144
|
+
- The core modules from the `datetime` package (but some methods such as `strftime()` will fail)
|
|
145
|
+
- A specialized function called `formatted()` to perform string formatting operations on a `datetime` object
|
|
146
|
+
- A restricted version of `Path()` that only operates within the current working directory
|
|
147
|
+
- Various other useful utilities, mostly read-only, such as the `math` module
|
|
148
|
+
- A special function called `resolve` which resolves data expressions using the same logic as in templates
|
|
149
|
+
- An array `args` containing arguments passed to the `dyngle run` command after the Operation name
|
|
150
|
+
|
|
151
|
+
**NOTE** Some capabilities of the Expression namespace might be limited in the future. The goal is support purely read-only operations within Expressions.
|
|
152
|
+
|
|
153
|
+
Expressions behave like functions that take no arguments, using the Data as a namespace. So Expressions reference Data directly as local names in Python.
|
|
154
|
+
|
|
155
|
+
YAML keys can contain hyphens, which are fully supported in Dyngle. To reference a hyphenated key in an Expression, choose:
|
|
156
|
+
|
|
157
|
+
- Reference the name using underscores instead of hyphens (they are automatically replaced), OR
|
|
158
|
+
- Use the built-in special-purpose `resolve()` function (which can also be used to reference other expressions)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
```yaml
|
|
163
|
+
dyngle:
|
|
164
|
+
expressions:
|
|
165
|
+
say-hello: >-
|
|
166
|
+
'Hello ' + full_name + '!'
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
... or using the `resolve()` function, which also allows expressions to essentially call other expressions, using the same underlying data set.
|
|
170
|
+
|
|
171
|
+
```yaml
|
|
172
|
+
dyngle:
|
|
173
|
+
expressions:
|
|
174
|
+
hello: >-
|
|
175
|
+
'Hello ' + resolve('formal-name') + '!'
|
|
176
|
+
formal-name: >-
|
|
177
|
+
'Ms. ' + full_name
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Note it's also _possible_ to call other expressions by name as functions, if they only return hard-coded values (i.e. constants).
|
|
181
|
+
|
|
182
|
+
```yaml
|
|
183
|
+
dyngle:
|
|
184
|
+
expressions:
|
|
185
|
+
author-name: Francis Potter
|
|
186
|
+
author-hello: >-
|
|
187
|
+
'Hello ' + author_name()
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Here are some slightly more sophisticated exercises using Expression reference syntax:
|
|
191
|
+
|
|
192
|
+
```yaml
|
|
193
|
+
dyngle:
|
|
194
|
+
operations:
|
|
195
|
+
reference-hyphenated-data-key:
|
|
196
|
+
expressions:
|
|
197
|
+
spaced-name: "' '.join([x for x in first_name])"
|
|
198
|
+
count-name: len(resolve('first-name'))
|
|
199
|
+
x-name: "'X' * int(resolve('count-name'))"
|
|
200
|
+
steps:
|
|
201
|
+
- echo "Your name is {{first-name}} with {{count-name}} characters, but I will call you '{{spaced-name}}' or maybe '{{x-name}}'"
|
|
202
|
+
reference-expression-using-function-syntax:
|
|
203
|
+
expressions:
|
|
204
|
+
name: "'George'"
|
|
205
|
+
works: "name()"
|
|
206
|
+
double: "name * 2"
|
|
207
|
+
fails: double()
|
|
208
|
+
steps:
|
|
209
|
+
- echo "It works to call you {{works}}"
|
|
210
|
+
# - echo "I have trouble calling you {{fails}}"
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Finally, here's an example using args:
|
|
214
|
+
|
|
215
|
+
```yaml
|
|
216
|
+
dyngle:
|
|
217
|
+
operations:
|
|
218
|
+
name-from-arg:
|
|
219
|
+
expressions:
|
|
220
|
+
name: "args[0]"
|
|
221
|
+
steps:
|
|
222
|
+
- echo "Hello {{name}}"
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Data assignment operator
|
|
226
|
+
|
|
227
|
+
The Steps parser supports one special operator which assigns the output (stdout) of its command to a Data field for use in subsequent steps.
|
|
228
|
+
|
|
229
|
+
The operator is `=>` and must go after the command and its arguments. Follow the operator with the name of the Data key to assign.
|
|
230
|
+
|
|
231
|
+
Example:
|
|
232
|
+
|
|
233
|
+
```yaml
|
|
234
|
+
dyngle:
|
|
235
|
+
operations:
|
|
236
|
+
today:
|
|
237
|
+
expressions:
|
|
238
|
+
just-the-date: resolve('full-date')[0:10]
|
|
239
|
+
steps:
|
|
240
|
+
- date => full-date
|
|
241
|
+
- echo "Today is {{just-the-date}}"
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
(Note Dyngle does provide Python date operations in the Expression namespace, which might provide a better way to perform the same operation as this example, but it suffices to demonstrate assignment)
|
|
245
|
+
|
|
246
|
+
## Lifecycle
|
|
247
|
+
|
|
248
|
+
The lifecycle of an operation is:
|
|
249
|
+
|
|
250
|
+
1. Load Data if it exists from YAML on stdin (if no tty)
|
|
251
|
+
2. Find the named Operation in the configuration
|
|
252
|
+
2. Perform template rendering on the first Step, using Data and Expressions
|
|
253
|
+
3. Execute the Step in a subprocess
|
|
254
|
+
4. Continue with the next Step
|
|
255
|
+
|
|
256
|
+
Note that operations in the config are _not_ full shell lines. They are passed directly to the system.
|
|
257
|
+
|
|
258
|
+
## Imports
|
|
259
|
+
|
|
260
|
+
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.
|
|
261
|
+
|
|
262
|
+
```yaml
|
|
263
|
+
dyngle:
|
|
264
|
+
imports:
|
|
265
|
+
- ~/.dyngle.yml
|
|
266
|
+
expressions:
|
|
267
|
+
operations:
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
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.
|
|
271
|
+
|
|
272
|
+
## Security
|
|
273
|
+
|
|
274
|
+
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.
|
|
275
|
+
|
|
276
|
+
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
dyngle/__init__.py,sha256=kCFjx6sIuf0hiuH2C4QyHIaC9DY6YKudvkiiziFybgk,2716
|
|
2
2
|
dyngle/__main__.py,sha256=pYRIwzix_AL8CdJaDDis_8yMBBWO2N72NNwkroo1dQo,95
|
|
3
3
|
dyngle/command/__init__.py,sha256=1S86gbef8MYvG-TWD5JRIWzFg7qV5xKhp9QXx9zEx5c,94
|
|
4
|
-
dyngle/command/run_command.py,sha256=
|
|
4
|
+
dyngle/command/run_command.py,sha256=0cRjCQIfbO0KZupp4f1h_TSZh7A0o_7slG-BXEJqjZs,1513
|
|
5
5
|
dyngle/error.py,sha256=CGcTa8L4O1qsHEYnzp_JBbkvntJTv2Qz46wj_TI8NLk,39
|
|
6
6
|
dyngle/expression.py,sha256=-uLVbrO8ovNZGGLNqMZWIy_StCK-0laZqcQ1gOPhU6w,3476
|
|
7
|
-
dyngle/operation.py,sha256=
|
|
7
|
+
dyngle/operation.py,sha256=SCiaFNAxigXep2W95mXaT94-n1EZgQeouAHM0_Wh6GY,1631
|
|
8
8
|
dyngle/safe_path.py,sha256=Hk2AhP6e3yKGh3kKrLLwhvAlMNx-j2jObBYJL-_doAU,3339
|
|
9
|
-
dyngle/template.py,sha256=
|
|
10
|
-
dyngle-0.
|
|
11
|
-
dyngle-0.
|
|
12
|
-
dyngle-0.
|
|
13
|
-
dyngle-0.
|
|
9
|
+
dyngle/template.py,sha256=IJtHu1R3hfniSeCnI4PhmJ_8ojh2YBhH9l-7_SqsfoE,1613
|
|
10
|
+
dyngle-0.7.0.dist-info/METADATA,sha256=8leyci3OsUBplVMVH4mQmZ7nkhzj4Lk6vYNX-MP9wwk,9072
|
|
11
|
+
dyngle-0.7.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
12
|
+
dyngle-0.7.0.dist-info/entry_points.txt,sha256=rekiGhtweiHKm9g1jdGb3FhzqDrk1kigJDeSNollZSA,48
|
|
13
|
+
dyngle-0.7.0.dist-info/RECORD,,
|
dyngle-0.6.0.dist-info/METADATA
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: dyngle
|
|
3
|
-
Version: 0.6.0
|
|
4
|
-
Summary: Run lightweight local workflows
|
|
5
|
-
License: MIT
|
|
6
|
-
Author: Steampunk Wizard
|
|
7
|
-
Author-email: dyngle@steamwiz.io
|
|
8
|
-
Requires-Python: >=3.11,<3.12
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
-
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
-
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
13
|
-
Requires-Dist: wizlib (>=3.1.4,<4.0.0)
|
|
14
|
-
Description-Content-Type: text/markdown
|
|
15
|
-
|
|
16
|
-
# Dyngle
|
|
17
|
-
|
|
18
|
-
## Run lightweight local workflows
|
|
19
|
-
|
|
20
|
-
Dyngle is a simple workflow runner that executes sequences of commands defined in configuration files. It's like a lightweight combination of Make and a task runner, designed for automating common development and operational tasks.
|
|
21
|
-
|
|
22
|
-
## Basic usage
|
|
23
|
-
|
|
24
|
-
Create a configuration file (e.g., `.dyngle.yml`) with your workflows:
|
|
25
|
-
|
|
26
|
-
```yaml
|
|
27
|
-
dyngle:
|
|
28
|
-
operations:
|
|
29
|
-
build:
|
|
30
|
-
- python -m pip install -e .
|
|
31
|
-
- python -m pytest
|
|
32
|
-
deploy:
|
|
33
|
-
- docker build -t myapp .
|
|
34
|
-
- docker push myapp
|
|
35
|
-
clean:
|
|
36
|
-
- rm -rf __pycache__
|
|
37
|
-
- rm -rf .pytest_cache
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Run an operation:
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
dyngle run build
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
## Configuration
|
|
47
|
-
|
|
48
|
-
Dyngle reads configuration from YAML files. You can specify the config file location using:
|
|
49
|
-
|
|
50
|
-
- `--config` command line option
|
|
51
|
-
- `DYNGLE_CONFIG` environment variable
|
|
52
|
-
- `.dyngle.yml` in current directory
|
|
53
|
-
- `~/.dyngle.yml` in home directory
|
|
54
|
-
|
|
55
|
-
The configuration has 2 parts: `operations:` and `expressions`.
|
|
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
|
-
|
|
69
|
-
## Data
|
|
70
|
-
|
|
71
|
-
Dyngle maintains a block of data throughout operations, which is parsed from YAML in stdin.
|
|
72
|
-
|
|
73
|
-
## Operations
|
|
74
|
-
|
|
75
|
-
Operations contain steps as a YAML array. The lifecycle of an operation is:
|
|
76
|
-
|
|
77
|
-
1. Load input data if it exists from YAML on stdin (if no tty)
|
|
78
|
-
2. Perform template rendering on a step, using data and expressions (see below)
|
|
79
|
-
3. Execute the step in a subprocess
|
|
80
|
-
4. Continue with the next step
|
|
81
|
-
|
|
82
|
-
Note that operations in the config are _not_ full shell lines. They are passed directly to the system.
|
|
83
|
-
|
|
84
|
-
## Templates
|
|
85
|
-
|
|
86
|
-
Prior to running commands, the line containing that command is processed as a template. Entries from the data set can be substituted into the command line using Jinja-like expressions in double-curly brackets (`{{` and `}}`).
|
|
87
|
-
|
|
88
|
-
For example, if stdin contains the following data:
|
|
89
|
-
|
|
90
|
-
```yaml
|
|
91
|
-
name: Francis
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
And the command looks like:
|
|
95
|
-
|
|
96
|
-
``` yaml
|
|
97
|
-
- echo "Hello {{name}}!"
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Then the command will output "Hello Francis!".
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
## Expressions
|
|
104
|
-
|
|
105
|
-
Configs can also contain expressions, written in Python, that can also be referenced in operation steps.
|
|
106
|
-
|
|
107
|
-
```yaml
|
|
108
|
-
dyngle:
|
|
109
|
-
expressions:
|
|
110
|
-
say-hello: >-
|
|
111
|
-
'Hello ' + name + '!'
|
|
112
|
-
operations:
|
|
113
|
-
say-hello: echo {{say-hello}}
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
Expressions can use a controlled subset of the Python standard library, including:
|
|
117
|
-
|
|
118
|
-
- Built-in data types such as `str()`
|
|
119
|
-
- Essential built-in functions such as `len()`
|
|
120
|
-
- The core modules from the `datetime` package (but some methods such as `strftime()` will fail)
|
|
121
|
-
- A specialized function called `formatted()` to perform string formatting operations on a `datetime` object
|
|
122
|
-
- A restricted version of `Path()` that only operates within the current working directory
|
|
123
|
-
- Various other useful utilities, mostly read-only, such as the `math` module
|
|
124
|
-
- A special function called `resolve` which resolves data expressions using the same logic as in templates
|
|
125
|
-
|
|
126
|
-
Data keys containing hyphens are converted to valid Python names by replacing hyphens with underscores.
|
|
127
|
-
|
|
128
|
-
Expressions can reference data directly as local names in Python (using the underscore replacements)...
|
|
129
|
-
|
|
130
|
-
```yaml
|
|
131
|
-
dyngle:
|
|
132
|
-
expressions:
|
|
133
|
-
say-hello: >-
|
|
134
|
-
'Hello ' + full_name + '!'
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
... or using the `resolve()` function, which also allows expressions to essentially call other expressions, using the same underlying data set.
|
|
138
|
-
|
|
139
|
-
```yaml
|
|
140
|
-
dyngle:
|
|
141
|
-
expressions:
|
|
142
|
-
hello: >-
|
|
143
|
-
'Hello ' + resolve('formal-name') + '!'
|
|
144
|
-
formal-name: >-
|
|
145
|
-
'Ms. ' + full_name
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
Note it's also _possible_ to call other expressions by name as functions, if they only return hard-coded values (i.e. constants).
|
|
149
|
-
|
|
150
|
-
```yaml
|
|
151
|
-
dyngle:
|
|
152
|
-
expressions:
|
|
153
|
-
author-name: Francis Potter
|
|
154
|
-
author-hello: >-
|
|
155
|
-
'Hello ' + author_name()
|
|
156
|
-
```
|
|
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
|
-
|
|
174
|
-
## Security
|
|
175
|
-
|
|
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.
|
|
177
|
-
|
|
178
|
-
## Quick installation (MacOS)
|
|
179
|
-
|
|
180
|
-
```bash
|
|
181
|
-
brew install python@3.11
|
|
182
|
-
python3.11 -m pip install pipx
|
|
183
|
-
pipx install dyngle
|
|
184
|
-
```
|
|
185
|
-
|
|
File without changes
|
|
File without changes
|