marsh-lib 0.1.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 ADDED
@@ -0,0 +1,2 @@
1
+ from marsh.core import *
2
+ from marsh import ssh
marsh/bash/__init__.py ADDED
@@ -0,0 +1,302 @@
1
+ import functools
2
+ from copy import deepcopy
3
+ from string import Template
4
+ from typing import Callable, Tuple
5
+
6
+ from marsh.core import Conveyor, LocalCommandExecutor, CmdRunDecorator, EnvCwdRelayExecutor
7
+ from marsh.bash.bash_grammar import BashGrammar
8
+ from marsh.bash.bash_config import BashConfig
9
+
10
+
11
+ def generate_bash_script(*statements: list[str],
12
+ shebang: str = "#!/usr/bin/env bash",
13
+ strict_mode: str = "set -eu -o pipefail",
14
+ sep: str = "\n\n",
15
+ **kwargs
16
+ ) -> str:
17
+ """
18
+ Generates a Bash script as a string from a list of statements.
19
+
20
+ Args:
21
+ statements (list[str]): A list of Bash commands to include in the script.
22
+ shebang (str, optional): Shebang line at the top of the script. Defaults to "#!/usr/bin/env bash".
23
+ strict_mode (str, optional): Strict mode options for Bash. Defaults to "set -eu -o pipefail".
24
+ sep (str, optional): Separator to be used between commands. Defaults to "\\n\\n".
25
+ **kwargs: Additional arguments to pass to the Template for dynamic substitution.
26
+
27
+ Returns:
28
+ str: The generated Bash script as a string.
29
+ """
30
+ # Example Usage:
31
+ # generate_bash_script("echo 1", "echo 2")
32
+ bash_template_str = fr"""{shebang}
33
+
34
+ {strict_mode}
35
+
36
+ $statements_
37
+ """
38
+ bash_template = Template(bash_template_str)
39
+ return bash_template.safe_substitute(
40
+ statements_=f"{sep}".join(statements),
41
+ **kwargs
42
+ )
43
+
44
+
45
+ def create_bash_cmd_runner(command: str, *args, bash_options: list[str] | None = None, pipe_prev_stdout: bool = False, **kwargs) -> Callable[[bytes, bytes], Tuple[bytes, bytes]]:
46
+ """
47
+ Creates a Bash command runner function that can be executed with specific arguments.
48
+
49
+ Args:
50
+ command (str): The bash command to run.
51
+ bash_options (list[str] | None, optional): List of Bash options. Defaults to None.
52
+ pipe_prev_stdout (bool, optional): Whether to pipe the previous command's stdout into this one. Defaults to False.
53
+ **kwargs: Additional keyword arguments to pass to the executor.
54
+
55
+ Returns:
56
+ Callable[[bytes, bytes], Tuple[bytes, bytes]]: A function that runs the command and returns the output as a tuple.
57
+ """
58
+ # Example Usage:
59
+ # bash_conveyor_belt = BashConveyorBelt(...)
60
+ # bash_conveyor_belt.add_cmd(create_bash_cmd_runner("command", ...), ...)
61
+ # bash_conveyor_belt.add_cmd(create_bash_cmd_runner("command", ...), ...)
62
+ # bash_conveyor_belt.add_cmd(create_bash_cmd_runner("command", ...), ...)
63
+ # ...
64
+ # bash_conveyor_belt.run(...)
65
+
66
+ bash_options = bash_options or ["-c"]
67
+
68
+ # Create customized BashGrammar
69
+ bash_grammar = BashGrammar(
70
+ bash_path="bash",
71
+ bash_options=bash_options,
72
+ bash_args=[command],
73
+ )
74
+
75
+ # Create a LocalCommandExecutor with the BashGrammar instance
76
+ local_cmd = LocalCommandExecutor(bash_grammar, pipe_prev_stdout=pipe_prev_stdout)
77
+ return functools.partial(local_cmd.run, *args, **kwargs)
78
+
79
+
80
+ class BashConveyorBelt:
81
+ """A class that manages the execution of multiple Bash commands sequentially, with global options."""
82
+ def __init__(self, *global_args, **global_kwargs) -> None:
83
+ self._global_args = global_args
84
+ self._global_kwargs = global_kwargs
85
+
86
+ # This will not inherit Conveyor. It will use composition over inheritance.
87
+ self.conveyor = Conveyor()
88
+
89
+ def reset(self) -> None:
90
+ """
91
+ Resets the conveyor belt, clearing all previously added commands.
92
+ """
93
+ self.conveyor = Conveyor()
94
+
95
+ def add_cmd(self, bash_cmd_runner: Callable, *args, cmd_run_decorator: CmdRunDecorator | None = None, **kwargs) -> None:
96
+ """
97
+ Adds a Bash command runner to the conveyor, with the option to override global arguments and keyword arguments.
98
+
99
+ Args:
100
+ bash_cmd_runner (Callable): The Bash command runner function to add.
101
+ *args: Additional arguments to pass to the command runner.
102
+ cmd_run_decorator (CmdRunDecorator | None, optional): A decorator to apply to the command runner. Defaults to None.
103
+ **kwargs: Additional keyword arguments to override global options for this command.
104
+ """
105
+ # Create a copy of the global kwargss
106
+ cb_kwargs = deepcopy(self._global_kwargs)
107
+
108
+ # Explicitly override global values with provided kwargs
109
+ for key, value in kwargs.items():
110
+ # Ensure critical keys are handled
111
+ if key in cb_kwargs.keys() and key not in ["env"]:
112
+ cb_kwargs[key] = value
113
+
114
+ # Handle Environment Variables `env` and ensure that the cmd_runner's specific env keys would override some keys in self._global_kwargs["env"]
115
+ if key == "env":
116
+ cmd_runner_env = value
117
+ assert isinstance(cmd_runner_env, dict), "cmd_runner_env is not a dictionary."
118
+ global_env = cb_kwargs.get("env", None)
119
+ if global_env:
120
+ assert isinstance(global_env, dict), "global_env is not a dictionary."
121
+ # Key-Value Pair from `cmd_runner_env` should override the Key-Value Pair in `global_env` only if the keys are the same.
122
+ cb_kwargs["env"].update(cmd_runner_env)
123
+
124
+ # Apply CmdRunDecorator.decorate() if given as parameter
125
+ bash_cmd_runner = cmd_run_decorator.decorate(bash_cmd_runner) if cmd_run_decorator else bash_cmd_runner
126
+
127
+ # Update Conveyor (Side-Effect) with new command runner
128
+ self.conveyor = self.conveyor.add_cmd_runner(
129
+ bash_cmd_runner,
130
+ *(args + self._global_args),
131
+ **cb_kwargs
132
+ )
133
+
134
+ def add_one_command(self,
135
+ command: str,
136
+ *args,
137
+ bash_options=None,
138
+ pipe_prev_stdout=False,
139
+ cmd_run_decorator: CmdRunDecorator | None = None,
140
+ **kwargs
141
+ ) -> None:
142
+ """
143
+ Adds a single Bash command to the conveyor.
144
+
145
+ Args:
146
+ command (str): The Bash command to add.
147
+ *args: Additional arguments to pass to the command runner.
148
+ bash_options (optional): Options to pass to Bash. Defaults to None.
149
+ pipe_prev_stdout (bool, optional): Whether to pipe the previous command's stdout into this one. Defaults to False.
150
+ cmd_run_decorator (CmdRunDecorator | None, optional): A decorator for the command. Defaults to None.
151
+ **kwargs: Additional keyword arguments to pass to the command runner.
152
+ """
153
+ self.add_cmd(
154
+ create_bash_cmd_runner(command, bash_options=bash_options, pipe_prev_stdout=pipe_prev_stdout),
155
+ *args,
156
+ cmd_run_decorator=cmd_run_decorator,
157
+ **kwargs
158
+ )
159
+
160
+ def add_multi_line_cmd(self,
161
+ commands: list[str],
162
+ *args,
163
+ bash_options=None,
164
+ pipe_prev_stdout=False,
165
+ cmd_run_decorator: CmdRunDecorator | None = None,
166
+ bash_script_args=(),
167
+ bash_script_kwargs=None,
168
+ **kwargs) -> None:
169
+ """
170
+ Adds multiple Bash commands (in the form of a script) to the conveyor.
171
+
172
+ Args:
173
+ commands (list[str]): List of Bash commands to run sequentially.
174
+ *args: Additional arguments to pass to the command runner.
175
+ bash_options (optional): Options to pass to Bash. Defaults to None.
176
+ pipe_prev_stdout (bool, optional): Whether to pipe the previous command's stdout into this one. Defaults to False.
177
+ cmd_run_decorator (CmdRunDecorator | None, optional): A decorator for the command. Defaults to None.
178
+ bash_script_args (tuple, optional): Arguments to include in the script. Defaults to an empty tuple.
179
+ bash_script_kwargs (dict, optional): Keyword arguments to pass to the script. Defaults to None.
180
+ **kwargs: Additional keyword arguments to pass to the command runner.
181
+ """
182
+ bash_script_kwargs = bash_script_kwargs or dict()
183
+ bash_script_args = [*commands, *bash_script_args]
184
+ bash_script = generate_bash_script(*bash_script_args, **bash_script_kwargs)
185
+ self.add_cmd(
186
+ create_bash_cmd_runner(bash_script, bash_options=bash_options, pipe_prev_stdout=pipe_prev_stdout),
187
+ *args,
188
+ cmd_run_decorator=cmd_run_decorator,
189
+ **kwargs
190
+ )
191
+
192
+ def run(self, *args, **kwargs) -> tuple[bytes, bytes]:
193
+ """
194
+ Executes all the commands in the conveyor and returns their output.
195
+
196
+ Args:
197
+ *args: Positional arguments to pass to the conveyor's run method.
198
+ **kwargs: Keyword arguments to pass to the conveyor's run method.
199
+
200
+ Returns:
201
+ tuple[bytes, bytes]: The output (stdout, stderr) from the execution of the commands.
202
+ """
203
+ return self.conveyor(*args, **kwargs)
204
+
205
+
206
+ class BashFactory:
207
+ """
208
+ A factory class that simplifies the creation of various Bash-related objects, such as command grammars and executors.
209
+ """
210
+ def create_one_command_grammar(self, command: str, bash_path: str="bash", bash_options: list[str] | None = None) -> BashGrammar:
211
+ """Creates a BashGrammar from one-line bash command.
212
+
213
+ Args:
214
+ command (str): Bash one-line command.
215
+ bash_path (str, optional): Path to bash program. Defaults to "bash".
216
+ bash_options (list[str] | None, optional): Options or Flags to be passed to the bash. Defaults to None.
217
+
218
+ Returns:
219
+ BashGrammar: Customized BashGrammar instance for one-line bash command.
220
+ """
221
+ bash_options = bash_options or ["-c"]
222
+ return BashGrammar(bash_path=bash_path, bash_options=bash_options, bash_args=[command])
223
+
224
+ def create_multi_line_command_grammar(self, commands: list[str], *script_args, bash_path="bash", bash_options=None, **script_kwargs) -> BashGrammar:
225
+ """Creates a BashGrammar from multi-line bash commands.
226
+
227
+ Args:
228
+ commands (list[str]): List of bash command to be run sequentially.
229
+ bash_path (str, optional): Path to bash program. Defaults to "bash".
230
+ bash_options (list[str] | None, optional): Options or Flags to be passed to the bash. Defaults to None.
231
+
232
+ Returns:
233
+ BashGrammar: Customized BashGrammar instance for multi-line bash commands.
234
+ """
235
+ bash_options = bash_options or ["-c"]
236
+ script_args = [*commands, *script_args]
237
+ bash_script: str = generate_bash_script(*script_args, **script_kwargs)
238
+ return BashGrammar(bash_path=bash_path, bash_options=bash_options, bash_args=[bash_script])
239
+
240
+ def create_local_command_executor(self, command: str | list[str], *executor_args, grammar_args=(), grammar_kwargs=None, **executor_kwargs) -> LocalCommandExecutor:
241
+ """Creates a LocalCommandExecutor from one-line or multi-line command.
242
+
243
+ Args:
244
+ command (str | list[str]): One-line bash command as string or multi-line commands as a list of strings.
245
+ grammar_args (tuple, optional): Positional arguments for be passed on to the bash grammar factory method. Defaults to ().
246
+ grammar_kwargs (dict, optional): Keyword arguments for be passed on to the bash grammar factory method. Defaults to None.
247
+
248
+ Returns:
249
+ LocalCommandExecutor: Customized LocalCommandExecutor.
250
+ """
251
+ grammar_kwargs = grammar_kwargs or dict()
252
+ if isinstance(command, str):
253
+ cmd_grammar = self.create_one_command_grammar(command, *grammar_args, **grammar_kwargs)
254
+ if isinstance(command, list):
255
+ cmd_grammar = self.create_multi_line_command_grammar(command, *grammar_args, **grammar_kwargs)
256
+ return LocalCommandExecutor(cmd_grammar, *executor_args, **executor_kwargs)
257
+
258
+ def create_cmd_runner(self, command: str | list[str], *runner_args, executor_args=(), executor_kwargs=None, **runner_kwargs) -> Callable[[bytes, bytes], tuple[bytes, bytes]]:
259
+ """Creates a command runner function from a given command(s) and other factory method parameters.
260
+
261
+ Args:
262
+ command (str | list[str]): One-line bash command as string or multi-line commands as a list of strings.
263
+ executor_args (tuple, optional): Positional arguments for be passed on to `create_local_command_executor()`. Defaults to ().
264
+ executor_kwargs (dict, optional): Keyword arguments for be passed on to `create_local_command_executor()`. Defaults to None.
265
+
266
+ Returns:
267
+ Callable[[bytes, bytes], tuple[bytes, bytes]]: Customized bash command runner ready to be called, this function can be further enhanced with command runner decorators.
268
+ """
269
+ executor_kwargs = executor_kwargs or dict()
270
+ local_cmd = self.create_local_command_executor(command, *executor_args, **executor_kwargs)
271
+ return functools.partial(local_cmd.run, *runner_args, **runner_kwargs)
272
+
273
+
274
+ class BashUnixPipes(BashConveyorBelt):
275
+ """
276
+ A subclass of BashConveyorBelt that automatically sets pipe_prev_stdout=True for all commands.
277
+ """
278
+ # This class only makes pipe_prev_stdout=True by default
279
+ def add_one_command(self, command, *args, bash_options=None, pipe_prev_stdout=True, cmd_run_decorator = None, **kwargs):
280
+ return super().add_one_command(command, *args, bash_options=bash_options, pipe_prev_stdout=pipe_prev_stdout, cmd_run_decorator=cmd_run_decorator, **kwargs)
281
+
282
+ def add_multi_line_cmd(self, commands, *args, bash_options=None, pipe_prev_stdout=True, cmd_run_decorator = None, bash_script_args=(), bash_script_kwargs=None, **kwargs):
283
+ return \
284
+ super().add_multi_line_cmd(
285
+ commands,
286
+ *args,
287
+ bash_options=bash_options,
288
+ pipe_prev_stdout=pipe_prev_stdout,
289
+ cmd_run_decorator=cmd_run_decorator,
290
+ bash_script_args=bash_script_args,
291
+ bash_script_kwargs=bash_script_kwargs,
292
+ **kwargs
293
+ )
294
+
295
+
296
+ __all__ = (
297
+ "generate_bash_script",
298
+ "create_bash_cmd_runner",
299
+ "BashConveyorBelt",
300
+ "BashUnixPipes",
301
+ "BashFactory"
302
+ )
@@ -0,0 +1,23 @@
1
+ from dataclasses import dataclass, field
2
+ import os
3
+
4
+
5
+ @dataclass
6
+ class BashGrammarConfig:
7
+ bash_path: str = "bash"
8
+ bash_options: list[str] = field(default_factory=lambda: ["-c"])
9
+
10
+
11
+ @dataclass
12
+ class BashScriptConfig:
13
+ shebang: str = "#!/usr/bin/env bash"
14
+ sep: str = "\n\n"
15
+ strict_mode: str = "set -eu -o pipefail"
16
+
17
+
18
+ @dataclass
19
+ class BashConfig:
20
+ bash_grammar: BashGrammarConfig = field(default_factory=lambda: BashGrammarConfig())
21
+ bash_script: BashScriptConfig = field(default_factory=lambda: BashScriptConfig())
22
+ env: dict = field(default_factory=dict)
23
+ cwd: str = os.getcwd()
@@ -0,0 +1,52 @@
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
+ @classmethod
26
+ def create_command(cls, command: str, *args, **kwargs) -> "BashGrammar":
27
+ return cls(*args, bash_path="bash", **kwargs).add_option("-c").add_arg(command)
28
+
29
+ @classmethod
30
+ def create_cmd_script(cls, script_path: str, *args, **kwargs) -> "BashGrammar":
31
+ return cls(*args, bash_path="bash", **kwargs).add_arg(script_path)
32
+
33
+ @property
34
+ def program_path(self) -> str:
35
+ return self._bash_path
36
+
37
+ @property
38
+ def options(self) -> list[str]:
39
+ return self._options
40
+
41
+ @property
42
+ def program_args(self) -> list[str]:
43
+ return self._args
44
+
45
+ def add_option(self, option: str) -> "BashGrammar":
46
+ return BashGrammar(bash_options=self._options+[option], bash_args=self._args)
47
+
48
+ def add_arg(self, arg: str) -> "BashGrammar":
49
+ return BashGrammar(bash_options=self._options, bash_args=self._args+[arg])
50
+
51
+ def build_cmd(self) -> list[str]:
52
+ return [self._bash_path, *self._options, " ".join(self._args)]
File without changes
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,7 @@
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.expression import *
6
+ from marsh.core.cmd_run_decorator import *
7
+ 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