dycw-utilities 0.174.15__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.15
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,23 +1,23 @@
1
- utilities/__init__.py,sha256=e_rDHgJr89spnVGWmvxwmI_TbjZtP1QsasPjzzI3FTs,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
5
5
  utilities/atomicwrites.py,sha256=tPo6r-Rypd9u99u66B9z86YBPpnLrlHtwox_8Z7T34Y,5790
6
6
  utilities/atools.py,sha256=6neeCcgXxK2dlsc0xp15Za7nSucbCgFtAJepGI_-WXU,2549
7
7
  utilities/cachetools.py,sha256=2S9LMHIunDYMIu8JGI7OLN04sQ7-xZGdEdP1Li0vksA,2775
8
- utilities/click.py,sha256=-_B7V5arQopuVJA6emnwBlZGQvcnq7nq6IYc3cPX5FA,19204
8
+ utilities/click.py,sha256=ScLzBLoBp8Si5YjgB18A0IVMAR-r4sGUnVfJbAaru98,19191
9
9
  utilities/concurrent.py,sha256=fHeW2SZ_TEMfFY0C8pyQI6aPlnecvx9x6SuUwBWj_JY,2853
10
10
  utilities/contextlib.py,sha256=iP7R2tIm6ZsbfLD5ks6UKBYwj50e9gBI8AkpLN-chro,7476
11
11
  utilities/contextvars.py,sha256=J8OhC7jqozAGYOCe2KUWysbPXNGe5JYz3HfaY_mIs08,883
12
12
  utilities/cryptography.py,sha256=5PFrzsNUGHay91dFgYnDKwYprXxahrBqztmUqViRzBk,956
13
13
  utilities/cvxpy.py,sha256=Rv1-fD-XYerosCavRF8Pohop2DBkU3AlFaGTfD8AEAA,13776
14
14
  utilities/dataclasses.py,sha256=xbU3QN1GFy7RC6hIJRZIeUZm7YRlodrgEWmahWG6k2g,32465
15
- utilities/docker.py,sha256=DBgSz-UPBDHk_XJLPNaNMkNym1kKK8l3Os8IqMwEyW8,7866
15
+ utilities/docker.py,sha256=l_R9Eyhc-_JbkkaYXYKGZ0PZ_oukrSj8aKtD1NyAhMM,7879
16
16
  utilities/enum.py,sha256=5l6pwZD1cjSlVW4ss-zBPspWvrbrYrdtJWcg6f5_J5w,5781
17
17
  utilities/errors.py,sha256=mFlDGSM0LI1jZ1pbqwLAH3ttLZ2JVIxyZLojw8tGVZU,1479
18
18
  utilities/fastapi.py,sha256=TqyKvBjiMS594sXPjrz-KRTLMb3l3D3rZ1zAYV7GfOk,1454
19
19
  utilities/fpdf2.py,sha256=dSiYz0FJTD2sQuxpxqFWwwIe2-p6Y7oTB9Tv0Jajit0,1866
20
- utilities/functions.py,sha256=82qCAaPIB0JmZ5wsQurA3MTYl7fh8LHcoBFkxPs7Zeg,21478
20
+ utilities/functions.py,sha256=18Zda7nTloARdcEudH8YJ4e13xAdWShAGhPNN4w2Gyc,21498
21
21
  utilities/functools.py,sha256=I00ru2gQPakZw2SHVeKIKXfTv741655s6HI0lUoE0D4,1552
22
22
  utilities/getpass.py,sha256=DfN5UgMAtFCqS3dSfFHUfqIMZX2shXvwphOz_6J6f6A,103
23
23
  utilities/git.py,sha256=U1RFvCTANGENgx9wVBDvllioqBQZM2ns12ivKhOsaO4,414
@@ -25,7 +25,7 @@ utilities/grp.py,sha256=1vV3gNR9dQsl1vtUtvC_2qgVdQzm7O8lLMSh56cTbeg,694
25
25
  utilities/gzip.py,sha256=fkGP3KdsBfXlstodT4wtlp-PwNyUsogpbDCVVVGdsm4,781
