cc.shellback-kit 0.3.0__tar.gz → 0.4.0__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.
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/PKG-INFO +27 -5
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/README.md +26 -4
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/pyproject.toml +1 -1
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/src/cc.shellback_kit.egg-info/PKG-INFO +27 -5
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/src/cc_shellback_kit/capsule/FileLogObserver.py +18 -16
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/src/cc_shellback_kit/capsule/JSONFileObserver.py +6 -5
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/src/cc_shellback_kit/core/ArgumentBuilder.py +2 -1
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/src/cc_shellback_kit/core/Command.py +3 -2
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/src/cc_shellback_kit/core/Shell.py +13 -9
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/tests/test_ArgumentBuilder.py +10 -10
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/tests/test_Bash.py +12 -5
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/tests/test_Command.py +6 -6
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/tests/test_CommandResult.py +14 -7
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/tests/test_ConsoleLogObserver.py +13 -20
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/tests/test_FileLogObserver.py +25 -19
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/tests/test_JSONFileObserver.py +11 -5
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/tests/test_MultiObserver.py +11 -3
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/tests/test_SessionContext.py +13 -13
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/tests/test_ShellObserver.py +6 -4
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/tests/test_Shell_handle_cd.py +16 -12
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/tests/test_Shell_handle_export.py +20 -13
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/tests/test_Shell_notify_error.py +10 -7
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/tests/test_Shell_parse_env_vars.py +14 -14
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/tests/test_Shell_resolve_path.py +16 -11
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/tests/test_Shell_run.py +30 -23
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/tests/test_Shell_run_external.py +31 -19
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/tests/test_Shell_validate_executable.py +12 -9
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/tests/test_SilentObserver.py +10 -8
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/setup.cfg +0 -0
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/src/cc.shellback_kit.egg-info/SOURCES.txt +0 -0
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/src/cc.shellback_kit.egg-info/dependency_links.txt +0 -0
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/src/cc.shellback_kit.egg-info/requires.txt +0 -0
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/src/cc.shellback_kit.egg-info/top_level.txt +0 -0
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/src/cc_shellback_kit/__init__.py +0 -0
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/src/cc_shellback_kit/capsule/Bash.py +0 -0
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/src/cc_shellback_kit/capsule/ConsoleLogObserver.py +0 -0
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/src/cc_shellback_kit/capsule/MultiObserver.py +0 -0
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/src/cc_shellback_kit/capsule/SilentObserver.py +0 -0
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/src/cc_shellback_kit/capsule/__init__.py +0 -0
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/src/cc_shellback_kit/core/CommandResult.py +0 -0
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/src/cc_shellback_kit/core/SessionContext.py +0 -0
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/src/cc_shellback_kit/core/ShellObserver.py +0 -0
- {cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/src/cc_shellback_kit/core/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cc.shellback-kit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Requires-Python: >=3.14
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -10,18 +10,40 @@ Requires-Dist: pytest>=9.0.2
|
|
|
10
10
|
Requires-Dist: pytest-timeout>=2.4.0
|
|
11
11
|
Requires-Dist: ruff>=0.15.6
|
|
12
12
|
|
|
13
|
-
#
|
|
13
|
+
# cc-shellback-kit
|
|
14
|
+
|
|
15
|
+
[](LICENSE)
|
|
16
|
+

|
|
17
|
+
[](https://github.com/Rick-torrellas/cc-shellback-kit/actions/workflows/main.yaml)
|
|
18
|
+
[](https://www.python.org/)
|
|
19
|
+
[](https://github.com/Rick-torrellas/cc-shellback-kit/releases)
|
|
20
|
+
[](https://deepwiki.com/Rick-torrellas/cc-shellback-kit)
|
|
21
|
+
[](https://rick-torrellas.github.io/cc-shellback-kit/)
|
|
14
22
|
|
|
15
23
|
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
24
|
|
|
17
25
|
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
26
|
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 📍 Contenido
|
|
30
|
+
* [Installation](#installation)
|
|
31
|
+
* [Usage](#usage)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
You can install cc-shellback-kit using pip:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install cc.shellback-kit
|
|
41
|
+
```
|
|
42
|
+
|
|
19
43
|
## Usage
|
|
20
44
|
|
|
21
45
|
```python
|
|
22
|
-
from cc_shellback_kit
|
|
23
|
-
from cc_shellback_kit.core import Command, SessionContext
|
|
24
|
-
from cc_shellback_kit.observers import ConsoleLogObserver
|
|
46
|
+
from cc_shellback_kit import Bash, ConsoleLogObserver, Command, SessionContext
|
|
25
47
|
|
|
26
48
|
# 1. Configuramos el observador para ver la actividad en consola
|
|
27
49
|
observer = ConsoleLogObserver()
|
|
@@ -1,15 +1,37 @@
|
|
|
1
|
-
#
|
|
1
|
+
# cc-shellback-kit
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+

|
|
5
|
+
[](https://github.com/Rick-torrellas/cc-shellback-kit/actions/workflows/main.yaml)
|
|
6
|
+
[](https://www.python.org/)
|
|
7
|
+
[](https://github.com/Rick-torrellas/cc-shellback-kit/releases)
|
|
8
|
+
[](https://deepwiki.com/Rick-torrellas/cc-shellback-kit)
|
|
9
|
+
[](https://rick-torrellas.github.io/cc-shellback-kit/)
|
|
2
10
|
|
|
3
11
|
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.
|
|
4
12
|
|
|
5
13
|
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.
|
|
6
14
|
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 📍 Contenido
|
|
18
|
+
* [Installation](#installation)
|
|
19
|
+
* [Usage](#usage)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
You can install cc-shellback-kit using pip:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install cc.shellback-kit
|
|
29
|
+
```
|
|
30
|
+
|
|
7
31
|
## Usage
|
|
8
32
|
|
|
9
33
|
```python
|
|
10
|
-
from cc_shellback_kit
|
|
11
|
-
from cc_shellback_kit.core import Command, SessionContext
|
|
12
|
-
from cc_shellback_kit.observers import ConsoleLogObserver
|
|
34
|
+
from cc_shellback_kit import Bash, ConsoleLogObserver, Command, SessionContext
|
|
13
35
|
|
|
14
36
|
# 1. Configuramos el observador para ver la actividad en consola
|
|
15
37
|
observer = ConsoleLogObserver()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cc.shellback-kit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Requires-Python: >=3.14
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -10,18 +10,40 @@ Requires-Dist: pytest>=9.0.2
|
|
|
10
10
|
Requires-Dist: pytest-timeout>=2.4.0
|
|
11
11
|
Requires-Dist: ruff>=0.15.6
|
|
12
12
|
|
|
13
|
-
#
|
|
13
|
+
# cc-shellback-kit
|
|
14
|
+
|
|
15
|
+
[](LICENSE)
|
|
16
|
+

|
|
17
|
+
[](https://github.com/Rick-torrellas/cc-shellback-kit/actions/workflows/main.yaml)
|
|
18
|
+
[](https://www.python.org/)
|
|
19
|
+
[](https://github.com/Rick-torrellas/cc-shellback-kit/releases)
|
|
20
|
+
[](https://deepwiki.com/Rick-torrellas/cc-shellback-kit)
|
|
21
|
+
[](https://rick-torrellas.github.io/cc-shellback-kit/)
|
|
14
22
|
|
|
15
23
|
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
24
|
|
|
17
25
|
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
26
|
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 📍 Contenido
|
|
30
|
+
* [Installation](#installation)
|
|
31
|
+
* [Usage](#usage)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
You can install cc-shellback-kit using pip:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install cc.shellback-kit
|
|
41
|
+
```
|
|
42
|
+
|
|
19
43
|
## Usage
|
|
20
44
|
|
|
21
45
|
```python
|
|
22
|
-
from cc_shellback_kit
|
|
23
|
-
from cc_shellback_kit.core import Command, SessionContext
|
|
24
|
-
from cc_shellback_kit.observers import ConsoleLogObserver
|
|
46
|
+
from cc_shellback_kit import Bash, ConsoleLogObserver, Command, SessionContext
|
|
25
47
|
|
|
26
48
|
# 1. Configuramos el observador para ver la actividad en consola
|
|
27
49
|
observer = ConsoleLogObserver()
|
{cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/src/cc_shellback_kit/capsule/FileLogObserver.py
RENAMED
|
@@ -7,22 +7,24 @@ class FileLogObserver(ShellObserver):
|
|
|
7
7
|
"""Guarda toda la actividad de la Shell en un archivo físico."""
|
|
8
8
|
|
|
9
9
|
def __init__(self, log_path: str = "shell_activity.log"):
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
10
|
+
self.log_path = Path(log_path)
|
|
11
|
+
|
|
12
|
+
self.logger = logging.getLogger("ShellFileLogger")
|
|
13
|
+
self.logger.setLevel(logging.INFO)
|
|
14
|
+
self.logger.propagate = (
|
|
15
|
+
False # Evita que los logs salgan por consola en los tests
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if self.logger.handlers:
|
|
19
|
+
for h in self.logger.handlers[:]:
|
|
20
|
+
h.close()
|
|
21
|
+
self.logger.removeHandler(h)
|
|
22
|
+
|
|
23
|
+
# Ahora creamos el nuevo handler con la ruta correcta
|
|
24
|
+
handler = logging.FileHandler(self.log_path, encoding="utf-8")
|
|
25
|
+
formatter = logging.Formatter("%(asctime)s | %(levelname)s | %(message)s")
|
|
26
|
+
handler.setFormatter(formatter)
|
|
27
|
+
self.logger.addHandler(handler)
|
|
26
28
|
|
|
27
29
|
def on_session_start(self, shell_name: str):
|
|
28
30
|
self.logger.info(f"=== INICIO DE SESIÓN: {shell_name} ===")
|
{cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/src/cc_shellback_kit/capsule/JSONFileObserver.py
RENAMED
|
@@ -4,6 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
from typing import Any, List, Optional, Dict
|
|
5
5
|
from ..core import ShellObserver, CommandResult
|
|
6
6
|
|
|
7
|
+
|
|
7
8
|
class JSONFileObserver(ShellObserver):
|
|
8
9
|
"""
|
|
9
10
|
Registra toda la actividad de la Shell en un archivo JSON.
|
|
@@ -20,7 +21,7 @@ class JSONFileObserver(ShellObserver):
|
|
|
20
21
|
try:
|
|
21
22
|
with open(self.log_path, "r", encoding="utf-8") as f:
|
|
22
23
|
return json.load(f)
|
|
23
|
-
except
|
|
24
|
+
except json.JSONDecodeError, FileNotFoundError:
|
|
24
25
|
return []
|
|
25
26
|
|
|
26
27
|
def _write_logs(self, logs: List[Dict[str, Any]]):
|
|
@@ -41,7 +42,7 @@ class JSONFileObserver(ShellObserver):
|
|
|
41
42
|
"exit_code": result.return_code,
|
|
42
43
|
"duration": round(result.execution_time, 4),
|
|
43
44
|
"stdout_len": len(result.standard_output),
|
|
44
|
-
"stderr": result.standard_error.strip() if result.standard_error else None
|
|
45
|
+
"stderr": result.standard_error.strip() if result.standard_error else None,
|
|
45
46
|
}
|
|
46
47
|
self._append_entry(entry)
|
|
47
48
|
|
|
@@ -49,7 +50,7 @@ class JSONFileObserver(ShellObserver):
|
|
|
49
50
|
entry = {
|
|
50
51
|
"timestamp": time.time(),
|
|
51
52
|
"event": "context_mutation",
|
|
52
|
-
"change": {key: str(value)}
|
|
53
|
+
"change": {key: str(value)},
|
|
53
54
|
}
|
|
54
55
|
self._append_entry(entry)
|
|
55
56
|
|
|
@@ -58,6 +59,6 @@ class JSONFileObserver(ShellObserver):
|
|
|
58
59
|
"timestamp": time.time(),
|
|
59
60
|
"event": "internal_error",
|
|
60
61
|
"message": message,
|
|
61
|
-
"exception": str(error) if error else None
|
|
62
|
+
"exception": str(error) if error else None,
|
|
62
63
|
}
|
|
63
|
-
self._append_entry(entry)
|
|
64
|
+
self._append_entry(entry)
|
{cc_shellback_kit-0.3.0 → cc_shellback_kit-0.4.0}/src/cc_shellback_kit/core/ArgumentBuilder.py
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from typing import Any, List
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
class ArgumentBuilder:
|
|
4
5
|
"""Garantiza la consistencia de los argumentos para el Sistema Operativo."""
|
|
5
6
|
|
|
@@ -27,4 +28,4 @@ class ArgumentBuilder:
|
|
|
27
28
|
return self
|
|
28
29
|
|
|
29
30
|
def build(self) -> List[str]:
|
|
30
|
-
return self._args
|
|
31
|
+
return self._args
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from .ArgumentBuilder import ArgumentBuilder
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
class Command:
|
|
4
5
|
"""Representa un comando ejecutable con sus argumentos."""
|
|
5
6
|
|
|
@@ -9,7 +10,7 @@ class Command:
|
|
|
9
10
|
|
|
10
11
|
def add_args(self, *args) -> "Command":
|
|
11
12
|
"""
|
|
12
|
-
Añade argumentos posicionales.
|
|
13
|
+
Añade argumentos posicionales.
|
|
13
14
|
Soporta elementos sueltos o listas gracias al nuevo builder.
|
|
14
15
|
"""
|
|
15
16
|
for arg in args:
|
|
@@ -19,4 +20,4 @@ class Command:
|
|
|
19
20
|
@property
|
|
20
21
|
def args(self) -> list[str]:
|
|
21
22
|
"""Obtiene la lista de argumentos construida y validada."""
|
|
22
|
-
return self.builder.build()
|
|
23
|
+
return self.builder.build()
|
|
@@ -15,6 +15,7 @@ from .ArgumentBuilder import ArgumentBuilder
|
|
|
15
15
|
|
|
16
16
|
class CommandNotFoundError(Exception):
|
|
17
17
|
"""Lanzada cuando el binario no existe en el PATH."""
|
|
18
|
+
|
|
18
19
|
pass
|
|
19
20
|
|
|
20
21
|
|
|
@@ -70,7 +71,9 @@ class Shell(ABC):
|
|
|
70
71
|
self.context = replace(self.context, cwd=new_path)
|
|
71
72
|
self.observer.on_context_change("cwd", new_path)
|
|
72
73
|
|
|
73
|
-
return CommandResult(
|
|
74
|
+
return CommandResult(
|
|
75
|
+
standard_output=f"Cambiado a: {new_path}", return_code=0
|
|
76
|
+
)
|
|
74
77
|
except Exception as e:
|
|
75
78
|
return self._notify_error("Error crítico en cd", e)
|
|
76
79
|
|
|
@@ -87,8 +90,7 @@ class Shell(ABC):
|
|
|
87
90
|
|
|
88
91
|
self.context = replace(self.context, env=new_env)
|
|
89
92
|
return CommandResult(
|
|
90
|
-
standard_output=f"Variables actualizadas: {len(updates)}",
|
|
91
|
-
return_code=0
|
|
93
|
+
standard_output=f"Variables actualizadas: {len(updates)}", return_code=0
|
|
92
94
|
)
|
|
93
95
|
except Exception as e:
|
|
94
96
|
return self._notify_error("Fallo al exportar variables", e)
|
|
@@ -100,7 +102,7 @@ class Shell(ABC):
|
|
|
100
102
|
try:
|
|
101
103
|
# 1. Preparación y validación del ejecutable
|
|
102
104
|
full_path = self._validate_executable(command.executable)
|
|
103
|
-
|
|
105
|
+
|
|
104
106
|
# 2. HELPER: Garantizamos consistencia de argumentos finales
|
|
105
107
|
# Usamos el ArgumentBuilder para aplanar cualquier lista residual
|
|
106
108
|
builder = ArgumentBuilder()
|
|
@@ -112,7 +114,7 @@ class Shell(ABC):
|
|
|
112
114
|
self.observer.on_command_start(command.executable, final_args)
|
|
113
115
|
|
|
114
116
|
start_time = time.perf_counter()
|
|
115
|
-
|
|
117
|
+
|
|
116
118
|
process = subprocess.run(
|
|
117
119
|
final_args,
|
|
118
120
|
cwd=self.context.cwd,
|
|
@@ -130,7 +132,7 @@ class Shell(ABC):
|
|
|
130
132
|
execution_time=time.perf_counter() - start_time,
|
|
131
133
|
command_sent=final_args,
|
|
132
134
|
)
|
|
133
|
-
|
|
135
|
+
|
|
134
136
|
self.observer.on_command_result(result)
|
|
135
137
|
return result
|
|
136
138
|
|
|
@@ -159,7 +161,7 @@ class Shell(ABC):
|
|
|
159
161
|
if not target.is_absolute():
|
|
160
162
|
return (self.context.cwd / target).resolve()
|
|
161
163
|
return target.resolve()
|
|
162
|
-
|
|
164
|
+
|
|
163
165
|
def _parse_env_vars(self, args: list[str]) -> Dict[str, str]:
|
|
164
166
|
updates = {}
|
|
165
167
|
for arg in args:
|
|
@@ -168,7 +170,9 @@ class Shell(ABC):
|
|
|
168
170
|
updates[key] = value
|
|
169
171
|
return updates
|
|
170
172
|
|
|
171
|
-
def _notify_error(
|
|
173
|
+
def _notify_error(
|
|
174
|
+
self, message: str, error: Exception = None, return_code: int = 1
|
|
175
|
+
) -> CommandResult:
|
|
172
176
|
"""Estandariza la notificación de errores y la respuesta."""
|
|
173
177
|
self.observer.on_error(message, error)
|
|
174
|
-
return CommandResult(standard_error=message, return_code=return_code)
|
|
178
|
+
return CommandResult(standard_error=message, return_code=return_code)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from cc_shellback_kit import ArgumentBuilder
|
|
2
2
|
|
|
3
|
-
class TestArgumentBuilder:
|
|
4
3
|
|
|
4
|
+
class TestArgumentBuilder:
|
|
5
5
|
def test_add_single_argument(self):
|
|
6
6
|
"""Verifica que se añadan argumentos simples correctamente."""
|
|
7
7
|
builder = ArgumentBuilder()
|
|
@@ -15,7 +15,7 @@ class TestArgumentBuilder:
|
|
|
15
15
|
builder.add_arg(["git", "commit"])
|
|
16
16
|
builder.add_arg("-m")
|
|
17
17
|
builder.add_arg(["Mensaje con espacios"])
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
assert builder.build() == ["git", "commit", "-m", "Mensaje con espacios"]
|
|
20
20
|
|
|
21
21
|
def test_ignore_none_and_empty_values(self):
|
|
@@ -23,16 +23,16 @@ class TestArgumentBuilder:
|
|
|
23
23
|
builder = ArgumentBuilder()
|
|
24
24
|
builder.add_arg(None)
|
|
25
25
|
builder.add_arg("")
|
|
26
|
-
builder.add_arg(" ")
|
|
26
|
+
builder.add_arg(" ") # Espacios en blanco
|
|
27
27
|
builder.add_arg("python")
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
assert builder.build() == ["python"]
|
|
30
30
|
|
|
31
31
|
def test_fluent_interface(self):
|
|
32
32
|
"""Verifica que los métodos sean encadenables (return self)."""
|
|
33
33
|
builder = ArgumentBuilder()
|
|
34
34
|
result = builder.add_arg("cmd").add_flag("v").add_arg("file.txt")
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
assert isinstance(result, ArgumentBuilder)
|
|
37
37
|
assert builder.build() == ["cmd", "--v", "file.txt"]
|
|
38
38
|
|
|
@@ -41,7 +41,7 @@ class TestArgumentBuilder:
|
|
|
41
41
|
builder = ArgumentBuilder(style="unix")
|
|
42
42
|
builder.add_flag("force")
|
|
43
43
|
builder.add_flag("output", "results.txt")
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
expected = ["--force", "--output", "results.txt"]
|
|
46
46
|
assert builder.build() == expected
|
|
47
47
|
|
|
@@ -50,7 +50,7 @@ class TestArgumentBuilder:
|
|
|
50
50
|
builder = ArgumentBuilder(style="windows")
|
|
51
51
|
builder.add_flag("all")
|
|
52
52
|
builder.add_flag("limit", 10)
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
expected = ["/all", "/limit", "10"]
|
|
55
55
|
assert builder.build() == expected
|
|
56
56
|
|
|
@@ -58,13 +58,13 @@ class TestArgumentBuilder:
|
|
|
58
58
|
"""Verifica que los espacios en los nombres de flags se conviertan en underscores."""
|
|
59
59
|
builder = ArgumentBuilder()
|
|
60
60
|
builder.add_flag("ignore case")
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
assert builder.build() == ["--ignore_case"]
|
|
63
63
|
|
|
64
64
|
def test_complex_nesting(self):
|
|
65
65
|
"""Verifica un caso complejo con anidación profunda de listas."""
|
|
66
66
|
builder = ArgumentBuilder()
|
|
67
67
|
builder.add_arg(["docker", ["run", ["-d", "--name"]], "my_container"])
|
|
68
|
-
|
|
68
|
+
|
|
69
69
|
expected = ["docker", "run", "-d", "--name", "my_container"]
|
|
70
|
-
assert builder.build() == expected
|
|
70
|
+
assert builder.build() == expected
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
from cc_shellback_kit import Command
|
|
3
3
|
|
|
4
|
+
|
|
4
5
|
def test_bash_format_command(bash_shell):
|
|
5
6
|
"""Verifica que Bash no altere los argumentos (la lógica de unión está en Shell)."""
|
|
6
7
|
# Bash._format_command según tu archivo solo devuelve los args
|
|
7
8
|
result = bash_shell._format_command("ls", ["-l", "-a"])
|
|
8
9
|
assert result == ["-l", "-a"]
|
|
9
10
|
|
|
11
|
+
|
|
10
12
|
def test_bash_run_external_success(bash_shell, mock_observer):
|
|
11
13
|
"""Prueba la ejecución de un comando real (ls o dir) y la notificación al observer."""
|
|
12
14
|
cmd = Command("echo").add_args("Hola Mundo")
|
|
@@ -18,6 +20,7 @@ def test_bash_run_external_success(bash_shell, mock_observer):
|
|
|
18
20
|
mock_observer.on_command_start.assert_called_once()
|
|
19
21
|
mock_observer.on_command_result.assert_called_once()
|
|
20
22
|
|
|
23
|
+
|
|
21
24
|
def test_bash_command_not_found(bash_shell, mock_observer):
|
|
22
25
|
"""Verifica el comportamiento cuando el binario no existe."""
|
|
23
26
|
cmd = Command("comando_que_no_existe_12345")
|
|
@@ -27,11 +30,12 @@ def test_bash_command_not_found(bash_shell, mock_observer):
|
|
|
27
30
|
assert "Comando no encontrado" in result.standard_error
|
|
28
31
|
mock_observer.on_error.assert_called_once()
|
|
29
32
|
|
|
33
|
+
|
|
30
34
|
def test_bash_virtual_cd(bash_shell, tmp_path, mock_observer):
|
|
31
35
|
"""Verifica que el comando virtual 'cd' cambie el contexto de la sesión."""
|
|
32
36
|
subdir = tmp_path / "test_dir"
|
|
33
37
|
subdir.mkdir()
|
|
34
|
-
|
|
38
|
+
|
|
35
39
|
cmd = Command("cd").add_args(str(subdir))
|
|
36
40
|
result = bash_shell.run(cmd)
|
|
37
41
|
|
|
@@ -40,6 +44,7 @@ def test_bash_virtual_cd(bash_shell, tmp_path, mock_observer):
|
|
|
40
44
|
# Verificar que se notificó el cambio de contexto
|
|
41
45
|
mock_observer.on_context_change.assert_called_with("cwd", subdir)
|
|
42
46
|
|
|
47
|
+
|
|
43
48
|
def test_bash_virtual_export(bash_shell, mock_observer):
|
|
44
49
|
"""Verifica que 'export' actualice las variables de entorno en el contexto."""
|
|
45
50
|
cmd = Command("export").add_args("VERSION=1.0.0", "DEBUG=true")
|
|
@@ -51,22 +56,24 @@ def test_bash_virtual_export(bash_shell, mock_observer):
|
|
|
51
56
|
# Verificar que se notificó cada cambio
|
|
52
57
|
assert mock_observer.on_context_change.call_count == 2
|
|
53
58
|
|
|
59
|
+
|
|
54
60
|
def test_bash_session_hooks(bash_shell, mock_observer):
|
|
55
61
|
"""Prueba que los hooks de inicio y fin de sesión funcionen con el context manager."""
|
|
56
62
|
with bash_shell as sh:
|
|
57
63
|
sh.run(Command("echo").add_args("test"))
|
|
58
|
-
|
|
64
|
+
|
|
59
65
|
mock_observer.on_session_start.assert_called_once_with("Bash")
|
|
60
66
|
mock_observer.on_session_end.assert_called_once()
|
|
61
67
|
|
|
68
|
+
|
|
62
69
|
@pytest.mark.timeout(2)
|
|
63
70
|
def test_bash_timeout(bash_shell):
|
|
64
71
|
"""Verifica que el timeout funcione correctamente (usando sleep)."""
|
|
65
72
|
# Nota: Este test depende de que 'sleep' esté disponible en el sistema
|
|
66
73
|
cmd = Command("sleep").add_args("5")
|
|
67
|
-
|
|
74
|
+
|
|
68
75
|
# Ejecutamos con un timeout corto
|
|
69
76
|
result = bash_shell.run(cmd, timeout=0.1)
|
|
70
|
-
|
|
77
|
+
|
|
71
78
|
assert result.return_code == 1
|
|
72
|
-
assert "Tiempo de espera agotado" in result.standard_error
|
|
79
|
+
assert "Tiempo de espera agotado" in result.standard_error
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from cc_shellback_kit import Command
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
class TestCommand:
|
|
4
|
-
|
|
5
5
|
def test_command_initialization(self):
|
|
6
6
|
"""Verifica que el comando se inicializa con el ejecutable correcto."""
|
|
7
7
|
cmd = Command("ls")
|
|
@@ -23,7 +23,7 @@ class TestCommand:
|
|
|
23
23
|
|
|
24
24
|
def test_add_list_of_arguments(self):
|
|
25
25
|
"""
|
|
26
|
-
Verifica que el builder aplane correctamente las listas
|
|
26
|
+
Verifica que el builder aplane correctamente las listas
|
|
27
27
|
pasadas como argumentos (gracias a ArgumentBuilder).
|
|
28
28
|
"""
|
|
29
29
|
cmd = Command("tar")
|
|
@@ -35,7 +35,7 @@ class TestCommand:
|
|
|
35
35
|
"""Verifica que add_args devuelva la instancia (patrón Fluent Interface)."""
|
|
36
36
|
cmd = Command("docker")
|
|
37
37
|
returned_cmd = cmd.add_args("ps").add_args("-a")
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
assert cmd is returned_cmd
|
|
40
40
|
assert cmd.args == ["ps", "-a"]
|
|
41
41
|
|
|
@@ -44,13 +44,13 @@ class TestCommand:
|
|
|
44
44
|
cmd = Command("echo")
|
|
45
45
|
# El ArgumentBuilder convierte internamente a string y aplana
|
|
46
46
|
cmd.add_args("User ID:", 1001, ["--force", "yes"])
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
assert cmd.args == ["User ID:", "1001", "--force", "yes"]
|
|
49
49
|
|
|
50
50
|
def test_empty_and_none_arguments(self):
|
|
51
51
|
"""Verifica que se ignoren valores None o vacíos si el builder así lo gestiona."""
|
|
52
52
|
cmd = Command("ls")
|
|
53
53
|
cmd.add_args(None, "", " ", "-l")
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
# Según ArgumentBuilder.py, ignora None y strings que queden vacíos tras .strip()
|
|
56
|
-
assert cmd.args == ["-l"]
|
|
56
|
+
assert cmd.args == ["-l"]
|
|
@@ -2,6 +2,7 @@ import pytest
|
|
|
2
2
|
import json
|
|
3
3
|
from cc_shellback_kit import CommandResult
|
|
4
4
|
|
|
5
|
+
|
|
5
6
|
def test_command_result_initialization():
|
|
6
7
|
"""Verifica que los valores por defecto se asignen correctamente."""
|
|
7
8
|
result = CommandResult()
|
|
@@ -11,34 +12,39 @@ def test_command_result_initialization():
|
|
|
11
12
|
assert result.execution_time == 0.0
|
|
12
13
|
assert result.command_sent == []
|
|
13
14
|
|
|
15
|
+
|
|
14
16
|
def test_is_success_true():
|
|
15
17
|
"""Verifica que is_success() sea True cuando el código de retorno es 0."""
|
|
16
18
|
result = CommandResult(return_code=0)
|
|
17
19
|
assert result.is_success() is True
|
|
18
20
|
|
|
21
|
+
|
|
19
22
|
def test_is_success_false():
|
|
20
23
|
"""Verifica que is_success() sea False cuando el código de retorno no es 0."""
|
|
21
24
|
result = CommandResult(return_code=1)
|
|
22
25
|
assert result.is_success() is False
|
|
23
|
-
|
|
26
|
+
|
|
24
27
|
result_error = CommandResult(return_code=127)
|
|
25
28
|
assert result_error.is_success() is False
|
|
26
29
|
|
|
30
|
+
|
|
27
31
|
def test_json_parsing_success():
|
|
28
32
|
"""Verifica que el método json() parsee correctamente un string JSON."""
|
|
29
33
|
data = {"status": "ok", "items": [1, 2, 3]}
|
|
30
34
|
json_str = json.dumps(data)
|
|
31
35
|
result = CommandResult(standard_output=json_str)
|
|
32
|
-
|
|
36
|
+
|
|
33
37
|
assert result.json() == data
|
|
34
38
|
assert result.json()["status"] == "ok"
|
|
35
39
|
|
|
40
|
+
|
|
36
41
|
def test_json_parsing_failure():
|
|
37
42
|
"""Verifica que el método json() lance un error con salida no válida."""
|
|
38
43
|
result = CommandResult(standard_output="No soy un JSON")
|
|
39
44
|
with pytest.raises(json.JSONDecodeError):
|
|
40
45
|
result.json()
|
|
41
46
|
|
|
47
|
+
|
|
42
48
|
def test_pipe_operator_syntax():
|
|
43
49
|
"""
|
|
44
50
|
Verifica el comportamiento del operador pipe (|).
|
|
@@ -46,13 +52,14 @@ def test_pipe_operator_syntax():
|
|
|
46
52
|
"""
|
|
47
53
|
stdout_content = "datos de salida"
|
|
48
54
|
result = CommandResult(standard_output=stdout_content)
|
|
49
|
-
|
|
55
|
+
|
|
50
56
|
# En la implementación actual: CommandResult | Any -> str (standard_output)
|
|
51
57
|
output = result | "otro_comando"
|
|
52
|
-
|
|
58
|
+
|
|
53
59
|
assert output == stdout_content
|
|
54
60
|
assert isinstance(output, str)
|
|
55
61
|
|
|
62
|
+
|
|
56
63
|
def test_complex_result_data():
|
|
57
64
|
"""Verifica la consistencia de un objeto con todos los campos llenos."""
|
|
58
65
|
cmd = ["ls", "-la"]
|
|
@@ -61,10 +68,10 @@ def test_complex_result_data():
|
|
|
61
68
|
standard_error="warning: low disk space",
|
|
62
69
|
return_code=0,
|
|
63
70
|
execution_time=0.1234,
|
|
64
|
-
command_sent=cmd
|
|
71
|
+
command_sent=cmd,
|
|
65
72
|
)
|
|
66
|
-
|
|
73
|
+
|
|
67
74
|
assert result.command_sent == cmd
|
|
68
75
|
assert "total" in result.standard_output
|
|
69
76
|
assert "warning" in result.standard_error
|
|
70
|
-
assert result.execution_time == 0.1234
|
|
77
|
+
assert result.execution_time == 0.1234
|