cloudru-qa-ssh-client 1.0.0__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.
@@ -0,0 +1,492 @@
1
+ Metadata-Version: 2.5
2
+ Name: cloudru-qa-ssh-client
3
+ Version: 1.0.0
4
+ Summary: Простой SSH-клиент для автотестов
5
+ Project-URL: Repository, https://github.com/cloud-ru-tech/qa-libraries
6
+ Project-URL: Issues, https://github.com/cloud-ru-tech/qa-libraries/issues
7
+ Author-email: akfernatii <akfernatii@cloud.ru>
8
+ License-Expression: Apache-2.0
9
+ License-File: LICENSE
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Topic :: Software Development :: Testing
16
+ Classifier: Topic :: System :: Networking
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.13
19
+ Requires-Dist: asyncssh<3,>=2.14.0
20
+ Requires-Dist: paramiko<5.2,>=5.1.0
21
+ Provides-Extra: structlog
22
+ Requires-Dist: structlog>=26.1.0; extra == 'structlog'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # SSH client
26
+
27
+ [![PyPI](https://img.shields.io/pypi/v/cloudru-qa-ssh-client)](https://pypi.org/project/cloudru-qa-ssh-client/)
28
+ [![Python](https://img.shields.io/pypi/pyversions/cloudru-qa-ssh-client)](https://pypi.org/project/cloudru-qa-ssh-client/)
29
+ [![License](https://img.shields.io/pypi/l/cloudru-qa-ssh-client)](https://github.com/cloud-ru-tech/qa-libraries/blob/master/LICENSE)
30
+
31
+ English | [Русский](README.ru.md)
32
+
33
+ A simple SSH client library for automated tests: synchronous and asynchronous clients with
34
+ constructor options, command decorators, and logging.
35
+
36
+ ## Installation
37
+
38
+ Requires Python 3.13 or later.
39
+
40
+ ```shell
41
+ pip install cloudru-qa-ssh-client
42
+ ```
43
+
44
+ For structured logging with structlog, install the `structlog` extra:
45
+
46
+ ```shell
47
+ pip install "cloudru-qa-ssh-client[structlog]"
48
+ ```
49
+
50
+ Import the library as the module `ssh_client`.
51
+
52
+ ## Skills for AI agents
53
+
54
+ This library does not have its own skill yet. If your project uses other
55
+ libraries from this suite with [skills for AI agents](https://github.com/cloud-ru-tech/qa-libraries#skills-for-ai-agents),
56
+ select and enable them from your project root (requires [`uv`](https://docs.astral.sh/uv/)):
57
+
58
+ ```shell
59
+ uvx library-skills
60
+ ```
61
+
62
+ For Claude Code, add `--claude`.
63
+
64
+ ## Main features
65
+
66
+ 1. Connect to remote hosts using a password or a key
67
+ 2. Run commands as strings or lists of shell command fragments
68
+ 3. Set timeouts for the client and individual requests
69
+ 4. Check exit codes with `raise_for_status=True` for individual commands
70
+ 5. Configure retry counts and intervals
71
+ 6. Retry connection and command failures
72
+ 7. Get result objects with stdout, stderr, and exit code fields
73
+ 8. Log requests and responses with a supplied logger
74
+ 9. Mask request and response logs using strings or regular expressions
75
+ 10. Use asynchronous clients through asyncssh
76
+ 11. Receive output in real time for interactive commands
77
+ 12. Interrupt commands with conditional callbacks
78
+
79
+ ## Usage example
80
+
81
+ ### Synchronous client
82
+
83
+ ```python
84
+ import logging
85
+ from ssh_client import SyncSSHClient, options
86
+
87
+ # Create a logger
88
+ logger = logging.getLogger(__name__)
89
+ logger.setLevel(logging.INFO)
90
+
91
+ # Create a client
92
+ client = SyncSSHClient(
93
+ "example.com",
94
+ 22,
95
+ options.with_password("user", "password"),
96
+ options.with_request_timeout(30),
97
+ options.with_logger(logger),
98
+ )
99
+
100
+ # Run a command
101
+ result = client.execute(["ls", "-la"])
102
+ print(f"Exit code: {result.exit_code}")
103
+ print(f"Stdout: {result.stdout}")
104
+ print(f"Stderr: {result.stderr}")
105
+
106
+ # Close the connection
107
+ client.close()
108
+ ```
109
+
110
+ ### Asynchronous client
111
+
112
+ ```python
113
+ import asyncio
114
+ import logging
115
+ from ssh_client import AsyncSSHClient, options
116
+
117
+
118
+ async def main():
119
+ # Create a logger
120
+ logger = logging.getLogger(__name__)
121
+ logger.setLevel(logging.INFO)
122
+
123
+ # Create a client
124
+ client = AsyncSSHClient(
125
+ "example.com",
126
+ 22,
127
+ options.with_private_key("user", "/path/to/private/key"),
128
+ options.with_request_timeout(30),
129
+ options.with_logger(logger),
130
+ )
131
+
132
+ # Run a command
133
+ result = await client.execute(["ls", "-la"])
134
+ print(f"Exit code: {result.exit_code}")
135
+ print(f"Stdout: {result.stdout}")
136
+ print(f"Stderr: {result.stderr}")
137
+
138
+ # Close the connection
139
+ await client.aclose()
140
+
141
+
142
+ # Run the asynchronous function
143
+ asyncio.run(main())
144
+ ```
145
+
146
+ ## Configuration
147
+
148
+ Configure the client using `ClientOption` options. Built-in options are in the `options` module.
149
+
150
+ By default, both clients verify server keys against the standard `~/.ssh/known_hosts` file.
151
+ Unknown or changed keys are rejected. To use a separate file, set
152
+ `options.with_known_hosts("/path/to/known_hosts")`; the file must contain a previously verified server key.
153
+
154
+ ### Available options
155
+
156
+ * `with_request_timeout`: set the SSH request timeout
157
+ * `with_connection_timeout`: set the SSH connection timeout
158
+ * `with_password`: set the username and password for authentication
159
+ * `with_private_key`: set the username and private key path for authentication
160
+ * `with_known_hosts`: choose a trusted server keys file
161
+ * `with_logger`: set a logger for SSH requests and responses
162
+ * `with_structlogger`: set a structured logger for SSH requests and responses (requires the structlog extra)
163
+ * `retry_if_connection_fails`: retry failed connections
164
+ * `retry_if_command_fails`: retry failed commands
165
+ * `with_connect_decorators`: add custom decorators to the connection method
166
+ * `with_execute_decorators`: add custom decorators to the command execution method
167
+ * `with_live_execute_callback`: add callbacks to process stdout and stderr lines in real time
168
+
169
+ ### Using with_live_execute_callback
170
+
171
+ ```python
172
+ import logging
173
+ from ssh_client import SyncSSHClient, options
174
+
175
+
176
+ # Define callbacks for live output
177
+ def stdout_callback(line):
178
+ print(f"STDOUT: {line}")
179
+
180
+
181
+ def stderr_callback(line):
182
+ print(f"STDERR: {line}")
183
+
184
+
185
+ # Create a client with callbacks
186
+ client = SyncSSHClient(
187
+ "example.com",
188
+ 22,
189
+ options.with_password("user", "password"),
190
+ options.with_live_execute_callback(stdout_callback, stderr_callback),
191
+ )
192
+
193
+ # Run a command with live output
194
+ result = client.execute_live(["ping", "-c", "3", "google.com"])
195
+
196
+ # Callbacks print all lines in real time
197
+ print(f"Exit code: {result.exit_code}")
198
+
199
+ # Close the connection
200
+ client.close()
201
+ ```
202
+
203
+ ### Interrupting commands with callbacks
204
+
205
+ Interrupt a command when a condition is met. This is useful for long-running commands that should stop when particular text appears in the output.
206
+
207
+ #### Interrupting on stdout (synchronous client)
208
+
209
+ ```python
210
+ import logging
211
+ from ssh_client import SyncSSHClient, options
212
+
213
+
214
+ # Define a conditional interrupt callback
215
+ def interrupt_on_keyword(line):
216
+ """Interrupt execution if the line contains 'ERROR'."""
217
+ print(f"Processing line: {line}")
218
+ return "ERROR" in line
219
+
220
+
221
+ # Create a client
222
+ client = SyncSSHClient(
223
+ "example.com",
224
+ 22,
225
+ options.with_password("user", "password"),
226
+ )
227
+
228
+ try:
229
+ # Run a command with conditional interruption
230
+ result = client.execute_live(
231
+ ["tail", "-f", "/var/log/application.log"], interrupt_stdout_callback=interrupt_on_keyword
232
+ )
233
+
234
+ print(f"Command was interrupted with exit code: {result.exit_code}")
235
+ print("Captured output:")
236
+ for line in result.stdout_lines:
237
+ print(f" {line}")
238
+ finally:
239
+ client.close()
240
+ ```
241
+
242
+ #### Interrupting on stderr (asynchronous client)
243
+
244
+ ```python
245
+ import asyncio
246
+ from ssh_client import AsyncSSHClient, options
247
+
248
+
249
+ async def main():
250
+ # Define a stderr interrupt callback
251
+ def interrupt_on_error(line):
252
+ """Interrupt execution if stderr contains a critical error."""
253
+ print(f"Error detected: {line}")
254
+ return "CRITICAL" in line or "FATAL" in line
255
+
256
+ # Create a client
257
+ client = AsyncSSHClient(
258
+ "example.com",
259
+ 22,
260
+ options.with_private_key("user", "/path/to/private/key"),
261
+ )
262
+
263
+ try:
264
+ # Run a command with stderr interruption
265
+ result = await client.execute_live(["some-long-running-command"], interrupt_stderr_callback=interrupt_on_error)
266
+
267
+ print(f"Command was interrupted with exit code: {result.exit_code}")
268
+ print("Captured stderr:")
269
+ for line in result.stderr_lines:
270
+ print(f" {line}")
271
+ finally:
272
+ await client.aclose()
273
+
274
+
275
+ # Run the asynchronous function
276
+ asyncio.run(main())
277
+ ```
278
+
279
+ Synchronous `execute_live` merges stdout and stderr through a terminal. Separate stderr callbacks are available on the asynchronous client.
280
+
281
+ When an interrupt callback fires, the command stops and `execute_live` returns a result with a negative exit code (-1 for the synchronous client, -2 for the asynchronous client), including all lines received before the interruption.
282
+
283
+ ## Advanced features
284
+
285
+ ### Temporary options with with_options
286
+
287
+ Use `with_options` to create temporary clients with additional options for a single command:
288
+
289
+ ```python
290
+ import logging
291
+ from ssh_client import SyncSSHClient, options
292
+
293
+ # Create the base client
294
+ client = SyncSSHClient(
295
+ "example.com",
296
+ 22,
297
+ options.with_password("user", "password"),
298
+ options.with_request_timeout(30),
299
+ options.with_logger(logging.getLogger(__name__)),
300
+ )
301
+
302
+ # Use with_options for a command with custom settings
303
+ result = client.with_options(
304
+ options.with_request_timeout(60), # Longer timeout for this command only
305
+ options.retry_if_command_fails(
306
+ retries_attempts_number=3, retries_interval_in_seconds=1
307
+ ), # Retries for this command only
308
+ ).execute(["sleep", "10", "&&", "echo", "Done"])
309
+
310
+ # The base client stays unchanged
311
+ client.close()
312
+ ```
313
+
314
+ ### Structured logging
315
+
316
+ Install the `structlog` extra to use structured logging:
317
+
318
+ ```shell
319
+ pip install "cloudru-qa-ssh-client[structlog]"
320
+ ```
321
+
322
+ Structured logging example:
323
+
324
+ ```python
325
+ import structlog
326
+ from ssh_client import SyncSSHClient, options
327
+
328
+ # Configure structlog
329
+ structlog.configure(
330
+ processors=[structlog.processors.TimeStamper(fmt="iso"), structlog.processors.JSONRenderer()],
331
+ context_class=dict,
332
+ logger_factory=structlog.PrintLoggerFactory(),
333
+ )
334
+
335
+ # Create a client with a structured logger
336
+ client = SyncSSHClient(
337
+ "example.com",
338
+ 22,
339
+ options.with_password("user", "password"),
340
+ options.with_structlogger(structlog.get_logger()),
341
+ )
342
+
343
+ # Run a command with structured logging
344
+ result = client.execute(["ls", "-la"])
345
+ print(f"Exit code: {result.exit_code}")
346
+
347
+ # Close the connection
348
+ client.close()
349
+ ```
350
+
351
+ ### Retries
352
+
353
+ Configure retries for both connections and command execution:
354
+
355
+ ```python
356
+ from ssh_client import SyncSSHClient, options
357
+
358
+ client = SyncSSHClient(
359
+ "unstable-server.com",
360
+ 22,
361
+ options.with_password("user", "password"),
362
+ # Connection retries
363
+ options.retry_if_connection_fails(
364
+ retries_attempts_number=5, retries_interval_in_seconds=10.0, reraise_exception=True
365
+ ),
366
+ # Command retries
367
+ options.retry_if_command_fails(retries_attempts_number=3, retries_interval_in_seconds=5.0, reraise_exception=True),
368
+ )
369
+
370
+ # Run a command with retries
371
+ result = client.execute(["some-unstable-command"])
372
+
373
+ # Close the connection
374
+ client.close()
375
+ ```
376
+
377
+ ### Masking log data
378
+
379
+ Mask confidential data in logs:
380
+
381
+ ```python
382
+ import logging
383
+ import re
384
+ from ssh_client import SyncSSHClient, options
385
+
386
+ # Configure a logger
387
+ logger = logging.getLogger(__name__)
388
+ logger.setLevel(logging.INFO)
389
+
390
+ client = SyncSSHClient(
391
+ "example.com",
392
+ 22,
393
+ options.with_password("user", "password"),
394
+ # Mask confidential log data
395
+ options.with_logger(
396
+ logger,
397
+ mask_patterns=[
398
+ "password", # String to replace
399
+ "secret", # Another string to replace
400
+ re.compile(r"token=\w+"), # Regular expression
401
+ ],
402
+ ),
403
+ )
404
+
405
+ # Run a command with confidential data
406
+ # The data is masked in logs
407
+ result = client.execute(["echo", "password=secret123", "token=abc123def456"])
408
+
409
+ # Close the connection
410
+ client.close()
411
+ ```
412
+
413
+ ### Custom decorators
414
+
415
+ Add custom decorators to connection and command execution methods:
416
+
417
+ ```python
418
+ import time
419
+ from functools import wraps
420
+ from ssh_client import SyncSSHClient, options
421
+
422
+
423
+ # Decorator to measure execution time
424
+ def timing_decorator(func):
425
+ @wraps(func)
426
+ def wrapper(*args, **kwargs):
427
+ start_time = time.time()
428
+ result = func(*args, **kwargs)
429
+ end_time = time.time()
430
+ print(f"{func.__name__} took {end_time - start_time:.2f} seconds")
431
+ return result
432
+
433
+ return wrapper
434
+
435
+
436
+ # Decorator to handle errors
437
+ def error_handler_decorator(func):
438
+ @wraps(func)
439
+ def wrapper(*args, **kwargs):
440
+ try:
441
+ return func(*args, **kwargs)
442
+ except Exception as e:
443
+ print(f"Error in {func.__name__}: {e}")
444
+ # Retry or take another action here
445
+ raise
446
+
447
+ return wrapper
448
+
449
+
450
+ client = SyncSSHClient(
451
+ "example.com",
452
+ 22,
453
+ options.with_password("user", "password"),
454
+ # Add connection decorators
455
+ options.with_connect_decorators(timing_decorator, error_handler_decorator),
456
+ # Add command execution decorators
457
+ options.with_execute_decorators(timing_decorator),
458
+ )
459
+
460
+ # Run a command with decorators
461
+ result = client.execute(["ls", "-la"])
462
+
463
+ # Close the connection
464
+ client.close()
465
+ ```
466
+
467
+ ## Development
468
+
469
+ This library is developed in the [qa-libraries](https://github.com/cloud-ru-tech/qa-libraries) monorepo
470
+ alongside the other `cloudru-qa-*` libraries. You need [`uv`](https://docs.astral.sh/uv/), GNU Make
471
+ and [`checkmake`](https://github.com/checkmake/checkmake);
472
+ run the commands from the repository root:
473
+
474
+ ```shell
475
+ make sync # dependencies for all libraries from uv.lock
476
+ make test-ssh-client # tests for this library
477
+ make typecheck-ssh-client # mypy for this library
478
+ make check # formatting, Ruff, mypy, and tests for the whole suite
479
+ ```
480
+
481
+ Tests require Docker to be running: the SSH server starts in a container automatically. Server
482
+ and client keys are generated for each run and are not stored in the repository.
483
+
484
+ Tests run on Python 3.13 and 3.14. Each version uses a separate environment:
485
+ `make sync test-ssh-client PYTHON_VERSION=3.14`.
486
+
487
+ Create a release with the Git tag `ssh-client/v<version>`. The tag version must match the version
488
+ in `packages/ssh-client/pyproject.toml`.
489
+
490
+ ## License
491
+
492
+ [Apache-2.0](https://github.com/cloud-ru-tech/qa-libraries/blob/master/LICENSE)
@@ -0,0 +1,21 @@
1
+ ssh_client/__init__.py,sha256=USdulJKVyGRgxgQaltU_gVISv9ZxGzAGOX6IhGsXLfQ,473
2
+ ssh_client/client.py,sha256=pkvpU-tMipDl0dEU8ICI8i_c15IXmKiVxkis0-0maQk,11419
3
+ ssh_client/config.py,sha256=lKz-nVMTV8-Xxd2N7yk3I3RYztnFRNtVqACe36JmGR4,1113
4
+ ssh_client/event_hooks.py,sha256=voDafOJXYtfH6oi2HqIFXcFv2XyP6SzrAlSEq-jIFIE,3601
5
+ ssh_client/options.py,sha256=m-Sb-cEprz8sG82rX7RjH_ls78YSFK12nPc18JEmW5k,6080
6
+ ssh_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ ssh_client/results.py,sha256=UL8_7w5M9XXhswvEF4TSbzVdA27PHzIExnX5jj3Uh18,896
8
+ ssh_client/types.py,sha256=CllecsyNY91UJb2bV8L8fnOMTNPoT_fH6dWLTeUns9k,2204
9
+ ssh_client/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ ssh_client/clients/asyncssh_client.py,sha256=dYdNaJeXNCqR-OqsKqHnTdgCGHRrbI9JZsZ_ZDttbBs,6705
11
+ ssh_client/clients/exceptions.py,sha256=-iohyrEYqjcAwMd2KTRXMJXGZolliZYxa7v5RstYPzM,1035
12
+ ssh_client/clients/paramiko_client.py,sha256=CfzW27U_0HYHN6ASaAimTOfn9NkiiCUqqGU1jHg_IKM,7927
13
+ ssh_client/examples/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ ssh_client/examples/basic_usage.py,sha256=AxRTnTN4EpqTzleyy_mz_eXiT_XKINfyC7gzQ6GYOdM,3076
15
+ ssh_client/utils/__init__.py,sha256=ERR092hr_in0zmtazHQQQjl279nlZoaNMbujw8Y-0YI,259
16
+ ssh_client/utils/masking.py,sha256=oacicWLdAObiSLUW61M38K9iF9_1q3nEYAbu3pD7k6k,1455
17
+ ssh_client/utils/retries.py,sha256=sjJWCVXQM7zTzNQe2ktB8bYVAjXDUbzrrLfnv93y5o4,6237
18
+ cloudru_qa_ssh_client-1.0.0.dist-info/METADATA,sha256=vpc5Kjs8nwiE5AO2vpKQ3z3b_3wk84KET_IXJjldTEQ,14166
19
+ cloudru_qa_ssh_client-1.0.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
20
+ cloudru_qa_ssh_client-1.0.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
21
+ cloudru_qa_ssh_client-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any