dyngle 0.4.1__tar.gz → 0.4.3__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.

@@ -0,0 +1,141 @@
1
+ # Dyngle
2
+
3
+ ## Run lightweight local workflows
4
+
5
+ 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.
6
+
7
+ ## Basic usage
8
+
9
+ Create a configuration file (e.g., `.dyngle.yml`) with your workflows:
10
+
11
+ ```yaml
12
+ dyngle:
13
+ operations:
14
+ build:
15
+ - python -m pip install -e .
16
+ - python -m pytest
17
+ deploy:
18
+ - docker build -t myapp .
19
+ - docker push myapp
20
+ clean:
21
+ - rm -rf __pycache__
22
+ - rm -rf .pytest_cache
23
+ ```
24
+
25
+ Run an operation:
26
+
27
+ ```bash
28
+ dyngle run build
29
+ ```
30
+
31
+ ## Configuration
32
+
33
+ Dyngle reads configuration from YAML files. You can specify the config file location using:
34
+
35
+ - `--config` command line option
36
+ - `DYNGLE_CONFIG` environment variable
37
+ - `.dyngle.yml` in current directory
38
+ - `~/.dyngle.yml` in home directory
39
+
40
+ The configuration has 2 parts: `operations:` and `expressions`.
41
+
42
+ ## Data
43
+
44
+ Dyngle maintains a block of data throughout operations, which is parsed from YAML in stdin.
45
+
46
+ ## Operations
47
+
48
+ Operations contain steps as a YAML array. The lifecycle of an operation is:
49
+
50
+ 1. Load input data if it exists from YAML on stdin (if no tty)
51
+ 2. Perform template rendering on a step, using data and expressions (see below)
52
+ 3. Execute the step in a subprocess
53
+ 4. Continue with the next step
54
+
55
+ Note that operations in the config are _not_ full shell lines. They are passed directly to the system.
56
+
57
+ ## Templates
58
+
59
+ 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 `}}`).
60
+
61
+ For example, if stdin contains the following data:
62
+
63
+ ```yaml
64
+ name: Francis
65
+ ```
66
+
67
+ And the command looks like:
68
+
69
+ ``` yaml
70
+ - echo "Hello {{name}}!"
71
+ ```
72
+
73
+ Then the command will output "Hello Francis!".
74
+
75
+
76
+ ## Expressions
77
+
78
+ Configs can also contain expressions, written in Python, that can also be referenced in operation steps.
79
+
80
+ ```yaml
81
+ dyngle:
82
+ expressions:
83
+ say-hello: >-
84
+ 'Hello ' + name + '!'
85
+ operations:
86
+ say-hello: echo {{say-hello}}
87
+ ```
88
+
89
+ Expressions can use a controlled subset of the Python standard library, including:
90
+
91
+ - Built-in data types such as `str()`
92
+ - Essential built-in functions such as `len()`
93
+ - The core modules from the `datetime` package (but some methods such as `strftime()` will fail)
94
+ - A specialized function called `formatted()` to perform string formatting operations on a `datetime` object
95
+ - A restricted version of `Path()` that only operates within the current working directory
96
+ - Various other useful utilities, mostly read-only, such as the `math` module
97
+ - A special function called `resolve` which resolves data expressions using the same logic as in templates
98
+
99
+ Data keys containing hyphens are converted to valid Python names by replacing hyphens with underscores.
100
+
101
+ Expressions can reference data directly as local names in Python (using the underscore replacements)...
102
+
103
+ ```yaml
104
+ dyngle:
105
+ expressions:
106
+ say-hello: >-
107
+ 'Hello ' + full_name + '!'
108
+ ```
109
+
110
+ ... or using the `resolve()` function, which also allows expressions to essentially call other expressions, using the same underlying data set.
111
+
112
+ ```yaml
113
+ dyngle:
114
+ expressions:
115
+ hello: >-
116
+ 'Hello ' + resolve('formal-name') + '!'
117
+ formal-name: >-
118
+ 'Ms. ' + full_name
119
+ ```
120
+
121
+ Note it's also _possible_ to call other expressions by name as functions, if they only return hard-coded values (i.e. constants).
122
+
123
+ ```yaml
124
+ dyngle:
125
+ expressions:
126
+ author-name: Francis Potter
127
+ author-hello: >-
128
+ 'Hello ' + author_name()
129
+ ```
130
+
131
+ ## Security
132
+
133
+ 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.
134
+
135
+ ## Quick installation (MacOS)
136
+
137
+ ```bash
138
+ brew install python@3.11
139
+ python3.11 -m pip install pipx
140
+ pipx install dyngle
141
+ ```
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dyngle
3
- Version: 0.4.1
3
+ Version: 0.4.3
4
4
  Summary: Run lightweight local workflows
