dyngle 0.1.1__tar.gz → 1.6.0__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.
@@ -0,0 +1,335 @@
1
+ # Dyngle
2
+
3
+ An experimantal, lightweight, easily configurable workflow engine for
4
+ automating development, operations, data processing, and content management
5
+ tasks.
6
+
7
+ Technical foundations
8
+
9
+ - Configuration, task definition, and flow control in YAML
10
+ - Operations as system commands using a familiar shell-like syntax
11
+ - Expressions and logic in pure Python
12
+
13
+ ## Quick installation (MacOS)
14
+
15
+ ```bash
16
+ brew install python@3.11
17
+ python3.11 -m pip install pipx
18
+ pipx install dyngle
19
+ ```
20
+
21
+ ## Getting started
22
+
23
+ Create a file `.dyngle.yml`:
24
+
25
+ ```yaml
26
+ dyngle:
27
+ operations:
28
+ hello:
29
+ - echo "Hello world"
30
+ ```
31
+
32
+ Run an operation:
33
+
34
+ ```bash
35
+ dyngle run hello
36
+ ```
37
+
38
+ ## Configuration
39
+
40
+ Dyngle reads configuration from YAML files. Specify the config file location using any of the following (in order of precedence):
41
+
42
+ 1. A `--config` command line option, OR
43
+ 2. A `DYNGLE_CONFIG` environment variable, OR
44
+ 3. `.dyngle.yml` in current directory, OR
45
+ 4. `~/.dyngle.yml` in home directory
46
+
47
+ ## Operations
48
+
49
+ Operations are defined under `dyngle:` in the configuration. In its simplest form, an Operation is a YAML array defining the Steps, as system commands 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.
50
+
51
+ As a serious example, consider the `init` operation from the Dyngle configuration delivered with the project's source code.
52
+
53
+ ```yaml
54
+ dyngle:
55
+ operations:
56
+ init:
57
+ - rm -rf .venv
58
+ - python3.11 -m venv .venv
59
+ - .venv/bin/pip install --upgrade pip poetry
60
+ ```
61
+
62
+ The elements of the YAML array _look_ like lines of Bash, but Dyngle processes them directly as system commands, allowing for template substitution and Python expression evaluation (described below). So shell-specific syntax such as `|`, `>`, and `$VARIABLE` won't work.
63
+
64
+ ## Data and Templates
65
+
66
+ Dyngle maintains a block of "Live 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.
67
+
68
+ 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.
69
+
70
+ For example, consider the following configuration:
71
+
72
+ ``` yaml
73
+ dyngle:
74
+ operations:
75
+ hello:
76
+ - echo "Hello {{name}}!"
77
+ ```
78
+
79
+ Cram some YAML into stdin to try it in your shell:
80
+
81
+ ```bash
82
+ echo "name: Francis" | dyngle run hello
83
+ ```
84
+
85
+ The output will say:
86
+
87
+ ```text
88
+ Hello Francis!
89
+ ```
90
+
91
+ ## Expressions
92
+
93
+ Operations may contain Expressions, written in Python, that can be referenced in Operation Step Templates 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.
94
+
95
+ Expressions may be defined in either of two ways in the configuration:
96
+
97
+ 1. Global Expressions, under the `dyngle:` mapping, using the `expressions:` key.
98
+ 2. Local Expressions, within a single Operation, in which case the Steps of the operation require a `steps:` key.
99
+
100
+ Here's an example of a global Expression
101
+
102
+ ```yaml
103
+ dyngle:
104
+ expressions:
105
+ count: len(name)
106
+ operations:
107
+ say-hello:
108
+ - echo "Hello {{name}}! Your name has {{count}} characters."
109
+ ```
110
+
111
+ For completeness, consider the following example using a local Expression for the same purpose.
112
+
113
+ ```yaml
114
+ dyngle:
115
+ operations:
116
+ say-hello:
117
+ expressions:
118
+ count: len(name)
119
+ steps:
120
+ - echo "Hello {{name}}! Your name has {{count}} characters."
121
+ ```
122
+
123
+ Expressions can use a controlled subset of the Python standard library, including:
124
+
125
+ - Built-in data types such as `str()`
126
+ - Essential built-in functions such as `len()`
127
+ - The core modules from the `datetime` package (but some methods such as `strftime()` will fail)
128
+ - A specialized function called `formatted()` to perform string formatting operations on a `datetime` object
129
+ - A restricted version of `Path()` that only operates within the current working directory
130
+ - Various other useful utilities, mostly read-only, such as the `math` module
131
+ - A special function called `resolve` which resolves data expressions using the same logic as in templates
132
+ - An array `args` containing arguments passed to the `dyngle run` command after the Operation name
133
+
134
+ **NOTE** Some capabilities of the Expression namespace might be limited in the future. The goal is support purely read-only operations within Expressions.
135
+
136
+ Expressions behave like functions that take no arguments, using the Data as a namespace. So Expressions reference Data directly as local names in Python.
137
+
138
+ YAML keys can contain hyphens, which are fully supported in Dyngle. To reference a hyphenated key in an Expression, choose:
139
+
140
+ - Reference the name using underscores instead of hyphens (they are automatically replaced), OR
141
+ - Use the built-in special-purpose `resolve()` function (which can also be used to reference other expressions)
142
+
143
+ ```yaml
144
+ dyngle:
145
+ expressions:
146
+ say-hello: >-
147
+ 'Hello ' + full_name + '!'
148
+ ```
149
+
150
+ ... or using the `resolve()` function, which also allows expressions to essentially call other expressions, using the same underlying data set.
151
+
152
+ ```yaml
153
+ dyngle:
154
+ expressions:
155
+ hello: >-
156
+ 'Hello ' + resolve('formal-name') + '!'
157
+ formal-name: >-
158
+ 'Ms. ' + full_name
159
+ ```
160
+
161
+ Note it's also _possible_ to call other expressions by name as functions, if they only return hard-coded values (i.e. constants).
162
+
163
+ ```yaml
164
+ dyngle:
165
+ expressions:
166
+ author-name: Francis Potter
167
+ author-hello: >-
168
+ 'Hello ' + author_name()
169
+ ```
170
+
171
+ Here are some slightly more sophisticated exercises using Expression reference syntax:
172
+
173
+ ```yaml
174
+ dyngle:
175
+ operations:
176
+ reference-hyphenated-data-key:
177
+ expressions:
178
+ spaced-name: "' '.join([x for x in first_name])"
179
+ count-name: len(resolve('first-name'))
180
+ x-name: "'X' * int(resolve('count-name'))"
181
+ steps:
182
+ - echo "Your name is {{first-name}} with {{count-name}} characters, but I will call you '{{spaced-name}}' or maybe '{{x-name}}'"
183
+ reference-expression-using-function-syntax:
184
+ expressions:
185
+ name: "'George'"
186
+ works: "name()"
187
+ double: "name * 2"
188
+ fails: double()
189
+ steps:
190
+ - echo "It works to call you {{works}}"
191
+ # - echo "I have trouble calling you {{fails}}"
192
+ ```
193
+
194
+ Finally, here's an example using args:
195
+
196
+ ```yaml
197
+ dyngle:
198
+ operations:
199
+ name-from-arg:
200
+ expressions:
201
+ name: "args[0]"
202
+ steps:
203
+ - echo "Hello {{name}}"
204
+ ```
205
+
206
+ ## Passing values between Steps in an Operation
207
+
208
+ The Steps parser supports two special operators designed to move data between Steps in an explicit way.
209
+
210
+ - The data assignment operator (`=>`) assigns the contents of stdout from the command to an element in the data
211
+ - The data input operator (`->`) assigns the value of an element in the data (or an evaluated expression) to stdin for the command
212
+
213
+ The operators must appear in order in the step and must be isolated with whitespace, i.e.
214
+
215
+ ```
216
+ <input-variable-name> -> <command and arguments> => <output-variable-name>
217
+ ```
218
+
219
+ Here we get into more useful functionality, where commands can be strung together in meaningful ways without the need for Bash.
220
+
221
+ ```yaml
222
+ dyngle:
223
+ operations:
224
+ weather:
225
+ - curl -s "https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&current_weather=true" => weather-data
226
+ - weather-data -> jq -j '.current_weather.temperature' => temperature
227
+ - echo "It's {{temperature}} degrees out there!"
228
+ ```
229
+
230
+ If names overlap, data items populated using the data assignment operator take precedence over expressions and data in the original input from the beginning of the Operation.
231
+
232
+ ## Sub-operations
233
+
234
+ Operations can call other operations as steps using the `sub:` key. This allows for composability and reuse of operation logic.
235
+
236
+ Basic example:
237
+
238
+ ```yaml
239
+ dyngle:
240
+ operations:
241
+ greet:
242
+ - echo "Hello!"
243
+
244
+ greet-twice:
245
+ steps:
246
+ - sub: greet
247
+ - sub: greet
248
+ ```
249
+
250
+ Sub-operations can accept arguments using the `args:` key. The called operation can access these via the `args` array in expressions:
251
+
252
+ ```yaml
253
+ dyngle:
254
+ operations:
255
+ greet-person:
256
+ expressions:
257
+ person: "args[0]"
258
+ steps:
259
+ - echo "Hello, {{person}}!"
260
+
261
+ greet-team:
262
+ steps:
263
+ - sub: greet-person
264
+ args: ['Alice']
265
+ - sub: greet-person
266
+ args: ['Bob']
267
+ ```
268
+
269
+ ### Scoping Rules
270
+
271
+ Sub-operations follow clear scoping rules that separate **declared values** from **live data**:
272
+
273
+ **Declared Values are Locally Scoped:**
274
+ - Values and expressions declared via `values:` or `expressions:` keys are local to each operation
275
+ - A parent operation's declared values are NOT visible to child sub-operations
276
+ - A child sub-operation's declared values do NOT leak to the parent operation
277
+ - Each operation only sees its own declared values plus global declared values
278
+
279
+ **Live Data is Globally Shared:**
280
+ - Data assigned via the `=>` operator persists across all operations
281
+ - Live data populated by a sub-operation IS available to the parent after the sub-operation completes
282
+ - This allows operations to communicate results through shared mutable state
283
+
284
+ Example demonstrating scoping:
285
+
286
+ ```yaml
287
+ dyngle:
288
+ values:
289
+ declared-val: global
290
+
291
+ operations:
292
+ child:
293
+ values:
294
+ declared-val: child-local
295
+ steps:
296
+ - echo {{declared-val}} # Outputs "child-local"
297
+ - echo "result" => live-data
298
+
299
+ parent:
300
+ steps:
301
+ - echo {{declared-val}} # Outputs "global"
302
+ - sub: child
303
+ - echo {{declared-val}} # Still outputs "global"
304
+ - echo {{live-data}} # Outputs "result" (persisted from child)
305
+ ```
306
+
307
+ ## Lifecycle
308
+
309
+ The lifecycle of an operation is:
310
+
311
+ 1. Load Data if it exists from YAML on stdin (if no tty)
312
+ 2. Find the named Operation in the configuration
313
+ 2. Perform template rendering on the first Step, using Data and Expressions
314
+ 3. Execute the Step in a subprocess, passing in an input value and populating an output value in the Data
315
+ 4. Continue with the next Step
316
+
317
+ Note that operations in the config are _not_ full shell lines. They are passed directly to the system.
318
+
319
+ ## Imports
320
+
321
+ 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.
322
+
323
+ ```yaml
324
+ dyngle:
325
+ imports:
326
+ - ~/.dyngle.yml
327
+ expressions:
328
+ operations:
329
+ ```
330
+
331
+ 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.
332
+
333
+ ## Security
334
+
335
+ 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.
dyngle-1.6.0/PKG-INFO ADDED
@@ -0,0 +1,352 @@
1
+ Metadata-Version: 2.4
2
+ Name: dyngle
3
+ Version: 1.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.13,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: requests (>=2.32.3,<3.0.0)
14
+ Requires-Dist: wizlib (>=3.3.11,<3.4.0)
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Dyngle
18
+
19
+ An experimantal, lightweight, easily configurable workflow engine for
20
+ automating development, operations, data processing, and content management
21
+ tasks.
22
+
23
+ Technical foundations
24
+
25
+ - Configuration, task definition, and flow control in YAML
26
+ - Operations as system commands using a familiar shell-like syntax
27
+ - Expressions and logic in pure Python
28
+
29
+ ## Quick installation (MacOS)
30
+
31
+ ```bash
32
+ brew install python@3.11
33
+ python3.11 -m pip install pipx
34
+ pipx install dyngle
35
+ ```
36
+
37
+ ## Getting started
38
+
39
+ Create a file `.dyngle.yml`:
40
+
41
+ ```yaml
42
+ dyngle:
43
+ operations:
44
+ hello:
45
+ - echo "Hello world"
46
+ ```
47
+
48
+ Run an operation:
49
+
50
+ ```bash
51
+ dyngle run hello
52
+ ```
53
+
54
+ ## Configuration
55
+
56
+ Dyngle reads configuration from YAML files. Specify the config file location using any of the following (in order of precedence):
57
+
58
+ 1. A `--config` command line option, OR
59
+ 2. A `DYNGLE_CONFIG` environment variable, OR
60
+ 3. `.dyngle.yml` in current directory, OR
61
+ 4. `~/.dyngle.yml` in home directory
62
+
63
+ ## Operations
64
+
65
+ Operations are defined under `dyngle:` in the configuration. In its simplest form, an Operation is a YAML array defining the Steps, as system commands 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.
66
+
67
+ As a serious example, consider the `init` operation from the Dyngle configuration delivered with the project's source code.
68
+
69
+ ```yaml
70
+ dyngle:
71
+ operations:
72
+ init:
73
+ - rm -rf .venv
74
+ - python3.11 -m venv .venv
75
+ - .venv/bin/pip install --upgrade pip poetry
76
+ ```
77
+
78
+ The elements of the YAML array _look_ like lines of Bash, but Dyngle processes them directly as system commands, allowing for template substitution and Python expression evaluation (described below). So shell-specific syntax such as `|`, `>`, and `$VARIABLE` won't work.
79
+
80
+ ## Data and Templates
81
+
82
+ Dyngle maintains a block of "Live 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.
83
+
84
+ 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.
85
+
86
+ For example, consider the following configuration:
87
+
88
+ ``` yaml
89
+ dyngle:
90
+ operations:
91
+ hello:
92
+ - echo "Hello {{name}}!"
93
+ ```
94
+
95
+ Cram some YAML into stdin to try it in your shell:
96
+
97
+ ```bash
98
+ echo "name: Francis" | dyngle run hello
99
+ ```
100
+
101
+ The output will say:
102
+
103
+ ```text
104
+ Hello Francis!
105
+ ```
106
+
107
+ ## Expressions
108
+
109
+ Operations may contain Expressions, written in Python, that can be referenced in Operation Step Templates 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.
110
+
111
+ Expressions may be defined in either of two ways in the configuration:
112
+
113
+ 1. Global Expressions, under the `dyngle:` mapping, using the `expressions:` key.
114
+ 2. Local Expressions, within a single Operation, in which case the Steps of the operation require a `steps:` key.
115
+
116
+ Here's an example of a global Expression
117
+
118
+ ```yaml
119
+ dyngle:
120
+ expressions:
121
+ count: len(name)
122
+ operations:
123
+ say-hello:
124
+ - echo "Hello {{name}}! Your name has {{count}} characters."
125
+ ```
126
+
127
+ For completeness, consider the following example using a local Expression for the same purpose.
128
+
129
+ ```yaml
130
+ dyngle:
131
+ operations:
132
+ say-hello:
133
+ expressions:
134
+ count: len(name)
135
+ steps:
136
+ - echo "Hello {{name}}! Your name has {{count}} characters."
137
+ ```
138
+
139
+ Expressions can use a controlled subset of the Python standard library, including:
140
+
141
+ - Built-in data types such as `str()`
142
+ - Essential built-in functions such as `len()`
143
+ - The core modules from the `datetime` package (but some methods such as `strftime()` will fail)
144
+ - A specialized function called `formatted()` to perform string formatting operations on a `datetime` object
145
+ - A restricted version of `Path()` that only operates within the current working directory
146
+ - Various other useful utilities, mostly read-only, such as the `math` module
147
+ - A special function called `resolve` which resolves data expressions using the same logic as in templates
148
+ - An array `args` containing arguments passed to the `dyngle run` command after the Operation name
149
+
150
+ **NOTE** Some capabilities of the Expression namespace might be limited in the future. The goal is support purely read-only operations within Expressions.
151
+
152
+ Expressions behave like functions that take no arguments, using the Data as a namespace. So Expressions reference Data directly as local names in Python.
153
+
154
+ YAML keys can contain hyphens, which are fully supported in Dyngle. To reference a hyphenated key in an Expression, choose:
155
+
156
+ - Reference the name using underscores instead of hyphens (they are automatically replaced), OR
157
+ - Use the built-in special-purpose `resolve()` function (which can also be used to reference other expressions)
158
+
159
+ ```yaml
160
+ dyngle:
161
+ expressions:
162
+ say-hello: >-
163
+ 'Hello ' + full_name + '!'
164
+ ```
165
+
166
+ ... or using the `resolve()` function, which also allows expressions to essentially call other expressions, using the same underlying data set.
167
+
168
+ ```yaml
169
+ dyngle:
170
+ expressions:
171
+ hello: >-
172
+ 'Hello ' + resolve('formal-name') + '!'
173
+ formal-name: >-
174
+ 'Ms. ' + full_name
175
+ ```
176
+
177
+ Note it's also _possible_ to call other expressions by name as functions, if they only return hard-coded values (i.e. constants).
178
+
179
+ ```yaml
180
+ dyngle:
181
+ expressions:
182
+ author-name: Francis Potter
183
+ author-hello: >-
184
+ 'Hello ' + author_name()
185
+ ```
186
+
187
+ Here are some slightly more sophisticated exercises using Expression reference syntax:
188
+
189
+ ```yaml
190
+ dyngle:
191
+ operations:
192
+ reference-hyphenated-data-key:
193
+ expressions:
194
+ spaced-name: "' '.join([x for x in first_name])"
195
+ count-name: len(resolve('first-name'))
196
+ x-name: "'X' * int(resolve('count-name'))"
197
+ steps:
198
+ - echo "Your name is {{first-name}} with {{count-name}} characters, but I will call you '{{spaced-name}}' or maybe '{{x-name}}'"
199
+ reference-expression-using-function-syntax:
200
+ expressions:
201
+ name: "'George'"
202
+ works: "name()"
203
+ double: "name * 2"
204
+ fails: double()
205
+ steps:
206
+ - echo "It works to call you {{works}}"
207
+ # - echo "I have trouble calling you {{fails}}"
208
+ ```
209
+
210
+ Finally, here's an example using args:
211
+
212
+ ```yaml
213
+ dyngle:
214
+ operations:
215
+ name-from-arg:
216
+ expressions:
217
+ name: "args[0]"
218
+ steps:
219
+ - echo "Hello {{name}}"
220
+ ```
221
+
222
+ ## Passing values between Steps in an Operation
223
+
224
+ The Steps parser supports two special operators designed to move data between Steps in an explicit way.
225
+
226
+ - The data assignment operator (`=>`) assigns the contents of stdout from the command to an element in the data
227
+ - The data input operator (`->`) assigns the value of an element in the data (or an evaluated expression) to stdin for the command
228
+
229
+ The operators must appear in order in the step and must be isolated with whitespace, i.e.
230
+
231
+ ```
232
+ <input-variable-name> -> <command and arguments> => <output-variable-name>
233
+ ```
234
+
235
+ Here we get into more useful functionality, where commands can be strung together in meaningful ways without the need for Bash.
236
+
237
+ ```yaml
238
+ dyngle:
239
+ operations:
240
+ weather:
241
+ - curl -s "https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&current_weather=true" => weather-data
242
+ - weather-data -> jq -j '.current_weather.temperature' => temperature
243
+ - echo "It's {{temperature}} degrees out there!"
244
+ ```
245
+
246
+ If names overlap, data items populated using the data assignment operator take precedence over expressions and data in the original input from the beginning of the Operation.
247
+
248
+ ## Sub-operations
249
+
250
+ Operations can call other operations as steps using the `sub:` key. This allows for composability and reuse of operation logic.
251
+
252
+ Basic example:
253
+
254
+ ```yaml
255
+ dyngle:
256
+ operations:
257
+ greet:
258
+ - echo "Hello!"
259
+
260
+ greet-twice:
261
+ steps:
262
+ - sub: greet
263
+ - sub: greet
264
+ ```
265
+
266
+ Sub-operations can accept arguments using the `args:` key. The called operation can access these via the `args` array in expressions:
267
+
268
+ ```yaml
269
+ dyngle:
270
+ operations:
271
+ greet-person:
272
+ expressions:
273
+ person: "args[0]"
274
+ steps:
275
+ - echo "Hello, {{person}}!"
276
+
277
+ greet-team:
278
+ steps:
279
+ - sub: greet-person
280
+ args: ['Alice']
281
+ - sub: greet-person
282
+ args: ['Bob']
283
+ ```
284
+
285
+ ### Scoping Rules
286
+
287
+ Sub-operations follow clear scoping rules that separate **declared values** from **live data**:
288
+
289
+ **Declared Values are Locally Scoped:**
290
+ - Values and expressions declared via `values:` or `expressions:` keys are local to each operation
291
+ - A parent operation's declared values are NOT visible to child sub-operations
292
+ - A child sub-operation's declared values do NOT leak to the parent operation
293
+ - Each operation only sees its own declared values plus global declared values
294
+
295
+ **Live Data is Globally Shared:**
296
+ - Data assigned via the `=>` operator persists across all operations
297
+ - Live data populated by a sub-operation IS available to the parent after the sub-operation completes
298
+ - This allows operations to communicate results through shared mutable state
299
+
300
+ Example demonstrating scoping:
301
+
302
+ ```yaml
303
+ dyngle:
304
+ values:
305
+ declared-val: global
306
+
307
+ operations:
308
+ child:
309
+ values:
310
+ declared-val: child-local
311
+ steps:
312
+ - echo {{declared-val}} # Outputs "child-local"
313
+ - echo "result" => live-data
314
+
315
+ parent:
316
+ steps:
317
+ - echo {{declared-val}} # Outputs "global"
318
+ - sub: child
319
+ - echo {{declared-val}} # Still outputs "global"
320
+ - echo {{live-data}} # Outputs "result" (persisted from child)
321
+ ```
322
+
323
+ ## Lifecycle
324
+
325
+ The lifecycle of an operation is:
326
+
327
+ 1. Load Data if it exists from YAML on stdin (if no tty)
328
+ 2. Find the named Operation in the configuration
329
+ 2. Perform template rendering on the first Step, using Data and Expressions
330
+ 3. Execute the Step in a subprocess, passing in an input value and populating an output value in the Data
331
+ 4. Continue with the next Step
332
+
333
+ Note that operations in the config are _not_ full shell lines. They are passed directly to the system.
334
+
335
+ ## Imports
336
+
337
+ 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.
338
+
339
+ ```yaml
340
+ dyngle:
341
+ imports:
342
+ - ~/.dyngle.yml
343
+ expressions:
344
+ operations:
345
+ ```
346
+
347
+ 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.
348
+
349
+ ## Security
350
+
351
+ 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.
352
+