dyngle 0.7.0__tar.gz → 1.0.1__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.

Potentially problematic release.


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

@@ -1,12 +1,8 @@
1
1
  # Dyngle
2
2
 
3
- Use cases
4
-
5
- - A task runner
6
- - A lightweight workflow engine
7
- - A replacement for Make in Python projects
8
- - A replacement for short functions in RC files
9
- - Freedom from quirky Bash syntax
3
+ An experimantal, lightweight, easily configurable workflow engine for
4
+ automating development, operations, data processing, and content management
5
+ tasks.
10
6
 
11
7
  Technical foundations
12
8
 
@@ -41,16 +37,18 @@ dyngle run hello
41
37
 
42
38
  ## Configuration
43
39
 
44
- Dyngle reads configuration from YAML files. You can specify the config file location using:
40
+ Dyngle reads configuration from YAML files. Specify the config file location using any of the following (in order of precedence):
45
41
 
46
- - `--config` command line option
47
- - `DYNGLE_CONFIG` environment variable
48
- - `.dyngle.yml` in current directory
49
- - `~/.dyngle.yml` in home directory
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
50
46
 
51
47
  ## Operations
52
48
 
53
- 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.
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.
54
52
 
55
53
  ```yaml
56
54
  dyngle:
@@ -61,11 +59,11 @@ dyngle:
61
59
  - .venv/bin/pip install --upgrade pip poetry
62
60
  ```
63
61
 
64
- 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.
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.
65
63
 
66
64
  ## Data and Templates
67
65
 
68
- 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.
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.
69
67
 
70
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.
71
69
 
@@ -92,7 +90,7 @@ Hello Francis!
92
90
 
93
91
  ## Expressions
94
92
 
95
- 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.
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.
96
94
 
97
95
  Expressions may be defined in either of two ways in the configuration:
98
96
 
@@ -142,8 +140,6 @@ YAML keys can contain hyphens, which are fully supported in Dyngle. To reference
142
140
  - Reference the name using underscores instead of hyphens (they are automatically replaced), OR
143
141
  - Use the built-in special-purpose `resolve()` function (which can also be used to reference other expressions)
144
142
 
145
-
146
-
147
143
  ```yaml
148
144
  dyngle:
149
145
  expressions:
@@ -207,26 +203,31 @@ dyngle:
207
203
  - echo "Hello {{name}}"
208
204
  ```
209
205
 
210
- ## Data assignment operator
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.
211
209
 
212
- The Steps parser supports one special operator which assigns the output (stdout) of its command to a Data field for use in subsequent steps.
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
213
212
 
214
- The operator is `=>` and must go after the command and its arguments. Follow the operator with the name of the Data key to assign.
213
+ The operators must appear in order in the step and must be isolated with whitespace, i.e.
215
214
 
216
- Example:
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.
217
220
 
218
221
  ```yaml
219
222
  dyngle:
220
223
  operations:
221
- today:
222
- expressions:
223
- just-the-date: resolve('full-date')[0:10]
224
- steps:
225
- - date => full-date
226
- - echo "Today is {{just-the-date}}"
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!"
227
228
  ```
228
229
 
229
- (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)
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.
230
231
 
231
232
  ## Lifecycle
232
233
 
@@ -235,7 +236,7 @@ The lifecycle of an operation is:
235
236
  1. Load Data if it exists from YAML on stdin (if no tty)
236
237
  2. Find the named Operation in the configuration
237
238
  2. Perform template rendering on the first Step, using Data and Expressions
238
- 3. Execute the Step in a subprocess
239
+ 3. Execute the Step in a subprocess, passing in an input value and populating an output value in the Data
239
240
  4. Continue with the next Step
240
241
 
241
242
  Note that operations in the config are _not_ full shell lines. They are passed directly to the system.
@@ -1,27 +1,25 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dyngle
3
- Version: 0.7.0
3
+ Version: 1.0.1
4
4
  Summary: Run lightweight local workflows
5
5
  License: MIT
6
6
  Author: Steampunk Wizard
7
7
  Author-email: dyngle@steamwiz.io
8
- Requires-Python: >=3.11,<3.12
8
+ Requires-Python: >=3.11,<4.0
9
9
  Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
12
14
  Requires-Dist: requests (>=2.32.3,<3.0.0)
13
- Requires-Dist: wizlib (>=3.1.4,<4.0.0)
15
+ Requires-Dist: wizlib (>=3.3.8,<3.4.0)
14
16
  Description-Content-Type: text/markdown
15
17
 
16
18
  # Dyngle
17
19
 
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
20
+ An experimantal, lightweight, easily configurable workflow engine for
21
+ automating development, operations, data processing, and content management
22
+ tasks.
25
23
 
26
24
  Technical foundations
27
25
 
@@ -56,16 +54,18 @@ dyngle run hello
56
54
 
57
55
  ## Configuration
58
56
 
59
- Dyngle reads configuration from YAML files. You can specify the config file location using:
57
+ Dyngle reads configuration from YAML files. Specify the config file location using any of the following (in order of precedence):
60
58
 
61
- - `--config` command line option
62
- - `DYNGLE_CONFIG` environment variable
63
- - `.dyngle.yml` in current directory
64
- - `~/.dyngle.yml` in home directory
59
+ 1. A `--config` command line option, OR
60
+ 2. A `DYNGLE_CONFIG` environment variable, OR
61
+ 3. `.dyngle.yml` in current directory, OR
62
+ 4. `~/.dyngle.yml` in home directory
65
63
 
66
64
  ## Operations