5
5
  License: MIT
6
6
  Author: Steampunk Wizard
@@ -52,30 +52,45 @@ Dyngle reads configuration from YAML files. You can specify the config file loca
52
52
  - `.dyngle.yml` in current directory
53
53
  - `~/.dyngle.yml` in home directory
54
54
 
55
- ## Workflow structure
55
+ The configuration has 2 parts: `operations:` and `expressions`.
56
56
 
57
- Each operation is defined as a list of tasks under `dyngle.operations`. Tasks are executed sequentially using Python's subprocess module for security.
57
+ ## Data
58
58
 
59
- Example with multiple operations:
59
+ Dyngle maintains a block of data throughout operations, which is parsed from YAML in stdin.
60
+
61
+ ## Operations
62
+
63
+ Operations contain steps as a YAML array. The lifecycle of an operation is:
64
+
65
+ 1. Load input data if it exists from YAML on stdin (if no tty)
66
+ 2. Perform template rendering on a step, using data and expressions (see below)
67
+ 3. Execute the step in a subprocess
68
+ 4. Continue with the next step
69
+
70
+ Note that operations in the config are _not_ full shell lines. They are passed directly to the system.
71
+
72
+ ## Templates
73
+
74
+ 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 `}}`).
75
+
76
+ For example, if stdin contains the following data:
60
77
 
61
78
  ```yaml
62
- dyngle:
63
- operations:
64
- test:
65
- - python -m unittest discover
66
- - python -m coverage report
67
- docs:
68
- - sphinx-build docs docs/_build
69
- - open docs/_build/index.html
70
- setup:
71
- - python -m venv venv
72
- - source venv/bin/activate
73
- - pip install -r requirements.txt
79
+ name: Francis
74
80
  ```
75
81
 
82
+ And the command looks like:
83
+
84
+ ``` yaml
85
+ - echo "Hello {{name}}!"
86
+ ```
87
+
88
+ Then the command will output "Hello Francis!".
89
+
90
+
76
91
  ## Expressions
77
92
 
78
- Configs can also contain expressions.
93
+ Configs can also contain expressions, written in Python, that can also be referenced in operation steps.
79
94
 
80
95
  ```yaml
81
96
  dyngle:
@@ -85,6 +100,7 @@ dyngle:
85
100
  operations:
86
101
  say-hello: echo {{say-hello}}
87
102
  ```
103
+
88
104
  Expressions can use a controlled subset of the Python standard library, including:
89
105
 
90
106
  - Built-in data types such as `str()`
@@ -92,10 +108,44 @@ Expressions can use a controlled subset of the Python standard library, includin
92
108
  - The core modules from the `datetime` package (but some methods such as `strftime()` will fail)
93
109
  - A specialized function called `formatted()` to perform string formatting operations on a `datetime` object
94
110
  - A restricted version of `Path()` that only operates within the current working directory
111
+ - Various other useful utilities, mostly read-only, such as the `math` module
112
+ - A special function called `resolve` which resolves data expressions using the same logic as in templates
113
+
114
+ Data keys containing hyphens are converted to valid Python names by replacing hyphens with underscores.
115
+
116
+ Expressions can reference data directly as local names in Python (using the underscore replacements)...
117
+
118
+ ```yaml
119
+ dyngle:
120
+ expressions:
121
+ say-hello: >-
122
+ 'Hello ' + full_name + '!'
123
+ ```
124
+
125
+ ... or using the `resolve()` function, which also allows expressions to essentially call other expressions, using the same underlying data set.
126
+
127
+ ```yaml
128
+ dyngle:
129
+ expressions:
130
+ hello: >-
131
+ 'Hello ' + resolve('formal-name') + '!'
132
+ formal-name: >-
133
+ 'Ms. ' + full_name
134
+ ```
135
+
136
+ Note it's also _possible_ to call other expressions by name as functions, if they only return hard-coded values (i.e. constants).
137
+
138
+ ```yaml
139
+ dyngle:
140
+ expressions:
141
+ author-name: Francis Potter
142
+ author-hello: >-
143
+ 'Hello ' + author_name()
144
+ ```
95
145
 
