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.
- dycw_utilities-0.178.1.dist-info/METADATA +34 -0
- dycw_utilities-0.178.1.dist-info/RECORD +105 -0
- dycw_utilities-0.178.1.dist-info/WHEEL +4 -0
- dycw_utilities-0.178.1.dist-info/entry_points.txt +4 -0
- utilities/__init__.py +1 -1
- utilities/altair.py +13 -10
- utilities/asyncio.py +312 -787
- utilities/atomicwrites.py +18 -6
- utilities/atools.py +64 -4
- utilities/cachetools.py +9 -6
- utilities/click.py +195 -77
- utilities/concurrent.py +1 -1
- utilities/contextlib.py +216 -17
- utilities/contextvars.py +20 -1
- utilities/cryptography.py +3 -3
- utilities/dataclasses.py +15 -28
- utilities/docker.py +387 -0
- utilities/enum.py +2 -2
- utilities/errors.py +17 -3
- utilities/fastapi.py +28 -59
- utilities/fpdf2.py +2 -2
- utilities/functions.py +24 -269
- utilities/git.py +9 -30
- utilities/grp.py +28 -0
- utilities/gzip.py +31 -0
- utilities/http.py +3 -2
- utilities/hypothesis.py +513 -159
- utilities/importlib.py +17 -1
- utilities/inflect.py +12 -4
- utilities/iterables.py +33 -58
- utilities/jinja2.py +148 -0
- utilities/json.py +70 -0
- utilities/libcst.py +38 -17
- utilities/lightweight_charts.py +4 -7
- utilities/logging.py +136 -93
- utilities/math.py +8 -4
- utilities/more_itertools.py +43 -45
- utilities/operator.py +27 -27
- utilities/orjson.py +189 -36
- utilities/os.py +61 -4
- utilities/packaging.py +115 -0
- utilities/parse.py +8 -5
- utilities/pathlib.py +269 -40
- utilities/permissions.py +298 -0
- utilities/platform.py +7 -6
- utilities/polars.py +1205 -413
- utilities/polars_ols.py +1 -1
- utilities/postgres.py +408 -0
- utilities/pottery.py +43 -19
- utilities/pqdm.py +3 -3
- utilities/psutil.py +5 -57
- utilities/pwd.py +28 -0
- utilities/pydantic.py +4 -52
- utilities/pydantic_settings.py +240 -0
- utilities/pydantic_settings_sops.py +76 -0
- utilities/pyinstrument.py +7 -7
- utilities/pytest.py +104 -143
- utilities/pytest_plugins/__init__.py +1 -0
- utilities/pytest_plugins/pytest_randomly.py +23 -0
- utilities/pytest_plugins/pytest_regressions.py +56 -0
- utilities/pytest_regressions.py +26 -46
- utilities/random.py +11 -6
- utilities/re.py +1 -1
- utilities/redis.py +220 -343
- utilities/sentinel.py +10 -0
- utilities/shelve.py +4 -1
- utilities/shutil.py +25 -0
- utilities/slack_sdk.py +35 -104
- utilities/sqlalchemy.py +496 -471
- utilities/sqlalchemy_polars.py +29 -54
- utilities/string.py +2 -3
- utilities/subprocess.py +1977 -0
- utilities/tempfile.py +112 -4
- utilities/testbook.py +50 -0
- utilities/text.py +174 -42
- utilities/throttle.py +158 -0
- utilities/timer.py +2 -2
- utilities/traceback.py +70 -35
- utilities/types.py +102 -30
- utilities/typing.py +479 -19
- utilities/uuid.py +42 -5
- utilities/version.py +27 -26
- utilities/whenever.py +1559 -361
- utilities/zoneinfo.py +80 -22
- dycw_utilities-0.135.0.dist-info/METADATA +0 -39
- dycw_utilities-0.135.0.dist-info/RECORD +0 -96
- dycw_utilities-0.135.0.dist-info/WHEEL +0 -4
- dycw_utilities-0.135.0.dist-info/licenses/LICENSE +0 -21
- utilities/aiolimiter.py +0 -25
- utilities/arq.py +0 -216
- utilities/eventkit.py +0 -388
- utilities/luigi.py +0 -183
- utilities/period.py +0 -152
- utilities/pudb.py +0 -62
- utilities/python_dotenv.py +0 -101
- utilities/streamlit.py +0 -105
- 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
|
|
4
|
-
from dataclasses import asdict, 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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__ = ["
|
|
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
|
-
@
|
|
25
|
+
@enhanced_context_manager
|
|
25
26
|
def yield_connection(
|
|
26
27
|
host: str, /, *, timeout: float | None = None
|
|
27
28
|
) -> Iterator[HTTPSConnection]:
|