dycw-utilities 0.175.17__py3-none-any.whl → 0.175.19__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.175.17
3
+ Version: 0.175.19
4
4
  Summary: Miscellaneous Python utilities
5
5
  Author: Derek Wan
6
6
  Author-email: Derek Wan <d.wan@icloud.com>
@@ -1,4 +1,4 @@
1
- utilities/__init__.py,sha256=8_tM295sqZUhpgJO-NkFFdbuntYma_XUNSt0IM3-C_0,61
1
+ utilities/__init__.py,sha256=Icf2ql4qG7C1V1W6jJzw-EbFjcuaZSkx7-OyX1zNNMY,61
2
2
  utilities/altair.py,sha256=TLfRFbG9HwG7SLXoJ-v0r-t49ZaGgTQZD82cpjVi4vs,9085
3
3
  utilities/asyncio.py,sha256=aJySVxBY0gqsIYnoNmH7-1r8djKuf4vSsU69VCD08t8,16772
4
4
  utilities/atomicwrites.py,sha256=tPo6r-Rypd9u99u66B9z86YBPpnLrlHtwox_8Z7T34Y,5790
@@ -80,8 +80,8 @@ utilities/sqlalchemy.py,sha256=HQYpd7LFxdTF5WYVWYtCJeEBI71EJm7ytvCGyAH9B-U,37163
80
80
  utilities/sqlalchemy_polars.py,sha256=JCGhB37raSR7fqeWV5dTsciRTMVzIdVT9YSqKT0piT0,13370
81
81
  utilities/statsmodels.py,sha256=koyiBHvpMcSiBfh99wFUfSggLNx7cuAw3rwyfAhoKpQ,3410
82
82
  utilities/string.py,sha256=shmBK87zZwzGyixuNuXCiUbqzfeZ9xlrFwz6JTaRvDk,582
83
- utilities/subprocess.py,sha256=nFPIXVzXomI_Oby8PYF9HNWrsWtcSsCtgFnj_22QYlI,43753
84
- utilities/tempfile.py,sha256=QyvIdfV4r4YZ0NeNYsg0tCijThLKa7Z32u5Kxy6ZsGo,3619
83
+ utilities/subprocess.py,sha256=F4sRJq9eNnXwZ46GgYxTQQCT-YUx_ghZ4o91dH4KgW4,45405
84
+ utilities/tempfile.py,sha256=a3_M1QyxGZql_VcGkBOQBeWbbkItjgkfIpVyzU1UAic,3843
85
85
  utilities/testbook.py,sha256=j1KmaVbrX9VrbeMgtPh5gk55myAsn3dyRUn7jGbPbRk,1294
86
86
  utilities/text.py,sha256=7SvwcSR2l_5cOrm1samGnR4C-ZI6qyFLHLzSpO1zeHQ,13958
87
87
  utilities/threading.py,sha256=GvBOp4CyhHfN90wGXZuA2VKe9fGzMaEa7oCl4f3nnPU,1009
@@ -97,7 +97,7 @@ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
97
97
  utilities/whenever.py,sha256=F4ek0-OBWxHYrZdmoZt76N2RnNyKY5KrEHt7rqO4AQE,60183
98
98
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
99
99
  utilities/zoneinfo.py,sha256=tdIScrTB2-B-LH0ukb1HUXKooLknOfJNwHk10MuMYvA,3619
100
- dycw_utilities-0.175.17.dist-info/WHEEL,sha256=RRVLqVugUmFOqBedBFAmA4bsgFcROUBiSUKlERi0Hcg,79
101
- dycw_utilities-0.175.17.dist-info/entry_points.txt,sha256=cOGtKeJI0KXLSV7MJ8Dhc2G8jPgDcBDm53MVNJU4ycI,136
102
- dycw_utilities-0.175.17.dist-info/METADATA,sha256=obLY4pbYFf5oCFvSPuNf1mXsv1VC5nCC7sGJtizR-AA,1443
103
- dycw_utilities-0.175.17.dist-info/RECORD,,
100
+ dycw_utilities-0.175.19.dist-info/WHEEL,sha256=RRVLqVugUmFOqBedBFAmA4bsgFcROUBiSUKlERi0Hcg,79
101
+ dycw_utilities-0.175.19.dist-info/entry_points.txt,sha256=cOGtKeJI0KXLSV7MJ8Dhc2G8jPgDcBDm53MVNJU4ycI,136
102
+ dycw_utilities-0.175.19.dist-info/METADATA,sha256=-Ng54LQmTd_U_-Vbb0rRL7bAKisLDxfPhIrEkrxtT9U,1443
103
+ dycw_utilities-0.175.19.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.175.17"
3
+ __version__ = "0.175.19"
utilities/subprocess.py CHANGED
@@ -5,6 +5,7 @@ import sys
5
5
  from contextlib import contextmanager
6
6
  from dataclasses import dataclass