96
146
  ## Security
97
147
 
98
- 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.
148
+ 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.
99
149
 
100
150
  ## Quick installation (MacOS)
101
151
 
@@ -18,6 +18,8 @@ class RunCommand(DyngleCommand):
18
18
  def add_args(cls, parser: WizParser):
19
19
  super().add_args(parser)
20
20
  parser.add_argument('operation', help='Operation name to run')
21
+ parser.add_argument(
22
+ 'args', nargs='*', help='Optional operation arguments')
21
23
 
22
24
  def handle_vals(self):
23
25
  super().handle_vals()
@@ -37,7 +39,8 @@ class RunCommand(DyngleCommand):
37
39
  self._validate_operation_exists(operations)
38
40
  steps = operations[self.operation]
39
41
  data_string = self.app.stream.text
40
- data = safe_load(data_string)
42
+ data = safe_load(data_string) or {}
43
+ data['args'] = self.args
41
44
  for step_template in steps:
42
45
  step = Template(step_template).render(data, expressions)
43
46
  parts = shlex.split(step)
@@ -9,6 +9,8 @@ import json
9
9
  import re
10
10
  import yaml
11
11
 
12
+ from dyngle.template import Template
13
+
12
14
 
13
15
  def formatted_datetime(dt: datetime, format_string=None) -> str:
14
16
  """Safe datetime formatting using string operations"""
@@ -72,9 +74,38 @@ GLOBALS = {
72
74
  }
73
75
 
74
76
 
75
- def _evaluate(expression: str, data: dict) -> str:
77
+ def _evaluate(expression: str, locals: dict) -> str:
78
+ """Evaluate a Python expression with safe globals and user data context.
79
+
80
+ Safely evaluates a Python expression string using a restricted set of
81
+ global functions and modules, combined with user-provided data. The
82
+ expression is evaluated in a sandboxed environment that includes basic
83
+ Python built-ins, mathematical operations, date/time handling, and data
84
+ manipulation utilities.
85
+
86
+ Parameters
87
+ ----------
88
+ expression : str
89
+ A valid Python expression string to be evaluated.
90
+ data : dict
91
+ Dictionary containing variables and values to be made available during
92
+ expression evaluation. Note that hyphens in keys will be replaced by
93
+ underscores to create valid Python names.
94
+
95
+ Returns
96
+ -------
97
+ str
98
+ String representation of the evaluated expression result. If the result
99
+ is a tuple, returns the string representation of the last element.
100
+
101
+ Raises
102
+ ------
103
+ DyngleError
104
+ If the expression contains invalid variable names that are not found in
105
+ the provided data dictionary or global context.
106
+ """
76
107
  try:
77
- result = eval(expression, GLOBALS, data)
108
+ result = eval(expression, GLOBALS, locals)
78
109
  except KeyError:
79
110
  raise DyngleError(f"The following expression contains " +
80
111
  f"at least one invalid name: {expression}")
@@ -83,6 +114,9 @@ def _evaluate(expression: str, data: dict) -> str:
83
114
 
84
115
 
85
116
  def expression(text: str) -> Callable[[dict], str]:
86
- def evaluate(data: dict) -> str:
87
- return _evaluate(text, data)
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})
88
122
  return evaluate
@@ -0,0 +1,51 @@
1
+ from dataclasses import dataclass
2
+ from functools import partial
3
+ import re
4
+
5
+
6
+ PATTERN = re.compile(r'\{\{\s*([^}]+)\s*\}\}')
7
+
8
+
9
+ @dataclass
10
+ class Template:
11
+
12
+ template: str
13
+
14
+ def render(self, data: dict = None, expressions: dict = None) -> str:
15
+ """Render the template with the provided data and expressions.
16
+
17
+ Parameters
18
+ ----------
19
+ data : dict
20
+ String data to insert
21
+ expressions : dict
22
+ Functions to call with data
23
+
24
+ Returns
25
+ -------
26
+ str
27
+ Template rendered with expression resolution and values inserted.
28
+ """
29
+
30
+ data = data if data else {}
31
+ expressions = expressions if expressions else {}
32
+ resolver = partial(self._resolve, live_data=data | expressions)
33
+ return PATTERN.sub(resolver, self.template)
34
+
35
+ def _resolve(self, match, *, live_data: dict):
36
+ """Resolve a single name/path from the template. The argument is a
37
+ merge of the raw data and the expressions, either of which are valid
38
+ substitutions."""
39
+ key = match.group(1).strip()
40
+ return self.resolve(key, live_data)
41
+
42
+ @staticmethod
43
+ def resolve(key: str, live_data: dict):
44
+ parts = key.split('.')
45
+ current = live_data
46
+ for part in parts:
47
+ current = current[part]
48
+ if callable(current):
49
+ return current(live_data)
50
+ else:
51
+ return current
@@ -8,7 +8,7 @@ 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.4.1"
11
+ version = "0.4.3"
12
12
 
