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

Sign up to get free protection for your applications and to get access to all the features.
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))