omlish 0.0.0.dev223__py3-none-any.whl → 0.0.0.dev225__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 (39) hide show
  1. omlish/__about__.py +2 -2
  2. omlish/asyncs/asyncio/subprocesses.py +15 -15
  3. omlish/asyncs/asyncs.py +0 -1
  4. omlish/bootstrap/sys.py +2 -2
  5. omlish/dataclasses/impl/metaclass.py +5 -0
  6. omlish/http/coro/server.py +5 -54
  7. omlish/http/coro/simple.py +1 -1
  8. omlish/http/coro/sockets.py +59 -0
  9. omlish/http/handlers.py +52 -1
  10. omlish/lang/__init__.py +1 -0
  11. omlish/lang/imports.py +22 -0
  12. omlish/libc.py +10 -0
  13. omlish/lite/timing.py +8 -0
  14. omlish/logs/timing.py +58 -0
  15. omlish/multiprocessing/__init__.py +0 -7
  16. omlish/os/pidfiles/__init__.py +0 -0
  17. omlish/os/pidfiles/manager.py +97 -0
  18. omlish/os/pidfiles/pidfile.py +142 -0
  19. omlish/secrets/crypto.py +1 -2
  20. omlish/secrets/openssl.py +1 -1
  21. omlish/secrets/tempssl.py +40 -21
  22. omlish/sockets/handlers.py +4 -0
  23. omlish/sockets/server/handlers.py +22 -0
  24. omlish/subprocesses/__init__.py +0 -0
  25. omlish/subprocesses/async_.py +96 -0
  26. omlish/subprocesses/base.py +215 -0
  27. omlish/subprocesses/run.py +98 -0
  28. omlish/subprocesses/sync.py +147 -0
  29. omlish/subprocesses/utils.py +22 -0
  30. omlish/subprocesses/wrap.py +23 -0
  31. {omlish-0.0.0.dev223.dist-info → omlish-0.0.0.dev225.dist-info}/METADATA +1 -1
  32. {omlish-0.0.0.dev223.dist-info → omlish-0.0.0.dev225.dist-info}/RECORD +37 -26
  33. omlish/os/pidfile.py +0 -69
  34. omlish/subprocesses.py +0 -491
  35. /omlish/{multiprocessing → os}/death.py +0 -0
  36. {omlish-0.0.0.dev223.dist-info → omlish-0.0.0.dev225.dist-info}/LICENSE +0 -0
  37. {omlish-0.0.0.dev223.dist-info → omlish-0.0.0.dev225.dist-info}/WHEEL +0 -0
  38. {omlish-0.0.0.dev223.dist-info → omlish-0.0.0.dev225.dist-info}/entry_points.txt +0 -0
  39. {omlish-0.0.0.dev223.dist-info → omlish-0.0.0.dev225.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,142 @@
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
+ """
14
+ import fcntl
15
+ import os
16
+ import signal
17
+ import typing as ta
18
+
19
+
20
+ ##
21
+
22
+
23
+ class Pidfile:
24
+ def __init__(
25
+ self,
26
+ path: str,
27
+ *,
28
+ inheritable: bool = True,
29
+ ) -> None:
30
+ super().__init__()
31
+
32
+ self._path = path
33
+ self._inheritable = inheritable
34
+
35
+ def __repr__(self) -> str:
36
+ return f'{self.__class__.__name__}({self._path!r})'
37
+
38
+ #
39
+
40
+ _f: ta.TextIO
41
+
42
+ def fileno(self) -> ta.Optional[int]:
43
+ if hasattr(self, '_f'):
44
+ return self._f.fileno()
45
+ else:
46
+ return None
47
+
48
+ #
49
+
50
+ def __enter__(self) -> 'Pidfile':
51
+ fd = os.open(self._path, os.O_RDWR | os.O_CREAT, 0o600)
52
+
53
+ try:
54
+ if self._inheritable:
55
+ os.set_inheritable(fd, True)
56
+
57
+ f = os.fdopen(fd, 'r+')
58
+
59
+ except Exception:
60
+ try:
61
+ os.close(fd)
62
+ except Exception: # noqa
63
+ pass
64
+ raise
65
+
66
+ self._f = f
67
+ return self
68
+
69
+ def __exit__(self, exc_type, exc_val, exc_tb):
70
+ self.close()
71
+
72
+ #
73
+
74
+ def __getstate__(self):
75
+ state = self.__dict__.copy()
76
+
77
+ if '_f' in state:
78
+ if os.get_inheritable(fd := state.pop('_f').fileno()):
79
+ state['__fd'] = fd
80
+
81
+ return state
82
+
83
+ def __setstate__(self, state):
84
+ if '_f' in state:
85
+ raise RuntimeError
86
+
87
+ if '__fd' in state:
88
+ state['_f'] = os.fdopen(state.pop('__fd'), 'r+')
89
+
90
+ self.__dict__.update(state)
91
+
92
+ #
93
+
94
+ def close(self) -> bool:
95
+ if not hasattr(self, '_f'):
96
+ return False
97
+
98
+ self._f.close()
99
+ del self._f
100
+ return True
101
+
102
+ def try_lock(self) -> bool:
103
+ try:
104
+ fcntl.flock(self._f, fcntl.LOCK_EX | fcntl.LOCK_NB)
105
+ return True
106
+
107
+ except OSError:
108
+ return False
109
+
110
+ def ensure_locked(self) -> None:
111
+ if not self.try_lock():
112
+ raise RuntimeError('Could not get lock')
113
+
114
+ #
115
+
116
+ def write(self, pid: ta.Optional[int] = None) -> None:
117
+ self.ensure_locked()
118
+
119
+ if pid is None:
120
+ pid = os.getpid()
121
+
122
+ self._f.seek(0)
123
+ self._f.truncate()
124
+ self._f.write(f'{pid}\n')
125
+ self._f.flush()
126
+
127
+ def clear(self) -> None:
128
+ self.ensure_locked()
129
+
130
+ self._f.seek(0)
131
+ self._f.truncate()
132
+
133
+ def read(self) -> int:
134
+ if self.try_lock():
135
+ raise RuntimeError('Got lock')
136
+
137
+ self._f.seek(0)
138
+ return int(self._f.read()) # FIXME: could be empty or hold old value, race w proc start
139
+
140
+ def kill(self, sig: int = signal.SIGTERM) -> None:
141
+ pid = self.read()
142
+ os.kill(pid, sig) # FIXME: Still racy - pidfd_send_signal?
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
@@ -1,10 +1,14 @@
1
1
  # @omlish-lite
2
2
  # ruff: noqa: UP006 UP007
3
+ import dataclasses as dc
3
4
  import os.path
4
- import subprocess
5
5
  import tempfile
6
6
  import typing as ta
7
7
 
8
+ from ..lite.cached import cached_nullary
9
+ from ..subprocesses.run import SubprocessRun
10
+ from ..subprocesses.run import SubprocessRunnable
11
+ from ..subprocesses.run import SubprocessRunOutput
8
12
  from .ssl import SslCert
9
13
 
10
14
 
@@ -13,11 +17,15 @@ class TempSslCert(ta.NamedTuple):
13
17
  temp_dir: str
14
18
 
15
19
 
16
- def generate_temp_localhost_ssl_cert() -> TempSslCert:
17
- temp_dir = tempfile.mkdtemp()
20
+ @dc.dataclass(frozen=True)
21
+ class TempSslCertGenerator(SubprocessRunnable[TempSslCert]):
22
+ @cached_nullary
23
+ def temp_dir(self) -> str:
24
+ return tempfile.mkdtemp()
18
25
 
19
- proc = subprocess.run(
20
- [
26
+ @cached_nullary
27
+ def make_run(self) -> SubprocessRun:
28
+ return SubprocessRun.of(
21
29
  'openssl',
22
30
  'req',
23
31
  '-x509',
@@ -32,19 +40,30 @@ def generate_temp_localhost_ssl_cert() -> TempSslCert:
32
40
 
33
41
  '-subj', '/CN=localhost',
34
42
  '-addext', 'subjectAltName = DNS:localhost,IP:127.0.0.1',
35
- ],
36
- cwd=temp_dir,
37
- capture_output=True,
38
- check=False,
39
- )
40
-
41
- if proc.returncode:
42
- raise RuntimeError(f'Failed to generate temp ssl cert: {proc.stderr=}')
43
-
44
- return TempSslCert(
45
- SslCert(
46
- key_file=os.path.join(temp_dir, 'key.pem'),
47
- cert_file=os.path.join(temp_dir, 'cert.pem'),
48
- ),
49
- temp_dir,
50
- )
43
+
44
+ cwd=self.temp_dir(),
45
+ capture_output=True,
46
+ check=False,
47
+ )
48
+
49
+ def handle_run_output(self, proc: SubprocessRunOutput) -> TempSslCert:
50
+ if proc.returncode:
51
+ raise RuntimeError(f'Failed to generate temp ssl cert: {proc.stderr=}')
52
+
53
+ key_file = os.path.join(self.temp_dir(), 'key.pem')
54
+ cert_file = os.path.join(self.temp_dir(), 'cert.pem')
55
+ for file in [key_file, cert_file]:
56
+ if not os.path.isfile(file):
57
+ raise RuntimeError(f'Failed to generate temp ssl cert (file not found): {file}')
58
+
59
+ return TempSslCert(
60
+ SslCert(
61
+ key_file=key_file,
62
+ cert_file=cert_file,
63
+ ),
64
+ temp_dir=self.temp_dir(),
65
+ )
66
+
67
+
68
+ def generate_temp_localhost_ssl_cert() -> TempSslCert:
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,96 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import abc
4
+ import sys
5
+ import typing as ta
6
+
7
+ from .base import BaseSubprocesses
8
+ from .run import SubprocessRun
9
+ from .run import SubprocessRunOutput
10
+
11
+
12
+ ##
13
+
14
+
15
+ class AbstractAsyncSubprocesses(BaseSubprocesses):
16
+ @abc.abstractmethod
17
+ async def run_(self, run: SubprocessRun) -> SubprocessRunOutput:
18
+ raise NotImplementedError
19
+
20
+ def run(
21
+ self,
22
+ *cmd: str,
23
+ input: ta.Any = None, # noqa
24
+ timeout: ta.Optional[float] = None,
25
+ check: bool = False,
26
+ capture_output: ta.Optional[bool] = None,
27
+ **kwargs: ta.Any,
28
+ ) -> ta.Awaitable[SubprocessRunOutput]:
29
+ return self.run_(SubprocessRun(
30
+ cmd=cmd,
31
+ input=input,
32
+ timeout=timeout,
33
+ check=check,
34
+ capture_output=capture_output,
35
+ kwargs=kwargs,
36
+ ))
37
+
38
+ #
39
+
40
+ @abc.abstractmethod
41
+ async def check_call(
42
+ self,
43
+ *cmd: str,
44
+ stdout: ta.Any = sys.stderr,
45
+ **kwargs: ta.Any,
46
+ ) -> None:
47
+ raise NotImplementedError
48
+
49
+ @abc.abstractmethod
50
+ async def check_output(
51
+ self,
52
+ *cmd: str,
53
+ **kwargs: ta.Any,
54
+ ) -> bytes:
55
+ raise NotImplementedError
56
+
57
+ #
58
+
59
+ async def check_output_str(
60
+ self,
61
+ *cmd: str,
62
+ **kwargs: ta.Any,
63
+ ) -> str:
64
+ return (await self.check_output(*cmd, **kwargs)).decode().strip()
65
+
66
+ #
67
+
68
+ async def try_call(
69
+ self,
70
+ *cmd: str,
71
+ **kwargs: ta.Any,
72
+ ) -> bool:
73
+ if isinstance(await self.async_try_fn(self.check_call, *cmd, **kwargs), Exception):
74
+ return False
75
+ else:
76
+ return True
77
+
78
+ async def try_output(
79
+ self,
80
+ *cmd: str,
81
+ **kwargs: ta.Any,
82
+ ) -> ta.Optional[bytes]:
83
+ if isinstance(ret := await self.async_try_fn(self.check_output, *cmd, **kwargs), Exception):
84
+ return None
85
+ else:
86
+ return ret
87
+
88
+ async def try_output_str(
89
+ self,
90
+ *cmd: str,
91
+ **kwargs: ta.Any,
92
+ ) -> ta.Optional[str]:
93
+ if (ret := await self.try_output(*cmd, **kwargs)) is None:
94
+ return None
95
+ else:
96
+ return ret.decode().strip()
@@ -0,0 +1,215 @@
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 .wrap import subprocess_maybe_shell_wrap_exec
12
+
13
+
14
+ T = ta.TypeVar('T')
15
+ SubprocessChannelOption = ta.Literal['pipe', 'stdout', 'devnull'] # ta.TypeAlias
16
+
17
+
18
+ ##
19
+
20
+
21
+ # Valid channel type kwarg values:
22
+ # - A special flag negative int
23
+ # - A positive fd int
24
+ # - A file-like object
25
+ # - None
26
+
27
+ SUBPROCESS_CHANNEL_OPTION_VALUES: ta.Mapping[SubprocessChannelOption, int] = {
28
+ 'pipe': subprocess.PIPE,
29
+ 'stdout': subprocess.STDOUT,
30
+ 'devnull': subprocess.DEVNULL,
31
+ }
32
+
33
+
34
+ ##
35
+
36
+
37
+ class VerboseCalledProcessError(subprocess.CalledProcessError):
38
+ @classmethod
39
+ def from_std(cls, e: subprocess.CalledProcessError) -> 'VerboseCalledProcessError':
40
+ return cls(
41
+ e.returncode,
42
+ e.cmd,
43
+ output=e.output,
44
+ stderr=e.stderr,
45
+ )
46
+
47
+ def __str__(self) -> str:
48
+ msg = super().__str__()
49
+ if self.output is not None:
50
+ msg += f' Output: {self.output!r}'
51
+ if self.stderr is not None:
52
+ msg += f' Stderr: {self.stderr!r}'
53
+ return msg
54
+
55
+
56
+ class BaseSubprocesses(abc.ABC): # noqa
57
+ DEFAULT_LOGGER: ta.ClassVar[ta.Optional[logging.Logger]] = None
58
+
59
+ def __init__(
60
+ self,
61
+ *,
62
+ log: ta.Optional[logging.Logger] = None,
63
+ try_exceptions: ta.Optional[ta.Tuple[ta.Type[Exception], ...]] = None,
64
+ ) -> None:
65
+ super().__init__()
66
+
67
+ self._log = log if log is not None else self.DEFAULT_LOGGER
68
+ self._try_exceptions = try_exceptions if try_exceptions is not None else self.DEFAULT_TRY_EXCEPTIONS
69
+
70
+ def set_logger(self, log: ta.Optional[logging.Logger]) -> None:
71
+ self._log = log
72
+
73
+ #
74
+
75
+ def prepare_args(
76
+ self,
77
+ *cmd: str,
78
+ env: ta.Optional[ta.Mapping[str, ta.Any]] = None,
79
+ extra_env: ta.Optional[ta.Mapping[str, ta.Any]] = None,
80
+ quiet: bool = False,
81
+ shell: bool = False,
82
+ **kwargs: ta.Any,
83
+ ) -> ta.Tuple[ta.Tuple[ta.Any, ...], ta.Dict[str, ta.Any]]:
84
+ if self._log:
85
+ self._log.debug('Subprocesses.prepare_args: cmd=%r', cmd)
86
+ if extra_env:
87
+ self._log.debug('Subprocesses.prepare_args: extra_env=%r', extra_env)
88
+
89
+ #
90
+
91
+ if extra_env:
92
+ env = {**(env if env is not None else os.environ), **extra_env}
93
+
94
+ #
95
+
96
+ if quiet and 'stderr' not in kwargs:
97
+ if self._log and not self._log.isEnabledFor(logging.DEBUG):
98
+ kwargs['stderr'] = subprocess.DEVNULL
99
+
100
+ for chk in ('stdout', 'stderr'):
101
+ try:
102
+ chv = kwargs[chk]
103
+ except KeyError:
104
+ continue
105
+ kwargs[chk] = SUBPROCESS_CHANNEL_OPTION_VALUES.get(chv, chv)
106
+
107
+ #
108
+
109
+ if not shell:
110
+ cmd = subprocess_maybe_shell_wrap_exec(*cmd)
111
+
112
+ #
113
+
114
+ return cmd, dict(
115
+ env=env,
116
+ shell=shell,
117
+ **kwargs,
118
+ )
119
+
120
+ @contextlib.contextmanager
121
+ def wrap_call(
122
+ self,
123
+ *cmd: ta.Any,
124
+ raise_verbose: bool = False,
125
+ **kwargs: ta.Any,
126
+ ) -> ta.Iterator[None]:
127
+ start_time = time.time()
128
+ try:
129
+ if self._log:
130
+ self._log.debug('Subprocesses.wrap_call.try: cmd=%r', cmd)
131
+
132
+ yield
133
+
134
+ except Exception as exc: # noqa
135
+ if self._log:
136
+ self._log.debug('Subprocesses.wrap_call.except: exc=%r', exc)
137
+
138
+ if (
139
+ raise_verbose and
140
+ isinstance(exc, subprocess.CalledProcessError) and
141
+ not isinstance(exc, VerboseCalledProcessError) and
142
+ (exc.output is not None or exc.stderr is not None)
143
+ ):
144
+ raise VerboseCalledProcessError.from_std(exc) from exc
145
+
146
+ raise
147
+
148
+ finally:
149
+ end_time = time.time()
150
+ elapsed_s = end_time - start_time
151
+
152
+ if self._log:
153
+ self._log.debug('Subprocesses.wrap_call.finally: elapsed_s=%f cmd=%r', elapsed_s, cmd)
154
+
155
+ @contextlib.contextmanager
156
+ def prepare_and_wrap(
157
+ self,
158
+ *cmd: ta.Any,
159
+ raise_verbose: bool = False,
160
+ **kwargs: ta.Any,
161
+ ) -> ta.Iterator[ta.Tuple[
162
+ ta.Tuple[ta.Any, ...],
163
+ ta.Dict[str, ta.Any],
164
+ ]]:
165
+ cmd, kwargs = self.prepare_args(*cmd, **kwargs)
166
+
167
+ with self.wrap_call(
168
+ *cmd,
169
+ raise_verbose=raise_verbose,
170
+ **kwargs,
171
+ ):
172
+ yield cmd, kwargs
173
+
174
+ #
175
+
176
+ DEFAULT_TRY_EXCEPTIONS: ta.Tuple[ta.Type[Exception], ...] = (
177
+ FileNotFoundError,
178
+ subprocess.CalledProcessError,
179
+ )
180
+
181
+ def try_fn(
182
+ self,
183
+ fn: ta.Callable[..., T],
184
+ *cmd: str,
185
+ try_exceptions: ta.Optional[ta.Tuple[ta.Type[Exception], ...]] = None,
186
+ **kwargs: ta.Any,
187
+ ) -> ta.Union[T, Exception]:
188
+ if try_exceptions is None:
189
+ try_exceptions = self._try_exceptions
190
+
191
+ try:
192
+ return fn(*cmd, **kwargs)
193
+
194
+ except try_exceptions as e: # noqa
195
+ if self._log and self._log.isEnabledFor(logging.DEBUG):
196
+ self._log.exception('command failed')
197
+ return e
198
+
199
+ async def async_try_fn(
200
+ self,
201
+ fn: ta.Callable[..., ta.Awaitable[T]],
202
+ *cmd: ta.Any,
203
+ try_exceptions: ta.Optional[ta.Tuple[ta.Type[Exception], ...]] = None,
204
+ **kwargs: ta.Any,
205
+ ) -> ta.Union[T, Exception]:
206
+ if try_exceptions is None:
207
+ try_exceptions = self._try_exceptions
208
+
209
+ try:
210
+ return await fn(*cmd, **kwargs)
211
+
212
+ except try_exceptions as e: # noqa
213
+ if self._log and self._log.isEnabledFor(logging.DEBUG):
214
+ self._log.exception('command failed')
215
+ return e
@@ -0,0 +1,98 @@
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
+
9
+
10
+ T = ta.TypeVar('T')
11
+
12
+
13
+ ##
14
+
15
+
16
+ @dc.dataclass(frozen=True)
17
+ class SubprocessRunOutput(ta.Generic[T]):
18
+ proc: T
19
+
20
+ returncode: int # noqa
21
+
22
+ stdout: ta.Optional[bytes] = None
23
+ stderr: ta.Optional[bytes] = None
24
+
25
+
26
+ ##
27
+
28
+
29
+ @dc.dataclass(frozen=True)
30
+ class SubprocessRun:
31
+ cmd: ta.Sequence[str]
32
+ input: ta.Any = None
33
+ timeout: ta.Optional[float] = None
34
+ check: bool = False
35
+ capture_output: ta.Optional[bool] = None
36
+ kwargs: ta.Optional[ta.Mapping[str, ta.Any]] = None
37
+
38
+ @classmethod
39
+ def of(
40
+ cls,
41
+ *cmd: str,
42
+ input: ta.Any = None, # noqa
43
+ timeout: ta.Optional[float] = None,
44
+ check: bool = False, # noqa
45
+ capture_output: ta.Optional[bool] = None,
46
+ **kwargs: ta.Any,
47
+ ) -> 'SubprocessRun':
48
+ return cls(
49
+ cmd=cmd,
50
+ input=input,
51
+ timeout=timeout,
52
+ check=check,
53
+ capture_output=capture_output,
54
+ kwargs=kwargs,
55
+ )
56
+
57
+ #
58
+
59
+ _DEFAULT_SUBPROCESSES: ta.ClassVar[ta.Optional[ta.Any]] = None # AbstractSubprocesses
60
+
61
+ def run(
62
+ self,
63
+ subprocesses: ta.Optional[ta.Any] = None, # AbstractSubprocesses
64
+ ) -> SubprocessRunOutput:
65
+ if subprocesses is None:
66
+ subprocesses = self._DEFAULT_SUBPROCESSES
67
+ return check.not_none(subprocesses).run_(self) # type: ignore[attr-defined]
68
+
69
+ _DEFAULT_ASYNC_SUBPROCESSES: ta.ClassVar[ta.Optional[ta.Any]] = None # AbstractAsyncSubprocesses
70
+
71
+ async def async_run(
72
+ self,
73
+ async_subprocesses: ta.Optional[ta.Any] = None, # AbstractAsyncSubprocesses
74
+ ) -> SubprocessRunOutput:
75
+ if async_subprocesses is None:
76
+ async_subprocesses = self._DEFAULT_ASYNC_SUBPROCESSES
77
+ return await check.not_none(async_subprocesses).run_(self) # type: ignore[attr-defined]
78
+
79
+
80
+ ##
81
+
82
+
83
+ class SubprocessRunnable(abc.ABC, ta.Generic[T]):
84
+ @abc.abstractmethod
85
+ def make_run(self) -> SubprocessRun:
86
+ raise NotImplementedError
87
+
88
+ @abc.abstractmethod
89
+ def handle_run_output(self, output: SubprocessRunOutput) -> T:
90
+ raise NotImplementedError
91
+
92
+ #
93
+
94
+ def run(self, subprocesses: ta.Optional[ta.Any] = None) -> T: # AbstractSubprocesses
95
+ return self.handle_run_output(self.make_run().run(subprocesses))
96
+
97
+ async def async_run(self, async_subprocesses: ta.Optional[ta.Any] = None) -> T: # AbstractAsyncSubprocesses
98
+ return self.handle_run_output(await self.make_run().async_run(async_subprocesses))