dycw-utilities 0.174.16__py3-none-any.whl → 0.174.17__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.16
3
+ Version: 0.174.17
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=vtFT93z2VkUoI-200aWbVdTniPgIRP-XjQ6slYwfpHo,61
1
+ utilities/__init__.py,sha256=kcADuh_nb8xfuYrat4AkfaKqdxp3l7xyrXWoJJ5JZLk,61
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
@@ -81,7 +81,7 @@ utilities/sqlalchemy.py,sha256=HQYpd7LFxdTF5WYVWYtCJeEBI71EJm7ytvCGyAH9B-U,37163
81
81
  utilities/sqlalchemy_polars.py,sha256=JCGhB37raSR7fqeWV5dTsciRTMVzIdVT9YSqKT0piT0,13370
82
82
  utilities/statsmodels.py,sha256=koyiBHvpMcSiBfh99wFUfSggLNx7cuAw3rwyfAhoKpQ,3410
83
83
  utilities/string.py,sha256=shmBK87zZwzGyixuNuXCiUbqzfeZ9xlrFwz6JTaRvDk,582
84
- utilities/subprocess.py,sha256=c9f3-HCXVOAMBkfcs4ytmtGUKRH24UlV1D_NdvPqfVA,28937
84
+ utilities/subprocess.py,sha256=FzVd1evvhaJR2U_tATJ37Y6YjF8O61XPeqYo8NLonG4,33123
85
85
  utilities/tempfile.py,sha256=Lx6qa16lL1XVH6WdmD_G9vlN6gLI8nrIurxmsFkPKvg,3022
86
86
  utilities/testbook.py,sha256=j1KmaVbrX9VrbeMgtPh5gk55myAsn3dyRUn7jGbPbRk,1294
87
87
  utilities/text.py,sha256=7SvwcSR2l_5cOrm1samGnR4C-ZI6qyFLHLzSpO1zeHQ,13958
@@ -98,7 +98,7 @@ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
98
98
  utilities/whenever.py,sha256=F4ek0-OBWxHYrZdmoZt76N2RnNyKY5KrEHt7rqO4AQE,60183
99
99
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
100
100
  utilities/zoneinfo.py,sha256=tdIScrTB2-B-LH0ukb1HUXKooLknOfJNwHk10MuMYvA,3619
101
- dycw_utilities-0.174.16.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
102
- dycw_utilities-0.174.16.dist-info/entry_points.txt,sha256=ykGI1ArwOPHqm2g5Cqh3ENdMxEej_a_FcOUov5EM5Oc,155
103
- dycw_utilities-0.174.16.dist-info/METADATA,sha256=aNMnbDY3Lh5LGRBgi6ySNdZ3PR2zd3cxGHv28VNPX44,1710
104
- dycw_utilities-0.174.16.dist-info/RECORD,,
101
+ dycw_utilities-0.174.17.dist-info/WHEEL,sha256=RRVLqVugUmFOqBedBFAmA4bsgFcROUBiSUKlERi0Hcg,79
102
+ dycw_utilities-0.174.17.dist-info/entry_points.txt,sha256=ykGI1ArwOPHqm2g5Cqh3ENdMxEej_a_FcOUov5EM5Oc,155
103
+ dycw_utilities-0.174.17.dist-info/METADATA,sha256=vF_qKrCXt5r90r7OjWFLitGHfDgcAcVUDj6Y47YMvEQ,1710
104
+ dycw_utilities-0.174.17.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.18
2
+ Generator: uv 0.9.21
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.174.16"
3
+ __version__ = "0.174.17"
utilities/subprocess.py CHANGED
@@ -17,7 +17,8 @@ from typing import IO, TYPE_CHECKING, Literal, assert_never, overload, override
17
17
  from utilities.errors import ImpossibleCaseError
18
18
  from utilities.iterables import always_iterable
19
19
  from utilities.logging import to_logger
20
- from utilities.permissions import ensure_perms
20
+ from utilities.permissions import Permissions, ensure_perms
21
+ from utilities.tempfile import TemporaryDirectory
21
22
  from utilities.text import strip_and_dedent
22
23
  from utilities.whenever import to_seconds
23
24
 
