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.
Files changed (44) hide show
  1. marsh/__init__.py +2 -0
  2. marsh/bash/__init__.py +3 -0
  3. marsh/bash/bash_factory.py +74 -0
  4. marsh/bash/bash_grammar.py +44 -0
  5. marsh/bash/bash_runner_decorators.py +0 -0
  6. marsh/bash/bash_script.py +71 -0
  7. marsh/constants.py +5 -0
  8. marsh/core/__init__.py +8 -0
  9. marsh/core/authenticator.py +7 -0
  10. marsh/core/cmd_run_decorator.py +289 -0
  11. marsh/core/command_grammar.py +45 -0
  12. marsh/core/connector.py +77 -0
  13. marsh/core/conveyor.py +105 -0
  14. marsh/core/executor.py +296 -0
  15. marsh/core/expression.py +208 -0
  16. marsh/core/script.py +38 -0
  17. marsh/dag/__init__.py +11 -0
  18. marsh/dag/dag.py +748 -0
  19. marsh/dag/node.py +52 -0
  20. marsh/dag/startable.py +48 -0
  21. marsh/docker/__init__.py +2 -0
  22. marsh/docker/docker_command_grammar.py +31 -0
  23. marsh/docker/docker_executor.py +181 -0
  24. marsh/exceptions.py +16 -0
  25. marsh/logger.py +116 -0
  26. marsh/modifier_functions/__init__.py +2 -0
  27. marsh/modifier_functions/case_conversion.py +17 -0
  28. marsh/modifier_functions/readers.py +11 -0
  29. marsh/powershell/__init__.py +0 -0
  30. marsh/processor_functions/__init__.py +3 -0
  31. marsh/processor_functions/printers.py +63 -0
  32. marsh/processor_functions/raisers.py +8 -0
  33. marsh/processor_functions/redirections.py +39 -0
  34. marsh/signals.py +1 -0
  35. marsh/ssh/__init__.py +3 -0
  36. marsh/ssh/ssh_command_grammar.py +50 -0
  37. marsh/ssh/ssh_connector.py +71 -0
  38. marsh/ssh/ssh_factory.py +93 -0
  39. marsh/utils/__init__.py +16 -0
  40. marsh/utils/output_streams.py +13 -0
  41. marsh_lib-0.2.0.dist-info/LICENSE +21 -0
  42. marsh_lib-0.2.0.dist-info/METADATA +380 -0
  43. marsh_lib-0.2.0.dist-info/RECORD +44 -0
  44. marsh_lib-0.2.0.dist-info/WHEEL +4 -0
marsh/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ from marsh.core import *
2
+ from marsh import ssh
marsh/bash/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from marsh.bash.bash_grammar import BashGrammar
2
+ from marsh.bash.bash_script import BashScript
3
+ from marsh.bash.bash_factory import BashFactory
@@ -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
@@ -0,0 +1,5 @@
1
+ import shutil
2
+
3
+ BASH_PATH: str | None = shutil.which("bash")
4
+ SH_PATH: str | None = shutil.which("sh")
5
+ PY_PATH: str | None = shutil.which("python")
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,7 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class Authenticator(ABC):
5
+ @abstractmethod
6
+ def authenticate(self, *args, **kwargs):
7
+ pass
@@ -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
@@ -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