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/dag/node.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from typing import Tuple
|
|
2
|
+
|
|
3
|
+
from ..logger import result_logging_decorator
|
|
4
|
+
from ..core import Conveyor
|
|
5
|
+
from .startable import Startable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Node(Startable):
|
|
9
|
+
"""
|
|
10
|
+
A Node represents an object that can be started, typically as part of a workflow or graph.
|
|
11
|
+
|
|
12
|
+
This class extends `Startable` and represents a node in a directed acyclic graph (DAG) or similar structure.
|
|
13
|
+
Each node is responsible for starting a process using a `Conveyor` object. The `start` method invokes the `Conveyor`
|
|
14
|
+
with the provided keyword arguments, returning the result as a 2-tuple of bytes.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
name (str): The name of the node, inherited from the `Startable` base class.
|
|
18
|
+
conveyor (Conveyor): The `Conveyor` object responsible for carrying out the node's operation.
|
|
19
|
+
_kwargs (dict): A dictionary of keyword arguments used when calling the `Conveyor`.
|
|
20
|
+
|
|
21
|
+
Methods:
|
|
22
|
+
start() -> Tuple[bytes, bytes]:
|
|
23
|
+
Executes the `Conveyor` with the provided arguments, returning the result as a tuple of bytes.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, name: str, conveyor: Conveyor, **kwargs):
|
|
27
|
+
"""
|
|
28
|
+
Initializes a Node object with a name and a Conveyor.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
name (str): The name of the node.
|
|
32
|
+
conveyor (Conveyor): The `Conveyor` object that will perform the operation associated with this node.
|
|
33
|
+
**kwargs: Additional keyword arguments to be passed to the `Conveyor` when called.
|
|
34
|
+
"""
|
|
35
|
+
super().__init__(name)
|
|
36
|
+
self.conveyor = conveyor
|
|
37
|
+
|
|
38
|
+
# Conveyor keyword arguments for __call__
|
|
39
|
+
self._kwargs = kwargs
|
|
40
|
+
|
|
41
|
+
@result_logging_decorator(__name__)
|
|
42
|
+
def start(self) -> Tuple[bytes, bytes]:
|
|
43
|
+
"""
|
|
44
|
+
Starts the operation associated with the Node by invoking the Conveyor.
|
|
45
|
+
|
|
46
|
+
This method calls the `Conveyor` object with the stored keyword arguments and returns a tuple of bytes
|
|
47
|
+
representing the result of the operation.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Tuple[bytes, bytes]: A tuple containing two byte sequences as the result of the Conveyor operation.
|
|
51
|
+
"""
|
|
52
|
+
return self.conveyor(**self._kwargs)
|
marsh/dag/startable.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Optional, Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Startable(ABC):
|
|
6
|
+
"""
|
|
7
|
+
Abstract base class for objects that can be started, such as nodes or DAGs.
|
|
8
|
+
|
|
9
|
+
This class defines a common interface for objects that need to be started.
|
|
10
|
+
Subclasses like `Node` and `Dag` will implement the `start` method with their own logic,
|
|
11
|
+
while inheriting from `Startable` to ensure they follow the expected contract for starting.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
name (str): A hashable name that identifies the object within a node or directed acyclic graph (DAG).
|
|
15
|
+
This will be used to define the graph in `graphlib.TopologicalSorter`.
|
|
16
|
+
|
|
17
|
+
Methods:
|
|
18
|
+
start(*args, **kwargs):
|
|
19
|
+
Starts the object with optional arguments. Subclasses must implement this method with their own logic,
|
|
20
|
+
defining how the object is started. It may return any type or `None`.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, name: str):
|
|
24
|
+
"""
|
|
25
|
+
Initializes a Startable object with a given name.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
name (str): A hashable name for the node or DAG.
|
|
29
|
+
This name is used to identify and reference the object.
|
|
30
|
+
"""
|
|
31
|
+
self.name = name # Hashable name for Node and Dag
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def start(self, *args, **kwargs) -> Optional[Any]:
|
|
35
|
+
"""
|
|
36
|
+
Abstract method that starts the object.
|
|
37
|
+
|
|
38
|
+
This method must be implemented by subclasses such as `Node` or `Dag` to define the specific behavior
|
|
39
|
+
for starting the object. It should handle the logic for initiating the object's state or execution.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
*args: Positional arguments passed to the method.
|
|
43
|
+
**kwargs: Keyword arguments passed to the method.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Optional[Any]: The result of the start operation, which may be of any type, or `None`.
|
|
47
|
+
"""
|
|
48
|
+
pass
|
marsh/docker/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from marsh import CommandGrammar
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DockerCommandGrammar(CommandGrammar):
|
|
5
|
+
def __init__(self, shell_command: str = "/bin/sh -c"):
|
|
6
|
+
if not shell_command.strip():
|
|
7
|
+
raise ValueError("The shell command cannot be empty. Example: Use '/bin/sh -c'")
|
|
8
|
+
self.shell_command = shell_command
|
|
9
|
+
|
|
10
|
+
def build_cmd(self,
|
|
11
|
+
command: str | list[str],
|
|
12
|
+
x_stdout: str | bytes | None = None
|
|
13
|
+
) -> list[str]:
|
|
14
|
+
shell_command = self.shell_command.split()
|
|
15
|
+
|
|
16
|
+
x_stdout = x_stdout.decode() if isinstance(x_stdout, bytes)\
|
|
17
|
+
else x_stdout if isinstance(x_stdout, str)\
|
|
18
|
+
else None
|
|
19
|
+
|
|
20
|
+
if isinstance(command, str):
|
|
21
|
+
full_command = [*shell_command, command]
|
|
22
|
+
else:
|
|
23
|
+
full_command = [*shell_command, " ".join(command)]
|
|
24
|
+
|
|
25
|
+
if x_stdout:
|
|
26
|
+
full_command[-1] = full_command[-1] + " " + x_stdout.strip()
|
|
27
|
+
|
|
28
|
+
return full_command
|
|
29
|
+
|
|
30
|
+
def pipe_prev_stdout(self, x_stdout: str | bytes) -> list[str]:
|
|
31
|
+
pass
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
import time
|
|
3
|
+
import threading
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import docker
|
|
7
|
+
from docker.models.containers import Container
|
|
8
|
+
from docker.errors import DockerException, NotFound
|
|
9
|
+
|
|
10
|
+
from marsh.exceptions import DockerError, DockerClientError
|
|
11
|
+
from marsh import Executor
|
|
12
|
+
from .docker_command_grammar import DockerCommandGrammar
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def generate_random_container_name(prefix="ephemeral-container"):
|
|
16
|
+
return prefix + "-" + str(uuid.uuid4()).replace('-', '')[:8]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DockerContainer:
|
|
20
|
+
def __init__(self,
|
|
21
|
+
image: str,
|
|
22
|
+
*create_args,
|
|
23
|
+
client_args=(),
|
|
24
|
+
client_kwargs=None,
|
|
25
|
+
name: str | None = None,
|
|
26
|
+
timeout: int = 600, # in seconds
|
|
27
|
+
start_timeout: int = 1.5, # Wait for few seconds after a container started
|
|
28
|
+
**create_kwargs
|
|
29
|
+
):
|
|
30
|
+
client_kwargs = client_kwargs or {}
|
|
31
|
+
|
|
32
|
+
self._image = image
|
|
33
|
+
self._name = name or generate_random_container_name()
|
|
34
|
+
|
|
35
|
+
self._start_timeout = start_timeout
|
|
36
|
+
self._timeout = timeout
|
|
37
|
+
self._timer: threading.Timer | None = None
|
|
38
|
+
|
|
39
|
+
self._container: Optional[Container] = None
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
self._client = docker.DockerClient(*client_args, **client_kwargs)
|
|
43
|
+
except Exception as err:
|
|
44
|
+
raise DockerClientError(err)
|
|
45
|
+
|
|
46
|
+
self._create_args = create_args
|
|
47
|
+
self._create_kwargs = create_kwargs
|
|
48
|
+
|
|
49
|
+
def __enter__(self) -> Container:
|
|
50
|
+
try:
|
|
51
|
+
self._container = self._client.containers.create(
|
|
52
|
+
self._image,
|
|
53
|
+
*self._create_args,
|
|
54
|
+
command="tail -f /dev/null", # To run the container continuously
|
|
55
|
+
name=self._name,
|
|
56
|
+
detach=True,
|
|
57
|
+
auto_remove=True,
|
|
58
|
+
**self._create_kwargs
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Start the container
|
|
62
|
+
self._container.start()
|
|
63
|
+
time.sleep(self._start_timeout) # Wait for few seconds
|
|
64
|
+
|
|
65
|
+
# Set the Timer to stop the container on timeout
|
|
66
|
+
self._timer = threading.Timer(self._timeout, self._throw_timeout_error)
|
|
67
|
+
self._timer.start()
|
|
68
|
+
|
|
69
|
+
except Exception as err:
|
|
70
|
+
self._clean()
|
|
71
|
+
raise DockerError(err)
|
|
72
|
+
|
|
73
|
+
return self._container
|
|
74
|
+
|
|
75
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
76
|
+
# Clean the resources
|
|
77
|
+
self._clean()
|
|
78
|
+
|
|
79
|
+
if exc_type is TimeoutError:
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
def _throw_timeout_error(self) -> None:
|
|
85
|
+
self._clean()
|
|
86
|
+
raise TimeoutError(f"Timeout reached for container '{self._name}'.")
|
|
87
|
+
|
|
88
|
+
def _clean(self) -> None:
|
|
89
|
+
# 1. Cancel the Timer
|
|
90
|
+
# 2. Remove the Container
|
|
91
|
+
# 3. Close the Docker Client
|
|
92
|
+
|
|
93
|
+
# Cancel the Timer
|
|
94
|
+
if self._timer:
|
|
95
|
+
self._timer.cancel()
|
|
96
|
+
|
|
97
|
+
# Remove the Container
|
|
98
|
+
all_containers: list[Container] = self._client.containers.list(all=True)
|
|
99
|
+
for container in all_containers:
|
|
100
|
+
if container.name == self._name:
|
|
101
|
+
try:
|
|
102
|
+
container.stop(timeout=0)
|
|
103
|
+
except NotFound as err:
|
|
104
|
+
# This occurs when timeout event occurred.
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
# Close the docker client
|
|
108
|
+
try:
|
|
109
|
+
self._client.close()
|
|
110
|
+
except DockerException as err:
|
|
111
|
+
raise DockerClientError(err)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class DockerCommandExecutor(Executor):
|
|
115
|
+
def __init__(self,
|
|
116
|
+
image: str,
|
|
117
|
+
*create_args,
|
|
118
|
+
container_name: str | None = None,
|
|
119
|
+
pipe_prev_stdout: bool = False,
|
|
120
|
+
timeout: int = 600,
|
|
121
|
+
shell_command="/bin/sh -c",
|
|
122
|
+
client_args: tuple = (),
|
|
123
|
+
client_kwargs: dict | None = None,
|
|
124
|
+
**create_kwargs,
|
|
125
|
+
):
|
|
126
|
+
|
|
127
|
+
self._client_args = client_args
|
|
128
|
+
self._client_kwargs = client_kwargs or {}
|
|
129
|
+
|
|
130
|
+
self._create_args = create_args
|
|
131
|
+
self._create_kwargs = create_kwargs
|
|
132
|
+
|
|
133
|
+
self._container_name = container_name
|
|
134
|
+
self.timeout = timeout
|
|
135
|
+
self.image = image # Docker container image
|
|
136
|
+
|
|
137
|
+
# Create a DockerCommandGrammar and clean the given command
|
|
138
|
+
self._command_grammar = DockerCommandGrammar(shell_command=shell_command)
|
|
139
|
+
|
|
140
|
+
self._pipe_prev_stdout = pipe_prev_stdout
|
|
141
|
+
|
|
142
|
+
def run(self,
|
|
143
|
+
x_stdout: bytes,
|
|
144
|
+
x_stderr: bytes,
|
|
145
|
+
command: str | list[str],
|
|
146
|
+
**run_kwargs
|
|
147
|
+
) -> tuple[bytes, bytes]:
|
|
148
|
+
|
|
149
|
+
stdout: bytes = b""
|
|
150
|
+
stderr: bytes = b""
|
|
151
|
+
|
|
152
|
+
with DockerContainer(self.image,
|
|
153
|
+
*self._create_args,
|
|
154
|
+
name=self._container_name,
|
|
155
|
+
timeout=self.timeout,
|
|
156
|
+
client_args=self._client_args,
|
|
157
|
+
client_kwargs=self._client_kwargs,
|
|
158
|
+
**self._create_kwargs,
|
|
159
|
+
) as container:
|
|
160
|
+
|
|
161
|
+
# Unix Pipes, if specified
|
|
162
|
+
if self._pipe_prev_stdout:
|
|
163
|
+
full_command = self._command_grammar.build_cmd(command, x_stdout)
|
|
164
|
+
else:
|
|
165
|
+
full_command = self._command_grammar.build_cmd(command)
|
|
166
|
+
|
|
167
|
+
# Run a command in the container
|
|
168
|
+
try:
|
|
169
|
+
result = container.exec_run(full_command, **run_kwargs)
|
|
170
|
+
except TimeoutError:
|
|
171
|
+
raise
|
|
172
|
+
|
|
173
|
+
# Check exit code and update output streams
|
|
174
|
+
if result.exit_code == 0:
|
|
175
|
+
# Update STDOUT (no strip)
|
|
176
|
+
stdout = result.output
|
|
177
|
+
else:
|
|
178
|
+
# Update STDERR (no strip)
|
|
179
|
+
stderr = result.output
|
|
180
|
+
|
|
181
|
+
return stdout, stderr
|
marsh/exceptions.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class CommandError(Exception):
|
|
2
|
+
"""Base Class for all Command Errors."""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ScriptError(Exception):
|
|
7
|
+
"""Base Class for all Script Errors."""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DockerError(Exception):
|
|
12
|
+
"""Base Class for all Docker Errors."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DockerClientError(DockerError):
|
|
16
|
+
pass
|
marsh/logger.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import functools
|
|
3
|
+
import logging
|
|
4
|
+
from logging.handlers import RotatingFileHandler
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LoggerTemplate:
|
|
8
|
+
def __init__(self,
|
|
9
|
+
logger_name: str,
|
|
10
|
+
log_level: int,
|
|
11
|
+
log_handlers: list,
|
|
12
|
+
log_formatter: logging.Formatter
|
|
13
|
+
):
|
|
14
|
+
self.logger_name = logger_name
|
|
15
|
+
self.log_level = log_level
|
|
16
|
+
self.log_handlers = log_handlers
|
|
17
|
+
self.log_formatter = log_formatter
|
|
18
|
+
|
|
19
|
+
def create_logger(self) -> logging.Logger:
|
|
20
|
+
logger = logging.getLogger(self.logger_name)
|
|
21
|
+
logger.setLevel(self.log_level)
|
|
22
|
+
|
|
23
|
+
for handler in self.log_handlers:
|
|
24
|
+
handler.setFormatter(self.log_formatter)
|
|
25
|
+
logger.addHandler(handler)
|
|
26
|
+
|
|
27
|
+
return logger
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CustomRotatingFileHandler(RotatingFileHandler):
|
|
31
|
+
def rotation_filename(self, default_name):
|
|
32
|
+
base_name = default_name.split(".")[0]
|
|
33
|
+
count = default_name.split(".")[-1]
|
|
34
|
+
return f"{base_name}_{count}.log"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def create_rotating_file_logger(logger_name: str,
|
|
38
|
+
log_file_path: str,
|
|
39
|
+
size_mb: int = 1,
|
|
40
|
+
bak_count: int = 2
|
|
41
|
+
) -> logging.Logger:
|
|
42
|
+
# Define the log format
|
|
43
|
+
log_formatter = logging.Formatter(
|
|
44
|
+
"[%(levelname)s] %(asctime)s | %(name)s | Line %(lineno)d | %(message)s"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
file_path = str(LOG_DIR_PATH / log_file_path)
|
|
48
|
+
|
|
49
|
+
# Set up the rotating file handler
|
|
50
|
+
rotating_handler = CustomRotatingFileHandler(
|
|
51
|
+
file_path,
|
|
52
|
+
maxBytes=size_mb * 1024 * 1024,
|
|
53
|
+
backupCount=bak_count
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Create a logger using the template
|
|
57
|
+
logger_template = LoggerTemplate(
|
|
58
|
+
logger_name=logger_name,
|
|
59
|
+
log_level=logging.DEBUG,
|
|
60
|
+
log_handler=rotating_handler,
|
|
61
|
+
log_formatter=log_formatter
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return logger_template.create_logger()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def create_console_logger(logger_name: str) -> logging.Logger:
|
|
68
|
+
# Define the log format
|
|
69
|
+
log_formatter = logging.Formatter(
|
|
70
|
+
"[%(levelname)s] %(asctime)s | %(name)s | Line %(lineno)d | %(message)s"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
console_handler = logging.StreamHandler()
|
|
74
|
+
|
|
75
|
+
# Create a logger using the template
|
|
76
|
+
logger_template = LoggerTemplate(
|
|
77
|
+
logger_name=logger_name,
|
|
78
|
+
log_level=logging.DEBUG,
|
|
79
|
+
log_handlers=[console_handler],
|
|
80
|
+
log_formatter=log_formatter
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return logger_template.create_logger()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def result_logging_decorator(name: str):
|
|
87
|
+
"""
|
|
88
|
+
Result logging decorator.
|
|
89
|
+
|
|
90
|
+
Usage:
|
|
91
|
+
Decorate any callable with return values
|
|
92
|
+
Step 1: Create Environment Variable `MARSH_USE_LOGGER` with value `true` in terminal.
|
|
93
|
+
- On Linux/MacOS: `export MARSH_USE_LOGGER=true`
|
|
94
|
+
- On Windows (PowerShell): `$env:MARSH_USE_LOGGER="true"`
|
|
95
|
+
Step 2: Decorate any callable with return values.
|
|
96
|
+
`@result_logging_decorator(__name__)`
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
use_logger = True if os.getenv("MARSH_USE_LOGGER") == "true" else False
|
|
100
|
+
logger = create_console_logger(name)
|
|
101
|
+
|
|
102
|
+
def outer(func):
|
|
103
|
+
@functools.wraps(func)
|
|
104
|
+
def wrapper(*args, **kwargs):
|
|
105
|
+
try:
|
|
106
|
+
result = func(*args, **kwargs)
|
|
107
|
+
if use_logger:
|
|
108
|
+
logger.info(f"{func.__name__} result: {result}")
|
|
109
|
+
return result
|
|
110
|
+
except Exception as err:
|
|
111
|
+
if use_logger:
|
|
112
|
+
logger.error(str(err))
|
|
113
|
+
|
|
114
|
+
return wrapper
|
|
115
|
+
|
|
116
|
+
return outer
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
def case_conversion(inp_stdout: bytes,
|
|
2
|
+
inp_stderr: bytes,
|
|
3
|
+
upper=True,
|
|
4
|
+
output_stream="stdout",
|
|
5
|
+
) -> tuple[bytes, bytes]:
|
|
6
|
+
if output_stream not in ["stdout", "stderr"]:
|
|
7
|
+
raise ValueError("Output stream must be 'stdout' or 'stderr'.")
|
|
8
|
+
|
|
9
|
+
if output_stream == "stdout":
|
|
10
|
+
return inp_stdout.upper() if upper else inp_stdout.lower(), inp_stderr
|
|
11
|
+
else:
|
|
12
|
+
return inp_stderr.upper() if upper else inp_stderr.lower(), inp_stderr
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = (
|
|
16
|
+
"case_conversion",
|
|
17
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
def read_file(inp_stdout: bytes, inp_stderr: bytes, file_path: str, encoding='utf-8') -> tuple[bytes, bytes]:
|
|
2
|
+
try:
|
|
3
|
+
with open(file_path, 'r') as file:
|
|
4
|
+
return file.read().encode(encoding), b""
|
|
5
|
+
except (FileNotFoundError, PermissionError, IsADirectoryError, OSError, UnicodeEncodeError, IOError) as e:
|
|
6
|
+
return b"", str(e).encode(encoding)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
__all__ = (
|
|
10
|
+
"read_file",
|
|
11
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def print_output_stream(inp_stdout: bytes,
|
|
5
|
+
inp_stderr: bytes,
|
|
6
|
+
*args,
|
|
7
|
+
output_stream="stdout",
|
|
8
|
+
encoding='utf-8',
|
|
9
|
+
**kwargs
|
|
10
|
+
) -> None:
|
|
11
|
+
if output_stream not in ["stdout", "stderr"]:
|
|
12
|
+
raise ValueError("Output stream must be 'stdout' or 'stderr'.")
|
|
13
|
+
|
|
14
|
+
if output_stream == "stdout":
|
|
15
|
+
if inp_stdout.strip():
|
|
16
|
+
print(inp_stdout.decode(encoding).strip(), *args, **kwargs)
|
|
17
|
+
else:
|
|
18
|
+
if inp_stderr.strip():
|
|
19
|
+
print(inp_stderr.decode(encoding).strip(), *args, **kwargs)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def print_stdout(inp_stdout: bytes, inp_stderr: bytes, *args, encoding='utf-8', **kwargs) -> None:
|
|
23
|
+
print_output_stream(inp_stdout, inp_stderr, *args, output_stream="stdout", encoding=encoding, **kwargs)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def print_stderr(inp_stdout: bytes, inp_stderr: bytes, *args, encoding='utf-8', **kwargs) -> None:
|
|
27
|
+
print_output_stream(inp_stdout, inp_stderr, *args, output_stream="stderr", encoding=encoding, **kwargs)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def print_all_output_streams(inp_stdout: bytes, inp_stderr: bytes, *args, encoding='utf-8', **kwargs) -> None:
|
|
31
|
+
print_stdout(inp_stdout, inp_stderr, *args, encoding=encoding, **kwargs)
|
|
32
|
+
print_stderr(inp_stdout, inp_stderr, *args, encoding=encoding, **kwargs)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def log_output_streams(inp_stdout: bytes,
|
|
36
|
+
inp_stderr: bytes,
|
|
37
|
+
name="ConsoleLogger",
|
|
38
|
+
log_level=logging.DEBUG,
|
|
39
|
+
format_="[%(levelname)s] %(asctime)s | %(message)s",
|
|
40
|
+
encoding='utf-8'
|
|
41
|
+
) -> None:
|
|
42
|
+
logger = logging.getLogger(name)
|
|
43
|
+
logger.setLevel(log_level)
|
|
44
|
+
stream_handler = logging.StreamHandler()
|
|
45
|
+
stream_handler.setLevel(logging.DEBUG)
|
|
46
|
+
formatter = logging.Formatter(format_)
|
|
47
|
+
stream_handler.setFormatter(formatter)
|
|
48
|
+
logger.addHandler(stream_handler)
|
|
49
|
+
|
|
50
|
+
if inp_stderr.strip():
|
|
51
|
+
logger.error(inp_stderr.decode(encoding).strip())
|
|
52
|
+
|
|
53
|
+
if inp_stdout.strip():
|
|
54
|
+
logger.info(inp_stdout.decode(encoding).strip())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
__all__ = (
|
|
58
|
+
"print_output_stream",
|
|
59
|
+
"print_stdout",
|
|
60
|
+
"print_stderr",
|
|
61
|
+
"print_all_output_streams",
|
|
62
|
+
"log_output_streams"
|
|
63
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
def redirect_output_stream(inp_stdout: bytes,
|
|
2
|
+
inp_stderr: bytes,
|
|
3
|
+
file_path: str,
|
|
4
|
+
output_stream="stdout",
|
|
5
|
+
mode='w',
|
|
6
|
+
encoding='utf-8',
|
|
7
|
+
) -> None:
|
|
8
|
+
if output_stream not in ["stdout", "stderr"]:
|
|
9
|
+
raise ValueError("Output stream must be 'stdout' or 'stderr'.")
|
|
10
|
+
|
|
11
|
+
with open(file_path, mode) as file:
|
|
12
|
+
if output_stream == "stdout":
|
|
13
|
+
file.write(inp_stdout.decode(encoding).strip())
|
|
14
|
+
else:
|
|
15
|
+
file.write(inp_stderr.decode(encoding).strip())
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def redirect_stdout(inp_stdout: bytes,
|
|
19
|
+
inp_stderr: bytes,
|
|
20
|
+
file_path: str,
|
|
21
|
+
mode='w',
|
|
22
|
+
encoding='utf-8') -> None:
|
|
23
|
+
redirect_output_stream(inp_stdout, inp_stderr, file_path, output_stream="stdout", mode=mode, encoding=encoding)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def redirect_stderr(inp_stdout: bytes,
|
|
27
|
+
inp_stderr: bytes,
|
|
28
|
+
file_path: str,
|
|
29
|
+
mode='w',
|
|
30
|
+
encoding='utf-8'
|
|
31
|
+
) -> None:
|
|
32
|
+
redirect_output_stream(inp_stdout, inp_stderr, file_path, output_stream="stderr", mode=mode, encoding=encoding)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__all__ = (
|
|
36
|
+
"redirect_output_stream",
|
|
37
|
+
"redirect_stdout",
|
|
38
|
+
"redirect_stderr",
|
|
39
|
+
)
|
marsh/signals.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import signal
|
marsh/ssh/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from marsh import CommandGrammar
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SshCommandGrammar(CommandGrammar):
|
|
7
|
+
"""
|
|
8
|
+
SSH Command Grammar for building SSH commands by adding custom logic.
|
|
9
|
+
|
|
10
|
+
Inherits from `CommandGrammar` and provides a method to build commands with
|
|
11
|
+
optional piping logic.
|
|
12
|
+
"""
|
|
13
|
+
def build_cmd(self,
|
|
14
|
+
commands: list[str],
|
|
15
|
+
prev_stdout: Optional[bytes] = None,
|
|
16
|
+
*pipe_args,
|
|
17
|
+
**pipe_kwargs,
|
|
18
|
+
) -> list[str]:
|
|
19
|
+
"""
|
|
20
|
+
Builds the full SSH command by optionally piping the stdout of a previous command to the next.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
commands (list[str]): The list of commands to execute.
|
|
24
|
+
prev_stdout (Optional[bytes]): The stdout from the previous command to pipe into the current one.
|
|
25
|
+
*pipe_args: Arguments for the piping logic.
|
|
26
|
+
**pipe_kwargs: Keyword arguments for the piping logic.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
list[str]: A list of strings representing the final SSH command with any necessary piping logic.
|
|
30
|
+
"""
|
|
31
|
+
if prev_stdout:
|
|
32
|
+
return commands + [self._pipe_stdout(prev_stdout, *pipe_args, **pipe_kwargs)]
|
|
33
|
+
return commands
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def _pipe_stdout(stdout: str | bytes, encoding: str = "utf-8") -> str:
|
|
37
|
+
"""
|
|
38
|
+
Creates a formatted string to pipe the stdout of a previous command into the next.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
stdout (str | bytes): The stdout to be piped.
|
|
42
|
+
encoding (str): The encoding to use if `stdout` is a byte string. Default is "utf-8".
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
str: The formatted string to pipe the stdout.
|
|
46
|
+
"""
|
|
47
|
+
return rf"""<<'EOF'
|
|
48
|
+
{stdout if isinstance(stdout, str) else stdout.decode(encoding)}
|
|
49
|
+
EOF
|
|
50
|
+
"""
|