dycw-utilities 0.174.20__py3-none-any.whl → 0.175.7__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.7
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=B5bIogCgY2363uBTrXOQ-P5Y0xUozj2Nfbz2ZiV4uhs,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=pTmRcfsIraSfsM182R0gzqvDrBS7-_dRmwOABU1ZB14,41018
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.7.dist-info/WHEEL,sha256=RRVLqVugUmFOqBedBFAmA4bsgFcROUBiSUKlERi0Hcg,79
101
+ dycw_utilities-0.175.7.dist-info/entry_points.txt,sha256=cOGtKeJI0KXLSV7MJ8Dhc2G8jPgDcBDm53MVNJU4ycI,136
102
+ dycw_utilities-0.175.7.dist-info/METADATA,sha256=inDD1FvpQAWndmJ70WVWYhpWPrGG1MIhPIOR8Yfz98o,1442
103
+ dycw_utilities-0.175.7.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.7"
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"
@@ -215,6 +217,13 @@ def echo_cmd(text: str, /) -> list[str]:
215
217
  ##
216
218
 
217
219
 
220
+ def env_cmds(env: StrStrMapping, /) -> list[str]:
221
+ return [f"{key}={value}" for key, value in env.items()]
222
+
223
+
224
+ ##
225
+
226
+
218
227
  def expand_path(
219
228
  path: PathLike, /, *, subs: StrMapping | None = None, sudo: bool = False
220
229
  ) -> Path:
@@ -523,6 +532,7 @@ def rsync_many(
523
532
  host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
524
533
  strict_host_key_checking: bool = True,
525
534
  print: bool = False, # noqa: A002
535
+ exclude: MaybeIterable[str] | None = None,
526
536
  ) -> None:
527
537
  cmds: list[list[str]] = [] # skipif-ci
