CapsuleCore-shellback 0.1.1__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.
- capsulecore_shellback-0.1.1.dist-info/METADATA +6 -0
- capsulecore_shellback-0.1.1.dist-info/RECORD +16 -0
- capsulecore_shellback-0.1.1.dist-info/WHEEL +5 -0
- capsulecore_shellback-0.1.1.dist-info/top_level.txt +1 -0
- shellback/__init__.py +4 -0
- shellback/capsule/ArgumentBuilder.py +17 -0
- shellback/capsule/Bash.py +22 -0
- shellback/capsule/Command.py +15 -0
- shellback/capsule/ConsoleLogger.py +37 -0
- shellback/capsule/NullLogger.py +19 -0
- shellback/capsule/__init__.py +7 -0
- shellback/core/CommandResult.py +25 -0
- shellback/core/Logger.py +46 -0
- shellback/core/SessionContext.py +10 -0
- shellback/core/Shell.py +88 -0
- shellback/core/__init__.py +6 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
shellback/__init__.py,sha256=lXvWZ9aM0TMy1F9_62fwNMPpKrEg9Lk495TIYUQKYkI,244
|
|
2
|
+
shellback/capsule/ArgumentBuilder.py,sha256=ydNk9eJaQGomPfAomLwg-gNM4M-YL3h0C4oJ9ZtLJi0,552
|
|
3
|
+
shellback/capsule/Bash.py,sha256=ahzUuS3kh_32IbtOHF0I-3QizH8AUEZcGuW8qY4M2ik,853
|
|
4
|
+
shellback/capsule/Command.py,sha256=3h5HPao_e6sMMRyStVNhA2OHvNiDCt3F7eLKlh-osM0,370
|
|
5
|
+
shellback/capsule/ConsoleLogger.py,sha256=eAbgzxNMQXK-owvnAkY7Iqp0ejlUO1YCrWkItXSmLBU,1126
|
|
6
|
+
shellback/capsule/NullLogger.py,sha256=Y2s5vlVXnIyT_LzHirl1I85flt0UbMIirsWc-lH6m2k,413
|
|
7
|
+
shellback/capsule/__init__.py,sha256=xo7wDQoOJuNemJQQPZOJg-qd6zlFMpt4hch-hRAx1O0,253
|
|
8
|
+
shellback/core/CommandResult.py,sha256=Gi6nCSPJxjFWHZnJrQ_vBJaz7MPeSITyT5Ev-7X8d8s,803
|
|
9
|
+
shellback/core/Logger.py,sha256=6VuuMjuNqsTrPZQ6t4b4d3VIm8nW2o6C7k9eGbBl5Y0,1240
|
|
10
|
+
shellback/core/SessionContext.py,sha256=ZMoxeQfTskb_FCV2T7gJSZOPiQd5QUL8__ePRF_F1jQ,338
|
|
11
|
+
shellback/core/Shell.py,sha256=oAzRLlv6M3dhTH4IHlRw9pNpWF_VbcWG0Sx9ezR4MPY,3301
|
|
12
|
+
shellback/core/__init__.py,sha256=5l6y_Los6he2QMtdCkNzjBG_X8dw1-xW2VouWPJ-9nQ,202
|
|
13
|
+
capsulecore_shellback-0.1.1.dist-info/METADATA,sha256=7t3NbM5mMFgnTRtnpePhuWIB0_54mOya4mSbb2phbmE,164
|
|
14
|
+
capsulecore_shellback-0.1.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
15
|
+
capsulecore_shellback-0.1.1.dist-info/top_level.txt,sha256=UMqhtAp7UJ7yuTXc49iqnumkbxyR1HcokQ7W0IFrJtQ,10
|
|
16
|
+
capsulecore_shellback-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
shellback
|
shellback/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
class ArgumentBuilder:
|
|
4
|
+
def __init__(self, style: str = "unix"):
|
|
5
|
+
self._args = []
|
|
6
|
+
self.style = style # "unix" para --flag o "ms" para /flag
|
|
7
|
+
|
|
8
|
+
def add_flag(self, name: str, value: Optional[Any] = None):
|
|
9
|
+
prefix = "--" if self.style == "unix" else "/"
|
|
10
|
+
if value is None:
|
|
11
|
+
self._args.append(f"{prefix}{name}")
|
|
12
|
+
else:
|
|
13
|
+
self._args.extend([f"{prefix}{name}", str(value)])
|
|
14
|
+
return self
|
|
15
|
+
|
|
16
|
+
def build(self) -> list[str]:
|
|
17
|
+
return self._args
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from ..core import Shell,SessionContext
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Bash(Shell):
|
|
6
|
+
def _format_command(self, executable: str, args: list[str]) -> list[str]:
|
|
7
|
+
# En Bash, usualmente es directo, pero aquí podríamos manejar
|
|
8
|
+
# escapes específicos si fuera necesario.
|
|
9
|
+
return [executable] + args
|
|
10
|
+
|
|
11
|
+
def cd(self, path: str | Path):
|
|
12
|
+
"""Actualiza el contexto virtualmente."""
|
|
13
|
+
new_path = (self.context.cwd / path).resolve()
|
|
14
|
+
if new_path.exists() and new_path.is_dir():
|
|
15
|
+
# Re-instanciar el contexto (es inmutable por el dataclass frozen)
|
|
16
|
+
self.context = SessionContext(
|
|
17
|
+
cwd=new_path,
|
|
18
|
+
env=self.context.env,
|
|
19
|
+
encoding=self.context.encoding
|
|
20
|
+
)
|
|
21
|
+
else:
|
|
22
|
+
raise FileNotFoundError(f"Directory {new_path} not found")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .ArgumentBuilder import ArgumentBuilder
|
|
2
|
+
|
|
3
|
+
class Command:
|
|
4
|
+
def __init__(self, executable: str):
|
|
5
|
+
self.executable = executable
|
|
6
|
+
self.builder = ArgumentBuilder()
|
|
7
|
+
|
|
8
|
+
def add_args(self, *args):
|
|
9
|
+
for arg in args:
|
|
10
|
+
self.builder._args.append(arg)
|
|
11
|
+
return self
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def args(self):
|
|
15
|
+
return self.builder.build()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from ..core import Logger
|
|
4
|
+
|
|
5
|
+
class ConsoleLogger(Logger):
|
|
6
|
+
"""
|
|
7
|
+
Implementación que imprime en la salida estándar con colores y timestamps.
|
|
8
|
+
Ideal para desarrollo y scripts interactivos.
|
|
9
|
+
"""
|
|
10
|
+
# Códigos ANSI para colores
|
|
11
|
+
COLORS = {
|
|
12
|
+
"DEBUG": "\033[94m", # Azul
|
|
13
|
+
"INFO": "\033[92m", # Verde
|
|
14
|
+
"WARNING": "\033[93m", # Amarillo
|
|
15
|
+
"ERROR": "\033[91m", # Rojo
|
|
16
|
+
"RESET": "\033[0m" # Reset
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
def _log(self, level: str, message: str):
|
|
20
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
21
|
+
color = self.COLORS.get(level, self.COLORS["RESET"])
|
|
22
|
+
reset = self.COLORS["RESET"]
|
|
23
|
+
|
|
24
|
+
# Formato: [14:30:05] [INFO] Mensaje
|
|
25
|
+
print(f"[{timestamp}] {color}[{level}]{reset} {message}", file=sys.stderr)
|
|
26
|
+
|
|
27
|
+
def info(self, message: str):
|
|
28
|
+
self._log("INFO", message)
|
|
29
|
+
|
|
30
|
+
def debug(self, message: str):
|
|
31
|
+
self._log("DEBUG", message)
|
|
32
|
+
|
|
33
|
+
def warning(self, message: str):
|
|
34
|
+
self._log("WARNING", message)
|
|
35
|
+
|
|
36
|
+
def error(self, message: str):
|
|
37
|
+
self._log("ERROR", message)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from ..core import Logger
|
|
2
|
+
|
|
3
|
+
class NullLogger(Logger):
|
|
4
|
+
"""
|
|
5
|
+
Implementación 'Null Object'.
|
|
6
|
+
No realiza ninguna acción, permitiendo que la Shell funcione
|
|
7
|
+
sin comprobaciones de nulidad constantes.
|
|
8
|
+
"""
|
|
9
|
+
def info(self, message: str):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
def debug(self, message: str):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
def warning(self, message: str):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
def error(self, message: str):
|
|
19
|
+
pass
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class CommandResult:
|
|
8
|
+
"""Encapsulates the output of an executed command."""
|
|
9
|
+
standand_output: str
|
|
10
|
+
standand_error: str
|
|
11
|
+
return_code: int
|
|
12
|
+
execution_time: float
|
|
13
|
+
command_sent: list[str]
|
|
14
|
+
|
|
15
|
+
def is_success(self) -> bool:
|
|
16
|
+
"""Returns True if the command executed successfully (return code 0)."""
|
|
17
|
+
return self.return_code == 0
|
|
18
|
+
|
|
19
|
+
def json(self) -> Any:
|
|
20
|
+
"""Parses the standard output as JSON and returns the deserialized object."""
|
|
21
|
+
return json.loads(self.standand_output)
|
|
22
|
+
|
|
23
|
+
def __or__(self, next_command):
|
|
24
|
+
"""Enables pipe syntax (sh.run(cmd1) | sh.run(cmd2)) for chaining command results by returning the standard output."""
|
|
25
|
+
return self.standand_output
|
shellback/core/Logger.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
class Logger(ABC):
|
|
4
|
+
"""Defines the interface for any logger that the application can use.
|
|
5
|
+
|
|
6
|
+
This abstract base class establishes the contract that all logger
|
|
7
|
+
implementations must follow, enabling dependency inversion and
|
|
8
|
+
allowing different logging backends to be swapped without affecting
|
|
9
|
+
application code.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def info(self, message: str):
|
|
14
|
+
"""Log an informational message.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
message: The string content to log at INFO level
|
|
18
|
+
"""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def debug(self, message: str):
|
|
23
|
+
"""Log a debug message for detailed diagnostic information.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
message: The string content to log at DEBUG level
|
|
27
|
+
"""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def warning(self, message: str):
|
|
32
|
+
"""Log a warning message indicating potential issues.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
message: The string content to log at WARNING level
|
|
36
|
+
"""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def error(self, message: str):
|
|
41
|
+
"""Log an error message for runtime errors or exceptions.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
message: The string content to log at ERROR level
|
|
45
|
+
"""
|
|
46
|
+
pass
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class SessionContext:
|
|
7
|
+
"""Mantiene el estado persistente entre ejecuciones de la Shell."""
|
|
8
|
+
cwd: Path = field(default_factory=Path.cwd)
|
|
9
|
+
env: Dict[str, str] = field(default_factory=dict)
|
|
10
|
+
encoding: str = "utf-8"
|
shellback/core/Shell.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
import shutil
|
|
3
|
+
import time
|
|
4
|
+
import subprocess
|
|
5
|
+
from .Logger import Logger # Asumiendo que usas tu interfaz de Logger
|
|
6
|
+
from ..capsule import NullLogger
|
|
7
|
+
from .SessionContext import SessionContext
|
|
8
|
+
from .CommandResult import CommandResult
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
class CommandNotFoundError(Exception):
|
|
12
|
+
"""Lanzada cuando el binario no existe en el PATH."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
class Shell(ABC):
|
|
16
|
+
def __init__(self, context: Optional[SessionContext] = None,
|
|
17
|
+
logger: Optional[Logger] = None,
|
|
18
|
+
default_timeout: float = 30.0):
|
|
19
|
+
self.context = context or SessionContext()
|
|
20
|
+
self.logger = logger or NullLogger()
|
|
21
|
+
self.default_timeout = default_timeout
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def _format_command(self, executable: str, args: list[str]) -> list[str]:
|
|
25
|
+
"""Cada shell hija decide cómo formatear la lista final (Strategy)."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
def __enter__(self):
|
|
29
|
+
self.logger.info(f"Iniciando sesión de {self.__class__.__name__}")
|
|
30
|
+
return self
|
|
31
|
+
|
|
32
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
33
|
+
if exc_type:
|
|
34
|
+
self.logger.error(f"Sesión finalizada con error: {exc_val}")
|
|
35
|
+
else:
|
|
36
|
+
self.logger.info(f"Sesión de {self.__class__.__name__} finalizada correctamente")
|
|
37
|
+
# No retornamos True para que las excepciones sigan propagándose si ocurren
|
|
38
|
+
|
|
39
|
+
def run(self, command, timeout: Optional[float] = None) -> CommandResult:
|
|
40
|
+
"""
|
|
41
|
+
Método 'Template' que coordina la ejecución.
|
|
42
|
+
"""
|
|
43
|
+
executable = command.executable
|
|
44
|
+
args = command.args # El ArgumentBuilder ya devuelve una lista
|
|
45
|
+
|
|
46
|
+
# 1. Lazy Check de binario
|
|
47
|
+
if not shutil.which(executable):
|
|
48
|
+
self.logger.error(f"Command not found: {executable}")
|
|
49
|
+
raise CommandNotFoundError(f"The executable '{executable}' was not found in PATH.")
|
|
50
|
+
|
|
51
|
+
# 2. Preparación según la Shell específica
|
|
52
|
+
final_args = self._format_command(executable, args)
|
|
53
|
+
|
|
54
|
+
self.logger.debug(f"Executing: {' '.join(final_args)} in {self.context.cwd}")
|
|
55
|
+
|
|
56
|
+
start_time = time.perf_counter()
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
process = subprocess.run(
|
|
60
|
+
final_args,
|
|
61
|
+
cwd=self.context.cwd,
|
|
62
|
+
env={**self.context.env}, # Merge de env actual
|
|
63
|
+
capture_output=True,
|
|
64
|
+
text=True,
|
|
65
|
+
encoding=self.context.encoding,
|
|
66
|
+
timeout=timeout or self.default_timeout
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
execution_time = time.perf_counter() - start_time
|
|
70
|
+
|
|
71
|
+
result = CommandResult(
|
|
72
|
+
standand_output=process.stdout,
|
|
73
|
+
standand_error=process.stderr,
|
|
74
|
+
return_code=process.returncode,
|
|
75
|
+
execution_time=execution_time,
|
|
76
|
+
command_sent=final_args
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if result.is_success():
|
|
80
|
+
self.logger.info(f"Success: {executable}")
|
|
81
|
+
else:
|
|
82
|
+
self.logger.warning(f"Failed: {executable} with code {result.return_code}")
|
|
83
|
+
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
except subprocess.TimeoutExpired as e:
|
|
87
|
+
self.logger.error(f"Timeout after {self.default_timeout}s")
|
|
88
|
+
raise e
|