26
26
  utilities/hashlib.py,sha256=SVTgtguur0P4elppvzOBbLEjVM3Pea0eWB61yg2ilxo,309
27
27
  utilities/http.py,sha256=TsavEfHlRtlLaeV21Z6KZh0qbPw-kvD1zsQdZ7Kep5Q,977
28
- utilities/hypothesis.py,sha256=wk1HiNdBg7tGPEKLZ5uiNVbtlSZl58QJjlediYoSHkA,46753
28
+ utilities/hypothesis.py,sha256=NUu30pl5kjL3tzo-m8SMRwTqLAmTWK-_Sau2NemJcQo,46773
29
29
  utilities/importlib.py,sha256=mV1xT_O_zt_GnZZ36tl3xOmMaN_3jErDWY54fX39F6Y,429
30
30
  utilities/inflect.py,sha256=v7YkOWSu8NAmVghPcf4F3YBZQoJCS47_DLf9jbfWIs0,581
31
31
  utilities/ipython.py,sha256=V2oMYHvEKvlNBzxDXdLvKi48oUq2SclRg5xasjaXStw,763
@@ -46,10 +46,10 @@ utilities/optuna.py,sha256=C-fhWYiXHVPo1l8QctYkFJ4DyhbSrGorzP1dJb_qvd8,1933
46
46
  utilities/orjson.py,sha256=T_0SlK811ysg46d3orvIPY3JpBa4FRMpP2wlPQo7-gU,41854
47
47
  utilities/os.py,sha256=kjKKSQfnRqFTTZ315iavaaGd3gGuYNoSWlxVLCJjyQs,4852
48
48
  utilities/parse.py,sha256=g7Qm9eBOIeDId2tGA021CIaeF6jp1TI8rx4srdvlyoo,17937
49
- utilities/pathlib.py,sha256=EKZn-wWxH7MEWFrQGqHIoB-GJzyXeiEj8iDIgvkr8Wk,9325
49
+ utilities/pathlib.py,sha256=N4Ip8R9eCM-6GfvxUJ3T9oQIle2C2P52F-13BCFRdTg,9345
50
50
  utilities/permissions.py,sha256=vLXlWztSVYffbrxptne7ksj6dU1HLekm4fEvS4ny_4Q,8944
51
51
  utilities/pickle.py,sha256=MBT2xZCsv0pH868IXLGKnlcqNx2IRVKYNpRcqiQQqxw,653
52
- utilities/platform.py,sha256=0pYO5v7L2sU5UN87zHhEEhTKsZ9NIEM8N6UCr0F7bLY,2778
52
+ utilities/platform.py,sha256=Grov52WxNOViJEZyRcm-b2m_Dp1T0waPjDCusR_9oqs,2791
53
53
  utilities/polars.py,sha256=cNFBLWgOMUAp_Sz4xtlto17uZswZRrcfQYC95QKyaY4,87483
54
54
  utilities/polars_ols.py,sha256=LNTFNLPuYW7fcAHymlbnams_DhitToblYvib3mhKbwI,5615
55
55
  utilities/postgres.py,sha256=g3tEwTI8TdmiCbRME61ffQ0xaibdpXPu8mJOOHvjPKc,12532
@@ -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=gsRaeBzUSINUBuMZTSz7F7rMdAmhUFCEhMQOEpS5Ro4,28897
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.15.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
102
- dycw_utilities-0.174.15.dist-info/entry_points.txt,sha256=ykGI1ArwOPHqm2g5Cqh3ENdMxEej_a_FcOUov5EM5Oc,155
103
- dycw_utilities-0.174.15.dist-info/METADATA,sha256=-vZ1ta-ZjRzQ0WaZ7uRspE0DJQCqajno8r1UEp8gF2Y,1710
104
- dycw_utilities-0.174.15.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.15"
3
+ __version__ = "0.174.17"
utilities/click.py CHANGED
@@ -210,8 +210,7 @@ class EnumPartial[E: enum.Enum](ParamType):
210
210
  self.fail(str(error), param, ctx)
