hopgo 0.1.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.
- hopgo-0.1.0/.gitignore +10 -0
- hopgo-0.1.0/.python-version +1 -0
- hopgo-0.1.0/PKG-INFO +7 -0
- hopgo-0.1.0/README.md +0 -0
- hopgo-0.1.0/hopgo/__init__.py +40 -0
- hopgo-0.1.0/hopgo/cli.py +11 -0
- hopgo-0.1.0/hopgo/exceptions.py +38 -0
- hopgo-0.1.0/hopgo/files.py +16 -0
- hopgo-0.1.0/hopgo/hops.py +91 -0
- hopgo-0.1.0/hopgo/session.py +36 -0
- hopgo-0.1.0/hopgo/shell.py +585 -0
- hopgo-0.1.0/main.py +12 -0
- hopgo-0.1.0/pyproject.toml +17 -0
- hopgo-0.1.0/tests/test_hopgo.py +335 -0
- hopgo-0.1.0/uv.lock +430 -0
hopgo-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
hopgo-0.1.0/PKG-INFO
ADDED
hopgo-0.1.0/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from .session import connect
|
|
2
|
+
from .shell import Shell, Result
|
|
3
|
+
from .hops import Hop, SSHHop, ADBHop, DockerHop
|
|
4
|
+
from .files import FileInfo
|
|
5
|
+
from .exceptions import (
|
|
6
|
+
HopError,
|
|
7
|
+
HopConnectionError,
|
|
8
|
+
HopSudoError,
|
|
9
|
+
HopSecurityError,
|
|
10
|
+
HopFileNotFoundError,
|
|
11
|
+
HopFileExistsError,
|
|
12
|
+
HopIsDirectoryError,
|
|
13
|
+
HopPermissionError,
|
|
14
|
+
HopTimeoutError,
|
|
15
|
+
HopFileSizeError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
MAX_READ_SIZE = 10 * 1024 * 1024
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"connect",
|
|
22
|
+
"Shell",
|
|
23
|
+
"Result",
|
|
24
|
+
"Hop",
|
|
25
|
+
"SSHHop",
|
|
26
|
+
"ADBHop",
|
|
27
|
+
"DockerHop",
|
|
28
|
+
"FileInfo",
|
|
29
|
+
"HopError",
|
|
30
|
+
"HopConnectionError",
|
|
31
|
+
"HopSudoError",
|
|
32
|
+
"HopSecurityError",
|
|
33
|
+
"HopFileNotFoundError",
|
|
34
|
+
"HopFileExistsError",
|
|
35
|
+
"HopIsDirectoryError",
|
|
36
|
+
"HopPermissionError",
|
|
37
|
+
"HopTimeoutError",
|
|
38
|
+
"HopFileSizeError",
|
|
39
|
+
"MAX_READ_SIZE",
|
|
40
|
+
]
|
hopgo-0.1.0/hopgo/cli.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""HopGo CLI - Multi-hop SSH/ADB/Docker automation framework."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def main() -> None:
|
|
6
|
+
print("HopGo - A Pythonic multi-hop automation framework")
|
|
7
|
+
print("Use 'from hopgo import connect' in Python to get started.")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
if __name__ == "__main__":
|
|
11
|
+
main()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
class HopError(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class HopConnectionError(HopError):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HopSudoError(HopError):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HopSecurityError(HopError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HopFileNotFoundError(HopError):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HopFileExistsError(HopError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HopIsDirectoryError(HopError):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class HopPermissionError(HopError):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class HopTimeoutError(HopError):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class HopFileSizeError(HopError):
|
|
38
|
+
pass
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class FileInfo:
|
|
10
|
+
name: str
|
|
11
|
+
path: str
|
|
12
|
+
size: int
|
|
13
|
+
is_dir: bool
|
|
14
|
+
is_symlink: bool
|
|
15
|
+
modified: Optional[datetime] = None
|
|
16
|
+
permissions: str = ""
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shlex
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Hop(ABC):
|
|
9
|
+
target_os: str = "linux"
|
|
10
|
+
|
|
11
|
+
def __init__(self, sudo_password: Optional[str] = None) -> None:
|
|
12
|
+
self.sudo_password = sudo_password
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def wrap(self, cmd: str) -> str:
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SSHHop(Hop):
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
host: str,
|
|
23
|
+
user: Optional[str] = None,
|
|
24
|
+
password: Optional[str] = None,
|
|
25
|
+
key_filename: Optional[str] = None,
|
|
26
|
+
port: int = 22,
|
|
27
|
+
sudo_password: Optional[str] = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
super().__init__(sudo_password=sudo_password)
|
|
30
|
+
self.host = host
|
|
31
|
+
self.user = user
|
|
32
|
+
self.password = password
|
|
33
|
+
self.key_filename = key_filename
|
|
34
|
+
self.port = port
|
|
35
|
+
self.target_os = "linux"
|
|
36
|
+
|
|
37
|
+
def wrap(self, cmd: str) -> str:
|
|
38
|
+
parts = ["ssh", "-o", "StrictHostKeyChecking=no"]
|
|
39
|
+
if self.port != 22:
|
|
40
|
+
parts += ["-p", str(self.port)]
|
|
41
|
+
if self.key_filename:
|
|
42
|
+
parts += ["-i", shlex.quote(self.key_filename)]
|
|
43
|
+
user_host = f"{self.user}@{self.host}" if self.user else self.host
|
|
44
|
+
parts.append(user_host)
|
|
45
|
+
parts.append(shlex.quote(cmd))
|
|
46
|
+
return " ".join(parts)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ADBHop(Hop):
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
serial: Optional[str] = None,
|
|
53
|
+
user: Optional[str] = None,
|
|
54
|
+
sudo_password: Optional[str] = None,
|
|
55
|
+
) -> None:
|
|
56
|
+
super().__init__(sudo_password=sudo_password)
|
|
57
|
+
self.serial = serial
|
|
58
|
+
self.user = user
|
|
59
|
+
self.target_os = "linux"
|
|
60
|
+
|
|
61
|
+
def wrap(self, cmd: str) -> str:
|
|
62
|
+
parts = ["adb"]
|
|
63
|
+
if self.serial:
|
|
64
|
+
parts += ["-s", shlex.quote(self.serial)]
|
|
65
|
+
parts.append("shell")
|
|
66
|
+
if self.user:
|
|
67
|
+
parts += ["run-as", shlex.quote(self.user)]
|
|
68
|
+
parts.append(shlex.quote(cmd))
|
|
69
|
+
return " ".join(parts)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class DockerHop(Hop):
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
container: str,
|
|
76
|
+
user: Optional[str] = None,
|
|
77
|
+
host_os: str = "linux",
|
|
78
|
+
sudo_password: Optional[str] = None,
|
|
79
|
+
) -> None:
|
|
80
|
+
super().__init__(sudo_password=sudo_password)
|
|
81
|
+
self.container = container
|
|
82
|
+
self.user = user
|
|
83
|
+
self.target_os = host_os
|
|
84
|
+
|
|
85
|
+
def wrap(self, cmd: str) -> str:
|
|
86
|
+
parts = ["docker", "exec", "-i"]
|
|
87
|
+
if self.user:
|
|
88
|
+
parts += ["-u", shlex.quote(self.user)]
|
|
89
|
+
parts.append(shlex.quote(self.container))
|
|
90
|
+
parts += ["sh", "-c", shlex.quote(cmd)]
|
|
91
|
+
return " ".join(parts)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Optional, List
|
|
5
|
+
|
|
6
|
+
from .shell import Shell
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Session:
|
|
10
|
+
def __init__(self, _ssh_conn=None) -> None:
|
|
11
|
+
self._ssh_conn = _ssh_conn
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def connect(
|
|
15
|
+
host: Optional[str] = None,
|
|
16
|
+
hops: Optional[List] = None,
|
|
17
|
+
**ssh_kwargs,
|
|
18
|
+
) -> Shell:
|
|
19
|
+
ssh_conn = None
|
|
20
|
+
if host is not None:
|
|
21
|
+
from fabric import Connection
|
|
22
|
+
ssh_conn = Connection(host, **ssh_kwargs)
|
|
23
|
+
try:
|
|
24
|
+
ssh_conn.open()
|
|
25
|
+
except Exception as e:
|
|
26
|
+
from .exceptions import HopConnectionError
|
|
27
|
+
raise HopConnectionError(f"SSH connection to {host} failed: {e}")
|
|
28
|
+
|
|
29
|
+
session = Session(_ssh_conn=ssh_conn)
|
|
30
|
+
target_os = "windows" if sys.platform == "win32" else "linux"
|
|
31
|
+
|
|
32
|
+
return Shell(
|
|
33
|
+
_session=session,
|
|
34
|
+
_cwd="/",
|
|
35
|
+
_target_os=target_os,
|
|
36
|
+
)
|