7
7
  from io import StringIO
8
+ from itertools import repeat
8
9
  from pathlib import Path
9
10
  from re import search
10
11
  from shlex import join
@@ -54,11 +55,36 @@ UPDATE_CA_CERTIFICATES: str = "update-ca-certificates"
54
55
  ##
55
56
 
56
57
 
58
+ def append_text(
59
+ path: PathLike,
60
+ text: str,
61
+ /,
62
+ *,
63
+ sudo: bool = False,
64
+ skip_if_present: bool = False,
65
+ flags: int = 0,
66
+ blank_lines: int = 1,
67
+ ) -> None:
68
+ """Append text to a file."""
69
+ try:
70
+ existing = cat(path, sudo=sudo)
71
+ except (CalledProcessError, FileNotFoundError):
72
+ tee(path, text, sudo=sudo, append=True)
73
+ return
74
+ if skip_if_present and (search(text, existing, flags=flags) is not None):
75
+ return
76
+ full = "".join([*repeat("\n", times=blank_lines), text])
77
+ tee(path, full, sudo=sudo, append=True)
78
+
79
+
80
+ ##
81
+
82
+
57
83
  def apt_install(package: str, /, *, update: bool = False, sudo: bool = False) -> None:
58
84
  """Install a package."""
59
85
  if update: # pragma: no cover
60
86
  run(*maybe_sudo_cmd(*APT_UPDATE, sudo=sudo))
61
- run(*maybe_sudo_cmd(*apt_install_cmd(package), sudo=sudo))
87
+ run(*maybe_sudo_cmd(*apt_install_cmd(package), sudo=sudo)) # pragma: no cover
62
88
 
63
89
 
64
90
  def apt_install_cmd(package: str, /) -> list[str]:
@@ -69,6 +95,13 @@ def apt_install_cmd(package: str, /) -> list[str]:
69
95
  ##
70
96
 
71
97
 
98
+ def cat(path: PathLike, /, *, sudo: bool = False) -> str:
99
+ """Concatenate a file."""
100
+ if sudo: # pragma: no cover
101
+ return run(*sudo_cmd(*cat_cmd(path)), return_=True)
102
+ return Path(path).read_text()
103
+
104
+
72
105
  def cat_cmd(path: PathLike, /) -> list[str]:
73
106
  """Command to use 'cat' to concatenate and print files."""
74
107
  return ["cat", str(path)]
@@ -170,6 +203,24 @@ def chpasswd(user_name: str, password: str, /, *, sudo: bool = False) -> None:
170
203
  ##
171
204
 
172
205
 