13
13
  [tool.poetry.dependencies]
14
14
  python = "~3.11"
dyngle-0.4.1/PACKAGE.md DELETED
@@ -1,91 +0,0 @@
1
- # Dyngle
2
-
3
- ## Run lightweight local workflows
4
-
5
- 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.
6
-
7
- ## Basic usage
8
-
9
- Create a configuration file (e.g., `.dyngle.yml`) with your workflows:
10
-
11
- ```yaml
12
- dyngle:
13
- operations:
14
- build:
15
- - python -m pip install -e .
16
- - python -m pytest
17
- deploy:
18
- - docker build -t myapp .
19
- - docker push myapp
20
- clean:
21
- - rm -rf __pycache__
22
- - rm -rf .pytest_cache
23
- ```
24
-
25
- Run an operation:
26
-
27
- ```bash
28
- dyngle run build
29
- ```
30
-
31
- ## Configuration
32
-
33
- Dyngle reads configuration from YAML files. You can specify the config file location using:
34
-
35
- - `--config` command line option
36
- - `DYNGLE_CONFIG` environment variable
37
- - `.dyngle.yml` in current directory
38
- - `~/.dyngle.yml` in home directory
39
-
40
- ## Workflow structure
41
-
42
- Each operation is defined as a list of tasks under `dyngle.operations`. Tasks are executed sequentially using Python's subprocess module for security.
43
-
44
- Example with multiple operations:
45
-
46
- ```yaml
47
- dyngle:
48
- operations:
49
- test:
50
- - python -m unittest discover
51
- - python -m coverage report
52
- docs:
53
- - sphinx-build docs docs/_build
54
- - open docs/_build/index.html
55
- setup:
56
- - python -m venv venv
57
- - source venv/bin/activate
58
- - pip install -r requirements.txt
59
- ```
60
-
61
- ## Expressions
62
-
63
- Configs can also contain expressions.
64
-
65
- ```yaml
66
- dyngle:
67
- expressions:
68
- say-hello: >-
69
- 'Hello ' + name + '!'
70
- operations:
71
- say-hello: echo {{say-hello}}
72
- ```
73
- Expressions can use a controlled subset of the Python standard library, including:
74
-
75
- - Built-in data types such as `str()`
76
- - Essential built-in functions such as `len()`
77
- - The core modules from the `datetime` package (but some methods such as `strftime()` will fail)
78
- - A specialized function called `formatted()` to perform string formatting operations on a `datetime` object
79
- - A restricted version of `Path()` that only operates within the current working directory
80
-
81
- ## Security
82
-
83
- 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.
84
-
85
- ## Quick installation (MacOS)
86
-
87
- ```bash
88
- brew install python@3.11
89
- python3.11 -m pip install pipx
90
- pipx install dyngle
91
- ```
@@ -1,31 +0,0 @@
1
- from dataclasses import dataclass
2
- from functools import partial
3
- import re
4
-
5
-
6
- PATTERN = re.compile(r'\{\{\s*([^}]+)\s*\}\}')
7
-
8
-
9
- @dataclass
10
- class Template:
11
-
12
- template: str
13
-
14
- def render(self, data: dict = None, expressions: dict = None) -> str:
15
- """Render the template with the provided data."""
16
- data = data if data else {}
17
- expressions = expressions if expressions else {}
18
- resolver = partial(self._resolve, data=data, expressions=expressions)
19
- return PATTERN.sub(resolver, self.template)
20
-
21
- def _resolve(self, match, *, data: dict, expressions: dict):
22
- """Resolve a single name/path from the template."""
23
- key = match.group(1).strip()
24
- if key in expressions:
25
- return expressions[key](data)
26
- else:
27
- parts = key.split('.')
28
- current = data
29
- for part in parts:
30
- current = current[part]
31
- return current
File without changes
File without changes
File without changes
File without changes