omlish 0.0.0.dev224__py3-none-any.whl → 0.0.0.dev226__py3-none-any.whl

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 (44) hide show
  1. omlish/__about__.py +2 -2
  2. omlish/asyncs/asyncio/subprocesses.py +19 -18
  3. omlish/asyncs/asyncio/timeouts.py +5 -2
  4. omlish/asyncs/asyncs.py +0 -1
  5. omlish/bootstrap/sys.py +2 -2
  6. omlish/dataclasses/impl/metaclass.py +5 -0
  7. omlish/diag/lslocks.py +64 -0
  8. omlish/diag/lsof.py +264 -0
  9. omlish/diag/ps.py +40 -21
  10. omlish/http/coro/server.py +5 -54
  11. omlish/http/coro/simple.py +1 -1
  12. omlish/http/coro/sockets.py +59 -0
  13. omlish/lang/__init__.py +8 -8
  14. omlish/lang/imports.py +22 -0
  15. omlish/libc.py +10 -0
  16. omlish/lite/dataclasses.py +23 -0
  17. omlish/lite/timeouts.py +202 -0
  18. omlish/multiprocessing/__init__.py +0 -7
  19. omlish/os/fcntl.py +59 -0
  20. omlish/os/pidfiles/__init__.py +0 -0
  21. omlish/os/pidfiles/manager.py +111 -0
  22. omlish/os/pidfiles/pidfile.py +152 -0
  23. omlish/secrets/crypto.py +1 -2
  24. omlish/secrets/openssl.py +1 -1
  25. omlish/secrets/tempssl.py +4 -7
  26. omlish/sockets/handlers.py +4 -0
  27. omlish/sockets/server/handlers.py +22 -0
  28. omlish/subprocesses/__init__.py +0 -0
  29. omlish/subprocesses/async_.py +97 -0
  30. omlish/subprocesses/base.py +221 -0
  31. omlish/subprocesses/run.py +138 -0
  32. omlish/subprocesses/sync.py +153 -0
  33. omlish/subprocesses/utils.py +22 -0
  34. omlish/subprocesses/wrap.py +23 -0
  35. {omlish-0.0.0.dev224.dist-info → omlish-0.0.0.dev226.dist-info}/METADATA +1 -1
  36. {omlish-0.0.0.dev224.dist-info → omlish-0.0.0.dev226.dist-info}/RECORD +41 -29
  37. omlish/lang/timeouts.py +0 -53
  38. omlish/os/pidfile.py +0 -69
  39. omlish/subprocesses.py +0 -510
  40. /omlish/{multiprocessing → os}/death.py +0 -0
  41. {omlish-0.0.0.dev224.dist-info → omlish-0.0.0.dev226.dist-info}/LICENSE +0 -0
  42. {omlish-0.0.0.dev224.dist-info → omlish-0.0.0.dev226.dist-info}/WHEEL +0 -0
  43. {omlish-0.0.0.dev224.dist-info → omlish-0.0.0.dev226.dist-info}/entry_points.txt +0 -0
  44. {omlish-0.0.0.dev224.dist-info → omlish-0.0.0.dev226.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,152 @@
1
+ # ruff: noqa: UP007
2
+ # @omlish-lite
3
+ """
4
+ TODO:
5
+ - reliable pid retrieval
6
+ - contents are *ignored*, just advisory
7
+ - check double-check:
8
+ - 1) get pid of flock holder
9
+ - 2) get pidfd to that
10
+ - 3) recheck current pid of flock holder == that pid
11
+ - racy as to if it's a different actual process as initial check, just with same pid, but due to 'identity' / semantic
12
+ meaning of the named pidfile the processes are considered equivalent
13
+ - read_checked(), contextmanager
14
+ """
15
+ import fcntl
16
+ import os
17
+ import signal
18
+ import typing as ta
19
+
20
+
21
+ ##
22
+
23
+
24
+ class Pidfile:
25
+ def __init__(
26
+ self,
27
+ path: str,
28
+ *,
29
+ inheritable: bool = True,
30
+ ) -> None:
31
+ super().__init__()
32
+
33
+ self._path = path
34
+ self._inheritable = inheritable
35
+
36
+ @property
37
+ def path(self) -> str:
38
+ return self._path
39
+
40
+ @property
41
+ def inheritable(self) -> bool:
42
+ return self._inheritable
43
+
44
+ def __repr__(self) -> str:
45
+ return f'{self.__class__.__name__}({self._path!r})'
46
+
47
+ #
48
+
49
+ _f: ta.TextIO
50
+
51
+ def fileno(self) -> ta.Optional[int]:
52
+ if hasattr(self, '_f'):
53
+ return self._f.fileno()
54
+ else:
55
+ return None
56
+
57
+ #
58
+
59
+ def __enter__(self) -> 'Pidfile':
60
+ fd = os.open(self._path, os.O_RDWR | os.O_CREAT, 0o600)
61
+
62
+ try:
63
+ if self._inheritable:
64
+ os.set_inheritable(fd, True)
65
+
66
+ f = os.fdopen(fd, 'r+')
67
+
68
+ except Exception:
69
+ try:
70
+ os.close(fd)
71
+ except Exception: # noqa
72
+ pass
73
+ raise
74
+
75
+ self._f = f
76
+ return self
77
+
78
+ def __exit__(self, exc_type, exc_val, exc_tb):
79
+ self.close()
80
+
81
+ #
82
+
83
+ def __getstate__(self):
84
+ state = self.__dict__.copy()
85
+
86
+ if '_f' in state:
87
+ # self._inheritable may be decoupled from actual file inheritability - for example when using the manager.
88
+ if os.get_inheritable(fd := state.pop('_f').fileno()):
89
+ state['__fd'] = fd
90
+
91
+ return state
92
+
93
+ def __setstate__(self, state):
94
+ if '_f' in state:
95
+ raise RuntimeError
96
+
97
+ if '__fd' in state:
98
+ state['_f'] = os.fdopen(state.pop('__fd'), 'r+')
99
+
100
+ self.__dict__.update(state)
101
+
102
+ #
103
+
104
+ def close(self) -> bool:
105
+ if not hasattr(self, '_f'):
106
+ return False
107
+
108
+ self._f.close()
109
+ del self._f
110
+ return True
111
+
112
+ def try_lock(self) -> bool:
113
+ try:
114
+ fcntl.flock(self._f, fcntl.LOCK_EX | fcntl.LOCK_NB)
115
+ return True
116
+
117
+ except OSError:
118
+ return False
119
+
120
+ def ensure_locked(self) -> None:
121
+ if not self.try_lock():
122
+ raise RuntimeError('Could not get lock')
123
+
124
+ #
125
+
126
+ def write(self, pid: ta.Optional[int] = None) -> None:
127
+ self.ensure_locked()
128
+
129
+ if pid is None:
130
+ pid = os.getpid()
131
+
132
+ self._f.seek(0)
133
+ self._f.truncate()
134
+ self._f.write(f'{pid}\n')
135
+ self._f.flush()
136
+
137
+ def clear(self) -> None:
138
+ self.ensure_locked()
139
+
140
+ self._f.seek(0)
141
+ self._f.truncate()
142
+
143
+ def read(self) -> int:
144
+ if self.try_lock():
145
+ raise RuntimeError('Got lock')
146
+
147
+ self._f.seek(0)
148
+ return int(self._f.read())
149
+
150
+ def kill(self, sig: int = signal.SIGTERM) -> None:
151
+ pid = self.read()
152
+ os.kill(pid, sig)
omlish/secrets/crypto.py CHANGED
@@ -76,7 +76,6 @@ class Crypto(abc.ABC):
76
76
 
77
77
 
78
78
  class FernetCrypto(Crypto):
79
-
80
79
  def generate_key(self) -> bytes:
81
80
  return cry_fernet.Fernet.generate_key()
82
81
 
@@ -98,7 +97,7 @@ class FernetCrypto(Crypto):
98
97
  raise DecryptionError from e
99
98
 
100
99
 
101
- class AesgsmCrypto(Crypto):
100
+ class AesgcmCrypto(Crypto):
102
101
  """https://stackoverflow.com/a/59835994"""
103
102
 
104
103
  def generate_key(self) -> bytes:
omlish/secrets/openssl.py CHANGED
@@ -33,7 +33,7 @@ else:
33
33
  DEFAULT_KEY_SIZE = 64
34
34
 
35
35
 
36
- def generate_key(self, sz: int = DEFAULT_KEY_SIZE) -> bytes:
36
+ def generate_key(sz: int = DEFAULT_KEY_SIZE) -> bytes:
37
37
  # !! https://docs.openssl.org/3.0/man7/passphrase-encoding/
38
38
  # Must not contain null bytes!
39
39
  return secrets.token_hex(sz).encode('ascii')
omlish/secrets/tempssl.py CHANGED
@@ -6,9 +6,9 @@ import tempfile
6
6
  import typing as ta
7
7
 
8
8
  from ..lite.cached import cached_nullary
9
- from ..subprocesses import SubprocessRun
10
- from ..subprocesses import SubprocessRunOutput
11
- from ..subprocesses import subprocesses
9
+ from ..subprocesses.run import SubprocessRun
10
+ from ..subprocesses.run import SubprocessRunnable
11
+ from ..subprocesses.run import SubprocessRunOutput
12
12
  from .ssl import SslCert
13
13
 
14
14
 
@@ -18,7 +18,7 @@ class TempSslCert(ta.NamedTuple):
18
18
 
19
19
 
20
20
  @dc.dataclass(frozen=True)
21
- class TempSslCertGenerator:
21
+ class TempSslCertGenerator(SubprocessRunnable[TempSslCert]):
22
22
  @cached_nullary
23
23
  def temp_dir(self) -> str:
24
24
  return tempfile.mkdtemp()
@@ -64,9 +64,6 @@ class TempSslCertGenerator:
64
64
  temp_dir=self.temp_dir(),
65
65
  )
