dycw-utilities 0.135.0__py3-none-any.whl → 0.178.1__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.

Potentially problematic release.


This version of dycw-utilities might be problematic. Click here for more details.

Files changed (97) hide show
  1. dycw_utilities-0.178.1.dist-info/METADATA +34 -0
  2. dycw_utilities-0.178.1.dist-info/RECORD +105 -0
  3. dycw_utilities-0.178.1.dist-info/WHEEL +4 -0
  4. dycw_utilities-0.178.1.dist-info/entry_points.txt +4 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +13 -10
  7. utilities/asyncio.py +312 -787
  8. utilities/atomicwrites.py +18 -6
  9. utilities/atools.py +64 -4
  10. utilities/cachetools.py +9 -6
  11. utilities/click.py +195 -77
  12. utilities/concurrent.py +1 -1
  13. utilities/contextlib.py +216 -17
  14. utilities/contextvars.py +20 -1
  15. utilities/cryptography.py +3 -3
  16. utilities/dataclasses.py +15 -28
  17. utilities/docker.py +387 -0
  18. utilities/enum.py +2 -2
  19. utilities/errors.py +17 -3
  20. utilities/fastapi.py +28 -59
  21. utilities/fpdf2.py +2 -2
  22. utilities/functions.py +24 -269
  23. utilities/git.py +9 -30
  24. utilities/grp.py +28 -0
  25. utilities/gzip.py +31 -0
  26. utilities/http.py +3 -2
  27. utilities/hypothesis.py +513 -159
  28. utilities/importlib.py +17 -1
  29. utilities/inflect.py +12 -4
  30. utilities/iterables.py +33 -58
  31. utilities/jinja2.py +148 -0
  32. utilities/json.py +70 -0
  33. utilities/libcst.py +38 -17
  34. utilities/lightweight_charts.py +4 -7
  35. utilities/logging.py +136 -93
  36. utilities/math.py +8 -4
  37. utilities/more_itertools.py +43 -45
  38. utilities/operator.py +27 -27
  39. utilities/orjson.py +189 -36
  40. utilities/os.py +61 -4
  41. utilities/packaging.py +115 -0
  42. utilities/parse.py +8 -5
  43. utilities/pathlib.py +269 -40
  44. utilities/permissions.py +298 -0
  45. utilities/platform.py +7 -6
  46. utilities/polars.py +1205 -413
  47. utilities/polars_ols.py +1 -1
  48. utilities/postgres.py +408 -0
  49. utilities/pottery.py +43 -19
  50. utilities/pqdm.py +3 -3
  51. utilities/psutil.py +5 -57
  52. utilities/pwd.py +28 -0
  53. utilities/pydantic.py +4 -52
  54. utilities/pydantic_settings.py +240 -0
  55. utilities/pydantic_settings_sops.py +76 -0
  56. utilities/pyinstrument.py +7 -7
  57. utilities/pytest.py +104 -143
  58. utilities/pytest_plugins/__init__.py +1 -0
  59. utilities/pytest_plugins/pytest_randomly.py +23 -0
  60. utilities/pytest_plugins/pytest_regressions.py +56 -0
  61. utilities/pytest_regressions.py +26 -46
  62. utilities/random.py +11 -6
  63. utilities/re.py +1 -1
  64. utilities/redis.py +220 -343
  65. utilities/sentinel.py +10 -0
  66. utilities/shelve.py +4 -1
  67. utilities/shutil.py +25 -0
  68. utilities/slack_sdk.py +35 -104
  69. utilities/sqlalchemy.py +496 -471
  70. utilities/sqlalchemy_polars.py +29 -54
  71. utilities/string.py +2 -3
  72. utilities/subprocess.py +1977 -0
  73. utilities/tempfile.py +112 -4
  74. utilities/testbook.py +50 -0
  75. utilities/text.py +174 -42
  76. utilities/throttle.py +158 -0
  77. utilities/timer.py +2 -2
  78. utilities/traceback.py +70 -35
  79. utilities/types.py +102 -30
  80. utilities/typing.py +479 -19
  81. utilities/uuid.py +42 -5
  82. utilities/version.py +27 -26
  83. utilities/whenever.py +1559 -361
  84. utilities/zoneinfo.py +80 -22
  85. dycw_utilities-0.135.0.dist-info/METADATA +0 -39
  86. dycw_utilities-0.135.0.dist-info/RECORD +0 -96
  87. dycw_utilities-0.135.0.dist-info/WHEEL +0 -4
  88. dycw_utilities-0.135.0.dist-info/licenses/LICENSE +0 -21
  89. utilities/aiolimiter.py +0 -25
  90. utilities/arq.py +0 -216
  91. utilities/eventkit.py +0 -388
  92. utilities/luigi.py +0 -183
  93. utilities/period.py +0 -152
  94. utilities/pudb.py +0 -62
  95. utilities/python_dotenv.py +0 -101
  96. utilities/streamlit.py +0 -105
  97. utilities/typed_settings.py +0 -123