528
538
  with ( # skipif-ci
@@ -560,6 +570,7 @@ def rsync_many(
560
570
  print=print,
561
571
  retry=retry,
562
572
  logger=logger,
573
+ exclude=exclude,
563
574
  )
564
575
  ssh(
565
576
  user,
@@ -625,6 +636,7 @@ def run(
625
636
  return_stdout: bool = False,
626
637
  return_stderr: bool = False,
627
638
  retry: Retry | None = None,
639
+ retry_skip: Callable[[int, str, str], bool] | None = None,
628
640
  logger: LoggerLike | None = None,
629
641
  ) -> str: ...
630
642
  @overload
@@ -645,6 +657,7 @@ def run(
645
657
  return_stdout: Literal[True],
646
658
  return_stderr: bool = False,
647
659
  retry: Retry | None = None,
660
+ retry_skip: Callable[[int, str, str], bool] | None = None,
648
661
  logger: LoggerLike | None = None,
649
662
  ) -> str: ...
650
663
  @overload
@@ -665,6 +678,7 @@ def run(
665
678
  return_stdout: bool = False,
666
679
  return_stderr: Literal[True],
667
680
  retry: Retry | None = None,
681
+ retry_skip: Callable[[int, str, str], bool] | None = None,
668
682
  logger: LoggerLike | None = None,
669
683
  ) -> str: ...
670
684
  @overload
@@ -685,6 +699,7 @@ def run(
685
699
  return_stdout: Literal[False] = False,
686
700
  return_stderr: Literal[False] = False,
687
701
  retry: Retry | None = None,
702
+ retry_skip: Callable[[int, str, str], bool] | None = None,
688
703
  logger: LoggerLike | None = None,
689
704
  ) -> None: ...
690
705
  @overload
@@ -705,6 +720,7 @@ def run(
705
720
  return_stdout: bool = False,
706
721
  return_stderr: bool = False,
707
722
  retry: Retry | None = None,
723
+ retry_skip: Callable[[int, str, str], bool] | None = None,
708
724
  logger: LoggerLike | None = None,
709
725
  ) -> str | None: ...
710
726
  def run(
@@ -724,6 +740,7 @@ def run(
724
740
  return_stdout: bool = False,
725
741
  return_stderr: bool = False,
726
742
  retry: Retry | None = None,
743
+ retry_skip: Callable[[int, str, str], bool] | None = None,
727
744
  logger: LoggerLike | None = None,
728
745
  ) -> str | None:
729
746
  """Run a command in a subprocess."""
@@ -781,14 +798,18 @@ def run(
781
798
  case 0, False, False:
782
799
  return None
783
800
  case _, _, _:
784
- if retry is None:
785
- attempts = delta = None
786
- else:
787
- attempts, delta = retry
788
801
  _ = stdout.seek(0)
789
802
  stdout_text = stdout.read()
790
803
  _ = stderr.seek(0)
791
804
  stderr_text = stderr.read()
805
+ if (retry is None) or (
806
+ (retry is not None)
807
+ and (retry_skip is not None)
808
+ and retry_skip(return_code, stdout_text, stderr_text)
809
+ ):
810
+ attempts = delta = None
811
+ else:
812
+ attempts, delta = retry
792
813
  if logger is not None:
793
814
  msg = strip_and_dedent(f"""
794
815
  'run' failed with:
@@ -879,10 +900,12 @@ def ssh(
879
900
  user: str,
880
901
  hostname: str,
881
902
  /,
882
- *cmd_and_cmds_or_args: str,
903
+ *cmd_and_args: str,
883
904
  batch_mode: bool = True,
884
905
  host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
885
906
  strict_host_key_checking: bool = True,
907
+ port: int | None = None,
908
+ env: StrStrMapping | None = None,
886
909
  input: str | None = None,
887
910
  print: bool = False,
888
911
  print_stdout: bool = False,
@@ -898,10 +921,12 @@ def ssh(
898
921
  user: str,
899
922
  hostname: str,
900
923
  /,
901
- *cmd_and_cmds_or_args: str,
924
+ *cmd_and_args: str,
902
925
  batch_mode: bool = True,
903
926
  host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
904
927
  strict_host_key_checking: bool = True,
928
+ port: int | None = None,
929
+ env: StrStrMapping | None = None,
905
930
  input: str | None = None,
906
931
  print: bool = False,
907
932
  print_stdout: bool = False,
@@ -917,10 +942,12 @@ def ssh(
917
942
  user: str,
918
943
  hostname: str,
919
944
  /,
920
- *cmd_and_cmds_or_args: str,
945
+ *cmd_and_args: str,
921
946
  batch_mode: bool = True,
922
947
  host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
923
948
  strict_host_key_checking: bool = True,
949
+ port: int | None = None,
950
+ env: StrStrMapping | None = None,
924
951
  input: str | None = None,
925
952
  print: bool = False,
926
953
  print_stdout: bool = False,
@@ -936,10 +963,12 @@ def ssh(
936
963
  user: str,
937
964
  hostname: str,
938
965
  /,
939
- *cmd_and_cmds_or_args: str,
966
+ *cmd_and_args: str,
940
967
  batch_mode: bool = True,
941
968
  host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
942
969
  strict_host_key_checking: bool = True,
970
+ port: int | None = None,
971
+ env: StrStrMapping | None = None,
943
972
  input: str | None = None,
944
973
  print: bool = False,
945
974
  print_stdout: bool = False,
@@ -955,10 +984,12 @@ def ssh(
955
984
  user: str,
956
985
  hostname: str,
957
986
  /,
958
- *cmd_and_cmds_or_args: str,
987
+ *cmd_and_args: str,
959
988
  batch_mode: bool = True,
960
989
  host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
961
990
  strict_host_key_checking: bool = True,
991
+ port: int | None = None,
992
+ env: StrStrMapping | None = None,
962
993
  input: str | None = None,
963
994
  print: bool = False,
964
995
  print_stdout: bool = False,
@@ -973,10 +1004,12 @@ def ssh(
973
1004
  user: str,
974
1005
  hostname: str,
975
1006
  /,
976
- *cmd_and_cmds_or_args: str,
1007
+ *cmd_and_args: str,
977
1008
  batch_mode: bool = True,
978
1009
  host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
979
1010
  strict_host_key_checking: bool = True,
1011
+ port: int | None = None,
1012
+ env: StrStrMapping | None = None,
980
1013
  input: str | None = None, # noqa: A002
981
1014
  print: bool = False, # noqa: A002
982
1015
  print_stdout: bool = False,
@@ -988,44 +1021,89 @@ def ssh(
988
1021
  logger: LoggerLike | None = None,
989
1022
  ) -> str | None:
990
1023
  """Execute a command on a remote machine."""
991
- cmd_and_args = ssh_cmd( # skipif-ci
1024
+ run_cmd_and_args = ssh_cmd( # skipif-ci
992
1025
  user,
993
1026
  hostname,
994
- *cmd_and_cmds_or_args,
1027
+ *cmd_and_args,
995
1028
  batch_mode=batch_mode,
996
1029
  host_key_algorithms=host_key_algorithms,
997
1030
  strict_host_key_checking=strict_host_key_checking,
1031
+ port=port,
1032
+ env=env,
998
1033
  )
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,
1034
+ try: # skipif-ci
1035
+ return run(
1036
+ *run_cmd_and_args,
1037
+ input=input,
1038
+ print=print,
1039
+ print_stdout=print_stdout,
1040
+ print_stderr=print_stderr,
1041
+ return_=return_,
1042
+ return_stdout=return_stdout,
1043
+ return_stderr=return_stderr,
1044
+ retry=retry,
1045
+ retry_skip=_ssh_retry_skip,
1046
+ logger=logger,
1047
+ )
1048
+ except CalledProcessError as error: # skipif-ci
1049
+ if not _ssh_is_strict_checking_error(error.stderr):
1050
+ raise
1051
+ ssh_keyscan(hostname, port=port)
1052
+ return ssh(
1053
+ user,
1054
+ hostname,
1055
+ *cmd_and_args,
1056
+ batch_mode=batch_mode,
1057
+ host_key_algorithms=host_key_algorithms,
1058
+ strict_host_key_checking=strict_host_key_checking,
1059
+ port=port,
1060
+ input=input,
1061
+ print=print,
1062
+ print_stdout=print_stdout,
1063
+ print_stderr=print_stderr,
1064
+ return_=return_,
1065
+ return_stdout=return_stdout,
1066
+ return_stderr=return_stderr,
1067
+ retry=retry,
1068
+ logger=logger,
1069
+ )
1070
+
1071
+
1072
+ def _ssh_retry_skip(return_code: int, stdout: str, stderr: str, /) -> bool:
1073
+ _ = (return_code, stdout)
1074
+ return _ssh_is_strict_checking_error(stderr)
1075
+
1076
+
1077
+ def _ssh_is_strict_checking_error(text: str, /) -> bool:
1078
+ match = search(
1079
+ "No ED25519 host key is known for .* and you have requested strict checking",
1080
+ text,
1010
1081
  )
1082
+ return match is not None
1011
1083
 
1012
1084
 
1013
1085
  def ssh_cmd(
1014
1086
  user: str,
1015
1087
  hostname: str,
1016
1088
  /,
1017
- *cmd_and_cmds_or_args: str,
1089
+ *cmd_and_args: str,
1018
1090
  batch_mode: bool = True,
1019
1091
  host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
1020
1092
  strict_host_key_checking: bool = True,
1093
+ port: int | None = None,
1094
+ env: StrStrMapping | None = None,
1021
1095
  ) -> list[str]:
1022
1096
  """Command to use 'ssh' to execute a command on a remote machine."""
1023
1097
  args: list[str] = ssh_opts_cmd(
1024
1098
  batch_mode=batch_mode,
1025
1099
  host_key_algorithms=host_key_algorithms,
1026
1100
  strict_host_key_checking=strict_host_key_checking,
1101
+ port=port,
1027
1102
  )
1028
- return [*args, f"{user}@{hostname}", *cmd_and_cmds_or_args]
1103
+ args.append(f"{user}@{hostname}")
1104
+ if env is not None:
1105
+ args.extend(env_cmds(env))
1106
+ return [*args, *cmd_and_args]
1029
1107
 
1030
1108
 
1031
1109
  def ssh_opts_cmd(
@@ -1033,6 +1111,7 @@ def ssh_opts_cmd(
1033
1111
  batch_mode: bool = True,
1034
1112
  host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
1035
1113
  strict_host_key_checking: bool = True,
1114
+ port: int | None = None,
1036
1115
  ) -> list[str]:
1037
1116
  """Command to use prepare 'ssh' to execute a command on a remote machine."""
1038
1117
  args: list[str] = ["ssh"]
@@ -1041,15 +1120,47 @@ def ssh_opts_cmd(
1041
1120
  args.extend(["-o", f"HostKeyAlgorithms={','.join(host_key_algorithms)}"])
1042
1121
  if strict_host_key_checking:
1043
1122
  args.extend(["-o", "StrictHostKeyChecking=yes"])
1123
+ if port is not None:
1124
+ args.extend(["-p", str(port)])
1044
1125
  return [*args, "-T"]
1045
1126
 
1046
1127
 
1047
1128
  ##
1048
1129
 
1049
1130
 
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]
1131
+ def ssh_keyscan(
1132
+ hostname: str, /, *, path: PathLike = KNOWN_HOSTS, port: int | None = None
1133
+ ) -> None:
1134
+ """Add a known host."""
1135
+ ssh_keygen_remove(hostname, path=path) # skipif-ci
1136
+ mkdir(path, parent=True) # skipif-ci
1137
+ with Path(path).open(mode="a") as fh: # skipif-ci
1138
+ _ = fh.write(run(*ssh_keyscan_cmd(hostname, port=port), return_=True))
1139
+
1140
+
1141
+ def ssh_keyscan_cmd(hostname: str, /, *, port: int | None = None) -> list[str]:
1142
+ """Command to use 'ssh-keyscan' to add a known host."""
1143
+ args: list[str] = ["ssh-keyscan"]
1144
+ if port is not None:
1145
+ args.extend(["-p", str(port)])
1146
+ return [*args, "-q", "-t", "ed25519", hostname]
1147
+
1148
+
1149
+ ##
1150
+
1151
+
1152
+ def ssh_keygen_remove(hostname: str, /, *, path: PathLike = KNOWN_HOSTS) -> None:
1153
+ """Remove a known host."""
1154
+ path = Path(path)
1155
+ if path.exists():
1156
+ run(*ssh_keygen_remove_cmd(hostname, path=path))
1157
+
1158
+
1159
+ def ssh_keygen_remove_cmd(
1160
+ hostname: str, /, *, path: PathLike = KNOWN_HOSTS
1161
+ ) -> list[str]:
1162
+ """Command to use 'ssh-keygen' to remove a known host."""
1163
+ return ["ssh-keygen", "-f", str(path), "-R", hostname]
1053
1164
 
1054
1165
 
1055
1166
  ##
@@ -1132,9 +1243,108 @@ def touch_cmd(path: PathLike, /) -> list[str]:
1132
1243
  ##
1133
1244
 
1134
1245
 
1135
- def uv_run(module: str, /, *args: str) -> None:
1246
+ @overload
1247
+ def uv_run(
1248
+ module: str,
1249
+ /,
1250
+ *args: str,
1251
+ cwd: PathLike | None = None,
1252
+ print: bool = False,
1253
+ print_stdout: bool = False,
1254
+ print_stderr: bool = False,
1255
+ return_: Literal[True],
1256
+ return_stdout: bool = False,
1257
+ return_stderr: bool = False,
1258
+ retry: Retry | None = None,
1259
+ logger: LoggerLike | None = None,
1260
+ ) -> str: ...
1261
+ @overload
1262
+ def uv_run(
1263
+ module: str,
1264
+ /,
1265
+ *args: str,
1266
+ cwd: PathLike | None = None,
1267
+ print: bool = False,
1268
+ print_stdout: bool = False,
1269
+ print_stderr: bool = False,
1270
+ return_: bool = False,
1271
+ return_stdout: Literal[True],
1272
+ return_stderr: bool = False,
1273
+ retry: Retry | None = None,
1274
+ logger: LoggerLike | None = None,
1275
+ ) -> str: ...
1276
+ @overload
1277
+ def uv_run(
1278
+ module: str,
1279
+ /,
1280
+ *args: str,
1281
+ cwd: PathLike | None = None,
1282
+ print: bool = False,
1283
+ print_stdout: bool = False,
1284
+ print_stderr: bool = False,
1285
+ return_: bool = False,
1286
+ return_stdout: bool = False,
1287
+ return_stderr: Literal[True],
1288
+ retry: Retry | None = None,
1289
+ logger: LoggerLike | None = None,
1290
+ ) -> str: ...
1291
+ @overload
1292
+ def uv_run(
1293
+ module: str,
1294
+ /,
1295
+ *args: str,
1296
+ cwd: PathLike | None = None,
1297
+ print: bool = False,
1298
+ print_stdout: bool = False,
1299
+ print_stderr: bool = False,
1300
+ return_: Literal[False] = False,
1301
+ return_stdout: Literal[False] = False,
1302
+ return_stderr: Literal[False] = False,
1303
+ retry: Retry | None = None,
1304
+ logger: LoggerLike | None = None,
1305
+ ) -> None: ...
1306
+ @overload
1307
+ def uv_run(
1308
+ module: str,
1309
+ /,
1310
+ *args: str,
1311
+ cwd: PathLike | None = None,
1312
+ print: bool = False,
1313
+ print_stdout: bool = False,
1314
+ print_stderr: bool = False,
1315
+ return_: bool = False,
1316
+ return_stdout: bool = False,
1317
+ return_stderr: bool = False,
1318
+ retry: Retry | None = None,
1319
+ logger: LoggerLike | None = None,
1320
+ ) -> str | None: ...
1321
+ def uv_run(
1322
+ module: str,
1323
+ /,
1324
+ *args: str,
1325
+ cwd: PathLike | None = None,
1326
+ print: bool = False, # noqa: A002
1327
+ print_stdout: bool = False,
1328
+ print_stderr: bool = False,
1329
+ return_: bool = False,
1330
+ return_stdout: bool = False,
1331
+ return_stderr: bool = False,
1332
+ retry: Retry | None = None,
1333
+ logger: LoggerLike | None = None,
1334
+ ) -> str | None:
1136
1335
  """Run a command or script."""
1137
- run(*uv_run_cmd(module, *args)) # pragma: no cover
1336
+ return run( # pragma: no cover
1337
+ *uv_run_cmd(module, *args),
1338
+ cwd=cwd,
1339
+ print=print,
1340
+ print_stdout=print_stdout,
1341
+ print_stderr=print_stderr,
1342
+ return_=return_,
1343
+ return_stdout=return_stdout,
1344
+ return_stderr=return_stderr,
1345
+ retry=retry,
1346
+ logger=logger,
1347
+ )
1138
1348
 
1139
1349
 
1140
1350
  def uv_run_cmd(module: str, /, *args: str) -> list[str]:
@@ -1215,6 +1425,7 @@ __all__ = [
1215
1425
  "cp",
1216
1426
  "cp_cmd",
1217
1427
  "echo_cmd",
1428
+ "env_cmds",
1218
1429
  "expand_path",
1219
1430
  "git_branch_current",
1220
1431
  "git_checkout",
@@ -1236,6 +1447,10 @@ __all__ = [
1236
1447
  "set_hostname_cmd",
1237
1448
  "ssh",
1238
1449
  "ssh_cmd",
1450
+ "ssh_keygen_remove",
1451
+ "ssh_keygen_remove_cmd",
1452
+ "ssh_keyscan",
1453
+ "ssh_keyscan_cmd",
1239
1454
  "ssh_opts_cmd",
1240
1455
  "sudo_cmd",
1241
1456
  "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
- ]