67
65
 
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.
66
+ 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.
67
+
68
+ As a serious example, consider the `init` operation from the Dyngle configuration delivered with the project's source code.
69
69
 
70
70
  ```yaml
71
71
  dyngle:
@@ -76,11 +76,11 @@ dyngle:
76
76
  - .venv/bin/pip install --upgrade pip poetry
77
77
  ```
78
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.
79
+ 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.
80
80
 
81
81
  ## Data and Templates
82
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.
83
+ 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.
84
84
 
85
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
86
 
@@ -107,7 +107,7 @@ Hello Francis!
107
107
 
108
108
  ## Expressions
109
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.
110
+ 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.
111
111
 
112
112
  Expressions may be defined in either of two ways in the configuration:
113
113
 
@@ -157,8 +157,6 @@ YAML keys can contain hyphens, which are fully supported in Dyngle. To reference
157
157
  - Reference the name using underscores instead of hyphens (they are automatically replaced), OR
158
158
  - Use the built-in special-purpose `resolve()` function (which can also be used to reference other expressions)
159
159
 
160
-
161
-
162
160
  ```yaml
163
161
  dyngle:
164
162
  expressions:
@@ -222,26 +220,31 @@ dyngle:
222
220
  - echo "Hello {{name}}"
223
221
  ```
224
222
 
225
- ## Data assignment operator
223
+ ## Passing values between Steps in an Operation
224
+
225
+ The Steps parser supports two special operators designed to move data between Steps in an explicit way.
226
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.
227
+ - The data assignment operator (`=>`) assigns the contents of stdout from the command to an element in the data
228
+ - The data input operator (`->`) assigns the value of an element in the data (or an evaluated expression) to stdin for the command
228
229
 
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
+ The operators must appear in order in the step and must be isolated with whitespace, i.e.
230
231
 
231
- Example:
232
+ ```
233
+ <input-variable-name> -> <command and arguments> => <output-variable-name>
234
+ ```
235
+
236
+ Here we get into more useful functionality, where commands can be strung together in meaningful ways without the need for Bash.
232
237
 
233
238
  ```yaml
234
239
  dyngle:
235
240
  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}}"
241
+ weather:
242
+ - curl -s "https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&current_weather=true" => weather-data
243
+ - weather-data -> jq -j '.current_weather.temperature' => temperature
244
+ - echo "It's {{temperature}} degrees out there!"
242
245
  ```
243
246
 
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)
247
+ 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.
245
248
 
246
249
  ## Lifecycle
247
250
 
@@ -250,7 +253,7 @@ The lifecycle of an operation is:
250
253
  1. Load Data if it exists from YAML on stdin (if no tty)
251
254
  2. Find the named Operation in the configuration
252
255
  2. Perform template rendering on the first Step, using Data and Expressions
253
- 3. Execute the Step in a subprocess
256
+ 3. Execute the Step in a subprocess, passing in an input value and populating an output value in the Data
254
257
  4. Continue with the next Step
255
258
 
256
259
  Note that operations in the config are _not_ full shell lines. They are passed directly to the system.
@@ -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
 
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
@@ -5,7 +5,8 @@ import shlex
5
5
  import subprocess
6
6
 
7
7
  from dyngle.error import DyngleError
8
- from dyngle.template import Template
8
+ from dyngle.model.live_data import LiveData
9
+ from dyngle.model.template import Template
9
10
 
10
11
 
11
12
  @dataclass
@@ -16,12 +17,12 @@ class Operation:
16
17
  steps: list
17
18
 
18
19
  def run(self, data: dict, global_expressions: dict):
19
- # The data dict is mutable
20
- steps = self.steps
21
20
  expressions = global_expressions | self.local_expressions
22
- for markup in steps:
21
+ # Data takes precedence if names match
22
+ live_data = LiveData(expressions) | data
23
+ for markup in self.steps:
23
24
  step = Step(markup)
24
- step.run(data, expressions)
25
+ step.run(live_data)
25
26
 
26
27
 
27
28
  STEP_PATTERN = re.compile(
@@ -46,10 +47,12 @@ class Step:
46
47
  self.input, self.command_template, self.output = \
47
48
  parse_step(self.markup)
48
49
 
49
- def run(self, data, expressions):
50
- command = [Template(word).render(data, expressions)
50
+ def run(self, live_data: LiveData):
51
+ command = [Template(word).render(live_data).strip()
51
52
  for word in self.command_template]
52
53
  pipes = {}
54
+ if self.input:
55
+ pipes["input"] = live_data.resolve(self.input)
53
56
  if self.output:
54
57
  pipes['stdout'] = subprocess.PIPE
55
58
  result = subprocess.run(command, text=True, **pipes)
@@ -57,4 +60,4 @@ class Step:
57
60
  raise DyngleError(
58
61
  f'Step failed with code {result.returncode}: {self.markup}')
59
62
  if self.output:
60
- data[self.output] = result.stdout
63
+ live_data[self.output] = result.stdout.rstrip()
@@ -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)
@@ -8,11 +8,11 @@ description = "Run lightweight local workflows"
8
8
  authors = ["Steampunk Wizard <dyngle@steamwiz.io>"]
9
9
  license = "MIT"
10
10
  readme = "PACKAGE.md"
11
- version = "0.7.0"
11
+ version = "1.0.1"
12
12
 
13
13
  [tool.poetry.dependencies]
14
- python = "~3.11"
15
- wizlib = "^3.1.4"
14
+ python = "^3.11"
15
+ wizlib = "~3.3.8"
16
16
  requests = "^2.32.3"
17
17
 
18
18
  [tool.poetry.group.dev.dependencies]
@@ -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
File without changes
File without changes