dyngle 0.4.2__tar.gz → 0.5.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.
Potentially problematic release.
This version of dyngle might be problematic. Click here for more details.
- {dyngle-0.4.2 → dyngle-0.5.0}/PACKAGE.md +33 -0
- {dyngle-0.4.2 → dyngle-0.5.0}/PKG-INFO +34 -1
- dyngle-0.5.0/dyngle/__init__.py +58 -0
- {dyngle-0.4.2 → dyngle-0.5.0}/dyngle/command/run_command.py +7 -4
- {dyngle-0.4.2 → dyngle-0.5.0}/dyngle/expression.py +38 -4
- dyngle-0.5.0/dyngle/template.py +51 -0
- {dyngle-0.4.2 → dyngle-0.5.0}/pyproject.toml +1 -1
- dyngle-0.4.2/dyngle/__init__.py +0 -23
- dyngle-0.4.2/dyngle/template.py +0 -31
- {dyngle-0.4.2 → dyngle-0.5.0}/dyngle/__main__.py +0 -0
- {dyngle-0.4.2 → dyngle-0.5.0}/dyngle/command/__init__.py +0 -0
- {dyngle-0.4.2 → dyngle-0.5.0}/dyngle/error.py +0 -0
- {dyngle-0.4.2 → dyngle-0.5.0}/dyngle/safe_path.py +0 -0
|
@@ -94,6 +94,39 @@ Expressions can use a controlled subset of the Python standard library, includin
|
|
|
94
94
|
- A specialized function called `formatted()` to perform string formatting operations on a `datetime` object
|
|
95
95
|
- A restricted version of `Path()` that only operates within the current working directory
|
|
96
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
|
+
```
|
|
97
130
|
|
|
98
131
|
## Security
|
|
99
132
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: dyngle
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Run lightweight local workflows
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Steampunk Wizard
|
|
@@ -109,6 +109,39 @@ Expressions can use a controlled subset of the Python standard library, includin
|
|
|
109
109
|
- A specialized function called `formatted()` to perform string formatting operations on a `datetime` object
|
|
110
110
|
- A restricted version of `Path()` that only operates within the current working directory
|
|
111
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
|
+
```
|
|
112
145
|
|
|
113
146
|
## Security
|
|
114
147
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from functools import cached_property
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from wizlib.app import WizApp
|
|
4
|
+
from wizlib.stream_handler import StreamHandler
|
|
5
|
+
from wizlib.config_handler import ConfigHandler
|
|
6
|
+
from wizlib.ui_handler import UIHandler
|
|
7
|
+
|
|
8
|
+
from dyngle.command import DyngleCommand
|
|
9
|
+
from dyngle.error import DyngleError
|
|
10
|
+
from dyngle.expression import expression
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DyngleApp(WizApp):
|
|
14
|
+
|
|
15
|
+
base = DyngleCommand
|
|
16
|
+
name = 'dyngle'
|
|
17
|
+
handlers = [StreamHandler, ConfigHandler, UIHandler]
|
|
18
|
+
|
|
19
|
+
# For possible upstreaming to WizLib, a mechanism to "import" configuration
|
|
20
|
+
# settings from external files.
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def _imported_configrations(self):
|
|
24
|
+
if not hasattr(self, '__imported_configurations'):
|
|
25
|
+
imports = self.config.get('dyngle-imports')
|
|
26
|
+
confs = []
|
|
27
|
+
if imports:
|
|
28
|
+
for filename in imports:
|
|
29
|
+
full_filename = Path(filename).expanduser()
|
|
30
|
+
confs.append(ConfigHandler(full_filename))
|
|
31
|
+
self.__imported_configurations = confs
|
|
32
|
+
return self.__imported_configurations
|
|
33
|
+
|
|
34
|
+
def _get_configuration_details(self, type: str):
|
|
35
|
+
label = f'dyngle-{type}'
|
|
36
|
+
details = {}
|
|
37
|
+
for conf in self._imported_configrations:
|
|
38
|
+
if (imported_details := conf.get(label)):
|
|
39
|
+
details |= imported_details
|
|
40
|
+
configured_details = self.config.get(label)
|
|
41
|
+
if configured_details:
|
|
42
|
+
details |= configured_details
|
|
43
|
+
return details
|
|
44
|
+
|
|
45
|
+
@cached_property
|
|
46
|
+
def operations(self):
|
|
47
|
+
operations = self._get_configuration_details('operations')
|
|
48
|
+
if not operations:
|
|
49
|
+
raise DyngleError("No operations defined")
|
|
50
|
+
return operations
|
|
51
|
+
|
|
52
|
+
@cached_property
|
|
53
|
+
def expressions(self):
|
|
54
|
+
expr_texts = self._get_configuration_details('expressions')
|
|
55
|
+
if expr_texts:
|
|
56
|
+
return {k: expression(t) for k, t in expr_texts.items()}
|
|
57
|
+
else:
|
|
58
|
+
return {}
|
|
@@ -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()
|
|
@@ -27,17 +29,18 @@ class RunCommand(DyngleCommand):
|
|
|
27
29
|
if self.operation not in operations:
|
|
28
30
|
available_operations = ', '.join(operations.keys())
|
|
29
31
|
raise DyngleError(
|
|
30
|
-
f
|
|
31
|
-
|
|
32
|
+
f"Operation '{self.operation}' not found. " +
|
|
33
|
+
f"Available operations: {available_operations}")
|
|
32
34
|
|
|
33
35
|
@DyngleCommand.wrap
|
|
34
36
|
def execute(self):
|
|
35
37
|
expressions = self.app.expressions
|
|
36
|
-
operations = self.app.
|
|
38
|
+
operations = self.app.operations
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
dyngle-0.4.2/dyngle/__init__.py
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
from functools import cached_property
|
|
2
|
-
from wizlib.app import WizApp
|
|
3
|
-
from wizlib.stream_handler import StreamHandler
|
|
4
|
-
from wizlib.config_handler import ConfigHandler
|
|
5
|
-
from wizlib.ui_handler import UIHandler
|
|
6
|
-
|
|
7
|
-
from dyngle.command import DyngleCommand
|
|
8
|
-
from dyngle.expression import expression
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class DyngleApp(WizApp):
|
|
12
|
-
|
|
13
|
-
base = DyngleCommand
|
|
14
|
-
name = 'dyngle'
|
|
15
|
-
handlers = [StreamHandler, ConfigHandler, UIHandler]
|
|
16
|
-
|
|
17
|
-
@cached_property
|
|
18
|
-
def expressions(self):
|
|
19
|
-
expr_texts = self.config.get('dyngle-expressions')
|
|
20
|
-
if expr_texts:
|
|
21
|
-
return {k: expression(t) for k, t in expr_texts.items()}
|
|
22
|
-
else:
|
|
23
|
-
return {}
|
dyngle-0.4.2/dyngle/template.py
DELETED
|
@@ -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
|