CapsuleCore-shellback 0.1.1__tar.gz

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.
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: CapsuleCore-shellback
3
+ Version: 0.1.1
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.14
6
+ Description-Content-Type: text/markdown
File without changes
@@ -0,0 +1,7 @@
1
+ [project]
2
+ name = "CapsuleCore-shellback"
3
+ version = "0.1.1"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ dependencies = []
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: CapsuleCore-shellback
3
+ Version: 0.1.1
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.14
6
+ Description-Content-Type: text/markdown
@@ -0,0 +1,18 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/CapsuleCore_shellback.egg-info/PKG-INFO
4
+ src/CapsuleCore_shellback.egg-info/SOURCES.txt
5
+ src/CapsuleCore_shellback.egg-info/dependency_links.txt
6
+ src/CapsuleCore_shellback.egg-info/top_level.txt
7
+ src/shellback/__init__.py
8
+ src/shellback/capsule/ArgumentBuilder.py
9
+ src/shellback/capsule/Bash.py
10
+ src/shellback/capsule/Command.py
11
+ src/shellback/capsule/ConsoleLogger.py
12
+ src/shellback/capsule/NullLogger.py
13
+ src/shellback/capsule/__init__.py
14
+ src/shellback/core/CommandResult.py
15
+ src/shellback/core/Logger.py
16
+ src/shellback/core/SessionContext.py
17
+ src/shellback/core/Shell.py
18
+ src/shellback/core/__init__.py
@@ -0,0 +1,4 @@
1
+ from .capsule import ArgumentBuilder, Command, NullLogger, Bash, ConsoleLogger
2
+ from .core import Shell, SessionContext, Logger
3
+
4
+ __all__ = ["ArgumentBuilder", "Command", "NullLogger", "Bash", "ConsoleLogger", "Shell", "SessionContext", "Logger"]
@@ -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,7 @@
1
+ from .ArgumentBuilder import ArgumentBuilder
2
+ from .Command import Command
3
+ from .NullLogger import NullLogger
4
+ from .Bash import Bash
5
+ from .ConsoleLogger import ConsoleLogger
6
+
7
+ __all__ = ["ArgumentBuilder", "Command", "NullLogger", "Bash", "ConsoleLogger"]
@@ -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
@@ -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"
@@ -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
@@ -0,0 +1,6 @@
1
+ from .CommandResult import CommandResult
2
+ from .Logger import Logger
3
+ from .SessionContext import SessionContext
4
+ from .Shell import Shell
5
+
6
+ __all__ = ["CommandResult", "Logger", "SessionContext", "Shell"]