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.

@@ -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.4.2
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'Operation "{self.operation}" not found. " + \
31
- f"Available operations: {available_operations}')
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.config.get('dyngle-operations')
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, 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.2"
11
+ version = "0.5.0"
12
12
 
13
13
  [tool.poetry.dependencies]
14
14
  python = "~3.11"
@@ -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 {}
@@ -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