marsh-lib 0.2.0__py3-none-any.whl
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.
- marsh/__init__.py +2 -0
- marsh/bash/__init__.py +3 -0
- marsh/bash/bash_factory.py +74 -0
- marsh/bash/bash_grammar.py +44 -0
- marsh/bash/bash_runner_decorators.py +0 -0
- marsh/bash/bash_script.py +71 -0
- marsh/constants.py +5 -0
- marsh/core/__init__.py +8 -0
- marsh/core/authenticator.py +7 -0
- marsh/core/cmd_run_decorator.py +289 -0
- marsh/core/command_grammar.py +45 -0
- marsh/core/connector.py +77 -0
- marsh/core/conveyor.py +105 -0
- marsh/core/executor.py +296 -0
- marsh/core/expression.py +208 -0
- marsh/core/script.py +38 -0
- marsh/dag/__init__.py +11 -0
- marsh/dag/dag.py +748 -0
- marsh/dag/node.py +52 -0
- marsh/dag/startable.py +48 -0
- marsh/docker/__init__.py +2 -0
- marsh/docker/docker_command_grammar.py +31 -0
- marsh/docker/docker_executor.py +181 -0
- marsh/exceptions.py +16 -0
- marsh/logger.py +116 -0
- marsh/modifier_functions/__init__.py +2 -0
- marsh/modifier_functions/case_conversion.py +17 -0
- marsh/modifier_functions/readers.py +11 -0
- marsh/powershell/__init__.py +0 -0
- marsh/processor_functions/__init__.py +3 -0
- marsh/processor_functions/printers.py +63 -0
- marsh/processor_functions/raisers.py +8 -0
- marsh/processor_functions/redirections.py +39 -0
- marsh/signals.py +1 -0
- marsh/ssh/__init__.py +3 -0
- marsh/ssh/ssh_command_grammar.py +50 -0
- marsh/ssh/ssh_connector.py +71 -0
- marsh/ssh/ssh_factory.py +93 -0
- marsh/utils/__init__.py +16 -0
- marsh/utils/output_streams.py +13 -0
- marsh_lib-0.2.0.dist-info/LICENSE +21 -0
- marsh_lib-0.2.0.dist-info/METADATA +380 -0
- marsh_lib-0.2.0.dist-info/RECORD +44 -0
- marsh_lib-0.2.0.dist-info/WHEEL +4 -0
marsh/__init__.py
ADDED
marsh/bash/__init__.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
from marsh.core import LocalCommandExecutor
|
|
5
|
+
from marsh.bash import BashGrammar
|
|
6
|
+
from marsh.bash import BashScript
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BashFactory:
|
|
10
|
+
"""
|
|
11
|
+
A factory class that simplifies the creation of various Bash-related objects, such as command grammars and executors.
|
|
12
|
+
"""
|
|
13
|
+
def create_one_command_grammar(self, command: str, bash_path: str="bash", bash_options: list[str] | None = None) -> BashGrammar:
|
|
14
|
+
"""Creates a BashGrammar from one-line bash command.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
command (str): Bash one-line command.
|
|
18
|
+
bash_path (str, optional): Path to bash program. Defaults to "bash".
|
|
19
|
+
bash_options (list[str] | None, optional): Options or Flags to be passed to the bash. Defaults to None.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
BashGrammar: Customized BashGrammar instance for one-line bash command.
|
|
23
|
+
"""
|
|
24
|
+
bash_options = bash_options or ["-c"]
|
|
25
|
+
return BashGrammar(bash_path=bash_path, bash_options=bash_options, bash_args=[command])
|
|
26
|
+
|
|
27
|
+
def create_multi_line_command_grammar(self, commands: list[str], bash_path="bash", bash_options=None, *bash_script_args, **bash_script_kwargs) -> BashGrammar:
|
|
28
|
+
"""Creates a BashGrammar from multi-line bash commands.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
commands (list[str]): List of bash command to be run sequentially.
|
|
32
|
+
bash_path (str, optional): Path to bash program. Defaults to "bash".
|
|
33
|
+
bash_options (list[str] | None, optional): Options or Flags to be passed to the bash. Defaults to None.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
BashGrammar: Customized BashGrammar instance for multi-line bash commands.
|
|
37
|
+
"""
|
|
38
|
+
bash_script = BashScript(*bash_script_args, **bash_script_kwargs)
|
|
39
|
+
bash_options = bash_options or ["-c"]
|
|
40
|
+
script_str: str = bash_script.generate(*commands)
|
|
41
|
+
return BashGrammar(bash_path=bash_path, bash_options=bash_options, bash_args=[script_str])
|
|
42
|
+
|
|
43
|
+
def create_local_command_executor(self, command: str | list[str], *executor_args, grammar_args=(), grammar_kwargs=None, **executor_kwargs) -> LocalCommandExecutor:
|
|
44
|
+
"""Creates a LocalCommandExecutor from one-line or multi-line command.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
command (str | list[str]): One-line bash command as string or multi-line commands as a list of strings.
|
|
48
|
+
grammar_args (tuple, optional): Positional arguments for be passed on to the bash grammar factory method. Defaults to ().
|
|
49
|
+
grammar_kwargs (dict, optional): Keyword arguments for be passed on to the bash grammar factory method. Defaults to None.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
LocalCommandExecutor: Customized LocalCommandExecutor.
|
|
53
|
+
"""
|
|
54
|
+
grammar_kwargs = grammar_kwargs or dict()
|
|
55
|
+
if isinstance(command, str):
|
|
56
|
+
cmd_grammar = self.create_one_command_grammar(command, *grammar_args, **grammar_kwargs)
|
|
57
|
+
if isinstance(command, list):
|
|
58
|
+
cmd_grammar = self.create_multi_line_command_grammar(command, *grammar_args, **grammar_kwargs)
|
|
59
|
+
return LocalCommandExecutor(cmd_grammar, *executor_args, **executor_kwargs)
|
|
60
|
+
|
|
61
|
+
def create_cmd_runner(self, command: str | list[str], *runner_args, executor_args=(), executor_kwargs=None, **runner_kwargs) -> Callable[[bytes, bytes], tuple[bytes, bytes]]:
|
|
62
|
+
"""Creates a command runner function from a given command(s) and other factory method parameters.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
command (str | list[str]): One-line bash command as string or multi-line commands as a list of strings.
|
|
66
|
+
executor_args (tuple, optional): Positional arguments for be passed on to `create_local_command_executor()`. Defaults to ().
|
|
67
|
+
executor_kwargs (dict, optional): Keyword arguments for be passed on to `create_local_command_executor()`. Defaults to None.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Callable[[bytes, bytes], tuple[bytes, bytes]]: Customized bash command runner ready to be called, this function can be further enhanced with command runner decorators.
|
|
71
|
+
"""
|
|
72
|
+
executor_kwargs = executor_kwargs or dict()
|
|
73
|
+
local_cmd = self.create_local_command_executor(command, *executor_args, **executor_kwargs)
|
|
74
|
+
return functools.partial(local_cmd.run, *runner_args, **runner_kwargs)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from marsh.core import CommandGrammar
|
|
2
|
+
from marsh.constants import BASH_PATH
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BashGrammar(CommandGrammar):
|
|
6
|
+
"""
|
|
7
|
+
A concrete implementation of the `CommandGrammar` class for constructing and managing Bash commands.
|
|
8
|
+
|
|
9
|
+
The `BashGrammar` class simplifies building Bash command-line invocations by providing methods to
|
|
10
|
+
add options, arguments, inline commands, or scripts. It allows users to create flexible, reusable
|
|
11
|
+
Bash commands programmatically and can be integrated with other tools that execute shell commands.
|
|
12
|
+
"""
|
|
13
|
+
# /path/to/bash [options] [args]
|
|
14
|
+
# /bin/bash -c command
|
|
15
|
+
# /bin/bash /path/to/file
|
|
16
|
+
def __init__(self,
|
|
17
|
+
bash_path: str | None = BASH_PATH,
|
|
18
|
+
bash_options: list[str] | None = None,
|
|
19
|
+
bash_args: list[str] | None =None,
|
|
20
|
+
):
|
|
21
|
+
self._bash_path = bash_path or "bash"
|
|
22
|
+
self._options = bash_options or []
|
|
23
|
+
self._args = bash_args or []
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def program_path(self) -> str:
|
|
27
|
+
return self._bash_path
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def options(self) -> list[str]:
|
|
31
|
+
return self._options
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def program_args(self) -> list[str]:
|
|
35
|
+
return self._args
|
|
36
|
+
|
|
37
|
+
def add_option(self, option: str) -> "BashGrammar":
|
|
38
|
+
return BashGrammar(bash_options=self._options+[option], bash_args=self._args)
|
|
39
|
+
|
|
40
|
+
def add_arg(self, arg: str) -> "BashGrammar":
|
|
41
|
+
return BashGrammar(bash_options=self._options, bash_args=self._args+[arg])
|
|
42
|
+
|
|
43
|
+
def build_cmd(self) -> list[str]:
|
|
44
|
+
return [self._bash_path, *self._options, " ".join(self._args)]
|
|
File without changes
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import string
|
|
2
|
+
|
|
3
|
+
from marsh.exceptions import ScriptError
|
|
4
|
+
from marsh import Script
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_BASH_SCRIPT_TEMPLATE = r"""$shebang_
|
|
8
|
+
|
|
9
|
+
$debugging_
|
|
10
|
+
|
|
11
|
+
$statements_
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BashScript(Script):
|
|
17
|
+
"""
|
|
18
|
+
An implementation of the `Script` ABC for generating Bash scripts from templates.
|
|
19
|
+
|
|
20
|
+
This class provides a way to construct Bash scripts by specifying a shebang,
|
|
21
|
+
debugging settings, and script statements. The resulting script can be rendered
|
|
22
|
+
as a string using the `generate` method.
|
|
23
|
+
"""
|
|
24
|
+
def __init__(self,
|
|
25
|
+
shebang: str = "#!/usr/bin/env bash",
|
|
26
|
+
debugging: str = "set -eu -o pipefail"
|
|
27
|
+
):
|
|
28
|
+
"""
|
|
29
|
+
Initializes a `BashScript` instance with a default shebang and debugging options.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
`shebang` (str): The shebang line to specify the shell for the script.
|
|
33
|
+
Defaults to `#!/usr/bin/env bash`.
|
|
34
|
+
`debugging` (str): Debugging options to set shell behavior. Defaults to
|
|
35
|
+
`set -eu -o pipefail`.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
ScriptError: If the provided `shebang` does not start with `#!`.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
# Make sure that shebang starts with `#!`.
|
|
42
|
+
if not shebang.startswith("#!"):
|
|
43
|
+
raise ScriptError("Shebang must start with '#!'.")
|
|
44
|
+
|
|
45
|
+
template = string.Template(_BASH_SCRIPT_TEMPLATE)
|
|
46
|
+
template_str = template.safe_substitute(
|
|
47
|
+
shebang_=shebang,
|
|
48
|
+
debugging_=debugging
|
|
49
|
+
)
|
|
50
|
+
super().__init__(template_str)
|
|
51
|
+
|
|
52
|
+
def generate(self,
|
|
53
|
+
*statements: list[str],
|
|
54
|
+
sep: str = "\n"
|
|
55
|
+
) -> str:
|
|
56
|
+
"""
|
|
57
|
+
Renders the Bash script by substituting the provided script statements
|
|
58
|
+
into the template.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
`*statements` (list[str]): A list of Bash statements to include in the script.
|
|
62
|
+
`sep` (str): Separator for joining multiple statements. Defaults to `\\n`.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
str: A string containing the rendered Bash script.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
statements_ = f"{sep}".join(statements)
|
|
69
|
+
return string.Template(self.script_template).safe_substitute(
|
|
70
|
+
statements_=statements_
|
|
71
|
+
).rstrip()
|
marsh/constants.py
ADDED
marsh/core/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from marsh.core.conveyor import Conveyor
|
|
2
|
+
from marsh.core.command_grammar import CommandGrammar
|
|
3
|
+
from marsh.core.authenticator import Authenticator
|
|
4
|
+
from marsh.core.connector import Connector
|
|
5
|
+
from marsh.core.script import Script
|
|
6
|
+
from marsh.core.expression import *
|
|
7
|
+
from marsh.core.cmd_run_decorator import *
|
|
8
|
+
from marsh.core.executor import *
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _is_proc_or_mod_func_args_valid(func: Callable) -> bool:
|
|
7
|
+
# Validate the signature of func
|
|
8
|
+
sig = inspect.signature(func)
|
|
9
|
+
params = list(sig.parameters.values())
|
|
10
|
+
if len(params) < 2: # Check if there are at least two parameters
|
|
11
|
+
return False
|
|
12
|
+
match params[0], params[1]: # Match first two parameters to check if they are positional and of type bytes
|
|
13
|
+
case (inspect.Parameter(kind=inspect.Parameter.POSITIONAL_OR_KEYWORD), inspect.Parameter(kind=inspect.Parameter.POSITIONAL_OR_KEYWORD)):
|
|
14
|
+
# Validate that they are of type 'bytes'
|
|
15
|
+
if not all(isinstance(param.annotation, type(bytes)) or param.annotation == inspect.Parameter.empty for param in params[:2]):
|
|
16
|
+
return False
|
|
17
|
+
case _:
|
|
18
|
+
return False
|
|
19
|
+
return True
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def processor_decorator(before: bool,
|
|
23
|
+
proc_func: Callable[[bytes, bytes], None],
|
|
24
|
+
*proc_args,
|
|
25
|
+
**proc_kwargs
|
|
26
|
+
) -> Callable[[bytes, bytes], tuple[bytes, bytes]]:
|
|
27
|
+
"""
|
|
28
|
+
A decorator to add pre-processing or post-processing behavior to a command runner function.
|
|
29
|
+
|
|
30
|
+
This decorator wraps a command runner function with pre- or post-processing logic. The `before` argument
|
|
31
|
+
specifies whether the processor function (`proc_func`) should run before or after the command runner.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
before (bool): If True, the processor runs before the command runner (pre-processing).
|
|
35
|
+
If False, it runs after the command runner (post-processing).
|
|
36
|
+
proc_func (Callable[[bytes, bytes], None]): The processor function that extends or modifies
|
|
37
|
+
the behavior of the command runner.
|
|
38
|
+
*proc_args: Additional positional arguments to pass to `proc_func`.
|
|
39
|
+
**proc_kwargs: Additional keyword arguments to pass to `proc_func`.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Callable[[bytes, bytes], tuple[bytes, bytes]]: A decorated command runner function that includes
|
|
43
|
+
pre- or post-processing logic.
|
|
44
|
+
"""
|
|
45
|
+
if not _is_proc_or_mod_func_args_valid(proc_func):
|
|
46
|
+
raise ValueError("The processor function has invalid signature.")
|
|
47
|
+
|
|
48
|
+
def outer(cmd_runner):
|
|
49
|
+
@functools.wraps(cmd_runner)
|
|
50
|
+
def wrapper(x_stdout, x_stderr, *args, **kwargs):
|
|
51
|
+
# Execute processor before or after
|
|
52
|
+
if before:
|
|
53
|
+
proc_func(x_stdout, x_stderr, *proc_args, **proc_kwargs)
|
|
54
|
+
stdout, stderr = cmd_runner(x_stdout, x_stderr, *args, **kwargs)
|
|
55
|
+
else:
|
|
56
|
+
stdout, stderr = cmd_runner(x_stdout, x_stderr, *args, **kwargs)
|
|
57
|
+
proc_func(stdout, stderr, *proc_args, **proc_kwargs)
|
|
58
|
+
return stdout, stderr
|
|
59
|
+
return wrapper
|
|
60
|
+
return outer
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def stdout_stderr_modifier(before: bool,
|
|
64
|
+
mod_func: Callable[[bytes, bytes], tuple[bytes, bytes]],
|
|
65
|
+
*mod_args,
|
|
66
|
+
**mod_kwargs
|
|
67
|
+
) -> Callable[[bytes, bytes], tuple[bytes, bytes]]:
|
|
68
|
+
"""
|
|
69
|
+
A decorator that modifies the standard output and error of a command runner function.
|
|
70
|
+
|
|
71
|
+
This decorator allows you to modify the stdout and stderr either before or after the command runner function
|
|
72
|
+
executes. The `mod_func` should return the modified stdout and stderr as a tuple.
|
|
73
|
+
|
|
74
|
+
In `processor_decorator()`, the `proc_func` does not return any result, which limits its ability to perform modifications.
|
|
75
|
+
This decorator is primarily suited for use cases such as validation, error handling, printing, logging, notifications, etc.
|
|
76
|
+
In contrast, the `mod_func` allows users to manipulate the `(x_stdout, x_stderr)` or `(stdout, stderr)` values and return the
|
|
77
|
+
modified results. However, it does not modify the arguments in place, as the bytes objects are immutable. Use cases for this
|
|
78
|
+
include data cleaning, ETL (Extract, Transform, Load) processes, and similar tasks.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
before (bool): If True, the modifier function is applied to the command's stdout and stderr before
|
|
82
|
+
the command runner is executed. If False, it is applied after.
|
|
83
|
+
mod_func (Callable[[bytes, bytes], tuple[bytes, bytes]]): A function that modifies the command's stdout and stderr.
|
|
84
|
+
*mod_args: Additional positional arguments passed to `mod_func`.
|
|
85
|
+
**mod_kwargs: Additional keyword arguments passed to `mod_func`.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Callable[[bytes, bytes], tuple[bytes, bytes]]: A new command runner function that includes the modifications.
|
|
89
|
+
"""
|
|
90
|
+
# Validate the Signature of Modifier Function
|
|
91
|
+
if not _is_proc_or_mod_func_args_valid(mod_func):
|
|
92
|
+
raise ValueError("The processor function has invalid signature.")
|
|
93
|
+
|
|
94
|
+
def outer(cmd_runner):
|
|
95
|
+
@functools.wraps(cmd_runner)
|
|
96
|
+
def wrapper(x_stdout: bytes, x_stderr: bytes, *args, **kwargs) -> tuple[bytes, bytes]:
|
|
97
|
+
if before:
|
|
98
|
+
# mod_func() will be used to transform the previous command runner's results (bytes, bytes), and use this "modified"
|
|
99
|
+
# results for the current command runner as arguments.
|
|
100
|
+
mod_x_stdout, mod_x_stderr = mod_func(x_stdout, x_stderr, *mod_args, **mod_kwargs)
|
|
101
|
+
mod_stdout, mod_stderr = cmd_runner(mod_x_stdout, mod_x_stderr, *args, **kwargs)
|
|
102
|
+
return mod_stdout, mod_stderr
|
|
103
|
+
else:
|
|
104
|
+
# mod_func() will be used to "modify" the current command runner's result and finally returns the modified results.
|
|
105
|
+
stdout, stderr = cmd_runner(x_stdout, x_stderr, *args, **kwargs)
|
|
106
|
+
mod_stdout, mod_stderr = mod_func(stdout, stderr, *mod_args, **mod_kwargs)
|
|
107
|
+
return mod_stdout, mod_stderr
|
|
108
|
+
return wrapper
|
|
109
|
+
return outer
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class CmdRunDecorator:
|
|
113
|
+
"""
|
|
114
|
+
A class for managing and applying chains of pre- and post-processors and modifiers to a command runner.
|
|
115
|
+
|
|
116
|
+
This class allows you to add processor and modifier functions to a command runner. Processors can be applied
|
|
117
|
+
before or after the command runner, and modifiers modify the stdout and stderr either before or after the runner.
|
|
118
|
+
|
|
119
|
+
Methods:
|
|
120
|
+
add_processor: Adds a processor function to the pre- or post-processor chain.
|
|
121
|
+
add_mod_processor: Adds a modifier function to the pre- or post-modifier chain.
|
|
122
|
+
decorate: Applies the added processors and modifiers to a command runner function.
|
|
123
|
+
"""
|
|
124
|
+
def __init__(self, decorated_runners: list[Callable]=None):
|
|
125
|
+
# Separate chains for processors and modifiers
|
|
126
|
+
self._pre_processors = []
|
|
127
|
+
self._post_processors = []
|
|
128
|
+
self._pre_modifiers = [] # To store mod_func processors to be applied before
|
|
129
|
+
self._post_modifiers = [] # To store mod_func processors to be applied after
|
|
130
|
+
|
|
131
|
+
if decorated_runners:
|
|
132
|
+
for runner in decorated_runners:
|
|
133
|
+
if runner["before"]:
|
|
134
|
+
self._pre_processors.append(runner["decorator"])
|
|
135
|
+
else:
|
|
136
|
+
self._post_processors.append(runner["decorator"])
|
|
137
|
+
|
|
138
|
+
def add_processor(self,
|
|
139
|
+
processor: Callable[[bytes, bytes], None],
|
|
140
|
+
before: bool = True,
|
|
141
|
+
proc_args: tuple = (),
|
|
142
|
+
proc_kwargs: dict | None = None) -> "CmdRunDecorator":
|
|
143
|
+
"""
|
|
144
|
+
Adds a processor function to the pre- or post-processor chain.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
processor (Callable[[bytes, bytes], None]): A function to process the command's stdout and stderr.
|
|
148
|
+
before (bool, optional): If True, the processor is added to the pre-processor chain. Defaults to True.
|
|
149
|
+
proc_args (tuple, optional): Positional arguments for the processor. Defaults to ().
|
|
150
|
+
proc_kwargs (dict, optional): Keyword arguments for the processor. Defaults to None.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
CmdRunDecorator: The current CmdRunDecorator instance to allow method chaining.
|
|
154
|
+
"""
|
|
155
|
+
proc_kwags_ = proc_kwargs or {}
|
|
156
|
+
cmd_runner_decorator = processor_decorator(before, processor, *proc_args, **proc_kwags_)
|
|
157
|
+
|
|
158
|
+
if before:
|
|
159
|
+
self._pre_processors.append(cmd_runner_decorator)
|
|
160
|
+
else:
|
|
161
|
+
self._post_processors.append(cmd_runner_decorator)
|
|
162
|
+
|
|
163
|
+
return self
|
|
164
|
+
|
|
165
|
+
def add_mod_processor(self,
|
|
166
|
+
mod_func: Callable[[bytes, bytes], tuple[bytes, bytes]],
|
|
167
|
+
before: bool = True,
|
|
168
|
+
mod_args: tuple = (),
|
|
169
|
+
mod_kwargs: dict | None = None) -> "CmdRunDecorator":
|
|
170
|
+
"""
|
|
171
|
+
Adds a modifier function to the pre- or post-modifier chain.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
mod_func (Callable[[bytes, bytes], tuple[bytes, bytes]]): A function that modifies the command's stdout and stderr.
|
|
175
|
+
before (bool, optional): If True, the modifier is added to the pre-modifier chain. Defaults to True.
|
|
176
|
+
mod_args (tuple, optional): Positional arguments for the modifier. Defaults to ().
|
|
177
|
+
mod_kwargs (dict, optional): Keyword arguments for the modifier. Defaults to None.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
CmdRunDecorator: The current CmdRunDecorator instance to allow method chaining.
|
|
181
|
+
"""
|
|
182
|
+
mod_kwags_ = mod_kwargs or {}
|
|
183
|
+
cmd_runner_decorator = stdout_stderr_modifier(before, mod_func, *mod_args, **mod_kwags_)
|
|
184
|
+
|
|
185
|
+
if before:
|
|
186
|
+
self._pre_modifiers.append(cmd_runner_decorator) # Store pre-modifiers
|
|
187
|
+
else:
|
|
188
|
+
self._post_modifiers.append(cmd_runner_decorator) # Store post-modifiers
|
|
189
|
+
|
|
190
|
+
return self
|
|
191
|
+
|
|
192
|
+
def decorate(self, cmd_runner: Callable[[bytes, bytes], tuple[bytes, bytes]]) -> Callable[[bytes, bytes], tuple[bytes, bytes]]:
|
|
193
|
+
"""
|
|
194
|
+
Decorates a command runner function with the pre- and post-processors and modifiers.
|
|
195
|
+
|
|
196
|
+
The order of evaluation:
|
|
197
|
+
1) Pre-Modifiers
|
|
198
|
+
2) Pre-Processors
|
|
199
|
+
3) Command Runner
|
|
200
|
+
4) Post-Modifiers
|
|
201
|
+
5) Post-Processors
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
cmd_runner (Callable[[bytes, bytes], tuple[bytes, bytes]]): The command runner function to decorate.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Callable[[bytes, bytes], tuple[bytes, bytes]]: The decorated command runner function.
|
|
208
|
+
"""
|
|
209
|
+
# 2nd
|
|
210
|
+
# Apply pre-processors (before the command runner)
|
|
211
|
+
for pre_processor in reversed(self._pre_processors): # Reverse order for before-processors
|
|
212
|
+
cmd_runner = pre_processor(cmd_runner)
|
|
213
|
+
|
|
214
|
+
# 1st
|
|
215
|
+
# Apply pre-modifiers (before the command runner)
|
|
216
|
+
for pre_modifier in reversed(self._pre_modifiers): # Reverse order for before-modifiers
|
|
217
|
+
cmd_runner = pre_modifier(cmd_runner)
|
|
218
|
+
|
|
219
|
+
# 3rd
|
|
220
|
+
# Apply post-modifiers (after the command runner)
|
|
221
|
+
for post_modifier in self._post_modifiers:
|
|
222
|
+
cmd_runner = post_modifier(cmd_runner)
|
|
223
|
+
|
|
224
|
+
# 4th
|
|
225
|
+
# Apply post-processors (after the command runner)
|
|
226
|
+
for post_processor in self._post_processors:
|
|
227
|
+
cmd_runner = post_processor(cmd_runner)
|
|
228
|
+
|
|
229
|
+
return cmd_runner
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def add_processors_and_modifiers(*tup_list: list[tuple]) -> Callable[[bytes, bytes], tuple[bytes, bytes]]:
|
|
233
|
+
"""
|
|
234
|
+
A decorator that allows adding multiple processors and modifiers to a command runner.
|
|
235
|
+
|
|
236
|
+
This decorator allows adding both processors and modifiers in one call, either to modify the output
|
|
237
|
+
or to perform additional processing before or after the command execution.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
*tup_list (list[tuple]): A list of tuples specifying processors and modifiers. Each tuple must contain
|
|
241
|
+
the classification ("proc" or "mod") followed by the appropriate function and arguments.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Callable[[bytes, bytes], tuple[bytes, bytes]]: A decorated command runner function.
|
|
245
|
+
"""
|
|
246
|
+
def outer(cmd_runner):
|
|
247
|
+
@functools.wraps(cmd_runner)
|
|
248
|
+
def wrapper(x_stdout, x_stderr, *args, **kwargs):
|
|
249
|
+
# Initialize the Command Run Decorator
|
|
250
|
+
cmd_runner_decorator = CmdRunDecorator()
|
|
251
|
+
|
|
252
|
+
for tup in tup_list:
|
|
253
|
+
assert isinstance(tup, (tuple, list))
|
|
254
|
+
assert isinstance(tup[0], str) and tup[0] in ["proc", "mod"], 'The classifier must be "proc" or "mod".'
|
|
255
|
+
|
|
256
|
+
# Classifier ("proc" or "mod")
|
|
257
|
+
proc_or_mod = tup[0]
|
|
258
|
+
|
|
259
|
+
# Set the Method for adding either a "processor" or "modifier"
|
|
260
|
+
adder_method = cmd_runner_decorator.add_processor if proc_or_mod == "proc" else cmd_runner_decorator.add_mod_processor
|
|
261
|
+
|
|
262
|
+
# Pattern Match on the given tuples
|
|
263
|
+
# Only match the required information and not the classification of "proc" or "mod"
|
|
264
|
+
match tup[1:]:
|
|
265
|
+
case (before, callback):
|
|
266
|
+
adder_method(callback, before=before)
|
|
267
|
+
case (before, callback, args_) if isinstance(args_, (tuple, list)):
|
|
268
|
+
adder_method(callback, before=before, mod_args=args_) if proc_or_mod == "mod" else adder_method(callback, before=before, proc_args=args_)
|
|
269
|
+
case (before, callback, kwargs_) if isinstance(kwargs_, dict):
|
|
270
|
+
adder_method(callback, before=before, mod_kwargs=kwargs_) if proc_or_mod == "mod" else adder_method(callback, before=before, proc_kwargs=kwargs_)
|
|
271
|
+
case (before, callback, args_, kwargs_) if isinstance(args_, (tuple, list)) and isinstance(kwargs_, dict):
|
|
272
|
+
adder_method(callback, before=before, mod_args=args_, mod_kwargs=kwargs_) if proc_or_mod == "mod" else adder_method(callback, before=before, proc_args=args_, proc_kwargs=kwargs_)
|
|
273
|
+
case _:
|
|
274
|
+
error_message = f"{tup} is invalid."
|
|
275
|
+
ValueError(error_message)
|
|
276
|
+
|
|
277
|
+
# Decorate the command runner
|
|
278
|
+
decorated_cmd_runner = cmd_runner_decorator.decorate(cmd_runner)
|
|
279
|
+
return decorated_cmd_runner(x_stdout, x_stderr, *args, **kwargs)
|
|
280
|
+
return wrapper
|
|
281
|
+
return outer
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
__all__ = (
|
|
285
|
+
"processor_decorator",
|
|
286
|
+
"stdout_stderr_modifier",
|
|
287
|
+
"CmdRunDecorator",
|
|
288
|
+
"add_processors_and_modifiers"
|
|
289
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from abc import abstractmethod, ABC
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# TODO: Utilize shlex.split() for parsing the full command.
|
|
5
|
+
# Reference: https://docs.python.org/3.11/library/shlex.html#shlex.split
|
|
6
|
+
class CommandGrammar(ABC):
|
|
7
|
+
"""
|
|
8
|
+
Abstract base class for defining the structure and behavior of program command grammars.
|
|
9
|
+
|
|
10
|
+
This class enforces a consistent interface for defining a program's executable path,
|
|
11
|
+
its options (e.g., flags or configuration switches), and its arguments. It also provides
|
|
12
|
+
a mechanism to build the final command to be executed (for subprocess.Popen).
|
|
13
|
+
|
|
14
|
+
Properties:
|
|
15
|
+
- program_path (str): The path to the executable program.
|
|
16
|
+
- options (list[str]): A list of options or flags to be passed to the program.
|
|
17
|
+
- program_args (list[str]): A list of arguments for the program.
|
|
18
|
+
|
|
19
|
+
Methods:
|
|
20
|
+
- build_cmd: Constructs the full command as a list of strings that can be passed to
|
|
21
|
+
`subprocess.Popen` or similar command-executing libraries.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def build_cmd(self, *args, **kwargs) -> list[str]:
|
|
26
|
+
"""
|
|
27
|
+
Constructs the full command as a list of strings.
|
|
28
|
+
|
|
29
|
+
This method combines the program path, options, and arguments into a single list
|
|
30
|
+
that can be passed to a command-execution library such as `subprocess.Popen`.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
*args: Additional arguments to include in the command.
|
|
34
|
+
**kwargs: Additional keyword arguments to customize the command-building process.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
list[str]: The full command to be passed to subprocess.Popen.
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
```python
|
|
41
|
+
def build_cmd(self, *args, **kwargs):
|
|
42
|
+
return [self.program_path] + self.options + self.program_args
|
|
43
|
+
```
|
|
44
|
+
"""
|
|
45
|
+
pass
|
marsh/core/connector.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from typing import Optional, Tuple, Any
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Connector(ABC):
|
|
6
|
+
"""
|
|
7
|
+
Abstract base class for managing connections and executing commands on remote systems.
|
|
8
|
+
|
|
9
|
+
The `Connector` class defines an interface for establishing, managing, and closing
|
|
10
|
+
connections to remote systems, as well as executing commands on those systems.
|
|
11
|
+
Subclasses must provide concrete implementations for connecting to a remote host,
|
|
12
|
+
executing a command, and disconnecting from the host.
|
|
13
|
+
|
|
14
|
+
Note:
|
|
15
|
+
Subclasses of `Connector` are responsible for handling specific connection
|
|
16
|
+
protocols, such as SSH, HTTP, or other custom communication mechanisms.
|
|
17
|
+
"""
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def connect(self, *args, **kwargs) -> Optional[Any]:
|
|
20
|
+
"""
|
|
21
|
+
Establishes a connection to a remote system.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
*args: Positional arguments specific to the connection implementation.
|
|
25
|
+
**kwargs: Keyword arguments for configuring the connection.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Optional[Any]: A connection object or `None` if the connection fails.
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
NotImplementedError: If not implemented in a subclass.
|
|
32
|
+
"""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def disconnect(self, connection: Any, *args, **kwargs) -> Optional[Any]:
|
|
37
|
+
"""
|
|
38
|
+
Closes an active connection to a remote system.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
connection (Any): The connection object to be closed.
|
|
42
|
+
*args: Additional positional arguments.
|
|
43
|
+
**kwargs: Additional keyword arguments.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Optional[Any]: Implementation-specific result or `None`.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
NotImplementedError: If not implemented in a subclass.
|
|
50
|
+
"""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def exec_cmd(self, command: list[str], connection: Any, *args, **kwargs) -> Tuple[bytes, bytes]:
|
|
55
|
+
"""
|
|
56
|
+
Executes a command on a remote system and retrieves its output.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
command (list[str]): The command to execute as a list of strings.
|
|
60
|
+
connection (Any): An active connection object to the remote system.
|
|
61
|
+
*args: Additional positional arguments.
|
|
62
|
+
**kwargs: Additional keyword arguments (e.g., timeout).
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Tuple[bytes, bytes]: A tuple containing:
|
|
66
|
+
- stdout (bytes): The standard output from the command execution.
|
|
67
|
+
- stderr (bytes): The standard error from the command execution.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
NotImplementedError: If not implemented in a subclass.
|
|
71
|
+
|
|
72
|
+
Note:
|
|
73
|
+
A `timeout` parameter may be included in `kwargs` to specify the
|
|
74
|
+
maximum duration for command execution.
|
|
75
|
+
"""
|
|
76
|
+
# TODO: Add timeout for running a remote command.
|
|
77
|
+
pass
|