shelltastic 0.2.2__tar.gz → 0.3.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.
- {shelltastic-0.2.2 → shelltastic-0.3.0}/PKG-INFO +18 -1
- {shelltastic-0.2.2 → shelltastic-0.3.0}/README.md +17 -0
- {shelltastic-0.2.2 → shelltastic-0.3.0}/pyproject.toml +1 -1
- {shelltastic-0.2.2 → shelltastic-0.3.0}/src/shelltastic/__init__.py +3 -1
- {shelltastic-0.2.2 → shelltastic-0.3.0}/src/shelltastic/backend/__init__.py +4 -6
- shelltastic-0.3.0/src/shelltastic/frontend/common.py +46 -0
- shelltastic-0.3.0/src/shelltastic/frontend/git.py +111 -0
- {shelltastic-0.2.2 → shelltastic-0.3.0}/src/shelltastic/frontend/scp.py +12 -20
- {shelltastic-0.2.2 → shelltastic-0.3.0}/src/shelltastic/frontend/shell.py +13 -21
- {shelltastic-0.2.2 → shelltastic-0.3.0}/src/shelltastic/host.py +1 -1
- {shelltastic-0.2.2 → shelltastic-0.3.0}/LICENSE +0 -0
- {shelltastic-0.2.2 → shelltastic-0.3.0}/src/shelltastic/backend/base.py +0 -0
- {shelltastic-0.2.2 → shelltastic-0.3.0}/src/shelltastic/enum.py +0 -0
- {shelltastic-0.2.2 → shelltastic-0.3.0}/src/shelltastic/frontend/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shelltastic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A fantastic shell command runner for python
|
|
5
5
|
Author: Bearmine
|
|
6
6
|
License-Expression: MPL-2.0
|
|
@@ -43,3 +43,20 @@ shell.host("example.com").run("echo Hello World!")
|
|
|
43
43
|
# Specifying Username and Port
|
|
44
44
|
shell.host("example.com", username="root", port=22).run("echo Hello World!")
|
|
45
45
|
```
|
|
46
|
+
|
|
47
|
+
To copy files to/from hosts use the scp frontend:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from shelltastic import scp
|
|
51
|
+
|
|
52
|
+
scp.host("example.com").send(...)
|
|
53
|
+
scp.host("example.com").receive(...)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
To interact with git, there's a git frontend:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from shelltastic import git
|
|
60
|
+
|
|
61
|
+
git.clone(...)
|
|
62
|
+
```
|
|
@@ -32,3 +32,20 @@ shell.host("example.com").run("echo Hello World!")
|
|
|
32
32
|
# Specifying Username and Port
|
|
33
33
|
shell.host("example.com", username="root", port=22).run("echo Hello World!")
|
|
34
34
|
```
|
|
35
|
+
|
|
36
|
+
To copy files to/from hosts use the scp frontend:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from shelltastic import scp
|
|
40
|
+
|
|
41
|
+
scp.host("example.com").send(...)
|
|
42
|
+
scp.host("example.com").receive(...)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
To interact with git, there's a git frontend:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from shelltastic import git
|
|
49
|
+
|
|
50
|
+
git.clone(...)
|
|
51
|
+
```
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
from shelltastic.frontend.git import LocalGitFrontend
|
|
1
2
|
from shelltastic.frontend.scp import SCP
|
|
2
3
|
from shelltastic.frontend.shell import LocalShellFrontend
|
|
3
4
|
|
|
4
|
-
__all__ = ["shell", "scp"]
|
|
5
|
+
__all__ = ["shell", "scp", "git"]
|
|
5
6
|
|
|
6
7
|
shell = LocalShellFrontend()
|
|
7
8
|
scp = SCP()
|
|
9
|
+
git = LocalGitFrontend()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import logging
|
|
3
4
|
import pathlib
|
|
4
5
|
import shlex
|
|
@@ -13,6 +14,8 @@ LOGGER = logging.getLogger(__name__)
|
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class LocalShellBackend(ShellBackend):
|
|
17
|
+
__slots__ = ()
|
|
18
|
+
|
|
16
19
|
def run(
|
|
17
20
|
self,
|
|
18
21
|
cmd: str | list[str],
|
|
@@ -78,10 +81,7 @@ class SSHConnectionError(Exception):
|
|
|
78
81
|
|
|
79
82
|
|
|
80
83
|
class SSHShellBackend(LocalShellBackend, RemoteShellBackend):
|
|
81
|
-
def __init__(
|
|
82
|
-
self,
|
|
83
|
-
host: Host
|
|
84
|
-
) -> None:
|
|
84
|
+
def __init__(self, host: Host) -> None:
|
|
85
85
|
super().__init__()
|
|
86
86
|
self.host: Host = host
|
|
87
87
|
|
|
@@ -89,8 +89,6 @@ class SSHShellBackend(LocalShellBackend, RemoteShellBackend):
|
|
|
89
89
|
def for_host(host: Host) -> RemoteShellBackend:
|
|
90
90
|
return SSHShellBackend(host)
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
94
92
|
def run(
|
|
95
93
|
self,
|
|
96
94
|
cmd: str | list[str],
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Generic, TypeVar, overload
|
|
3
|
+
|
|
4
|
+
from shelltastic.host import Host
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RemoteFrontend(ABC):
|
|
8
|
+
def __init__(self, host: Host) -> None:
|
|
9
|
+
self._host = host
|
|
10
|
+
|
|
11
|
+
def host(self) -> Host:
|
|
12
|
+
return self._host
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T", bound=RemoteFrontend)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RemoteFrontendFactory(Generic[T], ABC):
|
|
19
|
+
__slots__ = ()
|
|
20
|
+
|
|
21
|
+
@overload
|
|
22
|
+
def host(self, host_or_hostname: Host) -> T: ...
|
|
23
|
+
@overload
|
|
24
|
+
def host(
|
|
25
|
+
self,
|
|
26
|
+
host_or_hostname: str,
|
|
27
|
+
*,
|
|
28
|
+
username: str | None = None,
|
|
29
|
+
port: int | None = None,
|
|
30
|
+
) -> T: ...
|
|
31
|
+
def host(
|
|
32
|
+
self,
|
|
33
|
+
host_or_hostname: str | Host,
|
|
34
|
+
*,
|
|
35
|
+
username: str | None = None,
|
|
36
|
+
port: int | None = None,
|
|
37
|
+
) -> T:
|
|
38
|
+
if isinstance(host_or_hostname, Host):
|
|
39
|
+
host = host_or_hostname
|
|
40
|
+
else:
|
|
41
|
+
host = Host(host_or_hostname, port, username)
|
|
42
|
+
return self._create_remote_frontend(host)
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def _create_remote_frontend(self, host: Host) -> T:
|
|
46
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import overload
|
|
4
|
+
|
|
5
|
+
from shelltastic.backend import LocalShellBackend, SSHShellBackend
|
|
6
|
+
from shelltastic.backend.base import ShellBackend
|
|
7
|
+
from shelltastic.enum import OutputMode
|
|
8
|
+
from shelltastic.frontend.common import RemoteFrontend, RemoteFrontendFactory
|
|
9
|
+
from shelltastic.host import Host
|
|
10
|
+
|
|
11
|
+
LOGGER = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GitFrontend:
|
|
15
|
+
def __init__(self, backend: ShellBackend) -> None:
|
|
16
|
+
self._backend = backend
|
|
17
|
+
|
|
18
|
+
def clone(self, repo_url: str, target_location: str | Path):
|
|
19
|
+
LOGGER.info("Cloning %s into directory %s", repo_url, target_location)
|
|
20
|
+
self._backend.run(["git", "clone", repo_url, str(target_location)])
|
|
21
|
+
|
|
22
|
+
def pull(self, repo_path: str | Path, remote: str = "origin"):
|
|
23
|
+
LOGGER.info("Pulling %s for repo %s", remote, repo_path)
|
|
24
|
+
self._backend.run(["git", "pull", remote], cwd=repo_path)
|
|
25
|
+
|
|
26
|
+
def get_remote_url(self, repo_path: str | Path, remote: str = "origin") -> str:
|
|
27
|
+
result = self._backend.run(
|
|
28
|
+
["git", "remote", "get-url", remote],
|
|
29
|
+
stdout=OutputMode.CAPTURE,
|
|
30
|
+
cwd=repo_path,
|
|
31
|
+
)
|
|
32
|
+
return result.stdout.decode().strip()
|
|
33
|
+
|
|
34
|
+
def set_remote_url(
|
|
35
|
+
self, repo_path: str | Path, repo_url: str, remote: str = "origin"
|
|
36
|
+
):
|
|
37
|
+
self._backend.run(
|
|
38
|
+
["git", "remote", "set-url", remote, repo_url],
|
|
39
|
+
cwd=repo_path,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@overload
|
|
43
|
+
def global_config(self, key: str) -> list[str]: ...
|
|
44
|
+
@overload
|
|
45
|
+
def global_config(self, key: str, value: str | list[str]) -> None: ...
|
|
46
|
+
def global_config(self, key: str, value: str | list[str] | None = None):
|
|
47
|
+
if value is not None:
|
|
48
|
+
return self.set_global_config(key, value)
|
|
49
|
+
return self.get_global_config(key)
|
|
50
|
+
|
|
51
|
+
def get_global_config(self, key: str) -> list[str]:
|
|
52
|
+
LOGGER.info("Getting git config %s", key)
|
|
53
|
+
result = self._backend.run(
|
|
54
|
+
["git", "config", "--global", "--get-all", key],
|
|
55
|
+
check=False,
|
|
56
|
+
stdout=OutputMode.CAPTURE,
|
|
57
|
+
stderr=OutputMode.CAPTURE,
|
|
58
|
+
)
|
|
59
|
+
configs = [i.strip() for i in result.stdout.decode().split("\n") if i.strip()]
|
|
60
|
+
return configs
|
|
61
|
+
|
|
62
|
+
def set_global_config(self, key: str, value: str | list[str]):
|
|
63
|
+
LOGGER.info(
|
|
64
|
+
"Setting git config %s to %s",
|
|
65
|
+
key,
|
|
66
|
+
value,
|
|
67
|
+
)
|
|
68
|
+
self._backend.run(
|
|
69
|
+
["git", "config", "unset", "--global", "--all", key],
|
|
70
|
+
check=False,
|
|
71
|
+
stdout=OutputMode.CAPTURE,
|
|
72
|
+
stderr=OutputMode.CAPTURE,
|
|
73
|
+
)
|
|
74
|
+
if isinstance(value, str):
|
|
75
|
+
value = [value]
|
|
76
|
+
for v in value:
|
|
77
|
+
self._backend.run(
|
|
78
|
+
["git", "config", "set", "--global", "--append", key, v], check=True
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def git_credentials(self) -> list[str]:
|
|
82
|
+
response = self._backend.run(
|
|
83
|
+
"cat ~/.git-credentials",
|
|
84
|
+
check=False,
|
|
85
|
+
stdout=OutputMode.CAPTURE,
|
|
86
|
+
stderr=OutputMode.DEVNULL,
|
|
87
|
+
)
|
|
88
|
+
if response.returncode != 0:
|
|
89
|
+
return []
|
|
90
|
+
else:
|
|
91
|
+
gitcredentials = response.stdout.decode()
|
|
92
|
+
|
|
93
|
+
return gitcredentials.splitlines()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class RemoteGitFrontend(GitFrontend, RemoteFrontend):
|
|
97
|
+
__slots__ = ()
|
|
98
|
+
|
|
99
|
+
def __init__(self, host: Host) -> None:
|
|
100
|
+
super().__init__(SSHShellBackend.for_host(host))
|
|
101
|
+
super(GitFrontend, self).__init__(host)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class LocalGitFrontend(GitFrontend, RemoteFrontendFactory[RemoteGitFrontend]):
|
|
105
|
+
__slots__ = ()
|
|
106
|
+
|
|
107
|
+
def __init__(self) -> None:
|
|
108
|
+
super().__init__(LocalShellBackend())
|
|
109
|
+
|
|
110
|
+
def _create_remote_frontend(self, host: Host) -> RemoteGitFrontend:
|
|
111
|
+
return RemoteGitFrontend(host)
|
|
@@ -3,18 +3,17 @@ from pathlib import Path
|
|
|
3
3
|
|
|
4
4
|
from shelltastic.backend import LocalShellBackend, SSHShellBackend
|
|
5
5
|
from shelltastic.enum import OutputMode
|
|
6
|
+
from shelltastic.frontend.common import RemoteFrontend, RemoteFrontendFactory
|
|
6
7
|
from shelltastic.host import Host
|
|
7
8
|
|
|
8
9
|
LOGGER = logging.getLogger(__name__)
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
|
|
12
|
+
class SCPFrontend(RemoteFrontend):
|
|
11
13
|
def __init__(self, host: Host) -> None:
|
|
14
|
+
super().__init__(host)
|
|
12
15
|
self._local_backend = LocalShellBackend()
|
|
13
16
|
self._remote_backend = SSHShellBackend(host)
|
|
14
|
-
self._host = host
|
|
15
|
-
|
|
16
|
-
def host(self) -> Host:
|
|
17
|
-
return self._host
|
|
18
17
|
|
|
19
18
|
def _scp_flags(self, recursive) -> list[str]:
|
|
20
19
|
# Set Port
|
|
@@ -26,7 +25,7 @@ class SCPFrontend:
|
|
|
26
25
|
|
|
27
26
|
return flags
|
|
28
27
|
|
|
29
|
-
def
|
|
28
|
+
def send(
|
|
30
29
|
self,
|
|
31
30
|
source_path: str,
|
|
32
31
|
destination_path: str,
|
|
@@ -67,7 +66,7 @@ class SCPFrontend:
|
|
|
67
66
|
LOGGER.info("chmod %s to %s", dest, mode)
|
|
68
67
|
self._remote_backend.run(["chmod", mode, destination_path])
|
|
69
68
|
|
|
70
|
-
def
|
|
69
|
+
def receive(
|
|
71
70
|
self,
|
|
72
71
|
source_path: str,
|
|
73
72
|
destination_path: str,
|
|
@@ -100,16 +99,9 @@ class SCPFrontend:
|
|
|
100
99
|
LOGGER.info("chmod %s to %s", destination_path, mode)
|
|
101
100
|
self._remote_backend.run(["chmod", mode, destination_path])
|
|
102
101
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
port: int | None = None,
|
|
110
|
-
) -> SCPFrontend:
|
|
111
|
-
if isinstance(host_or_hostname, Host):
|
|
112
|
-
host = host_or_hostname
|
|
113
|
-
else:
|
|
114
|
-
host = Host(host_or_hostname, port, username)
|
|
115
|
-
return SCPFrontend(host)
|
|
102
|
+
|
|
103
|
+
class SCP(RemoteFrontendFactory[SCPFrontend]):
|
|
104
|
+
__slots__ = ()
|
|
105
|
+
|
|
106
|
+
def _create_remote_frontend(self, host: Host) -> SCPFrontend:
|
|
107
|
+
return SCPFrontend(host)
|
|
@@ -7,6 +7,7 @@ from subprocess import CompletedProcess
|
|
|
7
7
|
from shelltastic.backend import LocalShellBackend, SSHShellBackend
|
|
8
8
|
from shelltastic.backend.base import ShellBackend
|
|
9
9
|
from shelltastic.enum import OutputMode, SystemType
|
|
10
|
+
from shelltastic.frontend.common import RemoteFrontend, RemoteFrontendFactory
|
|
10
11
|
from shelltastic.host import Host
|
|
11
12
|
|
|
12
13
|
|
|
@@ -79,21 +80,21 @@ class ShellFrontend:
|
|
|
79
80
|
)
|
|
80
81
|
|
|
81
82
|
|
|
82
|
-
class
|
|
83
|
+
class RemoteShellFrontend(ShellFrontend, RemoteFrontend):
|
|
84
|
+
__slots__ = ()
|
|
85
|
+
|
|
86
|
+
def __init__(self, host: Host) -> None:
|
|
87
|
+
super().__init__(SSHShellBackend.for_host(host))
|
|
88
|
+
super(ShellFrontend, self).__init__(host)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class LocalShellFrontend(ShellFrontend, RemoteFrontendFactory[RemoteShellFrontend]):
|
|
92
|
+
__slots__ = ()
|
|
93
|
+
|
|
83
94
|
def __init__(self) -> None:
|
|
84
95
|
super().__init__(LocalShellBackend())
|
|
85
96
|
|
|
86
|
-
def host
|
|
87
|
-
self,
|
|
88
|
-
host_or_hostname: str | Host,
|
|
89
|
-
*,
|
|
90
|
-
username: str | None = None,
|
|
91
|
-
port: int | None = None,
|
|
92
|
-
) -> RemoteShellFrontend:
|
|
93
|
-
if isinstance(host_or_hostname, Host):
|
|
94
|
-
host = host_or_hostname
|
|
95
|
-
else:
|
|
96
|
-
host = Host(host_or_hostname, port, username)
|
|
97
|
+
def _create_remote_frontend(self, host: Host) -> RemoteShellFrontend:
|
|
97
98
|
return RemoteShellFrontend(host)
|
|
98
99
|
|
|
99
100
|
def which(self, command: str) -> Path | None:
|
|
@@ -101,12 +102,3 @@ class LocalShellFrontend(ShellFrontend):
|
|
|
101
102
|
if result:
|
|
102
103
|
return Path(result)
|
|
103
104
|
return None
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
class RemoteShellFrontend(ShellFrontend):
|
|
107
|
-
def __init__(self, host: Host) -> None:
|
|
108
|
-
super().__init__(SSHShellBackend.for_host(host))
|
|
109
|
-
self._host = host
|
|
110
|
-
|
|
111
|
-
def host(self) -> Host:
|
|
112
|
-
return self._host
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|