211
211
  if enum in self._members:
212
212
  return enum
213
- self.fail(f"{enum.value!r} is not a selected member")
214
- return None
213
+ return self.fail(f"{enum.value!r} is not a selected member")
215
214
 
216
215
  @override
217
216
  def get_metavar(self, param: Parameter, ctx: Context) -> str | None:
utilities/docker.py CHANGED
@@ -47,7 +47,7 @@ def docker_cp(
47
47
  sudo: bool = False,
48
48
  logger: LoggerLike | None = None,
49
49
  ) -> None:
50
- match src, dest:
50
+ match src, dest: # skipif-ci
51
51
  case Path() | str(), (str() as cont, Path() | str() as dest_path):
52
52
  docker_exec(
53
53
  cont, *maybe_sudo_cmd(*mkdir_cmd(dest_path, parent=True), sudo=sudo)
utilities/functions.py CHANGED
@@ -693,7 +693,7 @@ def second[U](pair: tuple[Any, U], /) -> U:
693
693
 
694
694
  def skip_if_optimize[**P](func: Callable[P, None], /) -> Callable[P, None]:
695
695
  """Skip a function if we are in the optimized mode."""
696
- if __debug__:
696
+ if __debug__: # pragma: no cover
697
697
  return func
698
698
 
699
699
  @wraps(func)
utilities/hypothesis.py CHANGED
@@ -1040,7 +1040,7 @@ def setup_hypothesis_profiles(
1040
1040
  assert_never(never)
1041
1041
 
1042
1042
  phases = {Phase.explicit, Phase.reuse, Phase.generate, Phase.target}
1043
- if "HYPOTHESIS_NO_SHRINK" not in environ:
1043
+ if "HYPOTHESIS_NO_SHRINK" not in environ: # pragma: no cover
1044
1044
  phases.add(Phase.shrink)
1045
1045
  for profile in Profile:
1046
1046
  try:
utilities/pathlib.py CHANGED
@@ -127,7 +127,7 @@ class GetRepoRootError(Exception): ...
127
127
  class _GetRepoRootGitNotFoundError(GetRepoRootError):
128
128
  @override
129
129
  def __str__(self) -> str:
130
- return "'git' not found"
130
+ return "'git' not found" # pragma: no cover
131
131
 
132
132
 
133
133
  @dataclass(kw_only=True, slots=True)
utilities/platform.py CHANGED
@@ -16,7 +16,7 @@ System = Literal["windows", "mac", "linux"]
16
16
  def get_system() -> System:
17
17
  """Get the system/OS name."""
18
18
  sys = system()
19
- if sys == "Windows":
19
+ if sys == "Windows": # skipif-ci
20
20
  return "windows"
21
21
  if sys == "Darwin": # skipif-not-macos
22
22
  return "mac"
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
 
@@ -164,7 +165,7 @@ def cp(
164
165
  ) -> None:
165
166
  """Copy a file/directory."""
166
167
  mkdir(dest, sudo=sudo, parent=True)
167
- if sudo:
168
+ if sudo: # pragma: no cover
168
169
  run(*sudo_cmd(*cp_cmd(src, dest)))
169
170
  else:
170
171
  src, dest = map(Path, [src, dest])
@@ -277,7 +278,7 @@ def mv(
277
278
  ) -> None:
278
279
  """Move a file/directory."""
279
280
  mkdir(dest, sudo=sudo, parent=True)
280
- if sudo:
281
+ if sudo: # pragma: no cover
281
282
  run(*sudo_cmd(*cp_cmd(src, dest)))
282
283
  else:
283
284
  src, dest = map(Path, [src, dest])
@@ -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",