shelltastic 0.2.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shelltastic
3
- Version: 0.2.1
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
@@ -11,6 +11,12 @@ Description-Content-Type: text/markdown
11
11
 
12
12
  # shelltastic
13
13
 
14
+ ## Install
15
+
16
+ ```sh
17
+ pip install shelltastic
18
+ ```
19
+
14
20
  ## Basic Usage
15
21
 
16
22
  Run shell commands on a local host:
@@ -37,3 +43,20 @@ shell.host("example.com").run("echo Hello World!")
37
43
  # Specifying Username and Port
38
44
  shell.host("example.com", username="root", port=22).run("echo Hello World!")
39
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
+ ```
@@ -1,5 +1,11 @@
1
1
  # shelltastic
2
2
 
3
+ ## Install
4
+
5
+ ```sh
6
+ pip install shelltastic
7
+ ```
8
+
3
9
  ## Basic Usage
4
10
 
5
11
  Run shell commands on a local host:
@@ -26,3 +32,20 @@ shell.host("example.com").run("echo Hello World!")
26
32
  # Specifying Username and Port
27
33
  shell.host("example.com", username="root", port=22).run("echo Hello World!")
28
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "shelltastic"
3
- version = "0.2.1"
3
+ version = "0.3.0"
4
4
  description = "A fantastic shell command runner for python"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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
- class SCPFrontend:
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 scp_send(
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 scp_receive(
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
- class SCP():
104
- def host(
105
- self,
106
- host_or_hostname: str | Host,
107
- *,
108
- username: str | None = None,
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 LocalShellFrontend(ShellFrontend):
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
@@ -1,7 +1,7 @@
1
1
  from dataclasses import dataclass
2
2
 
3
3
 
4
- @dataclass
4
+ @dataclass(slots=True)
5
5
  class Host:
6
6
  hostname: str
7
7
  port: int | None
File without changes