utilities/functions.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import Callable, Iterable, Iterator, Sequence
4
- from dataclasses import asdict, dataclass, is_dataclass
3
+ from collections.abc import Callable, Iterable, Iterator
4
+ from dataclasses import asdict, dataclass
5
5
  from functools import _lru_cache_wrapper, cached_property, partial, reduce, wraps
6
6
  from inspect import getattr_static
7
7
  from pathlib import Path
@@ -19,18 +19,11 @@ from typing import TYPE_CHECKING, Any, Literal, TypeGuard, cast, overload, overr
19
19
  from whenever import Date, PlainDateTime, Time, TimeDelta, ZonedDateTime
20
20
 
21
21
  from utilities.reprlib import get_repr, get_repr_and_class
22
- from utilities.sentinel import Sentinel, sentinel
23
- from utilities.types import (
24
- Dataclass,
25
- Number,
26
- StrMapping,
27
- SupportsRichComparison,
28
- TupleOrStrMapping,
29
- TypeLike,
30
- )
22
+ from utilities.sentinel import Sentinel, is_sentinel, sentinel
23
+ from utilities.types import Dataclass, Number, SupportsRichComparison, TypeLike
31
24
 
32
25
  if TYPE_CHECKING:
33
- from collections.abc import Container, Hashable, Sized
26
+ from collections.abc import Container
34
27
 
35
28
 
