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

Potentially problematic release.


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

dyngle/__init__.py CHANGED
@@ -7,8 +7,8 @@ from wizlib.ui_handler import UIHandler
7
7
 
8
8
  from dyngle.command import DyngleCommand
9
9
  from dyngle.error import DyngleError
10
- from dyngle.expression import expression
11
- from dyngle.operation import Operation
10
+ from dyngle.model.expression import expression
11
+ from dyngle.model.operation import Operation
12
12
 
13
13
 
14
14
  class DyngleApp(WizApp):
@@ -4,8 +4,8 @@ from wizlib.parser import WizParser
4
4
  from yaml import safe_load
5
5
 
6
6
  from dyngle.command import DyngleCommand
7
- from dyngle.expression import expression
8
- from dyngle.template import Template
7
+ from dyngle.model.expression import expression
8
+ from dyngle.model.template import Template
9
9
  from dyngle.error import DyngleError
10
10
 
11
11
 
@@ -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
- for step_template in steps:
47
- step = Template(step_template).render(data, expressions)
48
- parts = shlex.split(step)
49
- result = subprocess.run(parts)
50
- if result.returncode != 0:
51
- raise DyngleError(
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'
File without changes
@@ -1,7 +1,8 @@
1
1
  from typing import Callable
2
2
 
3
3
  from dyngle.error import DyngleError
4
- from dyngle.safe_path import SafePath
4
+ from dyngle.model.live_data import LiveData
5
+ from dyngle.model.safe_path import SafePath
5
6
 
6
7
  from datetime import datetime as datetime, date, timedelta
7
8
  import math
@@ -9,7 +10,7 @@ import json
9
10
  import re
10
11
  import yaml
11
12
 
12
- from dyngle.template import Template
13
+ from dyngle.model.template import Template
13
14
 
14
15
 
15
16
  def formatted_datetime(dt: datetime, format_string=None) -> str:
@@ -113,10 +114,33 @@ def _evaluate(expression: str, locals: dict) -> str:
113
114
  return str(result)
114
115
 
115
116
 
117
+ # The 'expression' function returns the expression object itself, which is
118
+ # really just a function.
119
+
116
120
  def expression(text: str) -> Callable[[dict], str]:
117
- def evaluate(data: dict = None) -> str:
118
- items = data.items() if data else ()
119
- locals = {k.replace('-', '_'): v for k, v in items}
120
- def resolve(s): return Template.resolve(s, data)
121
- return _evaluate(text, locals | {'resolve': resolve})
122
- return evaluate
121
+ """Generate an expression, which is a function based on a string
122
+ expression"""
123
+
124
+ def definition(live_data: LiveData | dict | None = None) -> str:
125
+ """The expression function itself"""
126
+
127
+ # Allow for blankness and testability
128
+ live_data = LiveData(live_data)
129
+
130
+ # Translate names to underscore-separated instead of hyphen-separated
131
+ # so they work within the Python namespace.
132
+
133
+ items = live_data.items() if live_data else ()
134
+ locals = LiveData({k.replace('-', '_'): v for k, v in items})
135
+
136
+ # Create a resolve function which allows references using the hyphen
137
+ # syntax too
138
+
139
+ def resolve(key):
140
+ return live_data.resolve(key)
141
+ locals = locals | {'resolve': resolve}
142
+
143
+ # Perform the Python eval, expanded above
144
+ return _evaluate(text, locals)
145
+
146
+ return definition
@@ -0,0 +1,22 @@
1
+ from collections import UserDict
2
+
3
+ from dyngle.error import DyngleError
4
+
5
+
6
+ class LiveData(UserDict):
7
+
8
+ def resolve(self, key: str):
9
+ """Given a key (which might be dot-separated), return
10
+ the value (which might include evaluating expressions)."""
11
+
12
+ parts = key.split('.')
13
+ current = self.data
14
+ for part in parts:
15
+ if part not in current:
16
+ raise DyngleError(
17
+ f"Invalid expression or data reference '{key}'")
18
+ current = current[part]
19
+ if callable(current):
20
+ return current(self)
21
+ else:
22
+ return current
@@ -0,0 +1,63 @@
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.model.live_data import LiveData
9
+ from dyngle.model.template import Template
10
+
11
+
12
+ @dataclass
13
+ class Operation:
14
+
15
+ local_expressions: dict
16
+
17
+ steps: list
18
+
19
+ def run(self, data: dict, global_expressions: dict):
20
+ expressions = global_expressions | self.local_expressions
21
+ # Data takes precedence if names match
22
+ live_data = LiveData(expressions) | data
23
+ for markup in self.steps:
24
+ step = Step(markup)
25
+ step.run(live_data)
26
+
27
+
28
+ STEP_PATTERN = re.compile(
29
+ r'^\s*(?:([\w.-]+)\s+->\s+)?(.+?)(?:\s+=>\s+([\w.-]+))?\s*$')
30
+
31
+
32
+ def parse_step(markup):
33
+ if match := STEP_PATTERN.match(markup):
34
+ input, command_text, output = match.groups()
35
+ command_template = shlex.split(command_text.strip())
36
+ return input, command_template, output
37
+ else:
38
+ raise DyngleError(f"Invalid step markup {{markup}}")
39
+
40
+
41
+ @dataclass
42
+ class Step:
43
+
44
+ markup: str
45
+
46
+ def __post_init__(self):
47
+ self.input, self.command_template, self.output = \
48
+ parse_step(self.markup)
49
+
50
+ def run(self, live_data: LiveData):
51
+ command = [Template(word).render(live_data)
52
+ for word in self.command_template]
53
+ pipes = {}
54
+ if self.input:
55
+ pipes["input"] = live_data.resolve(self.input)
56
+ if self.output:
57
+ pipes['stdout'] = subprocess.PIPE
58
+ result = subprocess.run(command, text=True, **pipes)
59
+ if result.returncode != 0:
60
+ raise DyngleError(
61
+ f'Step failed with code {result.returncode}: {self.markup}')
62
+ if self.output:
63
+ live_data[self.output] = result.stdout
@@ -0,0 +1,30 @@
1
+ from dataclasses import dataclass
2
+ from functools import partial
3
+ import re
4
+
5
+ from dyngle.error import DyngleError
6
+ from dyngle.model.live_data import LiveData
7
+
8
+
9
+ PATTERN = re.compile(r'\{\{\s*([^}]+)\s*\}\}')
10
+
11
+
12
+ @dataclass
13
+ class Template:
14
+
15
+ template: str
16
+
17
+ def render(self, live_data: LiveData | dict | None = None) -> str:
18
+ """Render the template with the provided LiveData (raw data and
19
+ expressions)."""
20
+
21
+ live_data = LiveData(live_data)
22
+ resolver = partial(self._resolve, live_data=live_data)
23
+ return PATTERN.sub(resolver, self.template)
24
+
25
+ def _resolve(self, match, *, live_data: LiveData):
26
+ """Resolve a single name/path from the template. The argument is a
27
+ merge of the raw data and the expressions, either of which are valid
28
+ substitutions."""
29
+ key = match.group(1).strip()
30
+ return live_data.resolve(key)
@@ -0,0 +1,277 @@
1
+ Metadata-Version: 2.3
2
+ Name: dyngle
3
+ Version: 1.0.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
+ An experimantal, lightweight, easily configurable workflow engine for
19
+ automating development, operations, data processing, and content management
20
+ tasks.
21
+
22
+ Technical foundations
23
+
24
+ - Configuration, task definition, and flow control in YAML
25
+ - Operations as system commands using a familiar shell-like syntax
26
+ - Expressions and logic in pure Python
27
+
28
+ ## Quick installation (MacOS)
29
+
30
+ ```bash
31
+ brew install python@3.11
32
+ python3.11 -m pip install pipx
33
+ pipx install dyngle
34
+ ```
35
+
36
+ ## Getting started
37
+
38
+ Create a file `.dyngle.yml`:
39
+
40
+ ```yaml
41
+ dyngle:
42
+ operations:
43
+ hello:
44
+ - echo "Hello world"
45
+ ```
46
+
47
+ Run an operation:
48
+
49
+ ```bash
50
+ dyngle run hello
51
+ ```
52
+
53
+ ## Configuration
54
+
55
+ Dyngle reads configuration from YAML files. Specify the config file location using any of the following (in order of precedence):
56
+
57
+ 1. A `--config` command line option, OR
58
+ 2. A `DYNGLE_CONFIG` environment variable, OR
59
+ 3. `.dyngle.yml` in current directory, OR
60
+ 4. `~/.dyngle.yml` in home directory
61
+
62
+ ## Operations
63
+
64
+ 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.
65
+
66
+ As a serious example, consider the `init` operation from the Dyngle configuration delivered with the project's source code.
67
+
68
+ ```yaml
69
+ dyngle:
70
+ operations:
71
+ init:
72
+ - rm -rf .venv
73
+ - python3.11 -m venv .venv
74
+ - .venv/bin/pip install --upgrade pip poetry
75
+ ```
76
+
77
+ 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.
78
+
79
+ ## Data and Templates
80
+
81
+ 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.
82
+
83
+ 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.
84
+
85
+ For example, consider the following configuration:
86
+
87
+ ``` yaml
88
+ dyngle:
89
+ operations:
90
+ hello:
91
+ - echo "Hello {{name}}!"
92
+ ```
93
+
94
+ Cram some YAML into stdin to try it in your shell:
95
+
96
+ ```bash
97
+ echo "name: Francis" | dyngle run hello
98
+ ```
99
+
100
+ The output will say:
101
+
102
+ ```text
103
+ Hello Francis!
104
+ ```
105
+
106
+ ## Expressions
107
+
108
+ 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.
109
+
110
+ Expressions may be defined in either of two ways in the configuration:
111
+
112
+ 1. Global Expressions, under the `dyngle:` mapping, using the `expressions:` key.
113
+ 2. Local Expressions, within a single Operation, in which case the Steps of the operation require a `steps:` key.
114
+
115
+ Here's an example of a global Expression
116
+
117
+ ```yaml
118
+ dyngle:
119
+ expressions:
120
+ count: len(name)
121
+ operations:
122
+ say-hello:
123
+ - echo "Hello {{name}}! Your name has {{count}} characters."
124
+ ```
125
+
126
+ For completeness, consider the following example using a local Expression for the same purpose.
127
+
128
+ ```yaml
129
+ dyngle:
130
+ operations:
131
+ say-hello:
132
+ expressions:
133
+ count: len(name)
134
+ steps:
135
+ - echo "Hello {{name}}! Your name has {{count}} characters."
136
+ ```
137
+
138
+ Expressions can use a controlled subset of the Python standard library, including:
139
+
140
+ - Built-in data types such as `str()`
141
+ - Essential built-in functions such as `len()`
142
+ - The core modules from the `datetime` package (but some methods such as `strftime()` will fail)
143
+ - A specialized function called `formatted()` to perform string formatting operations on a `datetime` object
144
+ - A restricted version of `Path()` that only operates within the current working directory
145
+ - Various other useful utilities, mostly read-only, such as the `math` module
146
+ - A special function called `resolve` which resolves data expressions using the same logic as in templates
147
+ - An array `args` containing arguments passed to the `dyngle run` command after the Operation name
148
+
149
+ **NOTE** Some capabilities of the Expression namespace might be limited in the future. The goal is support purely read-only operations within Expressions.
150
+
151
+ Expressions behave like functions that take no arguments, using the Data as a namespace. So Expressions reference Data directly as local names in Python.
152
+
153
+ YAML keys can contain hyphens, which are fully supported in Dyngle. To reference a hyphenated key in an Expression, choose:
154
+
155
+ - Reference the name using underscores instead of hyphens (they are automatically replaced), OR
156
+ - Use the built-in special-purpose `resolve()` function (which can also be used to reference other expressions)
157
+
158
+ ```yaml
159
+ dyngle:
160
+ expressions:
161
+ say-hello: >-
162
+ 'Hello ' + full_name + '!'
163
+ ```
164
+
165
+ ... or using the `resolve()` function, which also allows expressions to essentially call other expressions, using the same underlying data set.
166
+
167
+ ```yaml
168
+ dyngle:
169
+ expressions:
170
+ hello: >-
171
+ 'Hello ' + resolve('formal-name') + '!'
172
+ formal-name: >-
173
+ 'Ms. ' + full_name
174
+ ```
175
+
176
+ Note it's also _possible_ to call other expressions by name as functions, if they only return hard-coded values (i.e. constants).
177
+
178
+ ```yaml
179
+ dyngle:
180
+ expressions:
181
+ author-name: Francis Potter
182
+ author-hello: >-
183
+ 'Hello ' + author_name()
184
+ ```
185
+
186
+ Here are some slightly more sophisticated exercises using Expression reference syntax:
187
+
188
+ ```yaml
189
+ dyngle:
190
+ operations:
191
+ reference-hyphenated-data-key:
192
+ expressions:
193
+ spaced-name: "' '.join([x for x in first_name])"
194
+ count-name: len(resolve('first-name'))
195
+ x-name: "'X' * int(resolve('count-name'))"
196
+ steps:
197
+ - echo "Your name is {{first-name}} with {{count-name}} characters, but I will call you '{{spaced-name}}' or maybe '{{x-name}}'"
198
+ reference-expression-using-function-syntax:
199
+ expressions:
200
+ name: "'George'"
201
+ works: "name()"
202
+ double: "name * 2"
203
+ fails: double()
204
+ steps:
205
+ - echo "It works to call you {{works}}"
206
+ # - echo "I have trouble calling you {{fails}}"
207
+ ```
208
+
209
+ Finally, here's an example using args:
210
+
211
+ ```yaml
212
+ dyngle:
213
+ operations:
214
+ name-from-arg:
215
+ expressions:
216
+ name: "args[0]"
217
+ steps:
218
+ - echo "Hello {{name}}"
219
+ ```
220
+
221
+ ## Passing values between Steps in an Operation
222
+
223
+ The Steps parser supports two special operators designed to move data between Steps in an explicit way.
224
+
225
+ - The data assignment operator (`=>`) assigns the contents of stdout from the command to an element in the data
226
+ - The data input operator (`->`) assigns the value of an element in the data (or an evaluated expression) to stdin for the command
227
+
228
+ The operators must appear in order in the step and must be isolated with whitespace, i.e.
229
+
230
+ ```
231
+ <input-variable-name> -> <command and arguments> => <output-variable-name>
232
+ ```
233
+
234
+ Here we get into more useful functionality, where commands can be strung together in meaningful ways without the need for Bash.
235
+
236
+ ```yaml
237
+ dyngle:
238
+ operations:
239
+ weather:
240
+ - curl -s "https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&current_weather=true" => weather-data
241
+ - weather-data -> jq -j '.current_weather.temperature' => temperature
242
+ - echo "It's {{temperature}} degrees out there!"
243
+ ```
244
+
245
+ 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.
246
+
247
+ ## Lifecycle
248
+
249
+ The lifecycle of an operation is:
250
+
251
+ 1. Load Data if it exists from YAML on stdin (if no tty)
252
+ 2. Find the named Operation in the configuration
253
+ 2. Perform template rendering on the first Step, using Data and Expressions
254
+ 3. Execute the Step in a subprocess, passing in an input value and populating an output value in the Data
255
+ 4. Continue with the next Step
256
+
257
+ Note that operations in the config are _not_ full shell lines. They are passed directly to the system.
258
+
259
+ ## Imports
260
+
261
+ 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.
262
+
263
+ ```yaml
264
+ dyngle:
265
+ imports:
266
+ - ~/.dyngle.yml
267
+ expressions:
268
+ operations:
269
+ ```
270
+
271
+ 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.
272
+
273
+ ## Security
274
+
275
+ 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.
276
+
277
+
@@ -0,0 +1,15 @@
1
+ dyngle/__init__.py,sha256=Mku-FaOXp1HMBoQc6K9ZJfhr9n-CXxvtotRFzSZ042I,2728
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=yq5VJv3XWtzTf9F_rW-_nANFPGGkIMQO3kiU5atEATo,1525
5
+ dyngle/error.py,sha256=CGcTa8L4O1qsHEYnzp_JBbkvntJTv2Qz46wj_TI8NLk,39
6
+ dyngle/model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ dyngle/model/expression.py,sha256=FqYOfotUzTHDyON9cUCwyO5Cw4jTphY_TuWjQyhvSFY,4220
8
+ dyngle/model/live_data.py,sha256=6YdCfCDjhzMoCC5Znuwn_T-UpqeU_3I38M1mOBOLh2U,629
9
+ dyngle/model/operation.py,sha256=ZRP4XwsU8WaBgwKj6s8lx-VY4rGQaPmmB0HvrzTZjGk,1793
10
+ dyngle/model/safe_path.py,sha256=Hk2AhP6e3yKGh3kKrLLwhvAlMNx-j2jObBYJL-_doAU,3339
11
+ dyngle/model/template.py,sha256=MeXu--ZNtj_ujABU1GjjcQ1Ea_o_M-50LocuXFeOLRE,887
12
+ dyngle-1.0.0.dist-info/METADATA,sha256=ElDftWuR4ZtbLM5_RRGLBCiEdka5HNMng_4r2im6tDk,9660
13
+ dyngle-1.0.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
14
+ dyngle-1.0.0.dist-info/entry_points.txt,sha256=rekiGhtweiHKm9g1jdGb3FhzqDrk1kigJDeSNollZSA,48
15
+ dyngle-1.0.0.dist-info/RECORD,,
dyngle/operation.py DELETED
@@ -1,9 +0,0 @@
1
- from dataclasses import dataclass
2
-
3
-
4
- @dataclass
5
- class Operation:
6
-
7
- local_expressions: dict
8
-
9
- steps: list
dyngle/template.py DELETED
@@ -1,56 +0,0 @@
1
- from dataclasses import dataclass
2
- from functools import partial
3
- import re
4
-
5
- from dyngle.error import DyngleError
6
-
7
-
8
- PATTERN = re.compile(r'\{\{\s*([^}]+)\s*\}\}')
9
-
10
-
11
- @dataclass
12
- class Template:
13
-
14
- template: str
15
-
16
- def render(self, data: dict = None, expressions: dict = None) -> str:
17
- """Render the template with the provided data and expressions.
18
-
19
- Parameters
20
- ----------
21
- data : dict
22
- String data to insert
23
- expressions : dict
24
- Functions to call with data
25
-
26
- Returns
27
- -------
28
- str
29
- Template rendered with expression resolution and values inserted.
30
- """
31
-
32
- data = data if data else {}
33
- expressions = expressions if expressions else {}
34
- resolver = partial(self._resolve, live_data=data | expressions)
35
- return PATTERN.sub(resolver, self.template)
36
-
37
- def _resolve(self, match, *, live_data: dict):
38
- """Resolve a single name/path from the template. The argument is a
39
- merge of the raw data and the expressions, either of which are valid
40
- substitutions."""
41
- key = match.group(1).strip()
42
- return self.resolve(key, live_data)
43
-
44
- @staticmethod
45
- def resolve(key: str, live_data: dict):
46
- parts = key.split('.')
47
- current = live_data
48
- for part in parts:
49
- if part not in current:
50
- raise DyngleError(
51
- f"Invalid expression or data reference '{key}")
52
- current = current[part]
53
- if callable(current):
54
- return current(live_data)
55
- else:
56
- return current
@@ -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
-
@@ -1,13 +0,0 @@
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,,
File without changes
File without changes