arpakitlib 1.8.328__py3-none-any.whl → 1.8.333__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,391 @@
1
+ # arpakit
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from datetime import timedelta
8
+ from typing import Any
9
+
10
+ import asyncssh
11
+ import paramiko
12
+ from arpakitlib.ar_json_util import transfer_data_to_json_str
13
+ from pydantic import BaseModel, ConfigDict
14
+
15
+
16
+ class _BaseSSHException(Exception):
17
+ pass
18
+
19
+
20
+ class ConnectionSSHException(_BaseSSHException):
21
+ def __init__(self, ssh_runner: SSHRunner, base_exception: Exception | None = None):
22
+ self.ssh_runner = ssh_runner
23
+ self.base_exception = base_exception
24
+
25
+ def __str__(self):
26
+ parts = [
27
+ f"Connection error",
28
+ f"{self.ssh_runner.username}@{self.ssh_runner.hostname}:{self.ssh_runner.port}",
29
+ f"{type(self.base_exception)=}",
30
+ f"{self.base_exception=}"
31
+ ]
32
+ return ', '.join(parts)
33
+
34
+ def __repr__(self):
35
+ return str(self)
36
+
37
+
38
+ class ErrorInRunSSHException(_BaseSSHException):
39
+ def __init__(self, ssh_runner: SSHRunner, base_exception: Exception | None = None, message: str | None = None):
40
+ self.ssh_runner = ssh_runner
41
+ self.base_exception = base_exception
42
+ self.message = message
43
+
44
+ def __str__(self):
45
+ parts = [
46
+ f"Error in run",
47
+ f"{self.ssh_runner.username}@{self.ssh_runner.hostname}:{self.ssh_runner.port}",
48
+ ]
49
+ if self.base_exception is not None:
50
+ parts.append(f"{self.base_exception=}")
51
+ if self.message is not None:
52
+ parts.append(f"{self.message=}")
53
+ return ', '.join(parts)
54
+
55
+ def __repr__(self):
56
+ return str(self)
57
+
58
+
59
+ class SSHRunResultHasErrorSSHException(_BaseSSHException):
60
+ def __init__(self, ssh_run_result: SSHRunResult, message: str | None = None):
61
+ self.ssh_run_result = ssh_run_result
62
+ self.message = message
63
+
64
+ def __str__(self):
65
+ parts = [
66
+ f"SSHRunResult has error",
67
+ f"{self.ssh_run_result.ssh_runner.username}@{self.ssh_run_result.ssh_runner.hostname}:{self.ssh_run_result.ssh_runner.port}",
68
+ f"return_code={str(self.ssh_run_result.return_code)}",
69
+ f"err={str(self.ssh_run_result.err)}"
70
+ ]
71
+ if self.message is not None:
72
+ parts.append(f"message={self.message}")
73
+ return ', '.join(parts)
74
+
75
+ def __repr__(self):
76
+ return str(self)
77
+
78
+
79
+ class SSHRunResult(BaseModel):
80
+ out: str
81
+ err: str
82
+ return_code: int | None = None
83
+ ssh_runner: SSHRunner
84
+
85
+ model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True, from_attributes=True)
86
+
87
+ def simple_dict(self) -> dict[str, Any]:
88
+ return {
89
+ "out": self.out,
90
+ "err": self.err,
91
+ "return_code": self.return_code,
92
+ "has_bad_return_code": self.has_bad_return_code,
93
+ "has_err": self.has_err,
94
+ "has_out": self.has_out,
95
+ "username": self.ssh_runner.username,
96
+ "hostname": self.ssh_runner.hostname,
97
+ "port": self.ssh_runner.port,
98
+ }
99
+
100
+ def simple_json(self) -> str:
101
+ return transfer_data_to_json_str(
102
+ self.simple_dict(),
103
+ beautify=True,
104
+ fast=False
105
+ )
106
+
107
+ def __repr__(self) -> str:
108
+ return self.simple_json()
109
+
110
+ def __str__(self) -> str:
111
+ return self.simple_json()
112
+
113
+ @property
114
+ def has_bad_return_code(self) -> bool:
115
+ if self.return_code is None:
116
+ return False
117
+ return self.return_code != 0
118
+
119
+ @property
120
+ def has_err(self) -> bool:
121
+ if self.err:
122
+ return True
123
+ return False
124
+
125
+ @property
126
+ def has_out(self) -> bool:
127
+ if self.out:
128
+ return True
129
+ return False
130
+
131
+ def raise_for_bad_return_code(self):
132
+ if self.has_bad_return_code:
133
+ raise SSHRunResultHasErrorSSHException(ssh_run_result=self)
134
+
135
+ def raise_for_err(self):
136
+ if self.has_err:
137
+ raise SSHRunResultHasErrorSSHException(ssh_run_result=self)
138
+
139
+
140
+ class SSHRunner:
141
+
142
+ def __init__(
143
+ self,
144
+ *,
145
+ username: str = "root",
146
+ hostname: str, # ipv4, ipv6, domain
147
+ port: int = 22,
148
+ password: str | None = None,
149
+ base_timeout: float | None = None,
150
+ check_if_already_connected: bool | None = True
151
+ ):
152
+ self.username = username
153
+ self.hostname = hostname
154
+ self.port = port
155
+ self.password = password
156
+
157
+ if base_timeout is None:
158
+ base_timeout = timedelta(seconds=10).total_seconds()
159
+ self.base_timeout = base_timeout
160
+
161
+ if check_if_already_connected is None:
162
+ check_if_already_connected = True
163
+ self.check_if_already_connected = check_if_already_connected
164
+
165
+ self._logger = logging.getLogger(
166
+ f"{logging.getLogger(self.__class__.__name__)} - {self.username}@{self.hostname}:{self.port}"
167
+ )
168
+
169
+ self.async_conn: asyncssh.SSHClientConnection | None = None
170
+
171
+ self.sync_client: paramiko.SSHClient | None = None
172
+
173
+ """SYNC"""
174
+
175
+ def sync_connect(
176
+ self,
177
+ *,
178
+ common_timeout: float | None = None,
179
+ connect_kwargs: dict[str, Any] | None = None,
180
+ check_if_already_connected: bool | None = None
181
+ ) -> SSHRunner:
182
+ if check_if_already_connected is None:
183
+ check_if_already_connected = self.check_if_already_connected
184
+
185
+ if check_if_already_connected and self.sync_client is not None:
186
+ self._logger.info("already connected")
187
+ return self
188
+
189
+ if connect_kwargs is None:
190
+ connect_kwargs = {}
191
+ if common_timeout is None:
192
+ common_timeout = self.base_timeout
193
+
194
+ connect_kwargs["hostname"] = self.hostname
195
+ connect_kwargs["username"] = self.username
196
+ connect_kwargs["password"] = self.password
197
+ connect_kwargs["port"] = self.port
198
+
199
+ if connect_kwargs.get("timeout") is None:
200
+ connect_kwargs["timeout"] = common_timeout
201
+ if connect_kwargs.get("auth_timeout") is None:
202
+ connect_kwargs["auth_timeout"] = common_timeout
203
+ if connect_kwargs.get("banner_timeout") is None:
204
+ connect_kwargs["banner_timeout"] = common_timeout
205
+ if connect_kwargs.get("channel_timeout") is None:
206
+ connect_kwargs["channel_timeout"] = common_timeout
207
+
208
+ self._logger.info("connecting")
209
+
210
+ self.sync_client = paramiko.SSHClient()
211
+ self.sync_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
212
+
213
+ try:
214
+ self.sync_client.connect(**connect_kwargs)
215
+ except Exception as exception:
216
+ self._logger.error(f"not connected, {exception=}", exc_info=True)
217
+ raise ConnectionSSHException(ssh_runner=self, base_exception=exception)
218
+
219
+ self._logger.info("connected")
220
+
221
+ return self
222
+
223
+ def sync_check_connection(self):
224
+ self.sync_connect()
225
+
226
+ def sync_is_conn_good(self) -> bool:
227
+ try:
228
+ self.sync_check_connection()
229
+ except ConnectionSSHException:
230
+ return False
231
+ except Exception:
232
+ return False
233
+ return True
234
+
235
+ def sync_run(
236
+ self,
237
+ command: str,
238
+ *,
239
+ timeout: float | None = timedelta(seconds=10).total_seconds(),
240
+ raise_for_bad_return_code: bool = True
241
+ ) -> SSHRunResult:
242
+ if not command or not command.strip():
243
+ raise ValueError("command must be a non-empty string")
244
+
245
+ if timeout is None:
246
+ timeout = self.base_timeout
247
+
248
+ self.sync_connect()
249
+
250
+ self._logger.info(command)
251
+
252
+ try:
253
+ stdin, stdout, stderr = self.sync_client.exec_command(
254
+ command=command,
255
+ timeout=timeout
256
+ )
257
+ return_code = stdout.channel.recv_exit_status()
258
+ stdout = stdout.read().decode()
259
+ stderr = stderr.read().decode()
260
+ except Exception as exception:
261
+ raise ErrorInRunSSHException(ssh_runner=self, base_exception=exception)
262
+
263
+ ssh_run_result = SSHRunResult(
264
+ out=stdout,
265
+ err=stderr,
266
+ return_code=return_code,
267
+ ssh_runner=self
268
+ )
269
+
270
+ if raise_for_bad_return_code:
271
+ ssh_run_result.raise_for_bad_return_code()
272
+
273
+ return ssh_run_result
274
+
275
+ def sync_close(self):
276
+ if self.sync_client is not None:
277
+ self.sync_client.close()
278
+ self.sync_client = paramiko.SSHClient()
279
+ self.sync_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
280
+ self.sync_client = None
281
+
282
+ """ASYNC SYNC"""
283
+
284
+ async def async_connect(
285
+ self,
286
+ *,
287
+ common_timeout: float | None = None,
288
+ connect_kwargs: dict[str, Any] | None = None,
289
+ check_if_already_connected: bool | None = None
290
+ ) -> SSHRunner:
291
+ if check_if_already_connected is None:
292
+ check_if_already_connected = self.check_if_already_connected
293
+
294
+ if check_if_already_connected and self.async_conn is not None:
295
+ self._logger.info("already connected")
296
+ return self
297
+
298
+ if connect_kwargs is None:
299
+ connect_kwargs = {}
300
+ if common_timeout is None:
301
+ common_timeout = self.base_timeout
302
+
303
+ connect_kwargs["host"] = self.hostname
304
+ connect_kwargs["username"] = self.username
305
+ connect_kwargs["password"] = self.password
306
+ connect_kwargs["port"] = self.port
307
+
308
+ if connect_kwargs.get("connect_timeout") is None:
309
+ connect_kwargs["connect_timeout"] = common_timeout
310
+ if connect_kwargs.get("login_timeout") is None:
311
+ connect_kwargs["login_timeout"] = common_timeout
312
+
313
+ connect_kwargs["known_hosts"] = None
314
+
315
+ self._logger.info("connecting")
316
+
317
+ try:
318
+ self.async_conn = await asyncssh.connect(**connect_kwargs)
319
+ except Exception as exception:
320
+ self._logger.error(f"not connected, {exception=}", exc_info=True)
321
+ raise ConnectionSSHException(ssh_runner=self, base_exception=exception)
322
+
323
+ self._logger.info("connected")
324
+
325
+ return self
326
+
327
+ async def async_check_connection(self):
328
+ await self.async_connect()
329
+
330
+ async def async_is_conn_good(self) -> bool:
331
+ try:
332
+ await self.async_check_connection()
333
+ except ConnectionSSHException:
334
+ return False
335
+ return True
336
+
337
+ async def async_run(
338
+ self,
339
+ command: str,
340
+ *,
341
+ timeout: float | None = timedelta(seconds=10).total_seconds(),
342
+ raise_for_bad_return_code: bool = True
343
+ ) -> SSHRunResult:
344
+ if not command or not command.strip():
345
+ raise ValueError("command must be a non-empty string")
346
+
347
+ if timeout is None:
348
+ timeout = self.base_timeout
349
+
350
+ await self.async_connect()
351
+
352
+ self._logger.info(command)
353
+
354
+ try:
355
+ result: asyncssh.SSHCompletedProcess = await self.async_conn.run(
356
+ command,
357
+ check=False,
358
+ timeout=timeout
359
+ )
360
+ except Exception as exception:
361
+ raise ErrorInRunSSHException(ssh_runner=self, base_exception=exception)
362
+
363
+ ssh_run_result = SSHRunResult(
364
+ out=result.stdout,
365
+ err=result.stderr,
366
+ return_code=result.returncode,
367
+ ssh_runner=self
368
+ )
369
+
370
+ if raise_for_bad_return_code:
371
+ ssh_run_result.raise_for_bad_return_code()
372
+
373
+ return ssh_run_result
374
+
375
+ async def async_close(self):
376
+ if self.async_conn is not None:
377
+ self.async_conn.close()
378
+ self.async_conn = None
379
+
380
+
381
+ def __example():
382
+ pass
383
+
384
+
385
+ async def __async_example():
386
+ pass
387
+
388
+
389
+ if __name__ == '__main__':
390
+ __example()
391
+ asyncio.run(__async_example())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arpakitlib
3
- Version: 1.8.328
3
+ Version: 1.8.333
4
4
  Summary: arpakitlib
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Operating System :: OS Independent
18
18
  Classifier: Topic :: Software Development :: Libraries
19
19
  Requires-Dist: aio-pika (>=9.5.4,<10.0.0)
20
- Requires-Dist: aiogram (>=3.17.0,<4.0.0)
20
+ Requires-Dist: aiogram (>=3.22.0,<4.0.0)
21
21
  Requires-Dist: aiohttp (>=3.11.16,<4.0.0)
22
22
  Requires-Dist: aiohttp-socks (>=0.10.1,<0.11.0)
23
23
  Requires-Dist: aiosmtplib (>=4.0.0,<5.0.0)
@@ -45,7 +45,7 @@ Requires-Dist: pydantic (>=2.10.5,<3.0.0)
45
45
  Requires-Dist: pydantic-settings (>=2.7.1,<3.0.0)
46
46
  Requires-Dist: pyjwt (>=2.10.1,<3.0.0)
47
47
  Requires-Dist: pymongo (>=4.10.1,<5.0.0)
48
- Requires-Dist: pytelegrambotapi (>=4.27.0,<5.0.0)
48
+ Requires-Dist: pytelegrambotapi (>=4.29.1,<5.0.0)
49
49
  Requires-Dist: pytest (>=8.4.2,<9.0.0)
50
50
  Requires-Dist: pytz (>=2024.2,<2025.0)
51
51
  Requires-Dist: qrcode (>=8.2,<9.0)
@@ -418,19 +418,20 @@ arpakitlib/ar_really_validate_email_util.py,sha256=HBfhyiDB3INI6Iq6hR2WOMKA5wVWW
418
418
  arpakitlib/ar_really_validate_url_util.py,sha256=aaSPVMbz2DSqlC2yk2g44-kTIiHlITfJwIG97L-Y93U,1309
419
419
  arpakitlib/ar_retry_func_util.py,sha256=LB4FJRsu2cssnPw6X8bCEcaGpQsXhkLkgeU37w1t9fU,2250
420
420
  arpakitlib/ar_run_cmd_util.py,sha256=D_rPavKMmWkQtwvZFz-Io5Ak8eSODHkcFeLPzNVC68g,1072
421
- arpakitlib/ar_safe_func.py,sha256=JQNaM3q4Z6nUE8bDfzXNBGkWSXm0PZ-GMMvu6UWUIYk,2400
421
+ arpakitlib/ar_safe_func_util.py,sha256=JQNaM3q4Z6nUE8bDfzXNBGkWSXm0PZ-GMMvu6UWUIYk,2400
422
422
  arpakitlib/ar_settings_util.py,sha256=FeQQkuVrLVYkFAIg3Wy6ysyTt_sqLTX0REAe60gbM3k,2361
423
423
  arpakitlib/ar_sleep_util.py,sha256=ggaj7ML6QK_ADsHMcyu6GUmUpQ_9B9n-SKYH17h-9lM,1045
424
424
  arpakitlib/ar_sqladmin_util.py,sha256=SEoaowAPF3lhxPsNjwmOymNJ55Ty9rmzvsDm7gD5Ceo,861
425
425
  arpakitlib/ar_sqlalchemy_drop_check_constraints_util.py,sha256=uVktYLjNHrMPWQAq8eBpapShPKbLb3LrRBnnss3gaYY,3624
426
426
  arpakitlib/ar_sqlalchemy_ensure_check_constraints_util.py,sha256=gqZTPSCAPUMRiXcmv9xls5S8YkUAg-gwFIEvqQsJ_JM,5437
427
427
  arpakitlib/ar_sqlalchemy_util.py,sha256=hnwZW7v8FOjzYCkaoumkZYrtDkAv2gev9ZWH6it8H5k,16050
428
+ arpakitlib/ar_ssh_runner_util.py,sha256=3gKtLXf77AsYVZ7CSE4zhHJZPCoEUlQS5lwWytgN5e0,11844
428
429
  arpakitlib/ar_str_util.py,sha256=6KlLL-SB8gzK-6gwQEd3zuYbRvtjd9HFpJ9-xHbkH6U,4355
429
430
  arpakitlib/ar_type_util.py,sha256=Cs_tef-Fc5xeyAF54KgISCsP11NHyzIsglm4S3Xx7iM,4049
430
431
  arpakitlib/ar_uppercase_env_keys_util.py,sha256=BsUCJhfchBIav0AE54_tVgYcE4p1JYoWdPGCHWZnROA,2790
431
432
  arpakitlib/ar_yookassa_api_client_util.py,sha256=7DL_0GyIOTuSNBHUO3qWxAXMKlBRHjKgcA6ttst8k1A,5265
432
- arpakitlib-1.8.328.dist-info/METADATA,sha256=89Z-J3Lhic6uYoQgkP8PN0m5uTDxjwlUMK6Y6UaL24A,3804
433
- arpakitlib-1.8.328.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
434
- arpakitlib-1.8.328.dist-info/entry_points.txt,sha256=36xqR3PJFT2kuwjkM_EqoIy0qFUDPKSm_mJaI7emewE,87
435
- arpakitlib-1.8.328.dist-info/licenses/LICENSE,sha256=GPEDQMam2r7FSTYqM1mm7aKnxLaWcBotH7UvQtea-ec,11355
436
- arpakitlib-1.8.328.dist-info/RECORD,,
433
+ arpakitlib-1.8.333.dist-info/METADATA,sha256=ptp1W-410pxMx0hli2eUKJ-h6f_bYrsRWB45Vk9ALH8,3804
434
+ arpakitlib-1.8.333.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
435
+ arpakitlib-1.8.333.dist-info/entry_points.txt,sha256=36xqR3PJFT2kuwjkM_EqoIy0qFUDPKSm_mJaI7emewE,87
436
+ arpakitlib-1.8.333.dist-info/licenses/LICENSE,sha256=GPEDQMam2r7FSTYqM1mm7aKnxLaWcBotH7UvQtea-ec,11355
437
+ arpakitlib-1.8.333.dist-info/RECORD,,
File without changes