sshreader 6.1.0__tar.gz → 6.3.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.
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.1
2
+ Name: sshreader
3
+ Version: 6.3.0
4
+ Summary: Multi-threading/processing wrapper for Paramiko
5
+ Author-Email: Jesse Almanrode <jesse@almanrode.com>
6
+ License: LGPL-3.0-or-later
7
+ Classifier: Programming Language :: Python
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Project-URL: homepage, http://sshreader.readthedocs.io/
21
+ Project-URL: repository, https://bitbucket.org/isaiah1112/sshreader/
22
+ Requires-Python: <4.0,>3.9
23
+ Requires-Dist: click<9,>=8
24
+ Requires-Dist: colorama<1.0,>=0.4
25
+ Requires-Dist: paramiko<4,>=3
26
+ Requires-Dist: progressbar2<5,>=4
27
+ Requires-Dist: python-hostlist<2,>=1
28
+ Description-Content-Type: text/markdown
29
+
30
+ ![logo](./logo.png)
31
+
32
+ [![Python Tests](https://github.com/isaiah1112/sshreader/actions/workflows/lint_and_test.yml/badge.svg)](https://github.com/isaiah1112/sshreader/actions/workflows/lint_and_test.yml)
33
+
34
+ A Python Module for multiprocessing/threading ssh connections in order to make ssh operations
35
+ across multiple servers parallel. It utilizes the [Paramiko](http://www.paramiko.org/) module
36
+ for its ssh client.
37
+
38
+ In order to maintain the widest range of compatibility, [SSHreader][] is currently tested
39
+ using [all supported versions of Python!](https://devguide.python.org/versions/)
40
+
41
+ ## License
42
+
43
+ [SSHreader][] is released under [GNU Lesser General Public License v3.0][],
44
+ see the file LICENSE and LICENSE.lesser for the license text.
45
+
46
+ ## Installation
47
+
48
+ The most straightforward way to get the [SSHreader][] module working for you is:
49
+
50
+ ```commandline
51
+ pip install sshreader
52
+ ```
53
+ or, if you'd like to use a local install from the repo:
54
+ ```commandline
55
+ make install
56
+ ```
57
+
58
+ This ensures that all the requirements are met.
59
+
60
+ ## Documentation
61
+
62
+ The documentation for [SSHreader][] can be found at [ReadTheDocs.](https://sshreader.readthedocs.io)
63
+
64
+ ## Contributing
65
+
66
+ Comments and enhancements are very welcome.
67
+
68
+ Report any issues or feature requests on the [GitHub Issue Tracker](https://github.com/isaiah1112/sshreader/issues).
69
+ Please include a minimal not-working example which reproduces the bug and, if appropriate, the
70
+ traceback information. Please do not request features already being worked
71
+ towards.
72
+
73
+ Code contributions are encouraged: please feel free to [fork the project](https://github.com/isaiah1112/sshreader)
74
+ and submit pull requests to the develop branch.
75
+
76
+ ## Extras
77
+
78
+ Included with sshreader is a script called `pydsh`. This works very similar to [pdsh](https://computing.llnl.gov/linux/pdsh.html)
79
+ but uses sshreader at its core to perform ssh commands in parallel and return the results.
80
+ The output of `pydsh` can also be piped through the `dshbak` tool that comes with pdsh.
81
+
82
+ Pydsh uses [hostlist expressions](https://www.nsc.liu.se/~kent/python-hostlist/) to get its list of hosts
83
+ to process.
84
+
85
+
86
+ [GNU Lesser General Public License v3.0]: http://choosealicense.com/licenses/lgpl-3.0/ "LGPL v3"
87
+
88
+ [sshreader]: https://github.com/isaiah1112/sshreader "SSHreader Package"
@@ -0,0 +1,59 @@
1
+ ![logo](./logo.png)
2
+
3
+ [![Python Tests](https://github.com/isaiah1112/sshreader/actions/workflows/lint_and_test.yml/badge.svg)](https://github.com/isaiah1112/sshreader/actions/workflows/lint_and_test.yml)
4
+
5
+ A Python Module for multiprocessing/threading ssh connections in order to make ssh operations
6
+ across multiple servers parallel. It utilizes the [Paramiko](http://www.paramiko.org/) module
7
+ for its ssh client.
8
+
9
+ In order to maintain the widest range of compatibility, [SSHreader][] is currently tested
10
+ using [all supported versions of Python!](https://devguide.python.org/versions/)
11
+
12
+ ## License
13
+
14
+ [SSHreader][] is released under [GNU Lesser General Public License v3.0][],
15
+ see the file LICENSE and LICENSE.lesser for the license text.
16
+
17
+ ## Installation
18
+
19
+ The most straightforward way to get the [SSHreader][] module working for you is:
20
+
21
+ ```commandline
22
+ pip install sshreader
23
+ ```
24
+ or, if you'd like to use a local install from the repo:
25
+ ```commandline
26
+ make install
27
+ ```
28
+
29
+ This ensures that all the requirements are met.
30
+
31
+ ## Documentation
32
+
33
+ The documentation for [SSHreader][] can be found at [ReadTheDocs.](https://sshreader.readthedocs.io)
34
+
35
+ ## Contributing
36
+
37
+ Comments and enhancements are very welcome.
38
+
39
+ Report any issues or feature requests on the [GitHub Issue Tracker](https://github.com/isaiah1112/sshreader/issues).
40
+ Please include a minimal not-working example which reproduces the bug and, if appropriate, the
41
+ traceback information. Please do not request features already being worked
42
+ towards.
43
+
44
+ Code contributions are encouraged: please feel free to [fork the project](https://github.com/isaiah1112/sshreader)
45
+ and submit pull requests to the develop branch.
46
+
47
+ ## Extras
48
+
49
+ Included with sshreader is a script called `pydsh`. This works very similar to [pdsh](https://computing.llnl.gov/linux/pdsh.html)
50
+ but uses sshreader at its core to perform ssh commands in parallel and return the results.
51
+ The output of `pydsh` can also be piped through the `dshbak` tool that comes with pdsh.
52
+
53
+ Pydsh uses [hostlist expressions](https://www.nsc.liu.se/~kent/python-hostlist/) to get its list of hosts
54
+ to process.
55
+
56
+
57
+ [GNU Lesser General Public License v3.0]: http://choosealicense.com/licenses/lgpl-3.0/ "LGPL v3"
58
+
59
+ [sshreader]: https://github.com/isaiah1112/sshreader "SSHreader Package"
@@ -0,0 +1,86 @@
1
+ [project]
2
+ authors = [
3
+ { name = "Jesse Almanrode", email = "jesse@almanrode.com" },
4
+ ]
5
+ requires-python = "<4.0,>3.9"
6
+ dependencies = [
7
+ "click<9,>=8",
8
+ "colorama<1.0,>=0.4",
9
+ "paramiko<4,>=3",
10
+ "progressbar2<5,>=4",
11
+ "python-hostlist<2,>=1",
12
+ ]
13
+ name = "sshreader"
14
+ version = "6.3.0"
15
+ description = "Multi-threading/processing wrapper for Paramiko"
16
+ readme = "README.md"
17
+ classifiers = [
18
+ "Programming Language :: Python",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)",
21
+ "Development Status :: 5 - Production/Stable",
22
+ "Programming Language :: Python",
23
+ "Programming Language :: Python :: 3.9",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Programming Language :: Python :: 3.14",
29
+ "Topic :: Software Development :: Libraries",
30
+ "Topic :: Software Development :: Libraries :: Python Modules",
31
+ ]
32
+
33
+ [project.license]
34
+ text = "LGPL-3.0-or-later"
35
+
36
+ [project.urls]
37
+ homepage = "http://sshreader.readthedocs.io/"
38
+ repository = "https://bitbucket.org/isaiah1112/sshreader/"
39
+
40
+ [project.scripts]
41
+ pydsh = "sshreader.scripts.pydsh:cli"
42
+
43
+ [tool.pdm.build]
44
+ includes = [
45
+ "sshreader",
46
+ "sshreader/scripts",
47
+ ]
48
+
49
+ [tool.ruff.lint]
50
+ select = [
51
+ "E",
52
+ "F",
53
+ "B",
54
+ "UP",
55
+ "B",
56
+ "SIM",
57
+ "I",
58
+ ]
59
+ ignore = [
60
+ "E501",
61
+ "F401",
62
+ ]
63
+
64
+ [[tool.uv.index]]
65
+ name = "test-pypi"
66
+ url = "https://test.pypi.org/simple/"
67
+ publish-url = "https://test.pypi.org/legacy/"
68
+ explicit = true
69
+
70
+ [build-system]
71
+ requires = [
72
+ "pdm-backend",
73
+ ]
74
+ build-backend = "pdm.backend"
75
+
76
+ [dependency-groups]
77
+ test = [
78
+ "coverage<8,>=7",
79
+ "discover<1.0,>=0.4",
80
+ "pytest>=8.4.2",
81
+ "ruff<1.0.0,>=0.9.9",
82
+ "ty>=0.0.5",
83
+ ]
84
+ docs = [
85
+ "sphinx<8,>=7",
86
+ ]
@@ -1,7 +1,6 @@
1
- # coding=utf-8
2
1
  """A Python Package for parallelizing ssh connections via threading and multiprocessing.
3
2
  """
4
- # Copyright (C) 2015-2025 Jesse Almanrode
3
+ # Copyright (C) 2015-2026 Jesse Almanrode
5
4
  #
6
5
  # This program is free software: you can redistribute it and/or modify
7
6
  # it under the terms of the GNU Lesser General Public License as published by
@@ -17,11 +16,12 @@
17
16
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
17
 
19
18
  # For backwards compatibility
20
- from .ssh import SSH, envvars
21
- from .customtypes import Command, EnvVars, Timeout, TimeoutTuple
22
- from .utils import ServerJob, Hook, sshread, echo, cpu_limit, shell_command
23
19
  import logging
24
20
 
21
+ from .customtypes import Command, EnvVars, Timeout, TimeoutTuple
22
+ from .ssh import SSH, envvars
23
+ from .utils import Hook, ServerJob, cpu_limit, echo, shell_command, sshread
24
+
25
25
  __author__ = 'Jesse Almanrode (jesse@almanrode.com)'
26
26
  __all__ = ['ssh', 'customtypes', 'utils']
27
27
 
@@ -1,7 +1,6 @@
1
- # coding=utf-8
2
1
  """ Typing module for sshreader
3
2
  """
4
- # Copyright (C) 2015-2025 Jesse Almanrode
3
+ # Copyright (C) 2015-2026 Jesse Almanrode
5
4
  #
6
5
  # This program is free software: you can redistribute it and/or modify
7
6
  # it under the terms of the GNU Lesser General Public License as published by
@@ -15,7 +14,7 @@
15
14
  #
16
15
  # You should have received a copy of the GNU Lesser General Public License
17
16
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
- from typing import NamedTuple, Optional, Union, Tuple
17
+ from typing import NamedTuple, Optional, Union
19
18
 
20
19
 
21
20
  class Command(NamedTuple):
@@ -34,4 +33,4 @@ class EnvVars(NamedTuple):
34
33
 
35
34
 
36
35
  Timeout = Union[int, float]
37
- TimeoutTuple = Tuple[Timeout, Timeout]
36
+ TimeoutTuple = tuple[Timeout, Timeout]
@@ -1,7 +1,6 @@
1
- # coding=utf-8
2
1
  """ Scripts portion of sshreader package
3
2
  """
4
- # Copyright (C) 2015-2025 Jesse Almanrode
3
+ # Copyright (C) 2015-2026 Jesse Almanrode
5
4
  #
6
5
  # This program is free software: you can redistribute it and/or modify
7
6
  # it under the terms of the GNU Lesser General Public License as published by
@@ -1,7 +1,6 @@
1
- # coding=utf-8
2
1
  """ A Pythonic implementation of pdsh powered by sshreader
3
2
  """
4
- # Copyright (C) 2015-2025 Jesse Almanrode
3
+ # Copyright (C) 2015-2026 Jesse Almanrode
5
4
  #
6
5
  # This program is free software: you can redistribute it and/or modify
7
6
  # it under the terms of the GNU Lesser General Public License as published by
@@ -16,17 +15,20 @@
16
15
  # You should have received a copy of the GNU Lesser General Public License
17
16
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
17
 
18
+ import logging
19
+ import os
20
+ import sys
19
21
  from collections import defaultdict
20
22
  from hashlib import md5
21
- from hostlist import expand_hostlist, collect_hostlist
23
+
22
24
  import click
23
- import logging
24
- import os
25
+ from hostlist import collect_hostlist, expand_hostlist
26
+
25
27
  import sshreader
26
- import sys
28
+
27
29
  # GLOBALS
28
30
  __author__ = 'Jesse Almanrode'
29
- __version__ = '3.1.0'
31
+ __version__ = '3.2.0'
30
32
  __examples__ = """\b
31
33
  Examples:
32
34
  pydsh -w host1,host2,host3 "uname -r"
@@ -63,10 +65,7 @@ def output(thisjob):
63
65
  :param thisjob: <ServerJob> object
64
66
  :return: None
65
67
  """
66
- if thisjob.status == 255:
67
- result = thisjob.results[0]
68
- else:
69
- result = thisjob.results[0].stdout
68
+ result = thisjob.results[0] if thisjob.status == 255 else thisjob.results[0].stdout
70
69
  if len(result) != 0:
71
70
  for line in result.split('\n'):
72
71
  sshreader.echo(str(thisjob.name) + ': ' + str(line))
@@ -103,13 +102,10 @@ def coalesce(jobresults):
103
102
  job_hashes = defaultdict(list)
104
103
  output_hashes = dict()
105
104
  for job in jobresults:
106
- if job.status == 255:
107
- result = job.results[0]
108
- else:
109
- result = job.results[0].stdout
105
+ result = job.results[0] if job.status == 255 else job.results[0].stdout
110
106
  md5sum = md5(result.encode()).hexdigest()
111
107
  job_hashes[md5sum].append(job.name)
112
- if md5sum not in output_hashes.keys():
108
+ if md5sum not in output_hashes:
113
109
  output_hashes[md5sum] = result
114
110
 
115
111
  for md5sum, stdout in output_hashes.items():
@@ -130,7 +126,7 @@ def validate_hostlist(ctx, param, value):
130
126
  try:
131
127
  return expand_hostlist(value)
132
128
  except Exception:
133
- raise click.BadOptionUsage(param, 'Invalid hostlist expression')
129
+ raise click.BadOptionUsage(param, 'Invalid hostlist expression') from None
134
130
 
135
131
 
136
132
  @click.command(epilog=__examples__)
@@ -166,10 +162,11 @@ def cli(**kwargs):
166
162
  if kwargs['file']:
167
163
  mkpath = click.Path(exists=True, dir_okay=False)
168
164
  script_path = mkpath(kwargs['cmd'])
169
- script_name = os.path.split(script_path)[1]
165
+ # Ensure script_path is a str for os.path functions (type-checker safe)
166
+ script_name = os.path.basename(str(script_path))
170
167
  log.info('Creating copy_script prehook for: ' + script_name)
171
168
  prehook = sshreader.Hook(copy_script, args=[script_path], ssh_established=True)
172
- with open(script_path) as s:
169
+ with open(str(script_path)) as s:
173
170
  script = s.readline()
174
171
  if script.startswith('#!') is False:
175
172
  raise click.UsageError('Script must start with #!')
@@ -200,7 +197,7 @@ def cli(**kwargs):
200
197
  kwargs['keyfile'] = sshenv.dsa_key
201
198
  log.info('Using DSA private key file')
202
199
  else:
203
- if len(sshenv.agent_keys) == 0:
200
+ if sshenv.agent_keys is None or len(sshenv.agent_keys) == 0:
204
201
  if not all((kwargs['username'], kwargs['password'])):
205
202
  raise click.ClickException('Unable to find ssh key to use and password not supplied.')
206
203
  else:
@@ -218,7 +215,7 @@ def cli(**kwargs):
218
215
  # If you specify a password or prompt for one it overrides the ssh key
219
216
  if not kwargs['password']:
220
217
  if not kwargs['prompt']:
221
- if not kwargs['keyfile'] and len(sshenv.agent_keys) == 0:
218
+ if not kwargs['keyfile'] and (sshenv.agent_keys is None or len(sshenv.agent_keys) == 0):
222
219
  raise click.ClickException('Unable to find ssh key to use and password not supplied or prompt enabled.')
223
220
  else:
224
221
  log.info('Prompting for password and disabling discovered SSH keyfiles')
@@ -255,7 +252,7 @@ def cli(**kwargs):
255
252
  job.pre_hook = prehook
256
253
  jobs.append(job)
257
254
 
258
- log.info('Sending %s ServerJobs to sshreader module' % (len(jobs),))
255
+ log.info(f'Sending {len(jobs)} ServerJobs to sshreader module')
259
256
  if kwargs['dshbak'] is False and kwargs['coalesce'] is False:
260
257
  if kwargs['redline']:
261
258
  sshreader.sshread(jobs, pcount=0, tcount=0, print_lock=True)
@@ -1,7 +1,6 @@
1
- # coding=utf-8
2
1
  """A wrapper for Paramiko that attempts to make ssh sessions easier to work with.
3
2
  """
4
- # Copyright (C) 2015-2025 Jesse Almanrode
3
+ # Copyright (C) 2015-2026 Jesse Almanrode
5
4
  #
6
5
  # This program is free software: you can redistribute it and/or modify
7
6
  # it under the terms of the GNU Lesser General Public License as published by
@@ -17,12 +16,14 @@
17
16
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
17
  import logging
19
18
  import os
20
- import paramiko
21
19
  import socket
22
20
  from getpass import getuser
23
21
  from typing import Any, Optional
24
22
 
23
+ import paramiko
24
+
25
25
  from .customtypes import Command, EnvVars, Timeout
26
+
26
27
  log = logging.getLogger('sshreader')
27
28
 
28
29
 
@@ -56,7 +57,7 @@ def envvars() -> EnvVars:
56
57
  return EnvVars(**env)
57
58
 
58
59
 
59
- class SSH(object):
60
+ class SSH:
60
61
  """SSH Session class which can be used to send commands to a remote server. It also supports basic SFTP commands
61
62
  and can be used to push/pull files from a remote system.
62
63
 
@@ -80,16 +81,14 @@ class SSH(object):
80
81
  """
81
82
  def __init__(self, fqdn: str, username: str, password: Optional[str] = None, keyfile: Optional[str] = None,
82
83
  keypass: Optional[str] = None, port: int = 22, connect: bool = True, rsa_sha2: bool = True) -> None:
83
- if not keyfile:
84
- if len(paramiko.Agent().get_keys()) == 0:
85
- if not all((username, password)):
86
- paramiko.SSHException('username and password or keyfile not provided')
84
+ if not keyfile and len(paramiko.Agent().get_keys()) == 0 and not all((username, password)):
85
+ paramiko.SSHException('username and password or keyfile not provided')
87
86
  self.host = fqdn
88
87
  self.username = username
89
88
  self.password = password
90
89
  if keyfile:
91
90
  if not isinstance(keyfile, str):
92
- raise TypeError('expected %s for keyfile, got %s' % (str(str), str(type(keyfile))))
91
+ raise TypeError(f'expected {str(str)} for keyfile, got {str(type(keyfile))}')
93
92
  self.keyfile = os.path.abspath(os.path.expanduser(keyfile))
94
93
  else:
95
94
  self.keyfile = keyfile
@@ -122,7 +121,7 @@ class SSH(object):
122
121
  :return: Result of :meth:`paramiko.SFTPClient.put()`
123
122
  """
124
123
  if not self.__alive():
125
- raise paramiko.SSHException("connection to %s not established" % (self.host,))
124
+ raise paramiko.SSHException(f"connection to {self.host} not established")
126
125
  sftp = paramiko.SFTPClient.from_transport(self._connection.get_transport())
127
126
  try:
128
127
  result = sftp.put(os.path.expanduser(srcfile), os.path.expanduser(dstfile))
@@ -140,7 +139,7 @@ class SSH(object):
140
139
  :return: None
141
140
  """
142
141
  if not self.__alive():
143
- raise paramiko.SSHException("connection to %s not established" % (self.host,))
142
+ raise paramiko.SSHException(f"connection to {self.host} not established")
144
143
  sftp = paramiko.SFTPClient.from_transport(self._connection.get_transport())
145
144
  try:
146
145
  sftp.get(os.path.expanduser(srcfile), os.path.expanduser(dstfile))
@@ -164,7 +163,7 @@ class SSH(object):
164
163
  :raises: :class:`paramiko.SSHException`
165
164
  """
166
165
  if self.__alive() is False:
167
- raise paramiko.SSHException("connection to %s not established" % (self.host, ))
166
+ raise paramiko.SSHException(f"connection to {self.host} not established")
168
167
  if combine:
169
168
  try:
170
169
  stdin, stdout, stderr = self._connection.exec_command(command, timeout=timeout, get_pty=True)
@@ -174,7 +173,7 @@ class SSH(object):
174
173
  else:
175
174
  result = Command(cmd=command, stdout=stdout.read().strip(), stderr=None,
176
175
  return_code=stdout.channel.recv_exit_status())
177
- except (paramiko.buffered_pipe.PipeTimeout, socket.timeout):
176
+ except (paramiko.buffered_pipe.PipeTimeout, socket.timeout): # ty:ignore[possibly-missing-submodule]
178
177
  result = Command(cmd=command, stdout='command timed out', stderr=None, return_code=124)
179
178
  else:
180
179
  try:
@@ -185,7 +184,7 @@ class SSH(object):
185
184
  else:
186
185
  result = Command(cmd=command, stdout=stdout.read().strip(), stderr=stderr.read().strip(),
187
186
  return_code=stdout.channel.recv_exit_status())
188
- except (paramiko.buffered_pipe.PipeTimeout, socket.timeout):
187
+ except (paramiko.buffered_pipe.PipeTimeout, socket.timeout): # ty:ignore[possibly-missing-submodule]
189
188
  result = Command(cmd=command, stdout='', stderr='command timed out', return_code=124)
190
189
  return result
191
190
 
@@ -226,12 +225,10 @@ class SSH(object):
226
225
  :raises: :class:`paramiko.SSHException`
227
226
  """
228
227
  if self.__alive():
229
- raise paramiko.SSHException("connection to %s already established" % (self.host, ))
230
- paramiko.util.logging.getLogger().setLevel(logging.CRITICAL) # Keeping paramiko from logging errors to stdout
231
- if not self.keyfile:
232
- if len(paramiko.Agent().get_keys()) == 0:
233
- if not all((self.username, self.password)):
234
- paramiko.SSHException('username and password or keyfile not provided')
228
+ raise paramiko.SSHException(f"connection to {self.host} already established")
229
+ paramiko.util.logging.getLogger().setLevel(logging.CRITICAL) # Keeping paramiko from logging errors to stdout # ty:ignore[possibly-missing-submodule]
230
+ if not self.keyfile and len(paramiko.Agent().get_keys()) == 0 and not all((self.username, self.password)):
231
+ paramiko.SSHException('username and password or keyfile not provided')
235
232
  if self.keyfile:
236
233
  if self.rsa_sha2:
237
234
  self._connection.connect(self.host, port=self.port, username=self.username, password=self.keypass,