dycw-utilities 0.174.20__py3-none-any.whl → 0.175.6__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,7 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dycw-utilities
3
- Version: 0.174.20
3
+ Version: 0.175.6
4
+ Summary: Miscellaneous Python utilities
4
5
  Author: Derek Wan
5
6
  Author-email: Derek Wan <d.wan@icloud.com>
6
7
  Requires-Dist: atomicwrites>=1.4.1,<1.5
@@ -28,14 +29,6 @@ Provides-Extra: logging
28
29
  Provides-Extra: test
29
30
  Description-Content-Type: text/markdown
30
31
 
31
- [![PyPI version](https://badge.fury.io/py/dycw-utilities.svg)](https://badge.fury.io/py/dycw-utilities)
32
+ # `python-utilities`
32
33
 
33
- # `dycw-utilities`
34
-
35
- [All the Python functions I don't want to write twice.](https://github.com/nvim-lua/plenary.nvim)
36
-
37
- ## Installation
38
-
39
- - `pip install dycw-utilities`
40
-
41
- or with [extras](https://github.com/dycw/python-utilities/blob/master/pyproject.toml).
34
+ Miscellaneous Python utilities
@@ -1,6 +1,5 @@
1
- utilities/__init__.py,sha256=SXnJCLALsdKuftonx3Xk6nl9lsUbY1lII9DJii26lms,61
2
- utilities/aeventkit.py,sha256=OmDBhYGgbsKrB7cdC5FFpJHUatX9O76eTeKVVTksp2Y,12673
3
- utilities/altair.py,sha256=rUK99g9x6CYDDfiZrf-aTx5fSRbL1Q8ctgKORowzXHg,9060
1
+ utilities/__init__.py,sha256=7y6XPRHYRyZjE6Vfy185IzZfDUJT8hoQcrfKUT3_4RU,60
2
+ utilities/altair.py,sha256=TLfRFbG9HwG7SLXoJ-v0r-t49ZaGgTQZD82cpjVi4vs,9085
4
3
  utilities/asyncio.py,sha256=aJySVxBY0gqsIYnoNmH7-1r8djKuf4vSsU69VCD08t8,16772
5
4
  utilities/atomicwrites.py,sha256=tPo6r-Rypd9u99u66B9z86YBPpnLrlHtwox_8Z7T34Y,5790
6
5
  utilities/atools.py,sha256=6neeCcgXxK2dlsc0xp15Za7nSucbCgFtAJepGI_-WXU,2549
@@ -66,7 +65,7 @@ utilities/pytest.py,sha256=9HHwYgZQe6CRF0ekHQEFH05gmoP4Ne0V54RrtUNDfi4,10524
66
65
  utilities/pytest_plugins/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
67
66
  utilities/pytest_plugins/pytest_randomly.py,sha256=B1qYVlExGOxTywq2r1SMi5o7btHLk2PNdY_b1p98dkE,409
68
67
  utilities/pytest_plugins/pytest_regressions.py,sha256=mnHYBfdprz50UGVkVzV1bZERZN5CFfoF8YbokGxdFwU,1639
69
- utilities/pytest_regressions.py,sha256=nKdVZAa88_aMo1a8V-EIHg_smZYdk1iDQBm50R3FlO4,4044
68
+ utilities/pytest_regressions.py,sha256=tJxW38u-zpoyjW1N4zogBx4V_07r-ibDInddcEUyXmc,4763
70
69
  utilities/random.py,sha256=hZlH4gnAtoaofWswuJYjcygejrY8db4CzP-z_adO2Mo,4165
71
70
  utilities/re.py,sha256=S4h-DLL6ScMPqjboZ_uQ1BVTJajrqV06r_81D--_HCE,4573
72
71
  utilities/redis.py,sha256=gybjqKea33Jy50n4dHTS14JdquqHaJqHF2dixQljYWQ,30172
@@ -81,7 +80,7 @@ utilities/sqlalchemy.py,sha256=HQYpd7LFxdTF5WYVWYtCJeEBI71EJm7ytvCGyAH9B-U,37163
81
80
  utilities/sqlalchemy_polars.py,sha256=JCGhB37raSR7fqeWV5dTsciRTMVzIdVT9YSqKT0piT0,13370
82
81
  utilities/statsmodels.py,sha256=koyiBHvpMcSiBfh99wFUfSggLNx7cuAw3rwyfAhoKpQ,3410
83
82
  utilities/string.py,sha256=shmBK87zZwzGyixuNuXCiUbqzfeZ9xlrFwz6JTaRvDk,582
84
- utilities/subprocess.py,sha256=f8amdrbh48Y_3ETcIU_vthKMqAi7ehq3xmwMbAcldc8,34875
83
+ utilities/subprocess.py,sha256=C9H8GiV4gsNRRJY5mo8_2DcNqfeonxcfT5o6icQeSkg,40599
85
84
  utilities/tempfile.py,sha256=Lx6qa16lL1XVH6WdmD_G9vlN6gLI8nrIurxmsFkPKvg,3022
86
85
  utilities/testbook.py,sha256=j1KmaVbrX9VrbeMgtPh5gk55myAsn3dyRUn7jGbPbRk,1294
87
86
  utilities/text.py,sha256=7SvwcSR2l_5cOrm1samGnR4C-ZI6qyFLHLzSpO1zeHQ,13958
@@ -98,7 +97,7 @@ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
98
97
  utilities/whenever.py,sha256=F4ek0-OBWxHYrZdmoZt76N2RnNyKY5KrEHt7rqO4AQE,60183
99
98
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
100
99
  utilities/zoneinfo.py,sha256=tdIScrTB2-B-LH0ukb1HUXKooLknOfJNwHk10MuMYvA,3619
101
- dycw_utilities-0.174.20.dist-info/WHEEL,sha256=RRVLqVugUmFOqBedBFAmA4bsgFcROUBiSUKlERi0Hcg,79
102
- dycw_utilities-0.174.20.dist-info/entry_points.txt,sha256=ykGI1ArwOPHqm2g5Cqh3ENdMxEej_a_FcOUov5EM5Oc,155
103
- dycw_utilities-0.174.20.dist-info/METADATA,sha256=j2YRKAirmAe5WDN_hR-Nm9lV3oHLNWg_42bMAAPUTqE,1710
104
- dycw_utilities-0.174.20.dist-info/RECORD,,
100
+ dycw_utilities-0.175.6.dist-info/WHEEL,sha256=RRVLqVugUmFOqBedBFAmA4bsgFcROUBiSUKlERi0Hcg,79
101
+ dycw_utilities-0.175.6.dist-info/entry_points.txt,sha256=cOGtKeJI0KXLSV7MJ8Dhc2G8jPgDcBDm53MVNJU4ycI,136
102
+ dycw_utilities-0.175.6.dist-info/METADATA,sha256=761YNYgBWP5Xs0xN_xnVhdet11rc3wj-eIQfF_XoqNk,1442
103
+ dycw_utilities-0.175.6.dist-info/RECORD,,
@@ -1,5 +1,3 @@
1
- [console_scripts]
2
-
3
1
  [pytest11]
4
2
  pytest-randomly = utilities.pytest_plugins.pytest_randomly
5
3
  pytest-regressions = utilities.pytest_plugins.pytest_regressions
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.174.20"
3
+ __version__ = "0.175.6"
utilities/altair.py CHANGED
@@ -145,7 +145,9 @@ def plot_dataframes(
145
145
  ]
146
146
  zoom = selection_interval(bind="scales", encodings=["x"])
147
147
  chart = (
148
- vconcat(*layers).add_params(zoom).resolve_scale(color="independent", x="shared")
148
+ vconcat_charts(*layers)
149
+ .add_params(zoom)
150
+ .resolve_scale(color="independent", x="shared")
149
151
  )
150
152
  if title is not None:
151
153
  chart = chart.properties(title=title)
@@ -1,15 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from contextlib import suppress
4
+ from dataclasses import dataclass
4
5
  from json import loads
5
6
  from pathlib import Path
6
7
  from shutil import copytree
7
- from typing import TYPE_CHECKING, Any, assert_never
8
+ from typing import TYPE_CHECKING, Any, assert_never, override
8
9
 
9
10
  from pytest_regressions.file_regression import FileRegressionFixture
10
11
 
11
12
  from utilities.functions import ensure_str
12
13
  from utilities.operator import is_equal
14
+ from utilities.reprlib import get_repr
13
15
 
14
16
  if TYPE_CHECKING:
15
17
  from polars import DataFrame, Series
@@ -70,10 +72,28 @@ class OrjsonRegressionFixture:
70
72
  check_fn=self._check_fn,
71
73
  )
72
74
 
73
- def _check_fn(self, path1: Path, path2: Path, /) -> None:
74
- left = loads(path1.read_text())
75
- right = loads(path2.read_text())
76
- assert is_equal(left, right), f"{left=}, {right=}"
75
+ def _check_fn(self, path_obtained: Path, path_existing: Path, /) -> None:
76
+ obtained = loads(path_obtained.read_text())
77
+ existing = loads(path_existing.read_text())
78
+ if not is_equal(obtained, existing):
79
+ raise OrjsonRegressionError(
80
+ path_obtained=path_obtained,
81
+ path_existing=path_existing,
82
+ obtained=obtained,
83
+ existing=existing,
84
+ )
85
+
86
+
87
+ @dataclass(kw_only=True, slots=True)
88
+ class OrjsonRegressionError(Exception):
89
+ path_obtained: Path
90
+ path_existing: Path
91
+ obtained: Any
92
+ existing: Any
93
+
94
+ @override
95
+ def __str__(self) -> str:
96
+ return f"Obtained object (at {str(self.path_obtained)!r}) and existing object (at {str(self.path_existing)!r}) differ; got {get_repr(self.obtained)} and {get_repr(self.existing)}"
77
97
 
78
98
 
79
99
  ##
utilities/subprocess.py CHANGED
@@ -6,6 +6,7 @@ from contextlib import contextmanager
6
6
  from dataclasses import dataclass
7
7
  from io import StringIO
8
8
  from pathlib import Path
9
+ from re import search
9
10
  from shlex import join
10
11
  from shutil import copyfile, copytree, move, rmtree
11
12
  from string import Template
@@ -23,7 +24,7 @@ from utilities.text import strip_and_dedent
23
24
  from utilities.whenever import to_seconds
24
25
 
25
26
  if TYPE_CHECKING:
26
- from collections.abc import Iterator
27
+ from collections.abc import Callable, Iterator
27
28
 
28
29
  from utilities.permissions import PermissionsLike
29
30
  from utilities.types import (
@@ -41,6 +42,7 @@ APT_UPDATE = ["apt", "update", "-y"]
41
42
  BASH_LC = ["bash", "-lc"]
42
43
  BASH_LS = ["bash", "-ls"]
43
44
  GIT_BRANCH_SHOW_CURRENT = ["git", "branch", "--show-current"]
45
+ KNOWN_HOSTS = Path.home() / ".ssh/known_hosts"
44
46
  MKTEMP_DIR_CMD = ["mktemp", "-d"]
45
47
  RESTART_SSHD = ["systemctl", "restart", "sshd"]
46
48
  UPDATE_CA_CERTIFICATES: str = "update-ca-certificates"
@@ -523,6 +525,7 @@ def rsync_many(
523
525
  host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
524
526
  strict_host_key_checking: bool = True,
525
527
  print: bool = False, # noqa: A002
528
+ exclude: MaybeIterable[str] | None = None,
526
529
  ) -> None:
527
530
  cmds: list[list[str]] = [] # skipif-ci
528
531
  with ( # skipif-ci
@@ -560,6 +563,7 @@ def rsync_many(
560
563
  print=print,
561
564
  retry=retry,
562
565
  logger=logger,
566
+ exclude=exclude,
563
567
  )
564
568
  ssh(
565
569
  user,
@@ -625,6 +629,7 @@ def run(
625
629
  return_stdout: bool = False,
626
630
  return_stderr: bool = False,
627
631
  retry: Retry | None = None,
632
+ retry_skip: Callable[[int, str, str], bool] | None = None,
628
633
  logger: LoggerLike | None = None,
629
634
  ) -> str: ...
630
635
  @overload
@@ -645,6 +650,7 @@ def run(
645
650
  return_stdout: Literal[True],
646
651
  return_stderr: bool = False,
647
652
  retry: Retry | None = None,
653
+ retry_skip: Callable[[int, str, str], bool] | None = None,
648
654
  logger: LoggerLike | None = None,
649
655
  ) -> str: ...
650
656
  @overload
@@ -665,6 +671,7 @@ def run(
665
671
  return_stdout: bool = False,
666
672
  return_stderr: Literal[True],
667
673
  retry: Retry | None = None,
674
+ retry_skip: Callable[[int, str, str], bool] | None = None,
668
675
  logger: LoggerLike | None = None,
669
676
  ) -> str: ...
670
677
  @overload
@@ -685,6 +692,7 @@ def run(
685
692
  return_stdout: Literal[False] = False,
686
693
  return_stderr: Literal[False] = False,
687
694
  retry: Retry | None = None,
695
+ retry_skip: Callable[[int, str, str], bool] | None = None,
688
696
  logger: LoggerLike | None = None,
689
697
  ) -> None: ...
690
698
  @overload
@@ -705,6 +713,7 @@ def run(
705
713
  return_stdout: bool = False,
706
714
  return_stderr: bool = False,
707
715
  retry: Retry | None = None,
716
+ retry_skip: Callable[[int, str, str], bool] | None = None,
708
717
  logger: LoggerLike | None = None,
709
718
  ) -> str | None: ...
710
719
  def run(
@@ -724,6 +733,7 @@ def run(
724
733
  return_stdout: bool = False,
725
734
  return_stderr: bool = False,
726
735
  retry: Retry | None = None,
736
+ retry_skip: Callable[[int, str, str], bool] | None = None,
727
737
  logger: LoggerLike | None = None,
728
738
  ) -> str | None:
729
739
  """Run a command in a subprocess."""
@@ -781,14 +791,18 @@ def run(
781
791
  case 0, False, False:
782
792
  return None
783
793
  case _, _, _:
784
- if retry is None:
785
- attempts = delta = None
786
- else:
787
- attempts, delta = retry
788
794
  _ = stdout.seek(0)
789
795
  stdout_text = stdout.read()
790
796
  _ = stderr.seek(0)
791
797
  stderr_text = stderr.read()
798
+ if (retry is None) or (
799
+ (retry is not None)
800
+ and (retry_skip is not None)
801
+ and retry_skip(return_code, stdout_text, stderr_text)
802
+ ):
803
+ attempts = delta = None
804
+ else:
805
+ attempts, delta = retry
792
806
  if logger is not None:
793
807
  msg = strip_and_dedent(f"""
794
808
  'run' failed with:
@@ -883,6 +897,7 @@ def ssh(
883
897
  batch_mode: bool = True,
884
898
  host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
885
899
  strict_host_key_checking: bool = True,
900
+ port: int | None = None,
886
901
  input: str | None = None,
887
902
  print: bool = False,
888
903
  print_stdout: bool = False,
@@ -902,6 +917,7 @@ def ssh(
902
917
  batch_mode: bool = True,
903
918
  host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
904
919
  strict_host_key_checking: bool = True,
920
+ port: int | None = None,
905
921
  input: str | None = None,
906
922
  print: bool = False,
907
923
  print_stdout: bool = False,
@@ -921,6 +937,7 @@ def ssh(
921
937
  batch_mode: bool = True,
922
938
  host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
923
939
  strict_host_key_checking: bool = True,
940
+ port: int | None = None,
924
941
  input: str | None = None,
925
942
  print: bool = False,
926
943
  print_stdout: bool = False,
@@ -940,6 +957,7 @@ def ssh(
940
957
  batch_mode: bool = True,
941
958
  host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
942
959
  strict_host_key_checking: bool = True,
960
+ port: int | None = None,
943
961
  input: str | None = None,
944
962
  print: bool = False,
945
963
  print_stdout: bool = False,
@@ -959,6 +977,7 @@ def ssh(
959
977
  batch_mode: bool = True,
960
978
  host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
961
979
  strict_host_key_checking: bool = True,
980
+ port: int | None = None,
962
981
  input: str | None = None,
963
982
  print: bool = False,
964
983
  print_stdout: bool = False,
@@ -977,6 +996,7 @@ def ssh(
977
996
  batch_mode: bool = True,
978
997
  host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
979
998
  strict_host_key_checking: bool = True,
999
+ port: int | None = None,
980
1000
  input: str | None = None, # noqa: A002
981
1001
  print: bool = False, # noqa: A002
982
1002
  print_stdout: bool = False,
@@ -995,19 +1015,57 @@ def ssh(
995
1015
  batch_mode=batch_mode,
996
1016
  host_key_algorithms=host_key_algorithms,
997
1017
  strict_host_key_checking=strict_host_key_checking,
1018
+ port=port,
998
1019
  )
999
- return run( # skipif-ci
1000
- *cmd_and_args,
1001
- input=input,
1002
- print=print,
1003
- print_stdout=print_stdout,
1004
- print_stderr=print_stderr,
1005
- return_=return_,
1006
- return_stdout=return_stdout,
1007
- return_stderr=return_stderr,
1008
- retry=retry,
1009
- logger=logger,
1020
+ try: # skipif-ci
1021
+ return run(
1022
+ *cmd_and_args,
1023
+ input=input,
1024
+ print=print,
1025
+ print_stdout=print_stdout,
1026
+ print_stderr=print_stderr,
1027
+ return_=return_,
1028
+ return_stdout=return_stdout,
1029
+ return_stderr=return_stderr,
1030
+ retry=retry,
1031
+ retry_skip=_ssh_retry_skip,
1032
+ logger=logger,
1033
+ )
1034
+ except CalledProcessError as error: # skipif-ci
1035
+ if not _ssh_is_strict_checking_error(error.stderr):
1036
+ raise
1037
+ ssh_keyscan(hostname, port=port)
1038
+ return ssh(
1039
+ user,
1040
+ hostname,
1041
+ *cmd_and_cmds_or_args,
1042
+ batch_mode=batch_mode,
1043
+ host_key_algorithms=host_key_algorithms,
1044
+ strict_host_key_checking=strict_host_key_checking,
1045
+ port=port,
1046
+ input=input,
1047
+ print=print,
1048
+ print_stdout=print_stdout,
1049
+ print_stderr=print_stderr,
1050
+ return_=return_,
1051
+ return_stdout=return_stdout,
1052
+ return_stderr=return_stderr,
1053
+ retry=retry,
1054
+ logger=logger,
1055
+ )
1056
+
1057
+
1058
+ def _ssh_retry_skip(return_code: int, stdout: str, stderr: str, /) -> bool:
1059
+ _ = (return_code, stdout)
1060
+ return _ssh_is_strict_checking_error(stderr)
1061
+
1062
+
1063
+ def _ssh_is_strict_checking_error(text: str, /) -> bool:
1064
+ match = search(
1065
+ "No ED25519 host key is known for .* and you have requested strict checking",
1066
+ text,
1010
1067
  )
1068
+ return match is not None
1011
1069
 
1012
1070
 
1013
1071
  def ssh_cmd(
@@ -1018,12 +1076,14 @@ def ssh_cmd(
1018
1076
  batch_mode: bool = True,
1019
1077
  host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
1020
1078
  strict_host_key_checking: bool = True,
1079
+ port: int | None = None,
1021
1080
  ) -> list[str]:
1022
1081
  """Command to use 'ssh' to execute a command on a remote machine."""
1023
1082
  args: list[str] = ssh_opts_cmd(
1024
1083
  batch_mode=batch_mode,
1025
1084
  host_key_algorithms=host_key_algorithms,
1026
1085
  strict_host_key_checking=strict_host_key_checking,
1086
+ port=port,
1027
1087
  )
1028
1088
  return [*args, f"{user}@{hostname}", *cmd_and_cmds_or_args]
1029
1089
 
@@ -1033,6 +1093,7 @@ def ssh_opts_cmd(
1033
1093
  batch_mode: bool = True,
1034
1094
  host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
1035
1095
  strict_host_key_checking: bool = True,
1096
+ port: int | None = None,
1036
1097
  ) -> list[str]:
1037
1098
  """Command to use prepare 'ssh' to execute a command on a remote machine."""
1038
1099
  args: list[str] = ["ssh"]
@@ -1041,15 +1102,47 @@ def ssh_opts_cmd(
1041
1102
  args.extend(["-o", f"HostKeyAlgorithms={','.join(host_key_algorithms)}"])
1042
1103
  if strict_host_key_checking:
1043
1104
  args.extend(["-o", "StrictHostKeyChecking=yes"])
1105
+ if port is not None:
1106
+ args.extend(["-p", str(port)])
1044
1107
  return [*args, "-T"]
1045
1108
 
1046
1109
 
1047
1110
  ##
1048
1111
 
1049
1112
 
1050
- def ssh_keygen_cmd(hostname: str, /) -> list[str]:
1051
- """Command to use 'ssh-keygen' to add a known host."""
1052
- return ["ssh-keygen", "-f", "~/.ssh/known_hosts", "-R", hostname]
1113
+ def ssh_keyscan(
1114
+ hostname: str, /, *, path: PathLike = KNOWN_HOSTS, port: int | None = None
1115
+ ) -> None:
1116
+ """Add a known host."""
1117
+ ssh_keygen_remove(hostname, path=path) # skipif-ci
1118
+ mkdir(path, parent=True) # skipif-ci
1119
+ with Path(path).open(mode="a") as fh: # skipif-ci
1120
+ _ = fh.write(run(*ssh_keyscan_cmd(hostname, port=port), return_=True))
1121
+
1122
+
1123
+ def ssh_keyscan_cmd(hostname: str, /, *, port: int | None = None) -> list[str]:
1124
+ """Command to use 'ssh-keyscan' to add a known host."""
1125
+ args: list[str] = ["ssh-keyscan"]
1126
+ if port is not None:
1127
+ args.extend(["-p", str(port)])
1128
+ return [*args, "-q", "-t", "ed25519", hostname]
1129
+
1130
+
1131
+ ##
1132
+
1133
+
1134
+ def ssh_keygen_remove(hostname: str, /, *, path: PathLike = KNOWN_HOSTS) -> None:
1135
+ """Remove a known host."""
1136
+ path = Path(path)
1137
+ if path.exists():
1138
+ run(*ssh_keygen_remove_cmd(hostname, path=path))
1139
+
1140
+
1141
+ def ssh_keygen_remove_cmd(
1142
+ hostname: str, /, *, path: PathLike = KNOWN_HOSTS
1143
+ ) -> list[str]:
1144
+ """Command to use 'ssh-keygen' to remove a known host."""
1145
+ return ["ssh-keygen", "-f", str(path), "-R", hostname]
1053
1146
 
1054
1147
 
1055
1148
  ##
@@ -1132,9 +1225,108 @@ def touch_cmd(path: PathLike, /) -> list[str]:
1132
1225
  ##
1133
1226
 
1134
1227
 
1135
- def uv_run(module: str, /, *args: str) -> None:
1228
+ @overload
1229
+ def uv_run(
1230
+ module: str,
1231
+ /,
1232
+ *args: str,
1233
+ cwd: PathLike | None = None,
1234
+ print: bool = False,
1235
+ print_stdout: bool = False,
1236
+ print_stderr: bool = False,
1237
+ return_: Literal[True],
1238
+ return_stdout: bool = False,
1239
+ return_stderr: bool = False,
1240
+ retry: Retry | None = None,
1241
+ logger: LoggerLike | None = None,
1242
+ ) -> str: ...
1243
+ @overload
1244
+ def uv_run(
1245
+ module: str,
1246
+ /,
1247
+ *args: str,
1248
+ cwd: PathLike | None = None,
1249
+ print: bool = False,
1250
+ print_stdout: bool = False,
1251
+ print_stderr: bool = False,
1252
+ return_: bool = False,
1253
+ return_stdout: Literal[True],
1254
+ return_stderr: bool = False,
1255
+ retry: Retry | None = None,
1256
+ logger: LoggerLike | None = None,
1257
+ ) -> str: ...
1258
+ @overload
1259
+ def uv_run(
1260
+ module: str,
1261
+ /,
1262
+ *args: str,
1263
+ cwd: PathLike | None = None,
1264
+ print: bool = False,
1265
+ print_stdout: bool = False,
1266
+ print_stderr: bool = False,
1267
+ return_: bool = False,
1268
+ return_stdout: bool = False,
1269
+ return_stderr: Literal[True],
1270
+ retry: Retry | None = None,
1271
+ logger: LoggerLike | None = None,
1272
+ ) -> str: ...
1273
+ @overload
1274
+ def uv_run(
1275
+ module: str,
1276
+ /,
1277
+ *args: str,
1278
+ cwd: PathLike | None = None,
1279
+ print: bool = False,
1280
+ print_stdout: bool = False,
1281
+ print_stderr: bool = False,
1282
+ return_: Literal[False] = False,
1283
+ return_stdout: Literal[False] = False,
1284
+ return_stderr: Literal[False] = False,
1285
+ retry: Retry | None = None,
1286
+ logger: LoggerLike | None = None,
1287
+ ) -> None: ...
1288
+ @overload
1289
+ def uv_run(
1290
+ module: str,
1291
+ /,
1292
+ *args: str,
1293
+ cwd: PathLike | None = None,
1294
+ print: bool = False,
1295
+ print_stdout: bool = False,
1296
+ print_stderr: bool = False,
1297
+ return_: bool = False,
1298
+ return_stdout: bool = False,
1299
+ return_stderr: bool = False,
1300
+ retry: Retry | None = None,
1301
+ logger: LoggerLike | None = None,
1302
+ ) -> str | None: ...
1303
+ def uv_run(
1304
+ module: str,
1305
+ /,
1306
+ *args: str,
1307
+ cwd: PathLike | None = None,
1308
+ print: bool = False, # noqa: A002
1309
+ print_stdout: bool = False,
1310
+ print_stderr: bool = False,
1311
+ return_: bool = False,
1312
+ return_stdout: bool = False,
1313
+ return_stderr: bool = False,
1314
+ retry: Retry | None = None,
1315
+ logger: LoggerLike | None = None,
1316
+ ) -> str | None:
1136
1317
  """Run a command or script."""
1137
- run(*uv_run_cmd(module, *args)) # pragma: no cover
1318
+ return run( # pragma: no cover
1319
+ *uv_run_cmd(module, *args),
1320
+ cwd=cwd,
1321
+ print=print,
1322
+ print_stdout=print_stdout,
1323
+ print_stderr=print_stderr,
1324
+ return_=return_,
1325
+ return_stdout=return_stdout,
1326
+ return_stderr=return_stderr,
1327
+ retry=retry,
1328
+ logger=logger,
1329
+ )
1138
1330
 
1139
1331
 
1140
1332
  def uv_run_cmd(module: str, /, *args: str) -> list[str]:
@@ -1236,6 +1428,10 @@ __all__ = [
1236
1428
  "set_hostname_cmd",
1237
1429
  "ssh",
1238
1430
  "ssh_cmd",
1431
+ "ssh_keygen_remove",
1432
+ "ssh_keygen_remove_cmd",
1433
+ "ssh_keyscan",
1434
+ "ssh_keyscan_cmd",
1239
1435
  "ssh_opts_cmd",
1240
1436
  "sudo_cmd",
1241
1437
  "sudo_nopasswd_cmd",
utilities/aeventkit.py DELETED
@@ -1,389 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from functools import wraps
5
- from inspect import iscoroutinefunction
6
- from typing import TYPE_CHECKING, Any, Self, assert_never, cast, override
7
-
8
- from eventkit import (
9
- Constant,
10
- Count,
11
- DropWhile,
12
- Enumerate,
13
- Event,
14
- Filter,
15
- Fork,
16
- Iterate,
17
- Map,
18
- Pack,
19
- Partial,
20
- PartialRight,
21
- Pluck,
22
- Skip,
23
- Star,
24
- Take,
25
- TakeUntil,
26
- TakeWhile,
27
- Timestamp,
28
- )
29
-
30
- from utilities.functions import apply_decorators
31
- from utilities.iterables import always_iterable
32
- from utilities.logging import to_logger
33
-
34
- if TYPE_CHECKING:
35
- from collections.abc import Callable
36
-
37
- from utilities.types import Coro, LoggerLike, MaybeCoro, MaybeIterable, TypeLike
38
-
39
-
40
- ##
41
-
42
-
43
- def add_listener[E: Event, F: Callable](
44
- event: E,
45
- listener: Callable[..., MaybeCoro[None]],
46
- /,
47
- *,
48
- error: Callable[[Event, BaseException], MaybeCoro[None]] | None = None,
49
- ignore: TypeLike[BaseException] | None = None,
50
- logger: LoggerLike | None = None,
51
- decorators: MaybeIterable[Callable[[F], F]] | None = None,
52
- done: Callable[..., MaybeCoro[None]] | None = None,
53
- keep_ref: bool = False,
54
- ) -> E:
55
- """Connect a listener to an event."""
56
- lifted = lift_listener(
57
- listener,
58
- event,
59
- error=error,
60
- ignore=ignore,
61
- logger=logger,
62
- decorators=decorators,
63
- )
64
- return cast("E", event.connect(lifted, done=done, keep_ref=keep_ref))
65
-
66
-
67
- ##
68
-
69
-
70
- @dataclass(repr=False, kw_only=True)
71
- class LiftedEvent[F: Callable[..., MaybeCoro[None]]]:
72
- """A lifted version of `Event`."""
73
-
74
- event: Event
75
-
76
- def name(self) -> str:
77
- return self.event.name() # pragma: no cover
78
-
79
- def done(self) -> bool:
80
- return self.event.done() # pragma: no cover
81
-
82
- def set_done(self) -> None:
83
- self.event.set_done() # pragma: no cover
84
-
85
- def value(self) -> Any:
86
- return self.event.value() # pragma: no cover
87
-
88
- def connect[F2: Callable](
89
- self,
90
- listener: F,
91
- /,
92
- *,
93
- error: Callable[[Event, BaseException], MaybeCoro[None]] | None = None,
94
- ignore: TypeLike[BaseException] | None = None,
95
- logger: LoggerLike | None = None,
96
- decorators: MaybeIterable[Callable[[F2], F2]] | None = None,
97
- done: Callable[..., MaybeCoro[None]] | None = None,
98
- keep_ref: bool = False,
99
- ) -> Event:
100
- return add_listener(
101
- self.event,
102
- listener,
103
- error=error,
104
- ignore=ignore,
105
- logger=logger,
106
- decorators=decorators,
107
- done=done,
108
- keep_ref=keep_ref,
109
- )
110
-
111
- def disconnect(
112
- self, listener: Any, /, *, error: Any = None, done: Any = None
113
- ) -> Any:
114
- return self.event.disconnect( # pragma: no cover
115
- listener, error=error, done=done
116
- )
117
-
118
- def disconnect_obj(self, obj: Any, /) -> None:
119
- self.event.disconnect_obj(obj) # pragma: no cover
120
-
121
- def emit(self, *args: Any) -> None:
122
- self.event.emit(*args) # pragma: no cover
123
-
124
- def emit_threadsafe(self, *args: Any) -> None:
125
- self.event.emit_threadsafe(*args) # pragma: no cover
126
-
127
- def clear(self) -> None:
128
- self.event.clear() # pragma: no cover
129
-
130
- def run(self) -> list[Any]:
131
- return self.event.run() # pragma: no cover
132
-
133
- def pipe(self, *targets: Event) -> Event:
134
- return self.event.pipe(*targets) # pragma: no cover
135
-
136
- def fork(self, *targets: Event) -> Fork:
137
- return self.event.fork(*targets) # pragma: no cover
138
-
139
- def set_source(self, source: Any, /) -> None:
140
- self.event.set_source(source) # pragma: no cover
141
-
142
- def _onFinalize(self, ref: Any) -> None: # noqa: N802
143
- self.event._onFinalize(ref) # noqa: SLF001 # pragma: no cover
144
-
145
- async def aiter(self, *, skip_to_last: bool = False, tuples: bool = False) -> Any:
146
- async for i in self.event.aiter( # pragma: no cover
147
- skip_to_last=skip_to_last, tuples=tuples
148
- ):
149
- yield i
150
-
151
- __iadd__ = connect
152
- __isub__ = disconnect
153
- __call__ = emit
154
- __or__ = pipe
155
-
156
- @override
157
- def __repr__(self) -> str:
158
- return self.event.__repr__() # pragma: no cover
159
-
160
- def __len__(self) -> int:
161
- return self.event.__len__() # pragma: no cover
162
-
163
- def __bool__(self) -> bool:
164
- return self.event.__bool__() # pragma: no cover
165
-
166
- def __getitem__(self, fork_targets: Any, /) -> Fork:
167
- return self.event.__getitem__(fork_targets) # pragma: no cover
168
-
169
- def __await__(self) -> Any:
170
- return self.event.__await__() # pragma: no cover
171
-
172
- def __aiter__(self) -> Any:
173
- return self.event.aiter() # pragma: no cover
174
-
175
- def __contains__(self, c: Any, /) -> bool:
176
- return self.event.__contains__(c) # pragma: no cover
177
-
178
- @override
179
- def __reduce__(self) -> Any:
180
- return self.event.__reduce__() # pragma: no cover
181
-
182
- def filter(self, *, predicate: Any = bool) -> Filter:
183
- return self.event.filter(predicate=predicate) # pragma: no cover
184
-
185
- def skip(self, *, count: int = 1) -> Skip:
186
- return self.event.skip(count=count) # pragma: no cover
187
-
188
- def take(self, *, count: int = 1) -> Take:
189
- return self.event.take(count=count) # pragma: no cover
190
-
191
- def takewhile(self, *, predicate: Any = bool) -> TakeWhile:
192
- return self.event.takewhile(predicate=predicate) # pragma: no cover
193
-
194
- def dropwhile(self, *, predicate: Any = lambda x: not x) -> DropWhile: # pyright: ignore[reportUnknownLambdaType]
195
- return self.event.dropwhile(predicate=predicate) # pragma: no cover
196
-
197
- def takeuntil(self, notifier: Event, /) -> TakeUntil:
198
- return self.event.takeuntil(notifier) # pragma: no cover
199
-
200
- def constant(self, constant: Any, /) -> Constant:
201
- return self.event.constant(constant) # pragma: no cover
202
-
203
- def iterate(self, it: Any, /) -> Iterate:
204
- return self.event.iterate(it) # pragma: no cover
205
-
206
- def count(self, *, start: int = 0, step: int = 1) -> Count:
207
- return self.event.count(start=start, step=step) # pragma: no cover
208
-
209
- def enumerate(self, *, start: int = 0, step: int = 1) -> Enumerate:
210
- return self.event.enumerate(start=start, step=step) # pragma: no cover
211
-
212
- def timestamp(self) -> Timestamp:
213
- return self.event.timestamp() # pragma: no cover
214
-
215
- def partial(self, *left_args: Any) -> Partial:
216
- return self.event.partial(*left_args) # pragma: no cover
217
-
218
- def partial_right(self, *right_args: Any) -> PartialRight:
219
- return self.event.partial_right(*right_args) # pragma: no cover
220
-
221
- def star(self) -> Star:
222
- return self.event.star() # pragma: no cover
223
-
224
- def pack(self) -> Pack:
225
- return self.event.pack() # pragma: no cover
226
-
227
- def pluck(self, *selections: int | str) -> Pluck:
228
- return self.event.pluck(*selections) # pragma: no cover
229
-
230
- def map(
231
- self,
232
- func: Any,
233
- /,
234
- *,
235
- timeout: float | None = None,
236
- ordered: bool = True,
237
- task_limit: int | None = None,
238
- ) -> Map:
239
- return self.event.map( # pragma: no cover
240
- func, timeout=timeout, ordered=ordered, task_limit=task_limit
241
- )
242
-
243
-
244
- ##
245
-
246
-
247
- class TypedEvent[F: Callable[..., MaybeCoro[None]]](Event):
248
- """A typed version of `Event`."""
249
-
250
- @override
251
- def connect[F2: Callable](
252
- self,
253
- listener: F,
254
- error: Callable[[Self, BaseException], MaybeCoro[None]] | None = None,
255
- done: Callable[[Self], MaybeCoro[None]] | None = None,
256
- keep_ref: bool = False,
257
- *,
258
- ignore: TypeLike[BaseException] | None = None,
259
- logger: LoggerLike | None = None,
260
- decorators: MaybeIterable[Callable[[F2], F2]] | None = None,
261
- ) -> Self:
262
- lifted = lift_listener(
263
- listener,
264
- self,
265
- error=cast(
266
- "Callable[[Event, BaseException], MaybeCoro[None]] | None", error
267
- ),
268
- ignore=ignore,
269
- logger=logger,
270
- decorators=decorators,
271
- )
272
- return cast(
273
- "Self", super().connect(lifted, error=error, done=done, keep_ref=keep_ref)
274
- )
275
-
276
-
277
- ##
278
-
279
-
280
- def lift_listener[F1: Callable[..., MaybeCoro[None]], F2: Callable](
281
- listener: F1,
282
- event: Event,
283
- /,
284
- *,
285
- error: Callable[[Event, BaseException], MaybeCoro[None]] | None = None,
286
- ignore: TypeLike[BaseException] | None = None,
287
- logger: LoggerLike | None = None,
288
- decorators: MaybeIterable[Callable[[F2], F2]] | None = None,
289
- ) -> F1:
290
- match error, bool(iscoroutinefunction(listener)):
291
- case None, False:
292
- listener_typed = cast("Callable[..., None]", listener)
293
-
294
- @wraps(listener)
295
- def listener_no_error_sync(*args: Any, **kwargs: Any) -> None:
296
- try:
297
- listener_typed(*args, **kwargs)
298
- except Exception as exc: # noqa: BLE001
299
- if (ignore is not None) and isinstance(exc, ignore):
300
- return
301
- to_logger(logger).exception("")
302
-
303
- lifted = listener_no_error_sync
304
-
305
- case None, True:
306
- listener_typed = cast("Callable[..., Coro[None]]", listener)
307
-
308
- @wraps(listener)
309
- async def listener_no_error_async(*args: Any, **kwargs: Any) -> None:
310
- try:
311
- await listener_typed(*args, **kwargs)
312
- except Exception as exc: # noqa: BLE001
313
- if (ignore is not None) and isinstance(exc, ignore):
314
- return
315
- to_logger(logger).exception("")
316
-
317
- lifted = listener_no_error_async
318
- case _, _:
319
- match bool(iscoroutinefunction(listener)), bool(iscoroutinefunction(error)):
320
- case False, False:
321
- listener_typed = cast("Callable[..., None]", listener)
322
- error_typed = cast("Callable[[Event, Exception], None]", error)
323
-
324
- @wraps(listener)
325
- def listener_have_error_sync(*args: Any, **kwargs: Any) -> None:
326
- try:
327
- listener_typed(*args, **kwargs)
328
- except Exception as exc: # noqa: BLE001
329
- if (ignore is not None) and isinstance(exc, ignore):
330
- return
331
- error_typed(event, exc)
332
-
333
- lifted = listener_have_error_sync
334
- case False, True:
335
- listener_typed = cast("Callable[..., None]", listener)
336
- error_typed = cast(
337
- "Callable[[Event, Exception], Coro[None]]", error
338
- )
339
- raise LiftListenerError(listener=listener_typed, error=error_typed)
340
- case True, _:
341
- listener_typed = cast("Callable[..., Coro[None]]", listener)
342
-
343
- @wraps(listener)
344
- async def listener_have_error_async(
345
- *args: Any, **kwargs: Any
346
- ) -> None:
347
- try:
348
- await listener_typed(*args, **kwargs)
349
- except Exception as exc: # noqa: BLE001
350
- if (ignore is not None) and isinstance(exc, ignore):
351
- return None
352
- if iscoroutinefunction(error):
353
- error_typed = cast(
354
- "Callable[[Event, Exception], Coro[None]]", error
355
- )
356
- return await error_typed(event, exc)
357
- error_typed = cast(
358
- "Callable[[Event, Exception], None]", error
359
- )
360
- error_typed(event, exc)
361
-
362
- lifted = listener_have_error_async
363
- case never:
364
- assert_never(never)
365
- case never:
366
- assert_never(never)
367
-
368
- if decorators is not None:
369
- lifted = apply_decorators(lifted, *always_iterable(decorators))
370
- return cast("F1", lifted)
371
-
372
-
373
- @dataclass(kw_only=True, slots=True)
374
- class LiftListenerError(Exception):
375
- listener: Callable[..., None]
376
- error: Callable[[Event, Exception], Coro[None]]
377
-
378
- @override
379
- def __str__(self) -> str:
380
- return f"Synchronous listener {self.listener} cannot be paired with an asynchronous error handler {self.error}"
381
-
382
-
383
- __all__ = [
384
- "LiftListenerError",
385
- "LiftedEvent",
386
- "TypedEvent",
387
- "add_listener",
388
- "lift_listener",
389
- ]