36
29
  def apply_decorators[F1: Callable, F2: Callable](
@@ -230,25 +223,6 @@ class EnsureFloatError(Exception):
230
223
  ##
231
224
 
232
225
 
233
- def ensure_hashable(obj: Any, /) -> Hashable:
234
- """Ensure an object is hashable."""
235
- if is_hashable(obj):
236
- return obj
237
- raise EnsureHashableError(obj=obj)
238
-
239
-
240
- @dataclass(kw_only=True, slots=True)
241
- class EnsureHashableError(Exception):
242
- obj: Any
243
-
244
- @override
245
- def __str__(self) -> str:
246
- return _make_error_msg(self.obj, "hashable")
247
-
248
-
249
- ##
250
-
251
-
252
226
  @overload
253
227
  def ensure_int(obj: Any, /, *, nullable: bool) -> int | None: ...
254
228
  @overload
@@ -405,44 +379,6 @@ class EnsurePlainDateTimeError(Exception):
405
379
  ##
406
380
 
407
381
 
408
- def ensure_sized(obj: Any, /) -> Sized:
409
- """Ensure an object is sized."""
410
- if is_sized(obj):
411
- return obj
412
- raise EnsureSizedError(obj=obj)
413
-
414
-
415
- @dataclass(kw_only=True, slots=True)
416
- class EnsureSizedError(Exception):
417
- obj: Any
418
-
419
- @override
420
- def __str__(self) -> str:
421
- return _make_error_msg(self.obj, "sized")
422
-
423
-
424
- ##
425
-
426
-
427
- def ensure_sized_not_str(obj: Any, /) -> Sized:
428
- """Ensure an object is sized, but not a string."""
429
- if is_sized_not_str(obj):
430
- return obj
431
- raise EnsureSizedNotStrError(obj=obj)
432
-
433
-
434
- @dataclass(kw_only=True, slots=True)
435
- class EnsureSizedNotStrError(Exception):
436
- obj: Any
437
-
438
- @override
439
- def __str__(self) -> str:
440
- return _make_error_msg(self.obj, "sized and not a string")
441
-
442
-
443
- ##
444
-
445
-
446
382
  @overload
447
383
  def ensure_str(obj: Any, /, *, nullable: bool) -> str | None: ...
448
384
  @overload
@@ -633,67 +569,7 @@ def identity[T](obj: T, /) -> T:
633
569
  ##
634
570
 
635
571
 
636
- def is_dataclass_class(obj: Any, /) -> TypeGuard[type[Dataclass]]:
637
- """Check if an object is a dataclass."""
638
- return isinstance(obj, type) and is_dataclass(obj)
639
-
640
-
641
- ##
642
-
643
-
644
- def is_dataclass_instance(obj: Any, /) -> TypeGuard[Dataclass]:
645
- """Check if an object is an instance of a dataclass."""
646
- return (not isinstance(obj, type)) and is_dataclass(obj)
647
-
648
-
649
- ##
650
-
651
-
652
- def is_hashable(obj: Any, /) -> TypeGuard[Hashable]:
653
- """Check if an object is hashable."""
654
- try:
655
- _ = hash(obj)
656
- except TypeError:
657
- return False
658
- return True
659
-
660
-
661
- ##
662
-
663
-
664
- @overload
665
- def is_iterable_of[T](obj: Any, cls: type[T], /) -> TypeGuard[Iterable[T]]: ...
666
- @overload
667
- def is_iterable_of[T1](
668
- obj: Any, cls: tuple[type[T1]], /
669
- ) -> TypeGuard[Iterable[T1]]: ...
670
- @overload
671
- def is_iterable_of[T1, T2](
672
- obj: Any, cls: tuple[type[T1], type[T2]], /
673
- ) -> TypeGuard[Iterable[T1 | T2]]: ...
674
- @overload
675
- def is_iterable_of[T1, T2, T3](
676
- obj: Any, cls: tuple[type[T1], type[T2], type[T3]], /
677
- ) -> TypeGuard[Iterable[T1 | T2 | T3]]: ...
678
- @overload
679
- def is_iterable_of[T1, T2, T3, T4](
680
- obj: Any, cls: tuple[type[T1], type[T2], type[T3], type[T4]], /
681
- ) -> TypeGuard[Iterable[T1 | T2 | T3 | T4]]: ...
682
- @overload
683
- def is_iterable_of[T1, T2, T3, T4, T5](
684
- obj: Any, cls: tuple[type[T1], type[T2], type[T3], type[T4], type[T5]], /
685
- ) -> TypeGuard[Iterable[T1 | T2 | T3 | T4 | T5]]: ...
686
- @overload
687
- def is_iterable_of[T](obj: Any, cls: TypeLike[T], /) -> TypeGuard[Iterable[T]]: ...
688
- def is_iterable_of[T](obj: Any, cls: TypeLike[T], /) -> TypeGuard[Iterable[T]]:
689
- """Check if an object is a iterable of tuple or string mappings."""
690
- return isinstance(obj, Iterable) and all(map(make_isinstance(cls), obj))
691
-
692
-
693
- ##
694
-
695
-
696
- def is_none(obj: Any, /) -> bool:
572
+ def is_none(obj: Any, /) -> TypeGuard[None]:
697
573
  """Check if an object is `None`."""
698
574
  return obj is None
699
575
 
@@ -709,126 +585,6 @@ def is_not_none(obj: Any, /) -> bool:
709
585
  ##
710
586
 
711
587
 
712
- @overload
713
- def is_sequence_of[T](obj: Any, cls: type[T], /) -> TypeGuard[Sequence[T]]: ...
714
- @overload
715
- def is_sequence_of[T1](
716
- obj: Any, cls: tuple[type[T1]], /
717
- ) -> TypeGuard[Sequence[T1]]: ...
718
- @overload
719
- def is_sequence_of[T1, T2](
720
- obj: Any, cls: tuple[type[T1], type[T2]], /
721
- ) -> TypeGuard[Sequence[T1 | T2]]: ...
722
- @overload
723
- def is_sequence_of[T1, T2, T3](
724
- obj: Any, cls: tuple[type[T1], type[T2], type[T3]], /
725
- ) -> TypeGuard[Sequence[T1 | T2 | T3]]: ...
726
- @overload
727
- def is_sequence_of[T1, T2, T3, T4](
728
- obj: Any, cls: tuple[type[T1], type[T2], type[T3], type[T4]], /
729
- ) -> TypeGuard[Sequence[T1 | T2 | T3 | T4]]: ...
730
- @overload
731
- def is_sequence_of[T1, T2, T3, T4, T5](
732
- obj: Any, cls: tuple[type[T1], type[T2], type[T3], type[T4], type[T5]], /
733
- ) -> TypeGuard[Sequence[T1 | T2 | T3 | T4 | T5]]: ...
734
- @overload
735
- def is_sequence_of[T](obj: Any, cls: TypeLike[T], /) -> TypeGuard[Sequence[T]]: ...
736
- def is_sequence_of[T](obj: Any, cls: TypeLike[T], /) -> TypeGuard[Sequence[T]]:
737
- """Check if an object is a sequence of tuple or string mappings."""
738
- return isinstance(obj, Sequence) and is_iterable_of(obj, cls)
739
-
740
-
741
- ##
742
-
743
-
744
- def is_sequence_of_tuple_or_str_mapping(
745
- obj: Any, /
746
- ) -> TypeGuard[Sequence[TupleOrStrMapping]]:
747
- """Check if an object is a sequence of tuple or string mappings."""
748
- return isinstance(obj, Sequence) and all(map(is_tuple_or_str_mapping, obj))
749
-
750
-
751
- ##
752
-
753
-
754
- def is_sized(obj: Any, /) -> TypeGuard[Sized]:
755
- """Check if an object is sized."""
756
- try:
757
- _ = len(obj)
758
- except TypeError:
759
- return False
760
- return True
761
-
762
-
763
- ##
764
-
765
-
766
- def is_sized_not_str(obj: Any, /) -> TypeGuard[Sized]:
767
- """Check if an object is sized, but not a string."""
768
- return is_sized(obj) and not isinstance(obj, str)
769
-
770
-
771
- ##
772
-
773
-
774
- def is_string_mapping(obj: Any, /) -> TypeGuard[StrMapping]:
775
- """Check if an object is a string mapping."""
776
- return isinstance(obj, dict) and is_iterable_of(obj, str)
777
-
778
-
779
- ##
780
-
781
-
782
- def is_tuple(obj: Any, /) -> TypeGuard[tuple[Any, ...]]:
783
- """Check if an object is a tuple or string mapping."""
784
- return make_isinstance(tuple)(obj)
785
-
786
-
787
- ##
788
-
789
-
790
- def is_tuple_or_str_mapping(obj: Any, /) -> TypeGuard[TupleOrStrMapping]:
791
- """Check if an object is a tuple or string mapping."""
792
- return is_tuple(obj) or is_string_mapping(obj)
793
-
794
-
795
- ##
796
-
797
-
798
- @overload
799
- def make_isinstance[T](cls: type[T], /) -> Callable[[Any], TypeGuard[T]]: ...
800
- @overload
801
- def make_isinstance[T1](cls: tuple[type[T1]], /) -> Callable[[Any], TypeGuard[T1]]: ...
802
- @overload
803
- def make_isinstance[T1, T2](
804
- cls: tuple[type[T1], type[T2]], /
805
- ) -> Callable[[Any], TypeGuard[T1 | T2]]: ...
806
- @overload
807
- def make_isinstance[T1, T2, T3](
808
- cls: tuple[type[T1], type[T2], type[T3]], /
809
- ) -> Callable[[Any], TypeGuard[T1 | T2 | T3]]: ...
810
- @overload
811
- def make_isinstance[T1, T2, T3, T4](
812
- cls: tuple[type[T1], type[T2], type[T3], type[T4]], /
813
- ) -> Callable[[Any], TypeGuard[T1 | T2 | T3 | T4]]: ...
814
- @overload
815
- def make_isinstance[T1, T2, T3, T4, T5](
816
- cls: tuple[type[T1], type[T2], type[T3], type[T4], type[T5]], /
817
- ) -> Callable[[Any], TypeGuard[T1 | T2 | T3 | T4 | T5]]: ...
818
- @overload
819
- def make_isinstance[T](cls: TypeLike[T], /) -> Callable[[Any], TypeGuard[T]]: ...
820
- def make_isinstance[T](cls: TypeLike[T], /) -> Callable[[Any], TypeGuard[T]]:
821
- """Make a curried `isinstance` function."""
822
- return partial(_make_instance_core, cls=cls)
823
-
824
-
825
- def _make_instance_core[T](obj: Any, /, *, cls: TypeLike[T]) -> TypeGuard[T]:
826
- return isinstance(obj, cls)
827
-
828
-
829
- ##
830
-
831
-
832
588
  def map_object[T](
833
589
  func: Callable[[Any], Any], obj: T, /, *, before: Callable[[Any], Any] | None = None
834
590
  ) -> T:
@@ -864,7 +620,7 @@ def min_nullable[T: SupportsRichComparison, U](
864
620
  ) -> T | U:
865
621
  """Compute the minimum of a set of values; ignoring nulls."""
866
622
  values = (i for i in iterable if i is not None)
867
- if isinstance(default, Sentinel):
623
+ if is_sentinel(default):
868
624
  try:
869
625
  return min(values)
870
626
  except ValueError:
@@ -894,7 +650,7 @@ def max_nullable[T: SupportsRichComparison, U](
894
650
  ) -> T | U:
895
651
  """Compute the maximum of a set of values; ignoring nulls."""
896
652
  values = (i for i in iterable if i is not None)
897
- if isinstance(default, Sentinel):
653
+ if is_sentinel(default):
898
654
  try:
899
655
  return max(values)
900
656
  except ValueError:
@@ -935,6 +691,21 @@ def second[U](pair: tuple[Any, U], /) -> U:
935
691
  ##
936
692
 
937
693
 
694
+ def skip_if_optimize[**P](func: Callable[P, None], /) -> Callable[P, None]:
695
+ """Skip a function if we are in the optimized mode."""
696
+ if __debug__: # pragma: no cover
697
+ return func
698
+
699
+ @wraps(func)
700
+ def wrapped(*args: P.args, **kwargs: P.kwargs) -> None:
701
+ _ = (args, kwargs)
702
+
703
+ return wrapped
704
+
705
+
706
+ ##
707
+
708
+
938
709
  def yield_object_attributes(
939
710
  obj: Any,
940
711
  /,
@@ -985,15 +756,12 @@ __all__ = [
985
756
  "EnsureClassError",
986
757
  "EnsureDateError",
987
758
  "EnsureFloatError",
988
- "EnsureHashableError",
989
759
  "EnsureIntError",
990
760
  "EnsureMemberError",
991
761
  "EnsureNotNoneError",
992
762
  "EnsureNumberError",
993
763
  "EnsurePathError",
994
764
  "EnsurePlainDateTimeError",
995
- "EnsureSizedError",
996
- "EnsureSizedNotStrError",
997
765
  "EnsureStrError",
998
766
  "EnsureTimeDeltaError",
999
767
  "EnsureTimeError",
@@ -1006,15 +774,12 @@ __all__ = [
1006
774
  "ensure_class",
1007
775
  "ensure_date",
1008
776
  "ensure_float",
1009
- "ensure_hashable",
1010
777
  "ensure_int",
1011
778
  "ensure_member",
1012
779
  "ensure_not_none",
1013
780
  "ensure_number",
1014
781
  "ensure_path",
1015
782
  "ensure_plain_date_time",
1016
- "ensure_sized",
1017
- "ensure_sized_not_str",
1018
783
  "ensure_str",
1019
784
  "ensure_time",
1020
785
  "ensure_time_delta",
@@ -1025,24 +790,14 @@ __all__ = [
1025
790
  "get_func_name",
1026
791
  "get_func_qualname",
1027
792
  "identity",
1028
- "is_dataclass_class",
1029
- "is_dataclass_instance",
1030
- "is_hashable",
1031
- "is_iterable_of",
1032
793
  "is_none",
1033
794
  "is_not_none",
1034
- "is_sequence_of_tuple_or_str_mapping",
1035
- "is_sized",
1036
- "is_sized_not_str",
1037
- "is_string_mapping",
1038
- "is_tuple",
1039
- "is_tuple_or_str_mapping",
1040
- "make_isinstance",
1041
795
  "map_object",
1042
796
  "max_nullable",
1043
797
  "min_nullable",
1044
798
  "not_func",
1045
799
  "second",
800
+ "skip_if_optimize",
1046
801
  "yield_object_attributes",
1047
802
  "yield_object_cached_properties",
1048
803
  "yield_object_properties",
utilities/git.py CHANGED
@@ -1,40 +1,19 @@
1
1
  from __future__ import annotations
2
2
 
3
- from dataclasses import dataclass
4
3
  from pathlib import Path
5
- from re import IGNORECASE, search
6
- from subprocess import PIPE, CalledProcessError, check_output
7
- from typing import TYPE_CHECKING, override
4
+ from typing import TYPE_CHECKING
8
5
 
9
- from utilities.pathlib import PWD
10
-
11
- if TYPE_CHECKING:
12
- from utilities.types import PathLike
6
+ from git import Repo
13
7
 
8
+ from utilities.pathlib import to_path
14
9
 
15
- def get_repo_root(*, path: PathLike = PWD) -> Path:
16
- """Get the repo root."""
17
- try:
18
- output = check_output(
19
- ["git", "rev-parse", "--show-toplevel"], stderr=PIPE, cwd=path, text=True
20
- )
21
- except CalledProcessError as error:
22
- # newer versions of git report "Not a git repository", whilst older
23
- # versions report "not a git repository"
24
- if search("fatal: not a git repository", error.stderr, flags=IGNORECASE):
25
- raise GetRepoRootError(cwd=path) from error
26
- raise # pragma: no cover
27
- else:
28
- return Path(output.strip("\n"))
29
-
10
+ if TYPE_CHECKING:
11
+ from utilities.types import MaybeCallablePathLike
30
12
 
31
- @dataclass(kw_only=True, slots=True)
32
- class GetRepoRootError(Exception):
33
- cwd: PathLike
34
13
 
35
- @override
36
- def __str__(self) -> str:
37
- return f"Path is not part of a `git` repository: {self.cwd}"
14
+ def get_repo(path: MaybeCallablePathLike = Path.cwd, /) -> Repo:
15
+ """Get the repo object."""
16
+ return Repo(to_path(path), search_parent_directories=True)
38
17
 
39
18
 
40
- __all__ = ["GetRepoRootError", "get_repo_root"]
19
+ __all__ = ["get_repo"]
utilities/grp.py ADDED
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import assert_never
4
+
5
+ from utilities.os import EFFECTIVE_GROUP_ID
6
+ from utilities.platform import SYSTEM
7
+
8
+
9
+ def get_gid_name(gid: int, /) -> str | None:
10
+ """Get the name of a group."""
11
+ match SYSTEM:
12
+ case "windows": # skipif-not-windows
13
+ return None
14
+ case "mac" | "linux":
15
+ from grp import getgrgid
16
+
17
+ return getgrgid(gid).gr_name
18
+ case never:
19
+ assert_never(never)
20
+
21
+
22
+ ROOT_GROUP_NAME = get_gid_name(0)
23
+ EFFECTIVE_GROUP_NAME = (
24
+ None if EFFECTIVE_GROUP_ID is None else get_gid_name(EFFECTIVE_GROUP_ID)
25
+ )
26
+
27
+
28
+ __all__ = ["EFFECTIVE_GROUP_NAME", "ROOT_GROUP_NAME", "get_gid_name"]
utilities/gzip.py ADDED
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ import gzip
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING
6
+
7
+ from utilities.atomicwrites import writer
8
+
9
+ if TYPE_CHECKING:
10
+ from utilities.types import PathLike
11
+
12
+
13
+ def read_binary(path: PathLike, /, *, decompress: bool = False) -> bytes:
14
+ """Read a byte string from disk."""
15
+ path = Path(path)
16
+ if decompress:
17
+ with gzip.open(path) as gz:
18
+ return gz.read()
19
+ else:
20
+ return path.read_bytes()
21
+
22
+
23
+ def write_binary(
24
+ data: bytes, path: PathLike, /, *, compress: bool = False, overwrite: bool = False
25
+ ) -> None:
26
+ """Write a byte string to disk."""
27
+ with writer(path, compress=compress, overwrite=overwrite) as temp:
28
+ _ = temp.write_bytes(data)
29
+
30
+
31
+ __all__ = ["read_binary", "write_binary"]
utilities/http.py CHANGED
@@ -1,10 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
- from contextlib import contextmanager
4
3
  from http.client import HTTPSConnection
5
4
  from ipaddress import IPv4Address
6
5
  from typing import TYPE_CHECKING
7
6
 
7
+ from utilities.contextlib import enhanced_context_manager
8
+
8
9
  if TYPE_CHECKING:
9
10
  from collections.abc import Iterator
10
11
 
@@ -21,7 +22,7 @@ def get_public_ip(*, timeout: float | None = None) -> IPv4Address:
21
22
  ##
22
23
 
23
24
 
24
- @contextmanager
25
+ @enhanced_context_manager
25
26
  def yield_connection(
26
27
  host: str, /, *, timeout: float | None = None
27
28
  ) -> Iterator[HTTPSConnection]: