dycw-utilities 0.174.1__py3-none-any.whl → 0.174.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dycw-utilities
3
- Version: 0.174.1
3
+ Version: 0.174.3
4
4
  Author: Derek Wan
5
5
  Author-email: Derek Wan <d.wan@icloud.com>
6
6
  Requires-Dist: atomicwrites>=1.4.1,<1.5
@@ -1,4 +1,4 @@
1
- utilities/__init__.py,sha256=tLO7x-PFRNnbxnNKusLHsRm5KouG5gC6-6CMhoP9aSQ,60
1
+ utilities/__init__.py,sha256=l_p1vPbF4nOs4tHjj2e93HKXxeAoQ_-FUBDK_Tm0K0I,60
2
2
  utilities/aeventkit.py,sha256=OmDBhYGgbsKrB7cdC5FFpJHUatX9O76eTeKVVTksp2Y,12673
3
3
  utilities/altair.py,sha256=rUK99g9x6CYDDfiZrf-aTx5fSRbL1Q8ctgKORowzXHg,9060
4
4
  utilities/asyncio.py,sha256=aJySVxBY0gqsIYnoNmH7-1r8djKuf4vSsU69VCD08t8,16772
@@ -80,7 +80,7 @@ utilities/sqlalchemy.py,sha256=HQYpd7LFxdTF5WYVWYtCJeEBI71EJm7ytvCGyAH9B-U,37163
80
80
  utilities/sqlalchemy_polars.py,sha256=JCGhB37raSR7fqeWV5dTsciRTMVzIdVT9YSqKT0piT0,13370
81
81
  utilities/statsmodels.py,sha256=koyiBHvpMcSiBfh99wFUfSggLNx7cuAw3rwyfAhoKpQ,3410
82
82
  utilities/string.py,sha256=shmBK87zZwzGyixuNuXCiUbqzfeZ9xlrFwz6JTaRvDk,582
83
- utilities/subprocess.py,sha256=8kGrQLnPnlbdXeWE7OPk9Pih2KBNyjBKbntdiloQVrY,13066
83
+ utilities/subprocess.py,sha256=FlE_SWoA6nUCTR7CregeXtjM6xtqNuuT9y6hqgnb8ZY,15407
84
84
  utilities/tempfile.py,sha256=Lx6qa16lL1XVH6WdmD_G9vlN6gLI8nrIurxmsFkPKvg,3022
85
85
  utilities/testbook.py,sha256=j1KmaVbrX9VrbeMgtPh5gk55myAsn3dyRUn7jGbPbRk,1294
86
86
  utilities/text.py,sha256=7SvwcSR2l_5cOrm1samGnR4C-ZI6qyFLHLzSpO1zeHQ,13958
@@ -97,7 +97,7 @@ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
97
97
  utilities/whenever.py,sha256=F4ek0-OBWxHYrZdmoZt76N2RnNyKY5KrEHt7rqO4AQE,60183
98
98
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
99
99
  utilities/zoneinfo.py,sha256=tdIScrTB2-B-LH0ukb1HUXKooLknOfJNwHk10MuMYvA,3619
100
- dycw_utilities-0.174.1.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
101
- dycw_utilities-0.174.1.dist-info/entry_points.txt,sha256=ykGI1ArwOPHqm2g5Cqh3ENdMxEej_a_FcOUov5EM5Oc,155
102
- dycw_utilities-0.174.1.dist-info/METADATA,sha256=olH83qMZVFRCSDboBVmfiiCV1d4hKdtVyvCFi6s1Gp0,1709
103
- dycw_utilities-0.174.1.dist-info/RECORD,,
100
+ dycw_utilities-0.174.3.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
101
+ dycw_utilities-0.174.3.dist-info/entry_points.txt,sha256=ykGI1ArwOPHqm2g5Cqh3ENdMxEej_a_FcOUov5EM5Oc,155
102
+ dycw_utilities-0.174.3.dist-info/METADATA,sha256=XSqrLMadX1FSoqYHcNS8LSzYZ0eTrmXvGWWQk72wVhM,1709
103
+ dycw_utilities-0.174.3.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.174.1"
3
+ __version__ = "0.174.3"
utilities/subprocess.py CHANGED
@@ -7,11 +7,14 @@ from pathlib import Path
7
7
  from string import Template
8
8
  from subprocess import PIPE, CalledProcessError, Popen
9
9
  from threading import Thread
10
+ from time import sleep
10
11
  from typing import IO, TYPE_CHECKING, Literal, assert_never, overload
11
12
 
12
13
  from utilities.errors import ImpossibleCaseError
13
14
  from utilities.logging import to_logger
14
15
  from utilities.text import strip_and_dedent
16
+ from utilities.types import Delta
17
+ from utilities.whenever import to_seconds
15
18
 
16
19
  if TYPE_CHECKING:
17
20
  from collections.abc import Iterator
@@ -19,6 +22,7 @@ if TYPE_CHECKING:
19
22
  from utilities.types import LoggerLike, PathLike, StrMapping, StrStrMapping
20
23
 
21
24
 
25
+ type _Retry = tuple[int, Delta | None]
22
26
  _HOST_KEY_ALGORITHMS = ["ssh-ed25519"]
23
27
  BASH_LC = ["bash", "-lc"]
24
28
  BASH_LS = ["bash", "-ls"]
@@ -79,6 +83,7 @@ def run(
79
83
  return_: Literal[True],
80
84
  return_stdout: bool = False,
81
85
  return_stderr: bool = False,
86
+ retry: _Retry | None = None,
82
87
  logger: LoggerLike | None = None,
83
88
  ) -> str: ...
84
89
  @overload
@@ -98,6 +103,7 @@ def run(
98
103
  return_: bool = False,
99
104
  return_stdout: Literal[True],
100
105
  return_stderr: bool = False,
106
+ retry: _Retry | None = None,
101
107
  logger: LoggerLike | None = None,
102
108
  ) -> str: ...
103
109
  @overload
@@ -117,6 +123,7 @@ def run(
117
123
  return_: bool = False,
118
124
  return_stdout: bool = False,
119
125
  return_stderr: Literal[True],
126
+ retry: _Retry | None = None,
120
127
  logger: LoggerLike | None = None,
121
128
  ) -> str: ...
122
129
  @overload
@@ -136,6 +143,7 @@ def run(
136
143
  return_: Literal[False] = False,
137
144
  return_stdout: Literal[False] = False,
138
145
  return_stderr: Literal[False] = False,
146
+ retry: _Retry | None = None,
139
147
  logger: LoggerLike | None = None,
140
148
  ) -> None: ...
141
149
  @overload
@@ -155,6 +163,7 @@ def run(
155
163
  return_: bool = False,
156
164
  return_stdout: bool = False,
157
165
  return_stderr: bool = False,
166
+ retry: _Retry | None = None,
158
167
  logger: LoggerLike | None = None,
159
168
  ) -> str | None: ...
