dycw-utilities 0.174.16__tar.gz → 0.174.18__tar.gz

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.
Files changed (103) hide show
  1. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/PKG-INFO +1 -1
  2. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/pyproject.toml +2 -2
  3. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/__init__.py +1 -1
  4. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/docker.py +1 -0
  5. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/subprocess.py +182 -12
  6. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/README.md +0 -0
  7. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/aeventkit.py +0 -0
  8. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/altair.py +0 -0
  9. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/asyncio.py +0 -0
  10. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/atomicwrites.py +0 -0
  11. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/atools.py +0 -0
  12. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/cachetools.py +0 -0
  13. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/click.py +0 -0
  14. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/concurrent.py +0 -0
  15. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/contextlib.py +0 -0
  16. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/contextvars.py +0 -0
  17. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/cryptography.py +0 -0
  18. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/cvxpy.py +0 -0
  19. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/dataclasses.py +0 -0
  20. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/enum.py +0 -0
  21. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/errors.py +0 -0
  22. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/fastapi.py +0 -0
  23. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/fpdf2.py +0 -0
  24. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/functions.py +0 -0
  25. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/functools.py +0 -0
  26. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/getpass.py +0 -0
  27. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/git.py +0 -0
  28. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/grp.py +0 -0
  29. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/gzip.py +0 -0
  30. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/hashlib.py +0 -0
  31. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/http.py +0 -0
  32. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/hypothesis.py +0 -0
  33. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/importlib.py +0 -0
  34. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/inflect.py +0 -0
  35. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/ipython.py +0 -0
  36. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/iterables.py +0 -0
  37. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/jinja2.py +0 -0
  38. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/json.py +0 -0
  39. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/jupyter.py +0 -0
  40. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/libcst.py +0 -0
  41. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/lightweight_charts.py +0 -0
  42. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/logging.py +0 -0
  43. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/math.py +0 -0
  44. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/memory_profiler.py +0 -0
  45. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/modules.py +0 -0
  46. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/more_itertools.py +0 -0
  47. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/numpy.py +0 -0
  48. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/operator.py +0 -0
  49. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/optuna.py +0 -0
  50. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/orjson.py +0 -0
  51. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/os.py +0 -0
  52. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/parse.py +0 -0
  53. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/pathlib.py +0 -0
  54. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/permissions.py +0 -0
  55. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/pickle.py +0 -0
  56. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/platform.py +0 -0
  57. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/polars.py +0 -0
  58. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/polars_ols.py +0 -0
  59. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/postgres.py +0 -0
  60. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/pottery.py +0 -0
  61. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/pqdm.py +0 -0
  62. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/psutil.py +0 -0
  63. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/pwd.py +0 -0
  64. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/py.typed +0 -0
  65. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/pydantic.py +0 -0
  66. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/pydantic_settings.py +0 -0
  67. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/pydantic_settings_sops.py +0 -0
  68. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/pyinstrument.py +0 -0
  69. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/pytest.py +0 -0
  70. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/pytest_plugins/__init__.py +0 -0
  71. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/pytest_plugins/pytest_randomly.py +0 -0
  72. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/pytest_plugins/pytest_regressions.py +0 -0
  73. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/pytest_regressions.py +0 -0
  74. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/random.py +0 -0
  75. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/re.py +0 -0
  76. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/redis.py +0 -0
  77. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/reprlib.py +0 -0
  78. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/scipy.py +0 -0
  79. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/sentinel.py +0 -0
  80. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/shelve.py +0 -0
  81. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/shutil.py +0 -0
  82. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/slack_sdk.py +0 -0
  83. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/socket.py +0 -0
  84. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/sqlalchemy.py +0 -0
  85. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/sqlalchemy_polars.py +0 -0
  86. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/statsmodels.py +0 -0
  87. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/string.py +0 -0
  88. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/tempfile.py +0 -0
  89. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/testbook.py +0 -0
  90. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/text.py +0 -0
  91. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/threading.py +0 -0
  92. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/timer.py +0 -0
  93. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/traceback.py +0 -0
  94. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/types.py +0 -0
  95. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/typing.py +0 -0
  96. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/tzdata.py +0 -0
  97. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/tzlocal.py +0 -0
  98. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/uuid.py +0 -0
  99. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/version.py +0 -0
  100. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/warnings.py +0 -0
  101. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/whenever.py +0 -0
  102. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/zipfile.py +0 -0
  103. {dycw_utilities-0.174.16 → dycw_utilities-0.174.18}/src/utilities/zoneinfo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dycw-utilities