@@ -346,7 +347,7 @@ def rsync(
346
347
  chown_user: str | None = None,
347
348
  chown_group: str | None = None,
348
349
  exclude: MaybeIterable[str] | None = None,
349
- chmod: str | None = None,
350
+ chmod: PermissionsLike | None = None,
350
351
  ) -> None:
351
352
  """Remote & local file copying."""
352
353
  mkdir_args = maybe_sudo_cmd(*mkdir_cmd(dest, parent=True), sudo=sudo) # skipif-ci
@@ -361,13 +362,13 @@ def rsync(
361
362
  retry=retry,
362
363
  logger=logger,
363
364
  )
364
- is_dir = any(Path(s).is_dir() for s in always_iterable(src_or_srcs)) # skipif-ci
365
+ srcs = list(always_iterable(src_or_srcs)) # skipif-ci
365
366
  rsync_args = rsync_cmd( # skipif-ci
366
- src_or_srcs,
367
+ srcs,
367
368
  user,
368
369
  hostname,
369
370
  dest,
370
- archive=is_dir,
371
+ archive=any(Path(s).is_dir() for s in srcs),
371
372
  chown_user=chown_user,
372
373
  chown_group=chown_group,
373
374
  exclude=exclude,
@@ -375,7 +376,6 @@ def rsync(
375
376
  host_key_algorithms=host_key_algorithms,
376
377
  strict_host_key_checking=strict_host_key_checking,
377
378
  sudo=sudo,
378
- parent=is_dir,
379
379
  )
380
380
  run(*rsync_args, print=print, retry=retry, logger=logger) # skipif-ci
381
381
  if chmod is not None: # skipif-ci
@@ -408,7 +408,6 @@ def rsync_cmd(
408
408
  host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
409
409
  strict_host_key_checking: bool = True,
410
410
  sudo: bool = False,
411
- parent: bool = False,
412
411
  ) -> list[str]:
413
412
  """Command to use 'rsync' to do remote & local file copying."""
414
413
  args: list[str] = ["rsync"]
@@ -438,12 +437,141 @@ def rsync_cmd(
438
437
  args.extend(["--rsh", join(rsh_args)])
439
438
  if sudo:
440
439
  args.extend(["--rsync-path", join(sudo_cmd("rsync"))])
441
- dest_use = maybe_parent(dest, parent=parent)
442
- return [
443
- *args,
444
- *map(str, always_iterable(src_or_srcs)),
445
- f"{user}@{hostname}:{dest_use}",
440
+ srcs = list(always_iterable(src_or_srcs)) # do not Path()
441
+ if len(srcs) == 0:
442
+ raise RsyncCmdNoSourcesError(user=user, hostname=hostname, dest=dest)
443
+ missing = [s for s in srcs if not Path(s).exists()]
444
+ if len(missing) >= 1:
445
+ raise RsyncCmdSourcesNotFoundError(
446
+ sources=missing, user=user, hostname=hostname, dest=dest
447
+ )
448
+ return [*args, *map(str, srcs), f"{user}@{hostname}:{dest}"]
449
+
450
+
451
+ @dataclass(kw_only=True, slots=True)
452
+ class RsyncCmdError(Exception):
453
+ user: str
454
+ hostname: str
455
+ dest: PathLike
456
+
457
+ @override
458
+ def __str__(self) -> str:
459
+ return f"No sources selected to send to {self.user}@{self.hostname}:{self.dest}"
460
+
461
+
462
+ @dataclass(kw_only=True, slots=True)
463
+ class RsyncCmdNoSourcesError(RsyncCmdError):
464
+ @override
465
+ def __str__(self) -> str:
466
+ return f"No sources selected to send to {self.user}@{self.hostname}:{self.dest}"
467
+
468
+
469
+ @dataclass(kw_only=True, slots=True)
470
+ class RsyncCmdSourcesNotFoundError(RsyncCmdError):
471
+ sources: list[PathLike]
472
+
473
+ @override
474
+ def __str__(self) -> str:
475
+ desc = ", ".join(map(repr, map(str, self.sources)))
476
+ return f"Sources selected to send to {self.user}@{self.hostname}:{self.dest} but not found: {desc}"
477
+
478
+
479
+ ##
480
+
481
+
482
+ def rsync_many(
483
+ user: str,
484
+ hostname: str,
485
+ /,
486
+ *items: tuple[PathLike, PathLike]
487
+ | tuple[Literal["sudo"], PathLike, PathLike]
488
+ | tuple[PathLike, PathLike, PermissionsLike],
489
+ retry: Retry | None = None,
490
+ logger: LoggerLike | None = None,
491
+ keep: bool = False,
492
+ batch_mode: bool = True,
493
+ host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
494
+ strict_host_key_checking: bool = True,
495
+ print: bool = False, # noqa: A002
496
+ ) -> None:
497
+ cmds: list[list[str]] = [] # skipif-ci
498
+ with ( # skipif-ci
499
+ TemporaryDirectory() as temp_src,
500
+ yield_ssh_temp_dir(
501
+ user, hostname, retry=retry, logger=logger, keep=keep
502
+ ) as temp_dest,
503
+ ):
504
+ for item in items:
505
+ match item:
506
+ case Path() | str() as src, Path() | str() as dest:
507
+ cmds.extend(_rsync_many_prepare(src, dest, temp_src, temp_dest))
508
+ case "sudo", Path() | str() as src, Path() | str() as dest:
509
+ cmds.extend(
510
+ _rsync_many_prepare(src, dest, temp_src, temp_dest, sudo=True)
511
+ )
512
+ case (
513
+ Path() | str() as src,
514
+ Path() | str() as dest,
515
+ Permissions() | int() | str() as perms,
516
+ ):
517
+ cmds.extend(
518
+ _rsync_many_prepare(src, dest, temp_src, temp_dest, perms=perms)
519
+ )
520
+ case never:
521
+ assert_never(never)
522
+ rsync(
523
+ f"{temp_src}/",
524
+ user,
525
+ hostname,
526
+ temp_dest,
527
+ batch_mode=batch_mode,
528
+ host_key_algorithms=host_key_algorithms,
529
+ strict_host_key_checking=strict_host_key_checking,
530
+ print=print,
531
+ retry=retry,
532
+ logger=logger,
533
+ )
534
+ ssh(
535
+ user,
536
+ hostname,
537
+ *BASH_LS,
538
+ input="\n".join(map(join, cmds)),
539
+ print=print,
540
+ retry=retry,
541
+ logger=logger,
542
+ )
543
+
544
+
545
+ def _rsync_many_prepare(
546
+ src: PathLike,
547
+ dest: PathLike,
548
+ temp_src: PathLike,
549
+ temp_dest: PathLike,
550
+ /,
551
+ *,
552
+ sudo: bool = False,
553
+ perms: PermissionsLike | None = None,
554
+ ) -> list[list[str]]:
555
+ dest, temp_src, temp_dest = map(Path, [dest, temp_src, temp_dest])
556
+ n = len(list(temp_src.iterdir()))
557
+ name = str(n)
558
+ match src:
559
+ case Path():
560
+ cp(src, temp_src / name)
561
+ case str():
562
+ if Path(src).exists():
563
+ cp(src, temp_src / name)
564
+ else:
565
+ tee(temp_src / name, src)
566
+ case never:
567
+ assert_never(never)
568
+ cmds: list[list[str]] = [
569
+ maybe_sudo_cmd(*mkdir_cmd(dest, parent=True), sudo=sudo),
570
+ maybe_sudo_cmd(*cp_cmd(temp_dest / name, dest), sudo=sudo),
446
571
  ]
572
+ if perms is not None:
573
+ cmds.append(maybe_sudo_cmd(*chmod_cmd(dest, perms), sudo=sudo))
574
+ return cmds
447
575
 
448
576
 
449
577
  ##
@@ -1019,6 +1147,9 @@ __all__ = [
1019
1147
  "ChownCmdError",
1020
1148
  "CpError",
1021
1149
  "MvFileError",
1150
+ "RsyncCmdError",
1151
+ "RsyncCmdNoSourcesError",
1152
+ "RsyncCmdSourcesNotFoundError",
1022
1153
  "apt_install_cmd",
1023
1154
  "cd_cmd",
1024
1155
  "chmod",
@@ -1041,6 +1172,7 @@ __all__ = [
1041
1172
  "rm_cmd",
1042
1173
  "rsync",
1043
1174
  "rsync_cmd",
1175
+ "rsync_many",
1044
1176
  "run",
1045
1177
  "set_hostname_cmd",
1046
1178
  "ssh",