cc.shellback-kit 0.3.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.
- cc_shellback_kit/__init__.py +34 -0
- cc_shellback_kit/capsule/Bash.py +8 -0
- cc_shellback_kit/capsule/ConsoleLogObserver.py +16 -0
- cc_shellback_kit/capsule/FileLogObserver.py +49 -0
- cc_shellback_kit/capsule/JSONFileObserver.py +63 -0
- cc_shellback_kit/capsule/MultiObserver.py +39 -0
- cc_shellback_kit/capsule/SilentObserver.py +7 -0
- cc_shellback_kit/capsule/__init__.py +16 -0
- cc_shellback_kit/core/ArgumentBuilder.py +30 -0
- cc_shellback_kit/core/Command.py +22 -0
- cc_shellback_kit/core/CommandResult.py +26 -0
- cc_shellback_kit/core/SessionContext.py +12 -0
- cc_shellback_kit/core/Shell.py +174 -0
- cc_shellback_kit/core/ShellObserver.py +56 -0
- cc_shellback_kit/core/__init__.py +16 -0
- cc_shellback_kit-0.3.0.dist-info/METADATA +52 -0
- cc_shellback_kit-0.3.0.dist-info/RECORD +19 -0
- cc_shellback_kit-0.3.0.dist-info/WHEEL +5 -0
- cc_shellback_kit-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from .capsule import (
|
|
2
|
+
Bash,
|
|
3
|
+
ConsoleLogObserver,
|
|
4
|
+
FileLogObserver,
|
|
5
|
+
SilentObserver,
|
|
6
|
+
JSONFileObserver,
|
|
7
|
+
MultiObserver,
|
|
8
|
+
)
|
|
9
|
+
from .core import (
|
|
10
|
+
Shell,
|
|
11
|
+
ArgumentBuilder,
|
|
12
|
+
Command,
|
|
13
|
+
SessionContext,
|
|
14
|
+
CommandResult,
|
|
15
|
+
CommandNotFoundError,
|
|
16
|
+
ShellObserver,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"ArgumentBuilder",
|
|
22
|
+
"Command",
|
|
23
|
+
"Bash",
|
|
24
|
+
"Shell",
|
|
25
|
+
"SessionContext",
|
|
26
|
+
"CommandResult",
|
|
27
|
+
"JSONFileObserver",
|
|
28
|
+
"CommandNotFoundError",
|
|
29
|
+
"ShellObserver",
|
|
30
|
+
"ConsoleLogObserver",
|
|
31
|
+
"FileLogObserver",
|
|
32
|
+
"SilentObserver",
|
|
33
|
+
"MultiObserver",
|
|
34
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from ..core import CommandResult, ShellObserver
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ConsoleLogObserver(ShellObserver):
|
|
5
|
+
"""Muestra en consola lo que está pasando en tiempo real."""
|
|
6
|
+
|
|
7
|
+
def on_command_start(self, executable, final_args):
|
|
8
|
+
print(f"🛠️ Ejecutando: {' '.join(final_args)}")
|
|
9
|
+
|
|
10
|
+
def on_command_result(self, result: CommandResult):
|
|
11
|
+
if result.is_success():
|
|
12
|
+
print(f"✅ OK ({result.execution_time:.3f}s)")
|
|
13
|
+
else:
|
|
14
|
+
print(f"❌ FALLO (Código: {result.return_code})")
|
|
15
|
+
if result.standard_error:
|
|
16
|
+
print(f" Error: {result.standard_error.strip()}")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from ..core import ShellObserver, CommandResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FileLogObserver(ShellObserver):
|
|
7
|
+
"""Guarda toda la actividad de la Shell en un archivo físico."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, log_path: str = "shell_activity.log"):
|
|
10
|
+
self.log_path = Path(log_path)
|
|
11
|
+
|
|
12
|
+
self.logger = logging.getLogger("ShellFileLogger")
|
|
13
|
+
self.logger.setLevel(logging.INFO)
|
|
14
|
+
self.logger.propagate = False # Evita que los logs salgan por consola en los tests
|
|
15
|
+
|
|
16
|
+
if self.logger.handlers:
|
|
17
|
+
for h in self.logger.handlers[:]:
|
|
18
|
+
h.close()
|
|
19
|
+
self.logger.removeHandler(h)
|
|
20
|
+
|
|
21
|
+
# Ahora creamos el nuevo handler con la ruta correcta
|
|
22
|
+
handler = logging.FileHandler(self.log_path, encoding="utf-8")
|
|
23
|
+
formatter = logging.Formatter("%(asctime)s | %(levelname)s | %(message)s")
|
|
24
|
+
handler.setFormatter(formatter)
|
|
25
|
+
self.logger.addHandler(handler)
|
|
26
|
+
|
|
27
|
+
def on_session_start(self, shell_name: str):
|
|
28
|
+
self.logger.info(f"=== INICIO DE SESIÓN: {shell_name} ===")
|
|
29
|
+
|
|
30
|
+
def on_session_end(self, shell_name: str, error: Exception = None):
|
|
31
|
+
if error:
|
|
32
|
+
self.logger.error(f"=== FIN DE SESIÓN CON ERROR: {error} ===")
|
|
33
|
+
else:
|
|
34
|
+
self.logger.info("=== FIN DE SESIÓN EXITOSO ===")
|
|
35
|
+
|
|
36
|
+
def on_command_start(self, executable: str, final_args: list[str]):
|
|
37
|
+
cmd_str = " ".join(final_args)
|
|
38
|
+
self.logger.info(f"EJECUTANDO: {cmd_str}")
|
|
39
|
+
|
|
40
|
+
def on_command_result(self, result: CommandResult):
|
|
41
|
+
status = "SUCCESS" if result.is_success() else f"FAILED({result.return_code})"
|
|
42
|
+
self.logger.info(
|
|
43
|
+
f"RESULTADO: {status} | "
|
|
44
|
+
f"DURACIÓN: {result.execution_time:.4f}s | "
|
|
45
|
+
f"STDOUT: {result.standard_output.strip()[:100]}..."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def on_error(self, message: str, error: Exception = None):
|
|
49
|
+
self.logger.error(f"ERROR DEL SISTEMA: {message} | Detalle: {error}")
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, List, Optional, Dict
|
|
5
|
+
from ..core import ShellObserver, CommandResult
|
|
6
|
+
|
|
7
|
+
class JSONFileObserver(ShellObserver):
|
|
8
|
+
"""
|
|
9
|
+
Registra toda la actividad de la Shell en un archivo JSON.
|
|
10
|
+
Ideal para auditoría técnica y análisis de datos posterior.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, log_path: str = "shell_audit.json"):
|
|
14
|
+
self.log_path = Path(log_path)
|
|
15
|
+
# Inicializamos el archivo si no existe
|
|
16
|
+
if not self.log_path.exists():
|
|
17
|
+
self._write_logs([])
|
|
18
|
+
|
|
19
|
+
def _read_logs(self) -> List[Dict[str, Any]]:
|
|
20
|
+
try:
|
|
21
|
+
with open(self.log_path, "r", encoding="utf-8") as f:
|
|
22
|
+
return json.load(f)
|
|
23
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
24
|
+
return []
|
|
25
|
+
|
|
26
|
+
def _write_logs(self, logs: List[Dict[str, Any]]):
|
|
27
|
+
with open(self.log_path, "w", encoding="utf-8") as f:
|
|
28
|
+
json.dump(logs, f, indent=4, ensure_ascii=False)
|
|
29
|
+
|
|
30
|
+
def _append_entry(self, entry: Dict[str, Any]):
|
|
31
|
+
logs = self._read_logs()
|
|
32
|
+
logs.append(entry)
|
|
33
|
+
self._write_logs(logs)
|
|
34
|
+
|
|
35
|
+
def on_command_result(self, result: CommandResult):
|
|
36
|
+
entry = {
|
|
37
|
+
"timestamp": time.time(),
|
|
38
|
+
"event": "command_executed",
|
|
39
|
+
"command": result.command_sent,
|
|
40
|
+
"success": result.is_success(),
|
|
41
|
+
"exit_code": result.return_code,
|
|
42
|
+
"duration": round(result.execution_time, 4),
|
|
43
|
+
"stdout_len": len(result.standard_output),
|
|
44
|
+
"stderr": result.standard_error.strip() if result.standard_error else None
|
|
45
|
+
}
|
|
46
|
+
self._append_entry(entry)
|
|
47
|
+
|
|
48
|
+
def on_context_change(self, key: str, value: Any):
|
|
49
|
+
entry = {
|
|
50
|
+
"timestamp": time.time(),
|
|
51
|
+
"event": "context_mutation",
|
|
52
|
+
"change": {key: str(value)}
|
|
53
|
+
}
|
|
54
|
+
self._append_entry(entry)
|
|
55
|
+
|
|
56
|
+
def on_error(self, message: str, error: Optional[Exception] = None):
|
|
57
|
+
entry = {
|
|
58
|
+
"timestamp": time.time(),
|
|
59
|
+
"event": "internal_error",
|
|
60
|
+
"message": message,
|
|
61
|
+
"exception": str(error) if error else None
|
|
62
|
+
}
|
|
63
|
+
self._append_entry(entry)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import List, Any
|
|
2
|
+
from ..core import ShellObserver, CommandResult
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MultiObserver(ShellObserver):
|
|
6
|
+
"""
|
|
7
|
+
Patrón Composite: Permite registrar múltiples observadores y
|
|
8
|
+
notificar a todos ellos de cada evento de la Shell.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, observers: List[ShellObserver] = None):
|
|
12
|
+
self.observers = observers or []
|
|
13
|
+
|
|
14
|
+
def add_observer(self, observer: ShellObserver):
|
|
15
|
+
self.observers.append(observer)
|
|
16
|
+
|
|
17
|
+
def on_session_start(self, shell_name: str):
|
|
18
|
+
for o in self.observers:
|
|
19
|
+
o.on_session_start(shell_name)
|
|
20
|
+
|
|
21
|
+
def on_session_end(self, shell_name: str, error: Exception = None):
|
|
22
|
+
for o in self.observers:
|
|
23
|
+
o.on_session_end(shell_name, error)
|
|
24
|
+
|
|
25
|
+
def on_context_change(self, key: str, value: Any):
|
|
26
|
+
for o in self.observers:
|
|
27
|
+
o.on_context_change(key, value)
|
|
28
|
+
|
|
29
|
+
def on_command_start(self, executable: str, final_args: List[str]):
|
|
30
|
+
for o in self.observers:
|
|
31
|
+
o.on_command_start(executable, final_args)
|
|
32
|
+
|
|
33
|
+
def on_command_result(self, result: CommandResult):
|
|
34
|
+
for o in self.observers:
|
|
35
|
+
o.on_command_result(result)
|
|
36
|
+
|
|
37
|
+
def on_error(self, message: str, error: Exception = None):
|
|
38
|
+
for o in self.observers:
|
|
39
|
+
o.on_error(message, error)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .Bash import Bash
|
|
2
|
+
from .ConsoleLogObserver import ConsoleLogObserver
|
|
3
|
+
from .FileLogObserver import FileLogObserver
|
|
4
|
+
from .MultiObserver import MultiObserver
|
|
5
|
+
from .JSONFileObserver import JSONFileObserver
|
|
6
|
+
from .SilentObserver import SilentObserver
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Bash",
|
|
11
|
+
"ConsoleLogObserver",
|
|
12
|
+
"FileLogObserver",
|
|
13
|
+
"JSONFileObserver",
|
|
14
|
+
"MultiObserver",
|
|
15
|
+
"SilentObserver",
|
|
16
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import Any, List
|
|
2
|
+
|
|
3
|
+
class ArgumentBuilder:
|
|
4
|
+
"""Garantiza la consistencia de los argumentos para el Sistema Operativo."""
|
|
5
|
+
|
|
6
|
+
def __init__(self, style: str = "unix"):
|
|
7
|
+
self._args: List[str] = []
|
|
8
|
+
self.style = style
|
|
9
|
+
|
|
10
|
+
def add_arg(self, value: Any) -> "ArgumentBuilder":
|
|
11
|
+
"""Aplana listas/tuplas y convierte todo a string, ignorando vacíos."""
|
|
12
|
+
if isinstance(value, (list, tuple)):
|
|
13
|
+
for item in value:
|
|
14
|
+
self.add_arg(item)
|
|
15
|
+
elif value is not None:
|
|
16
|
+
val_str = str(value).strip()
|
|
17
|
+
if val_str:
|
|
18
|
+
self._args.append(val_str)
|
|
19
|
+
return self
|
|
20
|
+
|
|
21
|
+
def add_flag(self, name: str, value: Any = None) -> "ArgumentBuilder":
|
|
22
|
+
prefix = "--" if self.style == "unix" else "/"
|
|
23
|
+
clean_name = name.strip().replace(" ", "_")
|
|
24
|
+
self.add_arg(f"{prefix}{clean_name}")
|
|
25
|
+
if value is not None:
|
|
26
|
+
self.add_arg(value)
|
|
27
|
+
return self
|
|
28
|
+
|
|
29
|
+
def build(self) -> List[str]:
|
|
30
|
+
return self._args
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from .ArgumentBuilder import ArgumentBuilder
|
|
2
|
+
|
|
3
|
+
class Command:
|
|
4
|
+
"""Representa un comando ejecutable con sus argumentos."""
|
|
5
|
+
|
|
6
|
+
def __init__(self, executable: str):
|
|
7
|
+
self.executable = executable
|
|
8
|
+
self.builder = ArgumentBuilder()
|
|
9
|
+
|
|
10
|
+
def add_args(self, *args) -> "Command":
|
|
11
|
+
"""
|
|
12
|
+
Añade argumentos posicionales.
|
|
13
|
+
Soporta elementos sueltos o listas gracias al nuevo builder.
|
|
14
|
+
"""
|
|
15
|
+
for arg in args:
|
|
16
|
+
self.builder.add_arg(arg)
|
|
17
|
+
return self
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def args(self) -> list[str]:
|
|
21
|
+
"""Obtiene la lista de argumentos construida y validada."""
|
|
22
|
+
return self.builder.build()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
import json
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class CommandResult:
|
|
8
|
+
"""Encapsulates the output of an executed command."""
|
|
9
|
+
|
|
10
|
+
standard_output: str = ""
|
|
11
|
+
standard_error: str = ""
|
|
12
|
+
return_code: int = 0
|
|
13
|
+
execution_time: float = 0.0
|
|
14
|
+
command_sent: list[str] = field(default_factory=list)
|
|
15
|
+
|
|
16
|
+
def is_success(self) -> bool:
|
|
17
|
+
"""Returns True if the command executed successfully."""
|
|
18
|
+
return self.return_code == 0
|
|
19
|
+
|
|
20
|
+
def json(self) -> Any:
|
|
21
|
+
"""Parses the standard output as JSON."""
|
|
22
|
+
return json.loads(self.standard_output)
|
|
23
|
+
|
|
24
|
+
def __or__(self, next_command: Any) -> str:
|
|
25
|
+
"""Enables pipe syntax."""
|
|
26
|
+
return self.standard_output
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class SessionContext:
|
|
8
|
+
"""Maintains persistent state across Shell executions."""
|
|
9
|
+
|
|
10
|
+
cwd: Path = field(default_factory=Path.cwd)
|
|
11
|
+
env: Dict[str, str] = field(default_factory=dict)
|
|
12
|
+
encoding: str = "utf-8"
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
import shutil
|
|
3
|
+
import time
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from dataclasses import replace
|
|
7
|
+
from typing import Optional, Dict, Callable
|
|
8
|
+
|
|
9
|
+
# Importaciones del Core
|
|
10
|
+
from .SessionContext import SessionContext
|
|
11
|
+
from .CommandResult import CommandResult
|
|
12
|
+
from .ShellObserver import ShellObserver
|
|
13
|
+
from .ArgumentBuilder import ArgumentBuilder
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CommandNotFoundError(Exception):
|
|
17
|
+
"""Lanzada cuando el binario no existe en el PATH."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Shell(ABC):
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
context: Optional[SessionContext] = None,
|
|
25
|
+
observer: Optional[ShellObserver] = None,
|
|
26
|
+
default_timeout: float = 30.0,
|
|
27
|
+
):
|
|
28
|
+
self.context = context or SessionContext()
|
|
29
|
+
self.observer = observer or ShellObserver()
|
|
30
|
+
self.default_timeout = default_timeout
|
|
31
|
+
|
|
32
|
+
# Mapa de comandos virtuales (Manejo de Estado)
|
|
33
|
+
self._virtual_builtins: Dict[str, Callable] = {
|
|
34
|
+
"cd": self._handle_cd,
|
|
35
|
+
"export": self._handle_export,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def _format_command(self, executable: str, args: list[str]) -> list[str]:
|
|
40
|
+
"""Cada shell hija decide la sintaxis técnica (Bash, PowerShell, etc)."""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
def run(self, command, timeout: Optional[float] = None) -> CommandResult:
|
|
44
|
+
"""Dispatcher principal: decide si el comando es de Estado o de Ejecución."""
|
|
45
|
+
executable = command.executable
|
|
46
|
+
|
|
47
|
+
# 1. ¿Es un comando de Estado (Virtual)?
|
|
48
|
+
if executable in self._virtual_builtins:
|
|
49
|
+
return self._virtual_builtins[executable](command)
|
|
50
|
+
|
|
51
|
+
# 2. Ejecución externa (De Efecto)
|
|
52
|
+
return self._run_external(command, timeout)
|
|
53
|
+
|
|
54
|
+
def __enter__(self):
|
|
55
|
+
self.observer.on_session_start(self.__class__.__name__)
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
59
|
+
self.observer.on_session_end(self.__class__.__name__, exc_val)
|
|
60
|
+
|
|
61
|
+
# --- Manejadores de Estado (Comandos Virtuales) ---
|
|
62
|
+
|
|
63
|
+
def _handle_cd(self, command) -> CommandResult:
|
|
64
|
+
try:
|
|
65
|
+
new_path = self._resolve_path(command.args)
|
|
66
|
+
|
|
67
|
+
if not new_path.exists() or not new_path.is_dir():
|
|
68
|
+
return self._notify_error(f"Directorio no encontrado: {new_path}")
|
|
69
|
+
|
|
70
|
+
self.context = replace(self.context, cwd=new_path)
|
|
71
|
+
self.observer.on_context_change("cwd", new_path)
|
|
72
|
+
|
|
73
|
+
return CommandResult(standard_output=f"Cambiado a: {new_path}", return_code=0)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
return self._notify_error("Error crítico en cd", e)
|
|
76
|
+
|
|
77
|
+
def _handle_export(self, command) -> CommandResult:
|
|
78
|
+
if not command.args:
|
|
79
|
+
return CommandResult(standard_output=str(self.context.env), return_code=0)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
updates = self._parse_env_vars(command.args)
|
|
83
|
+
new_env = {**self.context.env, **updates}
|
|
84
|
+
|
|
85
|
+
for key, value in updates.items():
|
|
86
|
+
self.observer.on_context_change(f"env.{key}", value)
|
|
87
|
+
|
|
88
|
+
self.context = replace(self.context, env=new_env)
|
|
89
|
+
return CommandResult(
|
|
90
|
+
standard_output=f"Variables actualizadas: {len(updates)}",
|
|
91
|
+
return_code=0
|
|
92
|
+
)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
return self._notify_error("Fallo al exportar variables", e)
|
|
95
|
+
|
|
96
|
+
# --- Lógica de Ejecución Externa ---
|
|
97
|
+
|
|
98
|
+
def _run_external(self, command, timeout: Optional[float]) -> CommandResult:
|
|
99
|
+
"""Prepara y ejecuta un proceso en el Sistema Operativo."""
|
|
100
|
+
try:
|
|
101
|
+
# 1. Preparación y validación del ejecutable
|
|
102
|
+
full_path = self._validate_executable(command.executable)
|
|
103
|
+
|
|
104
|
+
# 2. HELPER: Garantizamos consistencia de argumentos finales
|
|
105
|
+
# Usamos el ArgumentBuilder para aplanar cualquier lista residual
|
|
106
|
+
builder = ArgumentBuilder()
|
|
107
|
+
builder.add_arg(full_path)
|
|
108
|
+
builder.add_arg(self._format_command(command.executable, command.args))
|
|
109
|
+
final_args = builder.build()
|
|
110
|
+
|
|
111
|
+
# 3. Notificación previa a la ejecución
|
|
112
|
+
self.observer.on_command_start(command.executable, final_args)
|
|
113
|
+
|
|
114
|
+
start_time = time.perf_counter()
|
|
115
|
+
|
|
116
|
+
process = subprocess.run(
|
|
117
|
+
final_args,
|
|
118
|
+
cwd=self.context.cwd,
|
|
119
|
+
env=self.context.env,
|
|
120
|
+
capture_output=True,
|
|
121
|
+
text=True,
|
|
122
|
+
encoding=self.context.encoding,
|
|
123
|
+
timeout=timeout or self.default_timeout,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
result = CommandResult(
|
|
127
|
+
standard_output=process.stdout,
|
|
128
|
+
standard_error=process.stderr,
|
|
129
|
+
return_code=process.returncode,
|
|
130
|
+
execution_time=time.perf_counter() - start_time,
|
|
131
|
+
command_sent=final_args,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
self.observer.on_command_result(result)
|
|
135
|
+
return result
|
|
136
|
+
|
|
137
|
+
except CommandNotFoundError as e:
|
|
138
|
+
return self._notify_error(str(e), return_code=127)
|
|
139
|
+
except subprocess.TimeoutExpired:
|
|
140
|
+
return self._notify_error(f"Tiempo de espera agotado tras {timeout}s")
|
|
141
|
+
except Exception as e:
|
|
142
|
+
# Error no controlado se notifica y se relanza si es necesario
|
|
143
|
+
self.observer.on_error("Error de ejecución inesperado", e)
|
|
144
|
+
raise e
|
|
145
|
+
|
|
146
|
+
# --- Helpers de Soporte ---
|
|
147
|
+
|
|
148
|
+
def _validate_executable(self, executable: str) -> str:
|
|
149
|
+
"""Verifica si el binario existe en el sistema."""
|
|
150
|
+
full_path = shutil.which(executable)
|
|
151
|
+
if not full_path:
|
|
152
|
+
raise CommandNotFoundError(f"Comando no encontrado: {executable}")
|
|
153
|
+
return full_path
|
|
154
|
+
|
|
155
|
+
def _resolve_path(self, args: list[str]) -> Path:
|
|
156
|
+
path_str = args[0] if args else str(Path.home())
|
|
157
|
+
target = Path(path_str).expanduser()
|
|
158
|
+
|
|
159
|
+
if not target.is_absolute():
|
|
160
|
+
return (self.context.cwd / target).resolve()
|
|
161
|
+
return target.resolve()
|
|
162
|
+
|
|
163
|
+
def _parse_env_vars(self, args: list[str]) -> Dict[str, str]:
|
|
164
|
+
updates = {}
|
|
165
|
+
for arg in args:
|
|
166
|
+
if "=" in arg:
|
|
167
|
+
key, value = arg.split("=", 1)
|
|
168
|
+
updates[key] = value
|
|
169
|
+
return updates
|
|
170
|
+
|
|
171
|
+
def _notify_error(self, message: str, error: Exception = None, return_code: int = 1) -> CommandResult:
|
|
172
|
+
"""Estandariza la notificación de errores y la respuesta."""
|
|
173
|
+
self.observer.on_error(message, error)
|
|
174
|
+
return CommandResult(standard_error=message, return_code=return_code)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from .CommandResult import CommandResult
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ShellObserver(ABC): # Puede heredar de ABC si quieres marcarla como base
|
|
8
|
+
"""
|
|
9
|
+
Interfaz base para la observación de eventos del ciclo de vida de la Shell.
|
|
10
|
+
|
|
11
|
+
Esta clase actúa como una 'caja de herramientas' de hooks. Los métodos
|
|
12
|
+
tienen una implementación vacía por defecto para permitir que las
|
|
13
|
+
clases hijas solo sobrescriban los eventos que les interesa capturar.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def on_session_start(self, shell_name: str):
|
|
17
|
+
"""
|
|
18
|
+
Se dispara al entrar en el contexto de la Shell (bloque 'with').
|
|
19
|
+
Útil para inicializar recursos de logging o telemetría.
|
|
20
|
+
"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
def on_session_end(self, shell_name: str, error: Exception = None):
|
|
24
|
+
"""
|
|
25
|
+
Se dispara al salir del contexto de la Shell.
|
|
26
|
+
Permite registrar si la sesión terminó de forma limpia o por una excepción.
|
|
27
|
+
"""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
def on_context_change(self, key: str, value: Any):
|
|
31
|
+
"""
|
|
32
|
+
Se dispara cuando un comando de estado (Virtual Builtin) muta el contexto.
|
|
33
|
+
Ejemplo: cambios en el CWD (Directorio de trabajo) o variables de entorno.
|
|
34
|
+
"""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
def on_command_start(self, executable: str, final_args: list[str]):
|
|
38
|
+
"""
|
|
39
|
+
Se dispara justo antes de enviar un comando de efecto al sistema operativo.
|
|
40
|
+
Proporciona los argumentos finales tal cual serán ejecutados por el adaptador.
|
|
41
|
+
"""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
def on_command_result(self, result: "CommandResult"):
|
|
45
|
+
"""
|
|
46
|
+
Se dispara tras recibir la respuesta de un comando de efecto.
|
|
47
|
+
Permite reaccionar al código de retorno, stdout y stderr.
|
|
48
|
+
"""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
def on_error(self, message: str, error: Exception = None):
|
|
52
|
+
"""
|
|
53
|
+
Se dispara cuando ocurre un error controlado o inesperado dentro del Core.
|
|
54
|
+
Funciona como un sumidero de errores para evitar que la lógica de negocio se ensucie.
|
|
55
|
+
"""
|
|
56
|
+
pass
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .Shell import Shell, CommandNotFoundError
|
|
2
|
+
from .Command import Command
|
|
3
|
+
from .CommandResult import CommandResult
|
|
4
|
+
from .ArgumentBuilder import ArgumentBuilder
|
|
5
|
+
from .SessionContext import SessionContext
|
|
6
|
+
from .ShellObserver import ShellObserver
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Shell",
|
|
10
|
+
"Command",
|
|
11
|
+
"CommandResult",
|
|
12
|
+
"ArgumentBuilder",
|
|
13
|
+
"SessionContext",
|
|
14
|
+
"ShellObserver",
|
|
15
|
+
"CommandNotFoundError",
|
|
16
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cc.shellback-kit
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Python: >=3.14
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: capsulecore-logger==0.3.0
|
|
8
|
+
Requires-Dist: gitchangelog>=3.0.4
|
|
9
|
+
Requires-Dist: pytest>=9.0.2
|
|
10
|
+
Requires-Dist: pytest-timeout>=2.4.0
|
|
11
|
+
Requires-Dist: ruff>=0.15.6
|
|
12
|
+
|
|
13
|
+
# CapsuleCore shellback
|
|
14
|
+
|
|
15
|
+
Shellback is a robust, architecturally-agnostic Python library designed to bridge terminal environments (Bash, CMD, PowerShell) with Python scripts. It provides a clean, decoupled abstraction layer to execute system commands while maintaining persistent session state and cross-platform compatibility.
|
|
16
|
+
|
|
17
|
+
Built with Hexagonal Architecture (Ports and Adapters) principles, Shellback ensures that your domain logic remains independent of the specific shell or operating system being used.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from cc_shellback_kit.capsule import Bash
|
|
23
|
+
from cc_shellback_kit.core import Command, SessionContext
|
|
24
|
+
from cc_shellback_kit.observers import ConsoleLogObserver
|
|
25
|
+
|
|
26
|
+
# 1. Configuramos el observador para ver la actividad en consola
|
|
27
|
+
observer = ConsoleLogObserver()
|
|
28
|
+
|
|
29
|
+
# 2. Iniciamos la Shell usando el manejador de contexto (with)
|
|
30
|
+
with Bash(observer=observer) as shell:
|
|
31
|
+
|
|
32
|
+
# --- EJECUCIÓN DE COMANDOS EXTERNOS ---
|
|
33
|
+
# Creamos un comando simple: 'ls -la'
|
|
34
|
+
cmd_list = Command("ls").add_args("-la")
|
|
35
|
+
result = shell.run(cmd_list)
|
|
36
|
+
|
|
37
|
+
if result.is_success():
|
|
38
|
+
print(f"Archivos encontrados:\n{result.standard_output}")
|
|
39
|
+
|
|
40
|
+
# --- MANEJO DE ESTADO (VIRTUAL BUILT-INS) ---
|
|
41
|
+
# Cambiamos de directorio (esto afecta al SessionContext, no solo al proceso)
|
|
42
|
+
shell.run(Command("cd").add_args("/tmp"))
|
|
43
|
+
|
|
44
|
+
# Verificamos el cambio ejecutando un 'pwd'
|
|
45
|
+
shell.run(Command("pwd"))
|
|
46
|
+
|
|
47
|
+
# --- VARIABLES DE ENTORNO ---
|
|
48
|
+
# Exportamos una variable que persistirá durante este bloque 'with'
|
|
49
|
+
shell.run(Command("export").add_args("APP_STAGE=development", "DEBUG=true"))
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
cc_shellback_kit/__init__.py,sha256=gWHRTg3cNy5jlFVt0F4uEImAe9VZOF40qNX-b1naK6A,579
|
|
2
|
+
cc_shellback_kit/capsule/Bash.py,sha256=rjYbDyXdv2TtfYf5fEPiNpfxdtKRghVpPm5iqB5Za74,275
|
|
3
|
+
cc_shellback_kit/capsule/ConsoleLogObserver.py,sha256=-FId8dUn5W4orw0MkcY5Gl0tO7ADUrxWXX6PelPpeJA,609
|
|
4
|
+
cc_shellback_kit/capsule/FileLogObserver.py,sha256=Sf5vWb240Bie1lluXWh3XzKvUNAXkAJhWzIbD1n2FMY,2005
|
|
5
|
+
cc_shellback_kit/capsule/JSONFileObserver.py,sha256=nwCpxkQ9bpUzovmR1aqrspfcIfxbQR5FOR_6i0Is3v4,2183
|
|
6
|
+
cc_shellback_kit/capsule/MultiObserver.py,sha256=bjUHF14ZHbLJs2G7U6bB3T8SBk9xo6UdQXkdUs5eI3E,1293
|
|
7
|
+
cc_shellback_kit/capsule/SilentObserver.py,sha256=YOeejIlMP5jpZCeoqTl6IrTKwKdBPZ9hqd8ceMNeXm0,164
|
|
8
|
+
cc_shellback_kit/capsule/__init__.py,sha256=0iGazGbvKDK83yzyG_mJ4u2VP4CkOfA8wizhoRzMtwY,394
|
|
9
|
+
cc_shellback_kit/core/ArgumentBuilder.py,sha256=votIpoIrSKXq4zDUvLs_P46kGROo0iaTdaShtpuK4co,1022
|
|
10
|
+
cc_shellback_kit/core/Command.py,sha256=AF4T6KBqRbkTmaiXBkce_kmDaJYApY9rasSsFsiZpkc,657
|
|
11
|
+
cc_shellback_kit/core/CommandResult.py,sha256=L4Y2vPwnB34-mrn3e_NBBQgaLfw74MKQQe5MKYXDcQM,726
|
|
12
|
+
cc_shellback_kit/core/SessionContext.py,sha256=X4nmlFaWmpjDDmtCQ8VcDSMZjO-F9m9LPQ84anDzXQs,331
|
|
13
|
+
cc_shellback_kit/core/Shell.py,sha256=WSR8nKg6zl_L9OhMFXtGTxkL5RnJCUmvoaVf6ZOjZpk,6508
|
|
14
|
+
cc_shellback_kit/core/ShellObserver.py,sha256=pbLRJIT_skRW1vekKyt-trUp1zr5-U7GH5o8_S3pNaI,2018
|
|
15
|
+
cc_shellback_kit/core/__init__.py,sha256=AsULp7Qv4Q6Gq9aZFoHx77c9j2yka83ard-dCK34hc8,404
|
|
16
|
+
cc_shellback_kit-0.3.0.dist-info/METADATA,sha256=70Hx90o-UsjKcv_1IIS2Wwj83wIeZ31gMM0C6GWwh8Y,1949
|
|
17
|
+
cc_shellback_kit-0.3.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
18
|
+
cc_shellback_kit-0.3.0.dist-info/top_level.txt,sha256=tPz20IiH1w8K89AFkA7gntYscT9eAVocowIFpuFHCeI,17
|
|
19
|
+
cc_shellback_kit-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cc_shellback_kit
|