exec-helpers 8.1.2__tar.gz → 8.2.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.
- exec_helpers-8.2.0/.pre-commit-config.yaml +34 -0
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/PKG-INFO +13 -17
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/classifiers.txt +3 -4
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers/__init__.py +1 -1
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers/_helpers.py +1 -1
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers/_ssh_base.py +25 -15
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers/_ssh_helpers.py +4 -5
- exec_helpers-8.2.0/exec_helpers/_version.py +24 -0
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers/api.py +16 -13
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers/async_api/api.py +13 -11
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers/async_api/subprocess.py +12 -10
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers/exceptions.py +1 -1
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers/exec_result.py +18 -26
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers/proc_enums.py +1 -1
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers/ssh_auth.py +9 -8
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers/subprocess.py +8 -8
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers.egg-info/PKG-INFO +13 -17
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers.egg-info/SOURCES.txt +0 -4
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers.egg-info/requires.txt +3 -6
- exec_helpers-8.2.0/pyproject.toml +425 -0
- exec_helpers-8.1.2/.pre-commit-config.yaml +0 -44
- exec_helpers-8.1.2/exec_helpers/_version.py +0 -16
- exec_helpers-8.1.2/flake8_requirements.txt +0 -14
- exec_helpers-8.1.2/mypy_requirements.txt +0 -1
- exec_helpers-8.1.2/pyproject.toml +0 -269
- exec_helpers-8.1.2/pytest_requirements.txt +0 -6
- exec_helpers-8.1.2/setup.py +0 -40
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/.editorconfig +0 -0
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/.readthedocs.yaml +0 -0
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/CI_REQUIREMENTS.txt +0 -0
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/LICENSE +0 -0
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/MANIFEST.in +0 -0
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/README.rst +0 -0
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/build_requirements.txt +0 -0
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers/__init__.pyi +0 -0
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers/_log_templates.py +0 -0
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers/_subprocess_helpers.py +0 -0
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers/async_api/__init__.py +0 -0
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers/async_api/__init__.pyi +0 -0
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers/async_api/exec_result.py +0 -0
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers/constants.py +0 -0
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers/py.typed +0 -0
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers/ssh.py +0 -0
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers.egg-info/dependency_links.txt +0 -0
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers.egg-info/not-zip-safe +0 -0
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/exec_helpers.egg-info/top_level.txt +0 -0
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/requirements.txt +0 -0
- {exec_helpers-8.1.2 → exec_helpers-8.2.0}/setup.cfg +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
+
rev: v6.0.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: check-yaml
|
|
6
|
+
- id: check-toml
|
|
7
|
+
- id: end-of-file-fixer
|
|
8
|
+
- id: trailing-whitespace
|
|
9
|
+
- id: mixed-line-ending
|
|
10
|
+
|
|
11
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
12
|
+
# Ruff version.
|
|
13
|
+
rev: v0.15.13
|
|
14
|
+
hooks:
|
|
15
|
+
# Run the linter.
|
|
16
|
+
- id: ruff-check
|
|
17
|
+
args: [ --fix ]
|
|
18
|
+
# Run the formatter.
|
|
19
|
+
- id: ruff-format
|
|
20
|
+
|
|
21
|
+
- repo: https://github.com/pre-commit/pygrep-hooks
|
|
22
|
+
rev: v1.10.0 # Use the ref you want to point at
|
|
23
|
+
hooks:
|
|
24
|
+
- id: python-check-blanket-noqa
|
|
25
|
+
- id: python-check-blanket-type-ignore
|
|
26
|
+
- id: rst-directive-colons
|
|
27
|
+
- id: rst-inline-touching-normal
|
|
28
|
+
|
|
29
|
+
- repo: https://github.com/tox-dev/pyproject-fmt
|
|
30
|
+
# Use the sha / tag you want to point at
|
|
31
|
+
# or use `pre-commit autoupdate` to get the latest version
|
|
32
|
+
rev: "v2.21.2"
|
|
33
|
+
hooks:
|
|
34
|
+
- id: pyproject-fmt
|
|
@@ -1,48 +1,44 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: exec-helpers
|
|
3
|
-
Version: 8.
|
|
3
|
+
Version: 8.2.0
|
|
4
4
|
Summary: Execution helpers for simplified usage of subprocess and ssh.
|
|
5
|
-
|
|
6
|
-
Author-email: Alexey Stepanov <penguinolog@gmail.com>
|
|
5
|
+
Author-email: Aleksei Stepanov <penguinolog@gmail.com>
|
|
7
6
|
Maintainer-email: Aleksei Stepanov <penguinolog@gmail.com>, Antonio Esposito <esposito.cloud@gmail.com>, Dennis Dmitriev <dis-xcom@gmail.com>
|
|
8
|
-
License: Apache-2.0
|
|
7
|
+
License-Expression: Apache-2.0
|
|
9
8
|
Project-URL: Documentation, https://exec-helpers.readthedocs.io/
|
|
10
9
|
Project-URL: Repository, https://github.com/python-useful-helpers/exec-helpers
|
|
11
10
|
Project-URL: Bug Tracker, https://github.com/python-useful-helpers/exec-helpers/issues
|
|
12
|
-
Keywords: subprocess
|
|
11
|
+
Keywords: ssh,subprocess
|
|
13
12
|
Classifier: Development Status :: 5 - Production/Stable
|
|
14
13
|
Classifier: Intended Audience :: Developers
|
|
15
14
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
17
15
|
Classifier: Programming Language :: Python :: 3
|
|
18
16
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
22
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
23
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
24
22
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
25
23
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
26
|
-
Requires-Python: >=3.
|
|
24
|
+
Requires-Python: >=3.11.0
|
|
27
25
|
Description-Content-Type: text/x-rst
|
|
28
26
|
License-File: LICENSE
|
|
29
27
|
Requires-Dist: paramiko>=2.4
|
|
30
28
|
Requires-Dist: tenacity>=4.4.0
|
|
31
29
|
Requires-Dist: psutil>=5.0
|
|
32
30
|
Provides-Extra: xml
|
|
33
|
-
Requires-Dist: defusedxml; extra == "xml"
|
|
34
31
|
Provides-Extra: lxml
|
|
35
32
|
Requires-Dist: lxml>=4.6.2; extra == "lxml"
|
|
36
33
|
Provides-Extra: yaml
|
|
37
|
-
Requires-Dist:
|
|
34
|
+
Requires-Dist: pyyaml>=3.12; extra == "yaml"
|
|
38
35
|
Provides-Extra: all-formats
|
|
39
|
-
Requires-Dist: defusedxml; extra == "all-formats"
|
|
40
36
|
Requires-Dist: lxml>=4.6.2; extra == "all-formats"
|
|
41
|
-
Requires-Dist:
|
|
37
|
+
Requires-Dist: pyyaml>=3.12; extra == "all-formats"
|
|
42
38
|
Provides-Extra: all-formats
|
|
43
|
-
Requires-Dist: defusedxml; extra == "all-formats"
|
|
44
39
|
Requires-Dist: lxml>=4.6.2; extra == "all-formats"
|
|
45
|
-
Requires-Dist:
|
|
40
|
+
Requires-Dist: pyyaml>=3.12; extra == "all-formats"
|
|
41
|
+
Dynamic: license-file
|
|
46
42
|
|
|
47
43
|
exec-helpers
|
|
48
44
|
============
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
Development Status :: 5 - Production/Stable
|
|
2
2
|
Intended Audience :: Developers
|
|
3
3
|
Topic :: Software Development :: Libraries :: Python Modules
|
|
4
|
-
License :: OSI Approved :: Apache Software License
|
|
5
4
|
Programming Language :: Python :: 3
|
|
6
5
|
Programming Language :: Python :: 3 :: Only
|
|
7
|
-
Programming Language :: Python :: 3.8
|
|
8
|
-
Programming Language :: Python :: 3.9
|
|
9
|
-
Programming Language :: Python :: 3.10
|
|
10
6
|
Programming Language :: Python :: 3.11
|
|
11
7
|
Programming Language :: Python :: 3.12
|
|
8
|
+
Programming Language :: Python :: 3.13
|
|
9
|
+
Programming Language :: Python :: 3.14
|
|
10
|
+
Programming Language :: Python :: 3.15
|
|
12
11
|
Programming Language :: Python :: Implementation :: CPython
|
|
13
12
|
Programming Language :: Python :: Implementation :: PyPy
|
|
@@ -94,5 +94,5 @@ def chroot_command(command: str, chroot_path: str | None = None, chroot_exe: str
|
|
|
94
94
|
if chroot_path and chroot_path != "/":
|
|
95
95
|
chroot_dst: str = shlex.quote(chroot_path.strip())
|
|
96
96
|
quoted_command = shlex.quote(command)
|
|
97
|
-
return f
|
|
97
|
+
return f"{chroot_exe} {chroot_dst} sh -c {shlex.quote(f'eval {quoted_command}')}"
|
|
98
98
|
return command
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
21
|
import concurrent.futures
|
|
22
|
+
import contextlib
|
|
22
23
|
import copy
|
|
23
24
|
import datetime
|
|
24
25
|
import getpass
|
|
@@ -49,8 +50,7 @@ if typing.TYPE_CHECKING:
|
|
|
49
50
|
from collections.abc import Iterable
|
|
50
51
|
from collections.abc import Sequence
|
|
51
52
|
from types import TracebackType
|
|
52
|
-
|
|
53
|
-
from typing_extensions import Self
|
|
53
|
+
from typing import Self
|
|
54
54
|
|
|
55
55
|
from exec_helpers.api import CalledProcessErrorSubClassT
|
|
56
56
|
from exec_helpers.api import CommandT
|
|
@@ -65,8 +65,8 @@ if typing.TYPE_CHECKING:
|
|
|
65
65
|
|
|
66
66
|
__all__ = ("SSHClientBase", "SshExecuteAsyncResult", "SupportPathT")
|
|
67
67
|
|
|
68
|
-
KeepAlivePeriodT =
|
|
69
|
-
SupportPathT =
|
|
68
|
+
KeepAlivePeriodT = int | bool
|
|
69
|
+
SupportPathT = str | pathlib.PurePath
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
class RetryOnExceptions(tenacity.retry_if_exception):
|
|
@@ -135,7 +135,7 @@ class SshExecuteAsyncResult(api.ExecuteAsyncResult):
|
|
|
135
135
|
return super().stdout
|
|
136
136
|
|
|
137
137
|
|
|
138
|
-
class _SSHExecuteContext(api.ExecuteContext,
|
|
138
|
+
class _SSHExecuteContext(api.ExecuteContext, contextlib.AbstractContextManager[SshExecuteAsyncResult]):
|
|
139
139
|
"""SSH Execute context."""
|
|
140
140
|
|
|
141
141
|
__slots__ = (
|
|
@@ -308,7 +308,7 @@ class _SSHExecuteContext(api.ExecuteContext, typing.ContextManager[SshExecuteAsy
|
|
|
308
308
|
stderr = None
|
|
309
309
|
_stdin: paramiko.channel.ChannelFile
|
|
310
310
|
with chan.makefile("wb") as _stdin:
|
|
311
|
-
started = datetime.datetime.now(tz=datetime.
|
|
311
|
+
started = datetime.datetime.now(tz=datetime.UTC)
|
|
312
312
|
if self.__sudo_mode:
|
|
313
313
|
chan.exec_command(self.command) # nosec # Sanitize on caller side
|
|
314
314
|
if not stdout.channel.closed:
|
|
@@ -358,7 +358,7 @@ class _SSHExecuteContext(api.ExecuteContext, typing.ContextManager[SshExecuteAsy
|
|
|
358
358
|
self.__chan = None
|
|
359
359
|
|
|
360
360
|
|
|
361
|
-
class _SudoContext(
|
|
361
|
+
class _SudoContext(contextlib.AbstractContextManager[None]):
|
|
362
362
|
"""Context manager for call commands with sudo.
|
|
363
363
|
|
|
364
364
|
:param ssh: Connection instance.
|
|
@@ -389,7 +389,7 @@ class _SudoContext(typing.ContextManager[None]):
|
|
|
389
389
|
self.__ssh.sudo_mode = self.__sudo_status
|
|
390
390
|
|
|
391
391
|
|
|
392
|
-
class _KeepAliveContext(
|
|
392
|
+
class _KeepAliveContext(contextlib.AbstractContextManager[None]):
|
|
393
393
|
"""Context manager for keepalive management.
|
|
394
394
|
|
|
395
395
|
:param ssh: Connection instance.
|
|
@@ -456,6 +456,8 @@ class SSHClientBase(api.ExecHelper):
|
|
|
456
456
|
:type keepalive: int | bool
|
|
457
457
|
:param allow_ssh_agent: Use SSH Agent if available.
|
|
458
458
|
:type allow_ssh_agent: bool
|
|
459
|
+
:param missing_host_key_policy: Missing host key policy class. See paramiko.MissingHostKeyPolicy for details.
|
|
460
|
+
:type missing_host_key_policy: type[paramiko.MissingHostKeyPolicy]
|
|
459
461
|
|
|
460
462
|
.. note:: auth has priority over username/password/private_keys.
|
|
461
463
|
.. note::
|
|
@@ -474,6 +476,7 @@ class SSHClientBase(api.ExecHelper):
|
|
|
474
476
|
.. versionchanged:: 7.0.0 keepalive_mode is removed.
|
|
475
477
|
.. versionchanged:: 7.4.0 Return of keepalive_mode to prevent mix with a keepalive period. Default is `False`.
|
|
476
478
|
.. versionchanged:: 8.0.0 Expose SSH Agent usage override.
|
|
479
|
+
.. versionchanged:: 8.1.4 Allow specifying missing_host_key_policy.
|
|
477
480
|
"""
|
|
478
481
|
|
|
479
482
|
__slots__ = (
|
|
@@ -490,6 +493,7 @@ class SSHClientBase(api.ExecHelper):
|
|
|
490
493
|
"__ssh_config",
|
|
491
494
|
"__sudo_mode",
|
|
492
495
|
"__verbose",
|
|
496
|
+
"_missing_host_key_policy",
|
|
493
497
|
)
|
|
494
498
|
|
|
495
499
|
def __hash__(self) -> int:
|
|
@@ -514,6 +518,7 @@ class SSHClientBase(api.ExecHelper):
|
|
|
514
518
|
sock: paramiko.ProxyCommand | paramiko.Channel | socket.socket | None = None,
|
|
515
519
|
keepalive: KeepAlivePeriodT = 1,
|
|
516
520
|
allow_ssh_agent: bool = True,
|
|
521
|
+
missing_host_key_policy: type[paramiko.MissingHostKeyPolicy] = paramiko.WarningPolicy,
|
|
517
522
|
) -> None:
|
|
518
523
|
"""Main SSH Client helper."""
|
|
519
524
|
self.__sudo_mode = False
|
|
@@ -521,6 +526,7 @@ class SSHClientBase(api.ExecHelper):
|
|
|
521
526
|
self.__keepalive_mode = False
|
|
522
527
|
self.__verbose: bool = verbose
|
|
523
528
|
self.__sock = sock
|
|
529
|
+
self._missing_host_key_policy = missing_host_key_policy
|
|
524
530
|
|
|
525
531
|
self.__ssh: paramiko.SSHClient
|
|
526
532
|
self.__sftp: paramiko.SFTPClient | None = None
|
|
@@ -754,7 +760,8 @@ class SSHClientBase(api.ExecHelper):
|
|
|
754
760
|
with self.lock:
|
|
755
761
|
if self.__sock is not None:
|
|
756
762
|
self.__ssh = paramiko.SSHClient()
|
|
757
|
-
self.__ssh.
|
|
763
|
+
self.__ssh.load_system_host_keys()
|
|
764
|
+
self.__ssh.set_missing_host_key_policy(self._missing_host_key_policy)
|
|
758
765
|
self.auth.connect(
|
|
759
766
|
client=self.__ssh,
|
|
760
767
|
hostname=self.hostname,
|
|
@@ -780,7 +787,8 @@ class SSHClientBase(api.ExecHelper):
|
|
|
780
787
|
"""
|
|
781
788
|
|
|
782
789
|
last_ssh_client: paramiko.SSHClient = paramiko.SSHClient()
|
|
783
|
-
last_ssh_client.
|
|
790
|
+
last_ssh_client.load_system_host_keys()
|
|
791
|
+
last_ssh_client.set_missing_host_key_policy(self._missing_host_key_policy)
|
|
784
792
|
|
|
785
793
|
config, auth = self.__conn_chain[0]
|
|
786
794
|
|
|
@@ -794,7 +802,8 @@ class SSHClientBase(api.ExecHelper):
|
|
|
794
802
|
|
|
795
803
|
for config, auth in self.__conn_chain[1:]: # start has another logic, so do it out of cycle
|
|
796
804
|
ssh = paramiko.SSHClient()
|
|
797
|
-
ssh.
|
|
805
|
+
ssh.load_system_host_keys()
|
|
806
|
+
ssh.set_missing_host_key_policy(self._missing_host_key_policy)
|
|
798
807
|
|
|
799
808
|
if config.proxyjump:
|
|
800
809
|
transport = last_ssh_client.get_transport()
|
|
@@ -869,7 +878,8 @@ class SSHClientBase(api.ExecHelper):
|
|
|
869
878
|
try:
|
|
870
879
|
self.__ssh.close()
|
|
871
880
|
except BaseException as e: # pragma: no cover # NOSONAR
|
|
872
|
-
|
|
881
|
+
with contextlib.suppress(AttributeError):
|
|
882
|
+
self.logger.debug(f"Exception in {self!s} destructor call: {e}")
|
|
873
883
|
self.__sftp = None
|
|
874
884
|
|
|
875
885
|
def __exit__(
|
|
@@ -995,12 +1005,12 @@ class SSHClientBase(api.ExecHelper):
|
|
|
995
1005
|
return super()._prepare_command(cmd=cmd, chroot_path=chroot_path, chroot_exe=chroot_exe)
|
|
996
1006
|
quoted_command: str = shlex.quote(cmd)
|
|
997
1007
|
if chroot_path is self._chroot_path is None:
|
|
998
|
-
return f
|
|
1008
|
+
return f"sudo -S sh -c {shlex.quote(f'eval {quoted_command}')}"
|
|
999
1009
|
if chroot_path is not None:
|
|
1000
1010
|
target_path: str = shlex.quote(chroot_path)
|
|
1001
1011
|
else:
|
|
1002
1012
|
target_path = shlex.quote(self._chroot_path) # type: ignore[arg-type]
|
|
1003
|
-
return f
|
|
1013
|
+
return f"{chroot_exe} {target_path} sudo sh -c {shlex.quote(f'eval {quoted_command}')}"
|
|
1004
1014
|
|
|
1005
1015
|
def _exec_command( # type: ignore[override]
|
|
1006
1016
|
self,
|
|
@@ -1777,7 +1787,7 @@ class SSHClientBase(api.ExecHelper):
|
|
|
1777
1787
|
results[remote.hostname, remote.port] = result
|
|
1778
1788
|
if result.exit_code not in prep_expected:
|
|
1779
1789
|
errors[remote.hostname, remote.port] = result
|
|
1780
|
-
except Exception as e:
|
|
1790
|
+
except Exception as e:
|
|
1781
1791
|
raised_exceptions[remote.hostname, remote.port] = e
|
|
1782
1792
|
|
|
1783
1793
|
if raised_exceptions: # always raise
|
|
@@ -5,17 +5,16 @@ from __future__ import annotations
|
|
|
5
5
|
import functools
|
|
6
6
|
import pathlib
|
|
7
7
|
import typing
|
|
8
|
+
from collections.abc import Collection
|
|
8
9
|
|
|
9
10
|
import paramiko
|
|
10
11
|
|
|
11
12
|
if typing.TYPE_CHECKING:
|
|
12
|
-
from collections.abc import Collection
|
|
13
|
-
|
|
14
13
|
# noinspection PyPackageRequirements
|
|
15
14
|
import logwrap
|
|
16
15
|
|
|
17
|
-
SSHConfigDictLikeT =
|
|
18
|
-
SSHConfigsDictT =
|
|
16
|
+
SSHConfigDictLikeT = dict[str, str | int | bool | Collection[str]]
|
|
17
|
+
SSHConfigsDictT = dict[str, SSHConfigDictLikeT]
|
|
19
18
|
|
|
20
19
|
|
|
21
20
|
# Parse default SSHConfig if available
|
|
@@ -379,7 +378,7 @@ class SSHConfig:
|
|
|
379
378
|
return self.__controlmaster
|
|
380
379
|
|
|
381
380
|
|
|
382
|
-
class HostsSSHConfigs(
|
|
381
|
+
class HostsSSHConfigs(dict[str, SSHConfig]):
|
|
383
382
|
"""Specific dictionary for managing SSHConfig records.
|
|
384
383
|
|
|
385
384
|
Instead of creating a new record by request, just generate default value and return if not exists.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '8.2.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (8, 2, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = 'g9b1f831aa'
|
|
@@ -21,10 +21,14 @@
|
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
23
|
import abc
|
|
24
|
+
import contextlib
|
|
24
25
|
import logging
|
|
25
26
|
import pathlib
|
|
27
|
+
import re
|
|
26
28
|
import threading
|
|
27
29
|
import typing
|
|
30
|
+
from collections.abc import Callable
|
|
31
|
+
from collections.abc import Iterable
|
|
28
32
|
|
|
29
33
|
from exec_helpers import constants
|
|
30
34
|
from exec_helpers import exceptions
|
|
@@ -39,8 +43,7 @@ if typing.TYPE_CHECKING:
|
|
|
39
43
|
import datetime
|
|
40
44
|
from collections.abc import Sequence
|
|
41
45
|
from types import TracebackType
|
|
42
|
-
|
|
43
|
-
from typing_extensions import Self
|
|
46
|
+
from typing import Self
|
|
44
47
|
|
|
45
48
|
__all__ = (
|
|
46
49
|
"CalledProcessErrorSubClassT",
|
|
@@ -55,13 +58,13 @@ __all__ = (
|
|
|
55
58
|
"OptionalTimeoutT",
|
|
56
59
|
)
|
|
57
60
|
|
|
58
|
-
CommandT =
|
|
59
|
-
LogMaskReT =
|
|
60
|
-
ErrorInfoT =
|
|
61
|
-
ChRootPathSetT =
|
|
62
|
-
ExpectedExitCodesT =
|
|
63
|
-
OptionalTimeoutT =
|
|
64
|
-
CalledProcessErrorSubClassT =
|
|
61
|
+
CommandT = str | Iterable[str]
|
|
62
|
+
LogMaskReT = str | re.Pattern[str] | None
|
|
63
|
+
ErrorInfoT = str | None
|
|
64
|
+
ChRootPathSetT = str | pathlib.Path | None
|
|
65
|
+
ExpectedExitCodesT = Iterable[ExitCodeT]
|
|
66
|
+
OptionalTimeoutT = int | float | None
|
|
67
|
+
CalledProcessErrorSubClassT = type[exceptions.CalledProcessError]
|
|
65
68
|
|
|
66
69
|
|
|
67
70
|
class ExecuteAsyncResult(typing.NamedTuple):
|
|
@@ -74,7 +77,7 @@ class ExecuteAsyncResult(typing.NamedTuple):
|
|
|
74
77
|
started: datetime.datetime
|
|
75
78
|
|
|
76
79
|
|
|
77
|
-
class ExecuteContext(
|
|
80
|
+
class ExecuteContext(contextlib.AbstractContextManager[ExecuteAsyncResult], abc.ABC):
|
|
78
81
|
"""Execute context manager."""
|
|
79
82
|
|
|
80
83
|
__slots__ = (
|
|
@@ -165,7 +168,7 @@ class ExecuteContext(typing.ContextManager[ExecuteAsyncResult], abc.ABC):
|
|
|
165
168
|
|
|
166
169
|
|
|
167
170
|
# noinspection PyProtectedMember
|
|
168
|
-
class _ChRootContext(
|
|
171
|
+
class _ChRootContext(contextlib.AbstractContextManager[None]):
|
|
169
172
|
"""Context manager for call commands with chroot.
|
|
170
173
|
|
|
171
174
|
:param conn: Connection instance.
|
|
@@ -219,8 +222,8 @@ class _ChRootContext(typing.ContextManager[None]):
|
|
|
219
222
|
|
|
220
223
|
|
|
221
224
|
class ExecHelper(
|
|
222
|
-
|
|
223
|
-
|
|
225
|
+
Callable[..., exec_result.ExecResult], # type: ignore[misc]
|
|
226
|
+
contextlib.AbstractContextManager["ExecHelper"],
|
|
224
227
|
abc.ABC,
|
|
225
228
|
):
|
|
226
229
|
"""ExecHelper global API.
|
|
@@ -21,9 +21,12 @@ from __future__ import annotations
|
|
|
21
21
|
|
|
22
22
|
import abc
|
|
23
23
|
import asyncio
|
|
24
|
+
import contextlib
|
|
24
25
|
import logging
|
|
25
26
|
import pathlib
|
|
26
27
|
import typing
|
|
28
|
+
from collections.abc import Awaitable
|
|
29
|
+
from collections.abc import Callable
|
|
27
30
|
|
|
28
31
|
from exec_helpers import api
|
|
29
32
|
from exec_helpers import constants
|
|
@@ -43,8 +46,7 @@ from .. import _helpers
|
|
|
43
46
|
if typing.TYPE_CHECKING:
|
|
44
47
|
import types
|
|
45
48
|
from collections.abc import Sequence
|
|
46
|
-
|
|
47
|
-
from typing_extensions import Self
|
|
49
|
+
from typing import Self
|
|
48
50
|
|
|
49
51
|
from exec_helpers.async_api import exec_result
|
|
50
52
|
from exec_helpers.proc_enums import ExitCodeT
|
|
@@ -62,7 +64,7 @@ __all__ = (
|
|
|
62
64
|
)
|
|
63
65
|
|
|
64
66
|
|
|
65
|
-
class ExecuteContext(
|
|
67
|
+
class ExecuteContext(contextlib.AbstractAsyncContextManager[api.ExecuteAsyncResult], abc.ABC):
|
|
66
68
|
"""Execute context manager."""
|
|
67
69
|
|
|
68
70
|
__slots__ = (
|
|
@@ -153,7 +155,7 @@ class ExecuteContext(typing.AsyncContextManager[api.ExecuteAsyncResult], abc.ABC
|
|
|
153
155
|
|
|
154
156
|
|
|
155
157
|
# noinspection PyProtectedMember
|
|
156
|
-
class _ChRootContext(
|
|
158
|
+
class _ChRootContext(contextlib.AbstractAsyncContextManager[None]):
|
|
157
159
|
"""Async extension for chroot.
|
|
158
160
|
|
|
159
161
|
:param conn: Connection instance.
|
|
@@ -203,8 +205,8 @@ class _ChRootContext(typing.AsyncContextManager[None]):
|
|
|
203
205
|
|
|
204
206
|
|
|
205
207
|
class ExecHelper(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
+
Callable[..., Awaitable["ExecHelper"]], # type: ignore[misc]
|
|
209
|
+
contextlib.AbstractAsyncContextManager["ExecHelper"],
|
|
208
210
|
abc.ABC,
|
|
209
211
|
):
|
|
210
212
|
"""Subprocess helper with timeouts and lock-free FIFO.
|
|
@@ -374,7 +376,7 @@ class ExecHelper(
|
|
|
374
376
|
self,
|
|
375
377
|
command: str,
|
|
376
378
|
async_result: api.ExecuteAsyncResult,
|
|
377
|
-
timeout: OptionalTimeoutT,
|
|
379
|
+
timeout: OptionalTimeoutT, # noqa: ASYNC109
|
|
378
380
|
*,
|
|
379
381
|
verbose: bool = False,
|
|
380
382
|
log_mask_re: LogMaskReT = None,
|
|
@@ -460,7 +462,7 @@ class ExecHelper(
|
|
|
460
462
|
self,
|
|
461
463
|
command: CommandT,
|
|
462
464
|
verbose: bool = False,
|
|
463
|
-
timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT,
|
|
465
|
+
timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT, # noqa: ASYNC109
|
|
464
466
|
*,
|
|
465
467
|
log_mask_re: LogMaskReT = None,
|
|
466
468
|
stdin: OptionalStdinT = None,
|
|
@@ -543,7 +545,7 @@ class ExecHelper(
|
|
|
543
545
|
self,
|
|
544
546
|
command: CommandT,
|
|
545
547
|
verbose: bool = False,
|
|
546
|
-
timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT,
|
|
548
|
+
timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT, # noqa: ASYNC109
|
|
547
549
|
*,
|
|
548
550
|
log_mask_re: LogMaskReT = None,
|
|
549
551
|
stdin: OptionalStdinT = None,
|
|
@@ -607,7 +609,7 @@ class ExecHelper(
|
|
|
607
609
|
self,
|
|
608
610
|
command: CommandT,
|
|
609
611
|
verbose: bool = False,
|
|
610
|
-
timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT,
|
|
612
|
+
timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT, # noqa: ASYNC109
|
|
611
613
|
error_info: ErrorInfoT = None,
|
|
612
614
|
expected: ExpectedExitCodesT = (proc_enums.EXPECTED,),
|
|
613
615
|
raise_on_err: bool = True,
|
|
@@ -685,7 +687,7 @@ class ExecHelper(
|
|
|
685
687
|
self,
|
|
686
688
|
command: CommandT,
|
|
687
689
|
verbose: bool = False,
|
|
688
|
-
timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT,
|
|
690
|
+
timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT, # noqa: ASYNC109
|
|
689
691
|
error_info: ErrorInfoT = None,
|
|
690
692
|
raise_on_err: bool = True,
|
|
691
693
|
*,
|
|
@@ -106,7 +106,9 @@ class SubprocessExecuteAsyncResult(subprocess.SubprocessExecuteAsyncResult):
|
|
|
106
106
|
return super().stdout # type: ignore[return-value]
|
|
107
107
|
|
|
108
108
|
|
|
109
|
-
class _SubprocessExecuteContext(
|
|
109
|
+
class _SubprocessExecuteContext(
|
|
110
|
+
api.ExecuteContext, contextlib.AbstractAsyncContextManager[SubprocessExecuteAsyncResult]
|
|
111
|
+
):
|
|
110
112
|
"""Subprocess Execute context."""
|
|
111
113
|
|
|
112
114
|
__slots__ = ("__cwd", "__env", "__process")
|
|
@@ -181,7 +183,7 @@ class _SubprocessExecuteContext(api.ExecuteContext, typing.AsyncContextManager[S
|
|
|
181
183
|
|
|
182
184
|
The Command is executed only in the context manager to be sure that everything will be cleaned up properly.
|
|
183
185
|
"""
|
|
184
|
-
started = datetime.datetime.now(tz=datetime.
|
|
186
|
+
started = datetime.datetime.now(tz=datetime.UTC)
|
|
185
187
|
|
|
186
188
|
self.__process = await asyncio.create_subprocess_shell(
|
|
187
189
|
cmd=self.command,
|
|
@@ -269,7 +271,7 @@ class Subprocess(api.ExecHelper):
|
|
|
269
271
|
self,
|
|
270
272
|
command: str,
|
|
271
273
|
async_result: SubprocessExecuteAsyncResult,
|
|
272
|
-
timeout: OptionalTimeoutT,
|
|
274
|
+
timeout: OptionalTimeoutT, # noqa: ASYNC109
|
|
273
275
|
*,
|
|
274
276
|
verbose: bool = False,
|
|
275
277
|
log_mask_re: LogMaskReT = None,
|
|
@@ -326,7 +328,7 @@ class Subprocess(api.ExecHelper):
|
|
|
326
328
|
# Wait real timeout here
|
|
327
329
|
exit_code: int = await asyncio.wait_for(async_result.interface.wait(), timeout=timeout)
|
|
328
330
|
result.exit_code = exit_code
|
|
329
|
-
except
|
|
331
|
+
except TimeoutError as exc:
|
|
330
332
|
# kill -9 for all subprocesses
|
|
331
333
|
_subprocess_helpers.kill_proc_tree(async_result.interface.pid)
|
|
332
334
|
exit_signal: int | None = await asyncio.wait_for(async_result.interface.wait(), timeout=0.001)
|
|
@@ -389,8 +391,8 @@ class Subprocess(api.ExecHelper):
|
|
|
389
391
|
"""
|
|
390
392
|
if env_patch is not None:
|
|
391
393
|
# make mutable copy
|
|
392
|
-
env = dict(copy.deepcopy(os.environ) if env is None else copy.deepcopy(env))
|
|
393
|
-
env.update(env_patch)
|
|
394
|
+
env = dict(copy.deepcopy(os.environ) if env is None else copy.deepcopy(env))
|
|
395
|
+
env.update(env_patch)
|
|
394
396
|
return _SubprocessExecuteContext(
|
|
395
397
|
command=f"{self._prepare_command(cmd=command, chroot_path=chroot_path, chroot_exe=chroot_exe)}\n",
|
|
396
398
|
stdin=None if stdin is None else self._string_bytes_bytearray_as_bytes(stdin),
|
|
@@ -406,7 +408,7 @@ class Subprocess(api.ExecHelper):
|
|
|
406
408
|
self,
|
|
407
409
|
command: CommandT,
|
|
408
410
|
verbose: bool = False,
|
|
409
|
-
timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT,
|
|
411
|
+
timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT, # noqa: ASYNC109
|
|
410
412
|
*,
|
|
411
413
|
log_mask_re: LogMaskReT = None,
|
|
412
414
|
stdin: OptionalStdinT = None,
|
|
@@ -485,7 +487,7 @@ class Subprocess(api.ExecHelper):
|
|
|
485
487
|
self,
|
|
486
488
|
command: CommandT,
|
|
487
489
|
verbose: bool = False,
|
|
488
|
-
timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT,
|
|
490
|
+
timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT, # noqa: ASYNC109
|
|
489
491
|
*,
|
|
490
492
|
log_mask_re: LogMaskReT = None,
|
|
491
493
|
stdin: OptionalStdinT = None,
|
|
@@ -562,7 +564,7 @@ class Subprocess(api.ExecHelper):
|
|
|
562
564
|
self,
|
|
563
565
|
command: CommandT,
|
|
564
566
|
verbose: bool = False,
|
|
565
|
-
timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT,
|
|
567
|
+
timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT, # noqa: ASYNC109
|
|
566
568
|
error_info: ErrorInfoT = None,
|
|
567
569
|
expected: ExpectedExitCodesT = (proc_enums.EXPECTED,),
|
|
568
570
|
raise_on_err: bool = True,
|
|
@@ -649,7 +651,7 @@ class Subprocess(api.ExecHelper):
|
|
|
649
651
|
self,
|
|
650
652
|
command: CommandT,
|
|
651
653
|
verbose: bool = False,
|
|
652
|
-
timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT,
|
|
654
|
+
timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT, # noqa: ASYNC109
|
|
653
655
|
error_info: ErrorInfoT = None,
|
|
654
656
|
raise_on_err: bool = True,
|
|
655
657
|
*,
|
|
@@ -274,7 +274,7 @@ class ParallelCallProcessError(ExecCalledProcessError):
|
|
|
274
274
|
class ParallelCallExceptionsError(ParallelCallProcessError):
|
|
275
275
|
"""Exception raised during parallel call as result of exceptions."""
|
|
276
276
|
|
|
277
|
-
__slots__ = ("
|
|
277
|
+
__slots__ = ("exceptions",)
|
|
278
278
|
|
|
279
279
|
def __init__(
|
|
280
280
|
self,
|