206
+ def copy_text(
207
+ src: PathLike,
208
+ dest: PathLike,
209
+ /,
210
+ *,
211
+ sudo: bool = False,
212
+ substitutions: StrMapping | None = None,
213
+ ) -> None:
214
+ """Copy the text contents of a file."""
215
+ text = cat(src, sudo=sudo)
216
+ if substitutions is not None:
217
+ text = Template(text).substitute(**substitutions)
218
+ tee(dest, text, sudo=sudo)
219
+
220
+
221
+ ##
222
+
223
+
173
224
  def cp(
174
225
  src: PathLike,
175
226
  dest: PathLike,
@@ -356,6 +407,20 @@ def mv_cmd(src: PathLike, dest: PathLike, /) -> list[str]:
356
407
  ##
357
408
 
358
409
 
410
+ def replace_text(
411
+ path: PathLike, /, *replacements: tuple[str, str], sudo: bool = False
412
+ ) -> None:
413
+ """Replace the text in a file."""
414
+ path = Path(path)
415
+ text = cat(path, sudo=sudo)
416
+ for old, new in replacements:
417
+ text = text.replace(old, new)
418
+ tee(path, text, sudo=sudo)
419
+
420
+
421
+ ##
422
+
423
+
359
424
  def ripgrep(*args: str, path: PathLike = PWD) -> str | None:
360
425
  """Search for lines."""
361
426
  try: # skipif-ci
@@ -632,6 +697,7 @@ def _rsync_many_prepare(
632
697
  case never:
633
698
  assert_never(never)
634
699
  cmds: list[list[str]] = [
700
+ maybe_sudo_cmd(*rm_cmd(dest), sudo=sudo),
635
701
  maybe_sudo_cmd(*mkdir_cmd(dest, parent=True), sudo=sudo),
636
702
  maybe_sudo_cmd(*cp_cmd(temp_dest / name, dest), sudo=sudo),
637
703
  ]
@@ -1259,7 +1325,8 @@ def symlink_cmd(target: PathLike, link: PathLike, /) -> list[str]:
1259
1325
  def tee(
1260
1326
  path: PathLike, text: str, /, *, sudo: bool = False, append: bool = False
1261
1327
  ) -> None:
1262
- """Use 'tee' to duplicate standard input."""
1328
+ """Duplicate standard input."""
1329
+ mkdir(path, sudo=sudo, parent=True)
1263
1330
  if sudo: # pragma: no cover
1264
1331
  run(*sudo_cmd(*tee_cmd(path, append=append)), input=text)
1265
1332
  else:
@@ -1514,14 +1581,17 @@ __all__ = [
1514
1581
  "RsyncCmdError",
1515
1582
  "RsyncCmdNoSourcesError",
1516
1583
  "RsyncCmdSourcesNotFoundError",
1584
+ "append_text",
1517
1585
  "apt_install",
1518
1586
  "apt_install_cmd",
1587
+ "cat",
1519
1588
  "cd_cmd",
1520
1589
  "chmod",
1521
1590
  "chmod_cmd",
1522
1591
  "chown",
1523
1592
  "chown_cmd",
1524
1593
  "chpasswd",
1594
+ "copy_text",
1525
1595
  "cp",
1526
1596
  "cp_cmd",
1527
1597
  "echo_cmd",
@@ -1538,6 +1608,7 @@ __all__ = [
1538
1608
  "mkdir_cmd",
1539
1609
  "mv",
1540
1610
  "mv_cmd",
1611
+ "replace_text",
1541
1612
  "ripgrep",
1542
1613
  "ripgrep_cmd",
1543
1614
  "rm",
utilities/tempfile.py CHANGED
@@ -73,23 +73,44 @@ class _TemporaryDirectoryNoResourceWarning(tempfile.TemporaryDirectory):
73
73
  @contextmanager
74
74
  def TemporaryFile( # noqa: N802
75
75
  *,
76
+ dir: PathLike | None = None, # noqa: A002
76
77
  suffix: str | None = None,
77
78
  prefix: str | None = None,
78
- dir: PathLike | None = None, # noqa: A002
79
79
  ignore_cleanup_errors: bool = False,
80
80
  delete: bool = True,
81
81
  name: str | None = None,
82
82
  text: str | None = None,
83
83
  ) -> Iterator[Path]:
84
84
  """Yield a temporary file."""
85
- with _temporary_file_inner(
86
- suffix=suffix,
87
- prefix=prefix,
88
- dir=dir,
89
- ignore_cleanup_errors=ignore_cleanup_errors,
90
- delete=delete,
91
- name=name,
92
- ) as temp:
85
+ if dir is None:
86
+ with (
87
+ TemporaryDirectory(
88
+ suffix=suffix,
89
+ prefix=prefix,
90
+ dir=dir,
91
+ ignore_cleanup_errors=ignore_cleanup_errors,
92
+ delete=delete,
93
+ ) as temp_dir,
94
+ _temporary_file_outer(
95
+ temp_dir, delete=delete, name=name, text=text
96
+ ) as temp,
97
+ ):
98
+ yield temp
99
+ else:
100
+ with _temporary_file_outer(dir, delete=delete, name=name, text=text) as temp:
101
+ yield temp
102
+
103
+
104
+ @contextmanager
105
+ def _temporary_file_outer(
106
+ path: PathLike,
107
+ /,
108
+ *,
109
+ delete: bool = True,
110
+ name: str | None = None,
111
+ text: str | None = None,
112
+ ) -> Iterator[Path]:
113
+ with _temporary_file_inner(path, delete=delete, name=name) as temp:
93
114
  if text is not None:
94
115
  _ = temp.write_text(text)
95
116
  yield temp
@@ -97,29 +118,17 @@ def TemporaryFile( # noqa: N802
97
118
 
98
119
  @contextmanager
99
120
  def _temporary_file_inner(
100
- *,
101
- suffix: str | None = None,
102
- prefix: str | None = None,
103
- dir: PathLike | None = None, # noqa: A002
104
- ignore_cleanup_errors: bool = False,
105
- delete: bool = True,
106
- name: str | None = None,
121
+ path: PathLike, /, *, delete: bool = True, name: str | None = None
107
122
  ) -> Iterator[Path]:
108
- with TemporaryDirectory(
109
- suffix=suffix,
110
- prefix=prefix,
111
- dir=dir,
112
- ignore_cleanup_errors=ignore_cleanup_errors,
113
- delete=delete,
114
- ) as temp_dir:
115
- temp_file = _NamedTemporaryFile( # noqa: SIM115
116
- dir=temp_dir, delete=delete, delete_on_close=False
117
- )
118
- if name is None:
119
- yield temp_dir / temp_file.name
120
- else:
121
- _ = move(temp_dir / temp_file.name, temp_dir / name)
122
- yield temp_dir / name
123
+ path = Path(path)
124
+ temp = _NamedTemporaryFile( # noqa: SIM115
125
+ dir=path, delete=delete, delete_on_close=False
126
+ )
127
+ if name is None:
128
+ yield path / temp.name
129
+ else:
130
+ _ = move(path / temp.name, path / name)
131
+ yield path / name
123
132
 
124
133
 
125
134
  ##