3
- Version: 0.174.16
3
+ Version: 0.174.18
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
@@ -101,7 +101,7 @@
101
101
  name = "dycw-utilities"
102
102
  readme = "README.md"
103
103
  requires-python = ">= 3.12"
104
- version = "0.174.16"
104
+ version = "0.174.18"
105
105
 
106
106
  [project.entry-points.pytest11]
107
107
  pytest-randomly = "utilities.pytest_plugins.pytest_randomly"
@@ -135,7 +135,7 @@
135
135
  # bump-my-version
136
136
  [tool.bumpversion]
137
137
  allow_dirty = true
138
- current_version = "0.174.16"
138
+ current_version = "0.174.18"
139
139
 
140
140
  [[tool.bumpversion.files]]
141
141
  filename = "src/utilities/__init__.py"
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.174.16"
3
+ __version__ = "0.174.18"
@@ -269,6 +269,7 @@ def yield_docker_temp_dir(
269
269
  logger: LoggerLike | None = None,
270
270
  keep: bool = False,
271
271
  ) -> Iterator[Path]:
272
+ """Yield a temporary directory in a Docker container."""
272
273
  path = Path( # skipif-ci
273
274
  docker_exec(
274
275
  container,
@@ -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
 
@@ -220,6 +221,16 @@ def expand_path(
220
221
  ##
221
222
 
222
223
 
224
+ def git_clone(
225
+ url: str, path: PathLike, /, *, sudo: bool = False, branch: str | None = None
226
+ ) -> None:
227
+ """Clone a repository."""
228
+ rm(path, sudo=sudo)
229
+ run(*maybe_sudo_cmd(*git_clone_cmd(url, path), sudo=sudo))
230
+ if branch is not None:
231
+ run(*maybe_sudo_cmd(*git_hard_reset_cmd(branch=branch), sudo=sudo), cwd=path)
232
+
233
+
223
234
  def git_clone_cmd(url: str, path: PathLike, /) -> list[str]:
224
235
  """Command to use 'git clone' to clone a repository."""
225
236
  return ["git", "clone", "--recurse-submodules", url, str(path)]
@@ -346,7 +357,7 @@ def rsync(
346
357
  chown_user: str | None = None,
347
358
  chown_group: str | None = None,
348
359
  exclude: MaybeIterable[str] | None = None,
349
- chmod: str | None = None,
360
+ chmod: PermissionsLike | None = None,
350
361
  ) -> None:
351
362
  """Remote & local file copying."""
352
363
  mkdir_args = maybe_sudo_cmd(*mkdir_cmd(dest, parent=True), sudo=sudo) # skipif-ci
@@ -361,13 +372,13 @@ def rsync(
361
372
  retry=retry,
362
373
  logger=logger,
363
374
  )
364
- is_dir = any(Path(s).is_dir() for s in always_iterable(src_or_srcs)) # skipif-ci
375
+ srcs = list(always_iterable(src_or_srcs)) # skipif-ci
365
376
  rsync_args = rsync_cmd( # skipif-ci
366
- src_or_srcs,
377
+ srcs,
367
378
  user,
368
379
  hostname,
369
380
  dest,
370
- archive=is_dir,
381
+ archive=any(Path(s).is_dir() for s in srcs),
371
382
  chown_user=chown_user,
372
383
  chown_group=chown_group,
373
384
  exclude=exclude,
@@ -375,7 +386,6 @@ def rsync(
375
386
  host_key_algorithms=host_key_algorithms,
376
387
  strict_host_key_checking=strict_host_key_checking,
377
388
  sudo=sudo,
378
- parent=is_dir,
379
389
  )
380
390
  run(*rsync_args, print=print, retry=retry, logger=logger) # skipif-ci
381
391
  if chmod is not None: # skipif-ci
@@ -408,7 +418,6 @@ def rsync_cmd(
408
418
  host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
409
419
  strict_host_key_checking: bool = True,
410
420
  sudo: bool = False,
411
- parent: bool = False,
412
421
  ) -> list[str]:
413
422
  """Command to use 'rsync' to do remote & local file copying."""
414
423
  args: list[str] = ["rsync"]
@@ -438,12 +447,141 @@ def rsync_cmd(
438
447
  args.extend(["--rsh", join(rsh_args)])
439
448
  if sudo:
440
449
  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}",
450
+ srcs = list(always_iterable(src_or_srcs)) # do not Path()
451
+ if len(srcs) == 0:
452
+ raise RsyncCmdNoSourcesError(user=user, hostname=hostname, dest=dest)
453
+ missing = [s for s in srcs if not Path(s).exists()]
454
+ if len(missing) >= 1:
455
+ raise RsyncCmdSourcesNotFoundError(
456
+ sources=missing, user=user, hostname=hostname, dest=dest
457
+ )
458
+ return [*args, *map(str, srcs), f"{user}@{hostname}:{dest}"]
459
+
460
+
461
+ @dataclass(kw_only=True, slots=True)
462
+ class RsyncCmdError(Exception):
463
+ user: str
464
+ hostname: str
465
+ dest: PathLike
466
+
467
+ @override
468
+ def __str__(self) -> str:
469
+ return f"No sources selected to send to {self.user}@{self.hostname}:{self.dest}"
470
+
471
+
472
+ @dataclass(kw_only=True, slots=True)
473
+ class RsyncCmdNoSourcesError(RsyncCmdError):
474
+ @override
475
+ def __str__(self) -> str:
476
+ return f"No sources selected to send to {self.user}@{self.hostname}:{self.dest}"
477
+
478
+
479
+ @dataclass(kw_only=True, slots=True)
480
+ class RsyncCmdSourcesNotFoundError(RsyncCmdError):
481
+ sources: list[PathLike]
482
+
483
+ @override
484
+ def __str__(self) -> str:
485
+ desc = ", ".join(map(repr, map(str, self.sources)))
486
+ return f"Sources selected to send to {self.user}@{self.hostname}:{self.dest} but not found: {desc}"
487
+
488
+
489
+ ##
490
+
491
+
492
+ def rsync_many(
493
+ user: str,
494
+ hostname: str,
495
+ /,
496
+ *items: tuple[PathLike, PathLike]
497
+ | tuple[Literal["sudo"], PathLike, PathLike]
498
+ | tuple[PathLike, PathLike, PermissionsLike],
499
+ retry: Retry | None = None,
500
+ logger: LoggerLike | None = None,
501
+ keep: bool = False,
502
+ batch_mode: bool = True,
503
+ host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
504
+ strict_host_key_checking: bool = True,
505
+ print: bool = False, # noqa: A002
506
+ ) -> None:
507
+ cmds: list[list[str]] = [] # skipif-ci
508
+ with ( # skipif-ci
509
+ TemporaryDirectory() as temp_src,
510
+ yield_ssh_temp_dir(
511
+ user, hostname, retry=retry, logger=logger, keep=keep
512
+ ) as temp_dest,
513
+ ):
514
+ for item in items:
515
+ match item:
516
+ case Path() | str() as src, Path() | str() as dest:
517
+ cmds.extend(_rsync_many_prepare(src, dest, temp_src, temp_dest))
518
+ case "sudo", Path() | str() as src, Path() | str() as dest:
519
+ cmds.extend(
520
+ _rsync_many_prepare(src, dest, temp_src, temp_dest, sudo=True)
521
+ )
522
+ case (
523
+ Path() | str() as src,
524
+ Path() | str() as dest,
525
+ Permissions() | int() | str() as perms,
526
+ ):
527
+ cmds.extend(
528
+ _rsync_many_prepare(src, dest, temp_src, temp_dest, perms=perms)
529
+ )
530
+ case never:
531
+ assert_never(never)
532
+ rsync(
533
+ f"{temp_src}/",
534
+ user,
535
+ hostname,
536
+ temp_dest,
537
+ batch_mode=batch_mode,
538
+ host_key_algorithms=host_key_algorithms,
539
+ strict_host_key_checking=strict_host_key_checking,
540
+ print=print,
541
+ retry=retry,
542
+ logger=logger,
543
+ )
544
+ ssh(
545
+ user,
546
+ hostname,
547
+ *BASH_LS,
548
+ input="\n".join(map(join, cmds)),
549
+ print=print,
550
+ retry=retry,
551
+ logger=logger,
552
+ )
553
+
554
+
555
+ def _rsync_many_prepare(
556
+ src: PathLike,
557
+ dest: PathLike,
558
+ temp_src: PathLike,
559
+ temp_dest: PathLike,
560
+ /,
561
+ *,
562
+ sudo: bool = False,
563
+ perms: PermissionsLike | None = None,
564
+ ) -> list[list[str]]:
565
+ dest, temp_src, temp_dest = map(Path, [dest, temp_src, temp_dest])
566
+ n = len(list(temp_src.iterdir()))
567
+ name = str(n)
568
+ match src:
569
+ case Path():
570
+ cp(src, temp_src / name)
571
+ case str():
572
+ if Path(src).exists():
573
+ cp(src, temp_src / name)
574
+ else:
575
+ tee(temp_src / name, src)
576
+ case never:
577
+ assert_never(never)
578
+ cmds: list[list[str]] = [
579
+ maybe_sudo_cmd(*mkdir_cmd(dest, parent=True), sudo=sudo),
580
+ maybe_sudo_cmd(*cp_cmd(temp_dest / name, dest), sudo=sudo),
446
581
  ]
582
+ if perms is not None:
583
+ cmds.append(maybe_sudo_cmd(*chmod_cmd(dest, perms), sudo=sudo))
584
+ return cmds
447
585
 
448
586
 
449
587
  ##
@@ -961,14 +1099,26 @@ def tee_cmd(path: PathLike, /, *, append: bool = False) -> list[str]:
961
1099
  ##
962
1100
 
963
1101
 
1102
+ def touch(path: PathLike, /, *, sudo: bool = False) -> None:
1103
+ """Change file access and modification times."""
1104
+ run(*maybe_sudo_cmd(*touch_cmd(path), sudo=sudo))
1105
+
1106
+
964
1107
  def touch_cmd(path: PathLike, /) -> list[str]:
1108
+ """Command to use 'touch' to change file access and modification times."""
965
1109
  return ["touch", str(path)]
966
1110
 
967
1111
 
968
1112
  ##
969
1113
 
970
1114
 
1115
+ def uv_run(module: str, /, *args: str) -> None:
1116
+ """Run a command or script."""
1117
+ run(*uv_run_cmd(module, *args)) # pragma: no cover
1118
+
1119
+
971
1120
  def uv_run_cmd(module: str, /, *args: str) -> list[str]:
1121
+ """Command to use 'uv' to run a command or script."""
972
1122
  return [
973
1123
  "uv",
974
1124
  "run",
@@ -986,6 +1136,17 @@ def uv_run_cmd(module: str, /, *args: str) -> list[str]:
986
1136
  ##
987
1137
 
988
1138
 
1139
+ @contextmanager
1140
+ def yield_git_repo(url: str, /, *, branch: str | None = None) -> Iterator[Path]:
1141
+ """Yield a temporary git repository."""
1142
+ with TemporaryDirectory() as temp_dir:
1143
+ git_clone(url, temp_dir, branch=branch)
1144
+ yield temp_dir
1145
+
1146
+
1147
+ ##
1148
+
1149
+
989
1150
  @contextmanager
990
1151
  def yield_ssh_temp_dir(
991
1152
  user: str,
@@ -996,6 +1157,7 @@ def yield_ssh_temp_dir(
996
1157
  logger: LoggerLike | None = None,
997
1158
  keep: bool = False,
998
1159
  ) -> Iterator[Path]:
1160
+ """Yield a temporary directory on a remote machine."""
999
1161
  path = Path( # skipif-ci
1000
1162
  ssh(user, hostname, *MKTEMP_DIR_CMD, return_=True, retry=retry, logger=logger)
1001
1163
  )
@@ -1019,6 +1181,9 @@ __all__ = [
1019
1181
  "ChownCmdError",
1020
1182
  "CpError",
1021
1183
  "MvFileError",
1184
+ "RsyncCmdError",
1185
+ "RsyncCmdNoSourcesError",
1186
+ "RsyncCmdSourcesNotFoundError",
1022
1187
  "apt_install_cmd",
1023
1188
  "cd_cmd",
1024
1189
  "chmod",
@@ -1029,6 +1194,7 @@ __all__ = [
1029
1194
  "cp_cmd",
1030
1195
  "echo_cmd",
1031
1196
  "expand_path",
1197
+ "git_clone",
1032
1198
  "git_clone_cmd",
1033
1199
  "git_hard_reset_cmd",
1034
1200
  "maybe_parent",
@@ -1041,6 +1207,7 @@ __all__ = [
1041
1207
  "rm_cmd",
1042
1208
  "rsync",
1043
1209
  "rsync_cmd",
1210
+ "rsync_many",
1044
1211
  "run",
1045
1212
  "set_hostname_cmd",
1046
1213
  "ssh",
@@ -1051,7 +1218,10 @@ __all__ = [
1051
1218
  "symlink",
1052
1219
  "symlink_cmd",
1053
1220
  "tee_cmd",
1221
+ "touch",
1054
1222
  "touch_cmd",
1223
+ "uv_run",
1055
1224
  "uv_run_cmd",
1225
+ "yield_git_repo",
1056
1226
  "yield_ssh_temp_dir",
1057
1227
  ]