shelltastic 0.4.8__tar.gz → 0.5.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.
Files changed (21) hide show
  1. {shelltastic-0.4.8 → shelltastic-0.5.0}/PKG-INFO +1 -1
  2. {shelltastic-0.4.8 → shelltastic-0.5.0}/pyproject.toml +1 -1
  3. {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/backend/default/common.py +72 -2
  4. {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/display.py +15 -9
  5. {shelltastic-0.4.8 → shelltastic-0.5.0}/LICENSE +0 -0
  6. {shelltastic-0.4.8 → shelltastic-0.5.0}/README.md +0 -0
  7. {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/__init__.py +0 -0
  8. {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/backend/__init__.py +0 -0
  9. {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/backend/base.py +0 -0
  10. {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/backend/default/__init__.py +0 -0
  11. {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/backend/default/local.py +0 -0
  12. {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/backend/default/remote.py +0 -0
  13. {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/enum.py +0 -0
  14. {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/exception.py +0 -0
  15. {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/frontend/__init__.py +0 -0
  16. {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/frontend/common.py +0 -0
  17. {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/frontend/git.py +0 -0
  18. {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/frontend/scp.py +0 -0
  19. {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/frontend/shell.py +0 -0
  20. {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/host.py +0 -0
  21. {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/result.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shelltastic
3
- Version: 0.4.8
3
+ Version: 0.5.0
4
4
  Summary: A fantastic shell command runner for python
5
5
  Author: Bearmine
6
6
  License-Expression: MPL-2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "shelltastic"
3
- version = "0.4.8"
3
+ version = "0.5.0"
4
4
  description = "A fantastic shell command runner for python"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,8 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
+ import os
4
5
  import shlex
5
6
  import subprocess
7
+ import time
6
8
  from multiprocessing.pool import ThreadPool
7
9
  from typing import IO, TYPE_CHECKING, Literal
8
10
 
@@ -17,6 +19,74 @@ if TYPE_CHECKING:
17
19
  LOGGER = logging.getLogger(__name__)
18
20
 
19
21
 
22
+ class IOTimeoutStream:
23
+ """
24
+ Iterable for reading from an IO stream. Return lines but
25
+ if no bytes have been read since the given timeout, return
26
+ whatever is in the buffer.
27
+ """
28
+
29
+ def __init__(
30
+ self, io: IO[bytes], *, timeout: float = 0.1, batch_size: int = 1000
31
+ ) -> None:
32
+ self.io = io
33
+ self.buffer = b""
34
+ self.batch_size = batch_size
35
+ self.timeout = timeout
36
+
37
+ def __iter__(self):
38
+ return self
39
+
40
+ def __next__(self) -> bytes:
41
+ last_read_time = time.time()
42
+ # Read into our buffer
43
+ while True:
44
+ # If we have no data in our buffer, then we block until we get data.
45
+ # Otherwise, we're waiting for end of line and should unblock.
46
+ if len(self.buffer) == 0:
47
+ os.set_blocking(self.io.fileno(), True)
48
+ batch_size = 1
49
+ else:
50
+ os.set_blocking(self.io.fileno(), False)
51
+ batch_size = self.batch_size
52
+
53
+ # Read from IO
54
+ batch = self.io.read(batch_size)
55
+
56
+ # Handle return batch
57
+ # None means no data to read, but NOT end of file
58
+ if batch is not None:
59
+ last_read_time = time.time()
60
+ self.buffer += batch
61
+
62
+ # If batch is not none,
63
+ # and the batch was 0 size,
64
+ # and our buffer is empty,
65
+ # we're at EOF
66
+ if len(self.buffer) == 0:
67
+ raise StopIteration()
68
+
69
+ # If buffer contains at least one line, return the first line, save the rest
70
+ if b"\n" in self.buffer:
71
+ buffer_lines = self.buffer.splitlines(keepends=True)
72
+ self.buffer = b""
73
+ for b in buffer_lines[1:]:
74
+ self.buffer += b
75
+ return buffer_lines[0]
76
+
77
+ # If we're at end of file, and we have no lines, just return what we have
78
+ if batch is not None and len(batch) == 0:
79
+ return_val = self.buffer
80
+ self.buffer = b""
81
+ return return_val
82
+
83
+ # Handle timeout
84
+ if time.time() - last_read_time >= self.timeout:
85
+ return_val = self.buffer
86
+ self.buffer = b""
87
+ return return_val
88
+
89
+
20
90
  def _output_and_collect(
21
91
  io: IO[bytes] | None, display: display.IODisplay | None, collect: bool = True
22
92
  ) -> bytes | None:
@@ -26,14 +96,14 @@ def _output_and_collect(
26
96
  if collect:
27
97
  if display:
28
98
  return_bytes: bytes = b""
29
- for line in io:
99
+ for line in IOTimeoutStream(io):
30
100
  return_bytes += line
31
101
  display.printbytes(line)
32
102
  return return_bytes
33
103
  return io.read()
34
104
 
35
105
  if display:
36
- for line in io:
106
+ for line in IOTimeoutStream(io):
37
107
  display.printbytes(line)
38
108
 
39
109
  return None
@@ -13,30 +13,36 @@ _io_display_lock = threading.RLock()
13
13
  class IODisplay(ABC):
14
14
  def printbytes(self, line: bytes):
15
15
  with _io_display_lock:
16
- self._emit_line("%s", line.decode().rstrip())
16
+ self._emit("%s", line.decode(), end="")
17
17
 
18
18
  def printline(self, msg: object, *args: object):
19
19
  with _io_display_lock:
20
- self._emit_line(msg, *args)
20
+ self._emit(msg, *args)
21
21
 
22
22
  @abstractmethod
23
- def _emit_line(self, msg: object, *args: object):
23
+ def _emit(self, msg: object, *args: object, end="\n"):
24
24
  raise NotImplementedError
25
25
 
26
26
 
27
27
  class DevNullDisplay(IODisplay):
28
- def _emit_line(self, msg: object, *args: object):
28
+ def _emit(self, msg: object, *args: object, end="\n"):
29
29
  pass
30
30
 
31
31
 
32
32
  class StdoutDisplay(IODisplay):
33
- def _emit_line(self, msg: object, *args: object):
34
- print(str(msg) % args, flush=True)
33
+ def _emit(self, msg: object, *args: object, end="\n"):
34
+ if args:
35
+ print(str(msg) % args, flush=True, end=end)
36
+ return
37
+ print(str(msg), flush=True, end=end)
35
38
 
36
39
 
37
40
  class StderrDisplay(IODisplay):
38
- def _emit_line(self, msg: object, *args: object):
39
- print(str(msg) % args, file=sys.stderr, flush=True)
41
+ def _emit(self, msg: object, *args: object, end="\n"):
42
+ if args:
43
+ print(str(msg) % args, file=sys.stderr, flush=True, end=end)
44
+ return
45
+ print(str(msg), file=sys.stderr, flush=True, end=end)
40
46
 
41
47
 
42
48
  class LogDisplay(IODisplay):
@@ -44,7 +50,7 @@ class LogDisplay(IODisplay):
44
50
  super().__init__()
45
51
  self.log_level = log_level
46
52
 
47
- def _emit_line(self, msg: object, *args: object):
53
+ def _emit(self, msg: object, *args: object, end="\n"):
48
54
  LOGGER.log(self.log_level, msg, *args, stacklevel=3)
49
55
 
50
56
 
File without changes
File without changes