dyngle 1.3.3__tar.gz → 1.4.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-1.3.3 → dyngle-1.4.0}/PKG-INFO +1 -1
- dyngle-1.4.0/dyngle/__init__.py +50 -0
- {dyngle-1.3.3 → dyngle-1.4.0}/dyngle/command/run_command.py +3 -4
- dyngle-1.4.0/dyngle/model/dyngleverse.py +37 -0
- dyngle-1.4.0/dyngle/model/operation.py +126 -0
- {dyngle-1.3.3 → dyngle-1.4.0}/pyproject.toml +1 -1
- dyngle-1.3.3/dyngle/__init__.py +0 -79
- dyngle-1.3.3/dyngle/model/operation.py +0 -62
- {dyngle-1.3.3 → dyngle-1.4.0}/PACKAGE.md +0 -0
- {dyngle-1.3.3 → dyngle-1.4.0}/dyngle/__main__.py +0 -0
- {dyngle-1.3.3 → dyngle-1.4.0}/dyngle/command/__init__.py +0 -0
- {dyngle-1.3.3 → dyngle-1.4.0}/dyngle/command/null_command.py +0 -0
- {dyngle-1.3.3 → dyngle-1.4.0}/dyngle/error.py +0 -0
- {dyngle-1.3.3 → dyngle-1.4.0}/dyngle/model/__init__.py +0 -0
- {dyngle-1.3.3 → dyngle-1.4.0}/dyngle/model/expression.py +0 -0
- {dyngle-1.3.3 → dyngle-1.4.0}/dyngle/model/live_data.py +0 -0
- {dyngle-1.3.3 → dyngle-1.4.0}/dyngle/model/safe_path.py +0 -0
- {dyngle-1.3.3 → dyngle-1.4.0}/dyngle/model/template.py +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
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.model.dyngleverse import Dyngleverse
|
|
11
|
+
from dyngle.model.expression import expression
|
|
12
|
+
from dyngle.model.operation import Operation
|
|
13
|
+
from dyngle.model.template import Template
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DyngleApp(WizApp):
|
|
17
|
+
|
|
18
|
+
base = DyngleCommand
|
|
19
|
+
name = 'dyngle'
|
|
20
|
+
handlers = [StreamHandler, ConfigHandler, UIHandler]
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def dyngleverse(self):
|
|
24
|
+
"""Offload the indexing of operation and expression definitions to
|
|
25
|
+
another class. But we keep import handling here in the app because we
|
|
26
|
+
might want to upstream import/include to WizLib at some point."""
|
|
27
|
+
|
|
28
|
+
if not hasattr(self, '_dyngleverse'):
|
|
29
|
+
self._dyngleverse = Dyngleverse()
|
|
30
|
+
imports = self._get_imports(self.config, [])
|
|
31
|
+
for imported_config in imports:
|
|
32
|
+
definitions = imported_config.get('dyngle')
|
|
33
|
+
self._dyngleverse.load_config(definitions)
|
|
34
|
+
self._dyngleverse.load_config(self.config.get('dyngle'))
|
|
35
|
+
return self._dyngleverse
|
|
36
|
+
|
|
37
|
+
def _get_imports(self,
|
|
38
|
+
config_handler: ConfigHandler,
|
|
39
|
+
no_loops: list) -> dict:
|
|
40
|
+
imports = config_handler.get('dyngle-imports')
|
|
41
|
+
confs = []
|
|
42
|
+
if imports:
|
|
43
|
+
for filename in imports:
|
|
44
|
+
full_filename = Path(filename).expanduser()
|
|
45
|
+
if full_filename not in no_loops:
|
|
46
|
+
no_loops.append(full_filename)
|
|
47
|
+
child_handler = ConfigHandler(full_filename)
|
|
48
|
+
confs += self._get_imports(child_handler, no_loops)
|
|
49
|
+
confs.append(ConfigHandler(full_filename))
|
|
50
|
+
return confs
|
|
@@ -25,7 +25,7 @@ class RunCommand(DyngleCommand):
|
|
|
25
25
|
|
|
26
26
|
def handle_vals(self):
|
|
27
27
|
super().handle_vals()
|
|
28
|
-
keys = self.app.operations.keys()
|
|
28
|
+
keys = self.app.dyngleverse.operations.keys()
|
|
29
29
|
if not self.provided('operation'):
|
|
30
30
|
self.operation = self.app.ui.get_text('Operation: ', sorted(keys))
|
|
31
31
|
if not self.operation:
|
|
@@ -37,7 +37,6 @@ class RunCommand(DyngleCommand):
|
|
|
37
37
|
def execute(self):
|
|
38
38
|
data_string = self.app.stream.text
|
|
39
39
|
data = safe_load(data_string) or {}
|
|
40
|
-
|
|
41
|
-
operation
|
|
42
|
-
operation.run(data, self.app.globals)
|
|
40
|
+
operation = self.app.dyngleverse.operations[self.operation]
|
|
41
|
+
operation.run(data, self.args)
|
|
43
42
|
return f'Operation "{self.operation}" completed successfully'
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from functools import cached_property
|
|
2
|
+
|
|
3
|
+
from dyngle.model.expression import expression
|
|
4
|
+
from dyngle.model.live_data import LiveData
|
|
5
|
+
from dyngle.model.operation import Operation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Dyngleverse:
|
|
9
|
+
"""Represents the entire immutable set of definitions for operations,
|
|
10
|
+
expresssions, and values. Operates as a sort of index/database."""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.operations = {}
|
|
14
|
+
self.all_globals = {}
|
|
15
|
+
|
|
16
|
+
def load_config(self, config: dict):
|
|
17
|
+
"""
|
|
18
|
+
Load additional configuration, which will always take higher precedence
|
|
19
|
+
than previously loaded configuration.
|
|
20
|
+
"""
|
|
21
|
+
ops_defs = config.get('operations') or {}
|
|
22
|
+
for key, op_def in ops_defs.items():
|
|
23
|
+
operation = Operation(self, op_def, key)
|
|
24
|
+
self.operations[key] = operation
|
|
25
|
+
self.all_globals |= Dyngleverse.parse_constants(config)
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def parse_constants(definition: dict):
|
|
29
|
+
"""
|
|
30
|
+
At either the global (dyngleverse) or local (within an operation)
|
|
31
|
+
level, we might find values and expressions.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
expr_texts = definition.get('expressions') or {}
|
|
35
|
+
expressions = {k: expression(t) for k, t in expr_texts.items()}
|
|
36
|
+
values = definition.get('values') or {}
|
|
37
|
+
return expressions | values
|
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
class Operation:
|
|
13
|
+
"""A named operation defined in configuration. Can be called from a Dyngle
|
|
14
|
+
command (i.e. `dyngle run`) or as a sub-operation."""
|
|
15
|
+
|
|
16
|
+
all_locals = {}
|
|
17
|
+
|
|
18
|
+
def __init__(self, dyngleverse, definition: dict | list, key: str):
|
|
19
|
+
"""
|
|
20
|
+
definition: Either a dict containing steps and local
|
|
21
|
+
expressions/values, or a list containing only steps
|
|
22
|
+
"""
|
|
23
|
+
self.dyngleverse = dyngleverse
|
|
24
|
+
if isinstance(definition, list):
|
|
25
|
+
steps_def = definition
|
|
26
|
+
elif isinstance(definition, dict):
|
|
27
|
+
steps_def = definition.get('steps') or []
|
|
28
|
+
self.all_locals = dyngleverse.parse_constants(definition)
|
|
29
|
+
self.sequence = Sequence(dyngleverse, self, steps_def)
|
|
30
|
+
|
|
31
|
+
def run(self, data: dict | LiveData, args: list):
|
|
32
|
+
"""
|
|
33
|
+
data - The main set of data going into the operation
|
|
34
|
+
|
|
35
|
+
args - Arguments to the operation
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# The tye of data tells us the run condition - if already a LiveData
|
|
39
|
+
# object then we don't recreate it (i.e. sub-operation)
|
|
40
|
+
|
|
41
|
+
if not isinstance(data, LiveData):
|
|
42
|
+
live_data = LiveData(data) | self.dyngleverse.all_globals
|
|
43
|
+
else:
|
|
44
|
+
live_data = data
|
|
45
|
+
live_data |= self.all_locals | {'args': args}
|
|
46
|
+
self.sequence.run(live_data)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Sequence:
|
|
50
|
+
"""We allow for the possibility that a sequence of steps might run at other
|
|
51
|
+
levels than the operation itself, for example in a conditional block."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, dyngleverse, operation: Operation, steps_def: list):
|
|
54
|
+
self.steps = [Step.parse_def(dyngleverse, d) for d in steps_def]
|
|
55
|
+
|
|
56
|
+
def run(self, live_data: LiveData):
|
|
57
|
+
for step in self.steps:
|
|
58
|
+
step.run(live_data)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Step:
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def parse_def(dyngleverse, definition: dict | str):
|
|
65
|
+
for step_type in [CommandStep, SubOperationStep]:
|
|
66
|
+
if step_type.fits(definition):
|
|
67
|
+
return step_type(dyngleverse, definition)
|
|
68
|
+
raise DyngleError(f"Unknown step definition\n{definition}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Ideally these would be subclasses in a ClassFamily (or use an ABC)
|
|
72
|
+
|
|
73
|
+
class CommandStep:
|
|
74
|
+
|
|
75
|
+
PATTERN = re.compile(
|
|
76
|
+
r'^\s*(?:([\w.-]+)\s+->\s+)?(.+?)(?:\s+=>\s+([\w.-]+))?\s*$')
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def fits(cls, definition: dict | str):
|
|
80
|
+
return isinstance(definition, str)
|
|
81
|
+
|
|
82
|
+
def __init__(self, dyngleverse, markup: str):
|
|
83
|
+
self.markup = markup
|
|
84
|
+
if match := self.PATTERN.match(markup):
|
|
85
|
+
self.input, command_text, self.output = match.groups()
|
|
86
|
+
command_template = shlex.split(command_text.strip())
|
|
87
|
+
self.command_template = command_template
|
|
88
|
+
else:
|
|
89
|
+
raise DyngleError(f"Invalid step markup {{markup}}")
|
|
90
|
+
|
|
91
|
+
def run(self, live_data: LiveData):
|
|
92
|
+
command = [Template(word).render(live_data).strip()
|
|
93
|
+
for word in self.command_template]
|
|
94
|
+
pipes = {}
|
|
95
|
+
if self.input:
|
|
96
|
+
pipes["input"] = live_data.resolve(self.input)
|
|
97
|
+
if self.output:
|
|
98
|
+
pipes['stdout'] = subprocess.PIPE
|
|
99
|
+
result = subprocess.run(command, text=True, **pipes)
|
|
100
|
+
if result.returncode != 0:
|
|
101
|
+
raise DyngleError(
|
|
102
|
+
f'Step failed with code {result.returncode}: {self.markup}')
|
|
103
|
+
if self.output:
|
|
104
|
+
live_data[self.output] = result.stdout.rstrip()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class SubOperationStep:
|
|
108
|
+
"""Instead of calling a system command, call another operation in the same
|
|
109
|
+
Dyngleverse"""
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def fits(cls, definition: dict | str):
|
|
113
|
+
return isinstance(definition, dict) and 'sub' in definition
|
|
114
|
+
|
|
115
|
+
def __init__(self, dyngleverse, definition: dict):
|
|
116
|
+
self.dyngleverse = dyngleverse
|
|
117
|
+
operation_key = definition['sub']
|
|
118
|
+
self.operation = self.dyngleverse.operations.get(operation_key)
|
|
119
|
+
if not self.operation:
|
|
120
|
+
raise DyngleError(f"Unknown operation {operation_key}")
|
|
121
|
+
self.args_template = definition.get('args') or ''
|
|
122
|
+
|
|
123
|
+
def run(self, live_data: LiveData):
|
|
124
|
+
args = [Template(word).render(live_data).strip()
|
|
125
|
+
for word in self.args_template]
|
|
126
|
+
self.operation.run(live_data, args)
|
dyngle-1.3.3/dyngle/__init__.py
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
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.model.expression import expression
|
|
11
|
-
from dyngle.model.operation import Operation
|
|
12
|
-
from dyngle.model.template import Template
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class DyngleApp(WizApp):
|
|
16
|
-
|
|
17
|
-
base = DyngleCommand
|
|
18
|
-
name = 'dyngle'
|
|
19
|
-
handlers = [StreamHandler, ConfigHandler, UIHandler]
|
|
20
|
-
|
|
21
|
-
# For possible upstreaming to WizLib, a mechanism to "import" configuration
|
|
22
|
-
# settings from external files.
|
|
23
|
-
|
|
24
|
-
@property
|
|
25
|
-
def _imported_configrations(self):
|
|
26
|
-
if not hasattr(self, '__imported_configurations'):
|
|
27
|
-
imports = self.config.get('dyngle-imports')
|
|
28
|
-
confs = []
|
|
29
|
-
if imports:
|
|
30
|
-
for filename in imports:
|
|
31
|
-
full_filename = Path(filename).expanduser()
|
|
32
|
-
confs.append(ConfigHandler(full_filename))
|
|
33
|
-
self.__imported_configurations = confs
|
|
34
|
-
return self.__imported_configurations
|
|
35
|
-
|
|
36
|
-
def _get_configuration_details(self, type: str):
|
|
37
|
-
label = f'dyngle-{type}'
|
|
38
|
-
details = {}
|
|
39
|
-
for conf in self._imported_configrations:
|
|
40
|
-
if (imported_details := conf.get(label)):
|
|
41
|
-
details |= imported_details
|
|
42
|
-
configured_details = self.config.get(label)
|
|
43
|
-
if configured_details:
|
|
44
|
-
details |= configured_details
|
|
45
|
-
return details
|
|
46
|
-
|
|
47
|
-
@cached_property
|
|
48
|
-
def operations(self):
|
|
49
|
-
operations_configs = self._get_configuration_details('operations')
|
|
50
|
-
if not operations_configs:
|
|
51
|
-
raise DyngleError("No operations defined in configuration")
|
|
52
|
-
operations = {}
|
|
53
|
-
for key, config in operations_configs.items():
|
|
54
|
-
if isinstance(config, list):
|
|
55
|
-
operation = Operation({}, config)
|
|
56
|
-
elif isinstance(config, dict):
|
|
57
|
-
expr_texts = config.get('expressions') or {}
|
|
58
|
-
expressions = _expressions_from_texts(expr_texts)
|
|
59
|
-
values = config.get('values') or {}
|
|
60
|
-
steps = config.get('steps') or []
|
|
61
|
-
operation = Operation(expressions | values, steps)
|
|
62
|
-
else:
|
|
63
|
-
raise DyngleError(f"Invalid operation configuration for {key}")
|
|
64
|
-
operations[key] = operation
|
|
65
|
-
return operations
|
|
66
|
-
|
|
67
|
-
@cached_property
|
|
68
|
-
def globals(self):
|
|
69
|
-
expr_texts = self._get_configuration_details('expressions')
|
|
70
|
-
expressions = _expressions_from_texts(expr_texts)
|
|
71
|
-
values = self._get_configuration_details('values')
|
|
72
|
-
return expressions | (values if values else {})
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def _expressions_from_texts(expr_texts):
|
|
76
|
-
if expr_texts:
|
|
77
|
-
return {k: expression(t) for k, t in expr_texts.items()}
|
|
78
|
-
else:
|
|
79
|
-
return {}
|
|
@@ -1,62 +0,0 @@
|
|
|
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
|
-
# Expressions and values defined within the operation
|
|
16
|
-
locals: dict
|
|
17
|
-
|
|
18
|
-
steps: list
|
|
19
|
-
|
|
20
|
-
def run(self, data: dict, globals: dict):
|
|
21
|
-
live_data = LiveData(globals) | self.locals | data
|
|
22
|
-
for markup in self.steps:
|
|
23
|
-
step = Step(markup)
|
|
24
|
-
step.run(live_data)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
STEP_PATTERN = re.compile(
|
|
28
|
-
r'^\s*(?:([\w.-]+)\s+->\s+)?(.+?)(?:\s+=>\s+([\w.-]+))?\s*$')
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def parse_step(markup):
|
|
32
|
-
if match := STEP_PATTERN.match(markup):
|
|
33
|
-
input, command_text, output = match.groups()
|
|
34
|
-
command_template = shlex.split(command_text.strip())
|
|
35
|
-
return input, command_template, output
|
|
36
|
-
else:
|
|
37
|
-
raise DyngleError(f"Invalid step markup {{markup}}")
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@dataclass
|
|
41
|
-
class Step:
|
|
42
|
-
|
|
43
|
-
markup: str
|
|
44
|
-
|
|
45
|
-
def __post_init__(self):
|
|
46
|
-
self.input, self.command_template, self.output = \
|
|
47
|
-
parse_step(self.markup)
|
|
48
|
-
|
|
49
|
-
def run(self, live_data: LiveData):
|
|
50
|
-
command = [Template(word).render(live_data).strip()
|
|
51
|
-
for word in self.command_template]
|
|
52
|
-
pipes = {}
|
|
53
|
-
if self.input:
|
|
54
|
-
pipes["input"] = live_data.resolve(self.input)
|
|
55
|
-
if self.output:
|
|
56
|
-
pipes['stdout'] = subprocess.PIPE
|
|
57
|
-
result = subprocess.run(command, text=True, **pipes)
|
|
58
|
-
if result.returncode != 0:
|
|
59
|
-
raise DyngleError(
|
|
60
|
-
f'Step failed with code {result.returncode}: {self.markup}')
|
|
61
|
-
if self.output:
|
|
62
|
-
live_data[self.output] = result.stdout.rstrip()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|