66
66
 
67
- def run(self) -> TempSslCert:
68
- return self.handle_run_output(subprocesses.run_(self.make_run()))
69
-
70
67
 
71
68
  def generate_temp_localhost_ssl_cert() -> TempSslCert:
72
69
  return TempSslCertGenerator().run()
@@ -17,3 +17,7 @@ class SocketHandler_(abc.ABC): # noqa
17
17
  @abc.abstractmethod
18
18
  def __call__(self, addr: SocketAddress, f: SocketIoPair) -> None:
19
19
  raise NotImplementedError
20
+
21
+
22
+ class SocketHandlerClose(Exception): # noqa
23
+ pass
@@ -3,6 +3,7 @@
3
3
  import abc
4
4
  import concurrent.futures as cf
5
5
  import dataclasses as dc
6
+ import logging
6
7
  import socket
7
8
  import typing as ta
8
9
 
@@ -132,3 +133,24 @@ class ExecutorSocketServerHandler(SocketServerHandler_):
132
133
 
133
134
  def __call__(self, conn: SocketAndAddress) -> None:
134
135
  self.executor.submit(self.handler, conn)
136
+
137
+
138
+ #
139
+
140
+
141
+ @dc.dataclass(frozen=True)
142
+ class ExceptionLoggingSocketServerHandler(SocketServerHandler_):
143
+ handler: SocketServerHandler
144
+ log: logging.Logger
145
+
146
+ ignored: ta.Optional[ta.Container[ta.Type[Exception]]] = None
147
+
148
+ def __call__(self, conn: SocketAndAddress) -> None:
149
+ try:
150
+ return self.handler(conn)
151
+
152
+ except Exception as e: # noqa
153
+ if (ignored := self.ignored) is None or type(e) not in ignored:
154
+ self.log.exception('Error in handler %r for conn %r', self.handler, conn)
155
+
156
+ raise
File without changes
@@ -0,0 +1,97 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import abc
4
+ import sys
5
+ import typing as ta
6
+
7
+ from ..lite.timeouts import TimeoutLike
8
+ from .base import BaseSubprocesses
9
+ from .run import SubprocessRun
10
+ from .run import SubprocessRunOutput
11
+
12
+
13
+ ##
14
+
15
+
16
+ class AbstractAsyncSubprocesses(BaseSubprocesses):
17
+ @abc.abstractmethod
18
+ async def run_(self, run: SubprocessRun) -> SubprocessRunOutput:
19
+ raise NotImplementedError
20
+
21
+ def run(
22
+ self,
23
+ *cmd: str,
24
+ input: ta.Any = None, # noqa
25
+ timeout: ta.Optional[TimeoutLike] = None,
26
+ check: bool = False,
27
+ capture_output: ta.Optional[bool] = None,
28
+ **kwargs: ta.Any,
29
+ ) -> ta.Awaitable[SubprocessRunOutput]:
30
+ return self.run_(SubprocessRun(
31
+ cmd=cmd,
32
+ input=input,
33
+ timeout=timeout,
34
+ check=check,
35
+ capture_output=capture_output,
36
+ kwargs=kwargs,
37
+ ))
38
+
39
+ #
40
+
41
+ @abc.abstractmethod
42
+ async def check_call(
43
+ self,
44
+ *cmd: str,
45
+ stdout: ta.Any = sys.stderr,
46
+ **kwargs: ta.Any,
47
+ ) -> None:
48
+ raise NotImplementedError
49
+
50
+ @abc.abstractmethod
51
+ async def check_output(
52
+ self,
53
+ *cmd: str,
54
+ **kwargs: ta.Any,
55
+ ) -> bytes:
56
+ raise NotImplementedError
57
+
58
+ #
59
+
60
+ async def check_output_str(
61
+ self,
62
+ *cmd: str,
63
+ **kwargs: ta.Any,
64
+ ) -> str:
65
+ return (await self.check_output(*cmd, **kwargs)).decode().strip()
66
+
67
+ #
68
+
69
+ async def try_call(
70
+ self,
71
+ *cmd: str,
72
+ **kwargs: ta.Any,
73
+ ) -> bool:
74
+ if isinstance(await self.async_try_fn(self.check_call, *cmd, **kwargs), Exception):
75
+ return False
76
+ else:
77
+ return True
78
+
79
+ async def try_output(
80
+ self,
81
+ *cmd: str,
82
+ **kwargs: ta.Any,
83
+ ) -> ta.Optional[bytes]:
84
+ if isinstance(ret := await self.async_try_fn(self.check_output, *cmd, **kwargs), Exception):
85
+ return None
86
+ else:
87
+ return ret
88
+
89
+ async def try_output_str(
90
+ self,
91
+ *cmd: str,
92
+ **kwargs: ta.Any,
93
+ ) -> ta.Optional[str]:
94
+ if (ret := await self.try_output(*cmd, **kwargs)) is None:
95
+ return None
96
+ else:
97
+ return ret.decode().strip()
@@ -0,0 +1,221 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import abc
4
+ import contextlib
5
+ import logging
6
+ import os
7
+ import subprocess
8
+ import time
9
+ import typing as ta
10
+
11
+ from ..lite.timeouts import Timeout
12
+ from .wrap import subprocess_maybe_shell_wrap_exec
13
+
14
+
15
+ T = ta.TypeVar('T')
16
+ SubprocessChannelOption = ta.Literal['pipe', 'stdout', 'devnull'] # ta.TypeAlias
17
+
18
+
19
+ ##
20
+
21
+
22
+ # Valid channel type kwarg values:
23
+ # - A special flag negative int
24
+ # - A positive fd int
25
+ # - A file-like object
26
+ # - None
27
+
28
+ SUBPROCESS_CHANNEL_OPTION_VALUES: ta.Mapping[SubprocessChannelOption, int] = {
29
+ 'pipe': subprocess.PIPE,
30
+ 'stdout': subprocess.STDOUT,
31
+ 'devnull': subprocess.DEVNULL,
32
+ }
33
+
34
+
35
+ ##
36
+
37
+
38
+ class VerboseCalledProcessError(subprocess.CalledProcessError):
39
+ @classmethod
40
+ def from_std(cls, e: subprocess.CalledProcessError) -> 'VerboseCalledProcessError':
41
+ return cls(
42
+ e.returncode,
43
+ e.cmd,
44
+ output=e.output,
45
+ stderr=e.stderr,
46
+ )
47
+
48
+ def __str__(self) -> str:
49
+ msg = super().__str__()
50
+ if self.output is not None:
51
+ msg += f' Output: {self.output!r}'
52
+ if self.stderr is not None:
53
+ msg += f' Stderr: {self.stderr!r}'
54
+ return msg
55
+
56
+
57
+ class BaseSubprocesses(abc.ABC): # noqa
58
+ DEFAULT_LOGGER: ta.ClassVar[ta.Optional[logging.Logger]] = None
59
+
60
+ def __init__(
61
+ self,
62
+ *,
63
+ log: ta.Optional[logging.Logger] = None,
64
+ try_exceptions: ta.Optional[ta.Tuple[ta.Type[Exception], ...]] = None,
65
+ ) -> None:
66
+ super().__init__()
67
+
68
+ self._log = log if log is not None else self.DEFAULT_LOGGER
69
+ self._try_exceptions = try_exceptions if try_exceptions is not None else self.DEFAULT_TRY_EXCEPTIONS
70
+
71
+ def set_logger(self, log: ta.Optional[logging.Logger]) -> None:
72
+ self._log = log
73
+
74
+ #
75
+
76
+ def prepare_args(
77
+ self,
78
+ *cmd: str,
79
+ env: ta.Optional[ta.Mapping[str, ta.Any]] = None,
80
+ extra_env: ta.Optional[ta.Mapping[str, ta.Any]] = None,
81
+ quiet: bool = False,
82
+ shell: bool = False,
83
+ **kwargs: ta.Any,
84
+ ) -> ta.Tuple[ta.Tuple[ta.Any, ...], ta.Dict[str, ta.Any]]:
85
+ if self._log:
86
+ self._log.debug('Subprocesses.prepare_args: cmd=%r', cmd)
87
+ if extra_env:
88
+ self._log.debug('Subprocesses.prepare_args: extra_env=%r', extra_env)
89
+
90
+ #
91
+
92
+ if extra_env:
93
+ env = {**(env if env is not None else os.environ), **extra_env}
94
+
95
+ #
96
+
97
+ if quiet and 'stderr' not in kwargs:
98
+ if self._log and not self._log.isEnabledFor(logging.DEBUG):
99
+ kwargs['stderr'] = subprocess.DEVNULL
100
+
101
+ for chk in ('stdout', 'stderr'):
102
+ try:
103
+ chv = kwargs[chk]
104
+ except KeyError:
105
+ continue
106
+ kwargs[chk] = SUBPROCESS_CHANNEL_OPTION_VALUES.get(chv, chv)
107
+
108
+ #
109
+
110
+ if not shell:
111
+ cmd = subprocess_maybe_shell_wrap_exec(*cmd)
112
+
113
+ #
114
+
115
+ if 'timeout' in kwargs:
116
+ kwargs['timeout'] = Timeout.of(kwargs['timeout']).or_(None)
117
+
118
+ #
119
+
120
+ return cmd, dict(
121
+ env=env,
122
+ shell=shell,
123
+ **kwargs,
124
+ )
125
+
126
+ @contextlib.contextmanager
127
+ def wrap_call(
128
+ self,
129
+ *cmd: ta.Any,
130
+ raise_verbose: bool = False,
131
+ **kwargs: ta.Any,
132
+ ) -> ta.Iterator[None]:
133
+ start_time = time.time()
134
+ try:
135
+ if self._log:
136
+ self._log.debug('Subprocesses.wrap_call.try: cmd=%r', cmd)
137
+
138
+ yield
139
+
140
+ except Exception as exc: # noqa
141
+ if self._log:
142
+ self._log.debug('Subprocesses.wrap_call.except: exc=%r', exc)
143
+
144
+ if (
145
+ raise_verbose and
146
+ isinstance(exc, subprocess.CalledProcessError) and
147
+ not isinstance(exc, VerboseCalledProcessError) and
148
+ (exc.output is not None or exc.stderr is not None)
149
+ ):
150
+ raise VerboseCalledProcessError.from_std(exc) from exc
151
+
152
+ raise
153
+
154
+ finally:
155
+ end_time = time.time()
156
+ elapsed_s = end_time - start_time
157
+
158
+ if self._log:
159
+ self._log.debug('Subprocesses.wrap_call.finally: elapsed_s=%f cmd=%r', elapsed_s, cmd)
160
+
161
+ @contextlib.contextmanager
162
+ def prepare_and_wrap(
163
+ self,
164
+ *cmd: ta.Any,
165
+ raise_verbose: bool = False,
166
+ **kwargs: ta.Any,
167
+ ) -> ta.Iterator[ta.Tuple[
168
+ ta.Tuple[ta.Any, ...],
169
+ ta.Dict[str, ta.Any],
170
+ ]]:
171
+ cmd, kwargs = self.prepare_args(*cmd, **kwargs)
172
+
173
+ with self.wrap_call(
174
+ *cmd,
175
+ raise_verbose=raise_verbose,
176
+ **kwargs,
177
+ ):
178
+ yield cmd, kwargs
179
+
180
+ #
181
+
182
+ DEFAULT_TRY_EXCEPTIONS: ta.Tuple[ta.Type[Exception], ...] = (
183
+ FileNotFoundError,
184
+ subprocess.CalledProcessError,
185
+ )
186
+
187
+ def try_fn(
188
+ self,
189
+ fn: ta.Callable[..., T],
190
+ *cmd: str,
191
+ try_exceptions: ta.Optional[ta.Tuple[ta.Type[Exception], ...]] = None,
192
+ **kwargs: ta.Any,
193
+ ) -> ta.Union[T, Exception]:
194
+ if try_exceptions is None:
195
+ try_exceptions = self._try_exceptions
196
+
197
+ try:
198
+ return fn(*cmd, **kwargs)
199
+
200
+ except try_exceptions as e: # noqa
201
+ if self._log and self._log.isEnabledFor(logging.DEBUG):
202
+ self._log.exception('command failed')
203
+ return e
204
+
205
+ async def async_try_fn(
206
+ self,
207
+ fn: ta.Callable[..., ta.Awaitable[T]],
208
+ *cmd: ta.Any,
209
+ try_exceptions: ta.Optional[ta.Tuple[ta.Type[Exception], ...]] = None,
210
+ **kwargs: ta.Any,
211
+ ) -> ta.Union[T, Exception]:
212
+ if try_exceptions is None:
213
+ try_exceptions = self._try_exceptions
214
+
215
+ try:
216
+ return await fn(*cmd, **kwargs)
217
+
218
+ except try_exceptions as e: # noqa
219
+ if self._log and self._log.isEnabledFor(logging.DEBUG):
220
+ self._log.exception('command failed')
221
+ return e
@@ -0,0 +1,138 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import abc
4
+ import dataclasses as dc
5
+ import typing as ta
6
+
7
+ from ..lite.check import check
8
+ from ..lite.timeouts import TimeoutLike
9
+
10
+
11
+ T = ta.TypeVar('T')
12
+
13
+
14
+ ##
15
+
16
+
17
+ @dc.dataclass(frozen=True)
18
+ class SubprocessRunOutput(ta.Generic[T]):
19
+ proc: T
20
+
21
+ returncode: int # noqa
22
+
23
+ stdout: ta.Optional[bytes] = None
24
+ stderr: ta.Optional[bytes] = None
25
+
26
+
27
+ ##
28
+
29
+
30
+ @dc.dataclass(frozen=True)
31
+ class SubprocessRun:
32
+ cmd: ta.Sequence[str]
33
+ input: ta.Any = None
34
+ timeout: ta.Optional[TimeoutLike] = None
35
+ check: bool = False
36
+ capture_output: ta.Optional[bool] = None
37
+ kwargs: ta.Optional[ta.Mapping[str, ta.Any]] = None
38
+
39
+ #
40
+
41
+ _FIELD_NAMES: ta.ClassVar[ta.FrozenSet[str]]
42
+
43
+ def replace(self, **kwargs: ta.Any) -> 'SubprocessRun':
44
+ if not kwargs:
45
+ return self
46
+
47
+ field_kws = {}
48
+ extra_kws = {}
49
+ for k, v in kwargs.items():
50
+ if k in self._FIELD_NAMES:
51
+ field_kws[k] = v
52
+ else:
53
+ extra_kws[k] = v
54
+
55
+ return dc.replace(self, **{
56
+ **dict(kwargs={
57
+ **(self.kwargs or {}),
58
+ **extra_kws,
59
+ }),
60
+ **field_kws, # passing a kwarg named 'kwargs' intentionally clobbers
61
+ })
62
+
63
+ #
64
+
65
+ @classmethod
66
+ def of(
67
+ cls,
68
+ *cmd: str,
69
+ input: ta.Any = None, # noqa
70
+ timeout: ta.Optional[TimeoutLike] = None,
71
+ check: bool = False, # noqa
72
+ capture_output: ta.Optional[bool] = None,
73
+ **kwargs: ta.Any,
74
+ ) -> 'SubprocessRun':
75
+ return cls(
76
+ cmd=cmd,
77
+ input=input,
78
+ timeout=timeout,
79
+ check=check,
80
+ capture_output=capture_output,
81
+ kwargs=kwargs,
82
+ )
83
+
84
+ #
85
+
86
+ _DEFAULT_SUBPROCESSES: ta.ClassVar[ta.Optional[ta.Any]] = None # AbstractSubprocesses
87
+
88
+ def run(
89
+ self,
90
+ subprocesses: ta.Optional[ta.Any] = None, # AbstractSubprocesses
91
+ **kwargs: ta.Any,
92
+ ) -> SubprocessRunOutput:
93
+ if subprocesses is None:
94
+ subprocesses = self._DEFAULT_SUBPROCESSES
95
+ return check.not_none(subprocesses).run_(self.replace(**kwargs)) # type: ignore[attr-defined]
96
+
97
+ _DEFAULT_ASYNC_SUBPROCESSES: ta.ClassVar[ta.Optional[ta.Any]] = None # AbstractAsyncSubprocesses
98
+
99
+ async def async_run(
100
+ self,
101
+ async_subprocesses: ta.Optional[ta.Any] = None, # AbstractAsyncSubprocesses
102
+ **kwargs: ta.Any,
103
+ ) -> SubprocessRunOutput:
104
+ if async_subprocesses is None:
105
+ async_subprocesses = self._DEFAULT_ASYNC_SUBPROCESSES
106
+ return await check.not_none(async_subprocesses).run_(self.replace(**kwargs)) # type: ignore[attr-defined]
107
+
108
+
109
+ SubprocessRun._FIELD_NAMES = frozenset(fld.name for fld in dc.fields(SubprocessRun)) # noqa
110
+
111
+
112
+ ##
113
+
114
+
115
+ class SubprocessRunnable(abc.ABC, ta.Generic[T]):
116
+ @abc.abstractmethod
117
+ def make_run(self) -> SubprocessRun:
118
+ raise NotImplementedError
119
+
120
+ @abc.abstractmethod
121
+ def handle_run_output(self, output: SubprocessRunOutput) -> T:
122
+ raise NotImplementedError
123
+
124
+ #
125
+
126
+ def run(
127
+ self,
128
+ subprocesses: ta.Optional[ta.Any] = None, # AbstractSubprocesses
129
+ **kwargs: ta.Any,
130
+ ) -> T:
131
+ return self.handle_run_output(self.make_run().run(subprocesses, **kwargs))
132
+
133
+ async def async_run(
134
+ self,
135
+ async_subprocesses: ta.Optional[ta.Any] = None, # AbstractAsyncSubprocesses
136
+ **kwargs: ta.Any,
137
+ ) -> T:
138
+ return self.handle_run_output(await self.make_run().async_run(async_subprocesses, **kwargs))