160
169
  def run(
@@ -173,10 +182,11 @@ def run(
173
182
  return_: bool = False,
174
183
  return_stdout: bool = False,
175
184
  return_stderr: bool = False,
185
+ retry: _Retry | None = None,
176
186
  logger: LoggerLike | None = None,
177
187
  ) -> str | None:
178
188
  args: list[str] = []
179
- if user is not None:
189
+ if user is not None: # pragma: no cover
180
190
  args.extend(["su", "-", str(user)])
181
191
  args.extend([cmd, *cmds_or_args])
182
192
  buffer = StringIO()
@@ -208,8 +218,8 @@ def run(
208
218
  if proc.stderr is None: # pragma: no cover
209
219
  raise ImpossibleCaseError(case=[f"{proc.stderr=}"])
210
220
  with (
211
- _yield_write(proc.stdout, "proc.stdout", *stdout_outputs),
212
- _yield_write(proc.stderr, "proc.stderr", *stderr_outputs),
221
+ _yield_write(proc.stdout, *stdout_outputs),
222
+ _yield_write(proc.stderr, *stderr_outputs),
213
223
  ):
214
224
  if input is not None:
215
225
  _ = proc.stdin.write(input)
@@ -229,6 +239,10 @@ def run(
229
239
  case 0, False, False:
230
240
  return None
231
241
  case _, _, _:
242
+ if retry is None:
243
+ attempts = delta = None
244
+ else:
245
+ attempts, delta = retry
232
246
  _ = stdout.seek(0)
233
247
  stdout_text = stdout.read()
234
248
  _ = stderr.seek(0)
@@ -243,24 +257,52 @@ def run(
243
257
  - shell = {shell}
244
258
  - cwd = {cwd}
245
259
  - env = {env}
246
- - input = {input}
247
260
 
261
+ -- stdin ----------------------------------------------------------------------
262
+ {"" if input is None else input}-------------------------------------------------------------------------------
248
263
  -- stdout ---------------------------------------------------------------------
249
264
  {stdout_text}-------------------------------------------------------------------------------
250
265
  -- stderr ---------------------------------------------------------------------
251
266
  {stderr_text}-------------------------------------------------------------------------------
252
267
  """)
268
+ if (attempts is not None) and (attempts >= 1):
269
+ if delta is None:
270
+ msg = f"{msg}\n\nRetrying {attempts} more time(s)..."
271
+ else:
272
+ msg = f"{msg}\n\nRetrying {attempts} more time(s) after {delta}..."
253
273
  to_logger(logger).error(msg)
254
- raise CalledProcessError(
274
+ error = CalledProcessError(
255
275
  return_code, args, output=stdout_text, stderr=stderr_text
256
276
  )
277
+ if (attempts is None) or (attempts <= 0):
278
+ raise error
279
+ if delta is not None:
280
+ sleep(to_seconds(delta))
281
+ return run(
282
+ cmd,
283
+ *cmds_or_args,
284
+ user=user,
285
+ executable=executable,
286
+ shell=shell,
287
+ cwd=cwd,
288
+ env=env,
289
+ input=input,
290
+ print=print,
291
+ print_stdout=print_stdout,
292
+ print_stderr=print_stderr,
293
+ return_=return_,
294
+ return_stdout=return_stdout,
295
+ return_stderr=return_stderr,
296
+ retry=(attempts - 1, delta),
297
+ logger=logger,
298
+ )
257
299
  case never:
258
300
  assert_never(never)
259
301
 
260
302
 
261
303
  @contextmanager
262
- def _yield_write(input_: IO[str], desc: str, /, *outputs: IO[str]) -> Iterator[None]:
263
- thread = Thread(target=_run_target, args=(input_, desc, *outputs), daemon=True)
304
+ def _yield_write(input_: IO[str], /, *outputs: IO[str]) -> Iterator[None]:
305
+ thread = Thread(target=_run_target, args=(input_, *outputs), daemon=True)
264
306
  thread.start()
265
307
  try:
266
308
  yield
@@ -268,14 +310,10 @@ def _yield_write(input_: IO[str], desc: str, /, *outputs: IO[str]) -> Iterator[N
268
310
  thread.join()
269
311
 
270
312
 
271
- def _run_target(input_: IO[str], desc: str, /, *outputs: IO[str]) -> None:
272
- try:
273
- with input_:
274
- for text in iter(input_.readline, ""):
275
- _write_to_streams(text, *outputs)
276
- except ValueError:
277
- _ = sys.stderr.write(f"Failed to write to {desc!r}...")
278
- raise
313
+ def _run_target(input_: IO[str], /, *outputs: IO[str]) -> None:
314
+ with input_:
315
+ for text in iter(input_.readline, ""):
316
+ _write_to_streams(text, *outputs)
279
317
 
280
318
 
281
319
  def _write_to_streams(text: str, /, *outputs: IO[str]) -> None:
@@ -299,6 +337,7 @@ def ssh(
299
337
  return_: Literal[True],
300
338
  return_stdout: bool = False,
301
339
  return_stderr: bool = False,
340
+ retry: _Retry | None = None,
302
341
  logger: LoggerLike | None = None,
303
342
  ) -> str: ...
304
343
  @overload
@@ -317,6 +356,7 @@ def ssh(
317
356
  return_: bool = False,
318
357
  return_stdout: Literal[True],
319
358
  return_stderr: bool = False,
359
+ retry: _Retry | None = None,
320
360
  logger: LoggerLike | None = None,
321
361
  ) -> str: ...
322
362
  @overload
@@ -335,6 +375,7 @@ def ssh(
335
375
  return_: bool = False,
336
376
  return_stdout: bool = False,
337
377
  return_stderr: Literal[True],
378
+ retry: _Retry | None = None,
338
379
  logger: LoggerLike | None = None,
339
380
  ) -> str: ...
340
381
  @overload
@@ -353,8 +394,28 @@ def ssh(
353
394
  return_: Literal[False] = False,
354
395
  return_stdout: Literal[False] = False,
355
396
  return_stderr: Literal[False] = False,
397
+ retry: _Retry | None = None,
356
398
  logger: LoggerLike | None = None,
357
399
  ) -> None: ...
400
+ @overload
401
+ def ssh(
402
+ user: str,
403
+ hostname: str,
404
+ /,
405
+ *cmd_and_cmds_or_args: str,
406
+ batch_mode: bool = True,
407
+ host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
408
+ strict_host_key_checking: bool = True,
409
+ input: str | None = None,
410
+ print: bool = False,
411
+ print_stdout: bool = False,
412
+ print_stderr: bool = False,
413
+ return_: bool = False,
414
+ return_stdout: bool = False,
415
+ return_stderr: bool = False,
416
+ retry: _Retry | None = None,
417
+ logger: LoggerLike | None = None,
418
+ ) -> str | None: ...
358
419
  def ssh(
359
420
  user: str,
360
421
  hostname: str,
@@ -370,6 +431,7 @@ def ssh(
370
431
  return_: bool = False,
371
432
  return_stdout: bool = False,
372
433
  return_stderr: bool = False,
434
+ retry: _Retry | None = None,
373
435
  logger: LoggerLike | None = None,
374
436
  ) -> str | None:
375
437
  cmd_and_args = ssh_cmd( # skipif-ci
@@ -389,6 +451,7 @@ def ssh(
389
451
  return_=return_,
390
452
  return_stdout=return_stdout,
391
453
  return_stderr=return_stderr,
454
+ retry=retry,
392
455
  logger=logger,
393
456
  )
394
457