dycw-utilities 0.148.5__py3-none-any.whl → 0.174.12__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.174.12.dist-info/METADATA +41 -0
- dycw_utilities-0.174.12.dist-info/RECORD +104 -0
- dycw_utilities-0.174.12.dist-info/WHEEL +4 -0
- {dycw_utilities-0.148.5.dist-info → dycw_utilities-0.174.12.dist-info}/entry_points.txt +3 -0
- utilities/__init__.py +1 -1
- utilities/{eventkit.py → aeventkit.py} +12 -11
- utilities/altair.py +7 -6
- utilities/asyncio.py +113 -64
- utilities/atomicwrites.py +1 -1
- utilities/atools.py +64 -4
- utilities/cachetools.py +9 -6
- utilities/click.py +145 -49
- utilities/concurrent.py +1 -1
- utilities/contextlib.py +4 -2
- utilities/contextvars.py +20 -1
- utilities/cryptography.py +3 -3
- utilities/dataclasses.py +15 -28
- utilities/docker.py +292 -0
- utilities/enum.py +2 -2
- utilities/errors.py +1 -1
- utilities/fastapi.py +8 -3
- utilities/fpdf2.py +2 -2
- utilities/functions.py +20 -297
- utilities/git.py +19 -0
- utilities/grp.py +28 -0
- utilities/hypothesis.py +360 -78
- utilities/inflect.py +1 -1
- utilities/iterables.py +12 -58
- utilities/jinja2.py +148 -0
- utilities/json.py +1 -1
- utilities/libcst.py +7 -7
- utilities/logging.py +74 -85
- utilities/math.py +8 -4
- utilities/more_itertools.py +4 -6
- utilities/operator.py +1 -1
- utilities/orjson.py +86 -34
- utilities/os.py +49 -2
- utilities/parse.py +2 -2
- utilities/pathlib.py +66 -34
- utilities/permissions.py +297 -0
- utilities/platform.py +5 -5
- utilities/polars.py +932 -420
- utilities/polars_ols.py +1 -1
- utilities/postgres.py +296 -174
- utilities/pottery.py +8 -73
- utilities/pqdm.py +3 -3
- utilities/pwd.py +28 -0
- utilities/pydantic.py +11 -0
- utilities/pydantic_settings.py +240 -0
- utilities/pydantic_settings_sops.py +76 -0
- utilities/pyinstrument.py +5 -5
- utilities/pytest.py +155 -46
- utilities/pytest_plugins/pytest_randomly.py +1 -1
- utilities/pytest_plugins/pytest_regressions.py +7 -3
- utilities/pytest_regressions.py +2 -3
- utilities/random.py +11 -6
- utilities/re.py +1 -1
- utilities/redis.py +101 -64
- utilities/sentinel.py +10 -0
- utilities/shelve.py +4 -1
- utilities/shutil.py +25 -0
- utilities/slack_sdk.py +8 -3
- utilities/sqlalchemy.py +422 -352
- utilities/sqlalchemy_polars.py +28 -52
- utilities/string.py +1 -1
- utilities/subprocess.py +864 -0
- utilities/tempfile.py +62 -4
- utilities/testbook.py +50 -0
- utilities/text.py +165 -42
- utilities/timer.py +2 -2
- utilities/traceback.py +46 -36
- utilities/types.py +62 -23
- utilities/typing.py +479 -19
- utilities/uuid.py +42 -5
- utilities/version.py +27 -26
- utilities/whenever.py +661 -151
- utilities/zoneinfo.py +80 -22
- dycw_utilities-0.148.5.dist-info/METADATA +0 -41
- dycw_utilities-0.148.5.dist-info/RECORD +0 -95
- dycw_utilities-0.148.5.dist-info/WHEEL +0 -4
- dycw_utilities-0.148.5.dist-info/licenses/LICENSE +0 -21
- utilities/period.py +0 -237
- utilities/typed_settings.py +0 -144
utilities/orjson.py
CHANGED
|
@@ -5,7 +5,7 @@ import re
|
|
|
5
5
|
from collections.abc import Callable, Iterable, Mapping, Sequence
|
|
6
6
|
from contextlib import suppress
|
|
7
7
|
from dataclasses import dataclass, field, replace
|
|
8
|
-
from enum import Enum, unique
|
|
8
|
+
from enum import Enum, StrEnum, unique
|
|
9
9
|
from functools import cached_property, partial
|
|
10
10
|
from itertools import chain
|
|
11
11
|
from logging import Formatter, LogRecord
|
|
@@ -20,6 +20,7 @@ from orjson import (
|
|
|
20
20
|
OPT_PASSTHROUGH_DATACLASS,
|
|
21
21
|
OPT_PASSTHROUGH_DATETIME,
|
|
22
22
|
OPT_SORT_KEYS,
|
|
23
|
+
JSONDecodeError,
|
|
23
24
|
dumps,
|
|
24
25
|
loads,
|
|
25
26
|
)
|
|
@@ -37,7 +38,7 @@ from whenever import (
|
|
|
37
38
|
|
|
38
39
|
from utilities.concurrent import concurrent_map
|
|
39
40
|
from utilities.dataclasses import dataclass_to_dict
|
|
40
|
-
from utilities.functions import ensure_class
|
|
41
|
+
from utilities.functions import ensure_class
|
|
41
42
|
from utilities.gzip import read_binary
|
|
42
43
|
from utilities.iterables import (
|
|
43
44
|
OneEmptyError,
|
|
@@ -50,9 +51,15 @@ from utilities.json import write_formatted_json
|
|
|
50
51
|
from utilities.logging import get_logging_level_number
|
|
51
52
|
from utilities.math import MAX_INT64, MIN_INT64
|
|
52
53
|
from utilities.types import Dataclass, LogLevel, MaybeIterable, PathLike, StrMapping
|
|
54
|
+
from utilities.typing import is_string_mapping
|
|
53
55
|
from utilities.tzlocal import LOCAL_TIME_ZONE
|
|
54
56
|
from utilities.version import Version, parse_version
|
|
55
|
-
from utilities.whenever import
|
|
57
|
+
from utilities.whenever import (
|
|
58
|
+
DatePeriod,
|
|
59
|
+
TimePeriod,
|
|
60
|
+
ZonedDateTimePeriod,
|
|
61
|
+
from_timestamp,
|
|
62
|
+
)
|
|
56
63
|
|
|
57
64
|
if TYPE_CHECKING:
|
|
58
65
|
from collections.abc import Set as AbstractSet
|
|
@@ -65,10 +72,11 @@ if TYPE_CHECKING:
|
|
|
65
72
|
|
|
66
73
|
|
|
67
74
|
@unique
|
|
68
|
-
class _Prefixes(
|
|
75
|
+
class _Prefixes(StrEnum):
|
|
69
76
|
dataclass = "dc"
|
|
70
77
|
date = "d"
|
|
71
78
|
date_delta = "dd"
|
|
79
|
+
date_period = "dp"
|
|
72
80
|
date_time_delta = "D"
|
|
73
81
|
enum = "e"
|
|
74
82
|
exception_class = "Ex"
|
|
@@ -77,7 +85,7 @@ class _Prefixes(Enum):
|
|
|
77
85
|
frozenset_ = "fr"
|
|
78
86
|
list_ = "l"
|
|
79
87
|
month_day = "md"
|
|
80
|
-
none = "
|
|
88
|
+
none = "0"
|
|
81
89
|
path = "p"
|
|
82
90
|
plain_date_time = "pd"
|
|
83
91
|
py_date = "!d"
|
|
@@ -87,12 +95,14 @@ class _Prefixes(Enum):
|
|
|
87
95
|
set_ = "s"
|
|
88
96
|
time = "ti"
|
|
89
97
|
time_delta = "td"
|
|
98
|
+
time_period = "tp"
|
|
90
99
|
tuple_ = "tu"
|
|
91
100
|
unserializable = "un"
|
|
92
101
|
uuid = "uu"
|
|
93
102
|
version = "v"
|
|
94
103
|
year_month = "ym"
|
|
95
104
|
zoned_date_time = "zd"
|
|
105
|
+
zoned_date_time_period = "zp"
|
|
96
106
|
|
|
97
107
|
|
|
98
108
|
type _DataclassHook = Callable[[type[Dataclass], StrMapping], StrMapping]
|
|
@@ -167,8 +177,10 @@ def _pre_process(
|
|
|
167
177
|
return f"[{_Prefixes.date.value}]{date}"
|
|
168
178
|
case DateDelta() as date:
|
|
169
179
|
return f"[{_Prefixes.date_delta.value}]{date}"
|
|
170
|
-
case
|
|
171
|
-
return f"[{_Prefixes.
|
|
180
|
+
case DatePeriod() as period:
|
|
181
|
+
return f"[{_Prefixes.date_period.value}]{period.start},{period.end}"
|
|
182
|
+
case DateTimeDelta() as date_time_delta:
|
|
183
|
+
return f"[{_Prefixes.date_time_delta.value}]{date_time_delta}"
|
|
172
184
|
case Exception() as error_:
|
|
173
185
|
return {
|
|
174
186
|
f"[{_Prefixes.exception_instance.value}|{type(error_).__qualname__}]": pre(
|
|
@@ -187,14 +199,16 @@ def _pre_process(
|
|
|
187
199
|
return f"[{_Prefixes.month_day.value}]{month_day!s}"
|
|
188
200
|
case Path() as path:
|
|
189
201
|
return f"[{_Prefixes.path.value}]{path!s}"
|
|
190
|
-
case PlainDateTime() as
|
|
191
|
-
return f"[{_Prefixes.plain_date_time.value}]{
|
|
192
|
-
case str() as
|
|
193
|
-
return
|
|
202
|
+
case PlainDateTime() as date_time:
|
|
203
|
+
return f"[{_Prefixes.plain_date_time.value}]{date_time}"
|
|
204
|
+
case str() as text:
|
|
205
|
+
return text
|
|
194
206
|
case Time() as time:
|
|
195
207
|
return f"[{_Prefixes.time.value}]{time}"
|
|
196
208
|
case TimeDelta() as time_delta:
|
|
197
209
|
return f"[{_Prefixes.time_delta.value}]{time_delta}"
|
|
210
|
+
case TimePeriod() as period:
|
|
211
|
+
return f"[{_Prefixes.time_period.value}]{period.start},{period.end}"
|
|
198
212
|
case type() as error_cls if issubclass(error_cls, Exception):
|
|
199
213
|
return f"[{_Prefixes.exception_class.value}|{error_cls.__qualname__}]"
|
|
200
214
|
case UUID() as uuid:
|
|
@@ -203,8 +217,10 @@ def _pre_process(
|
|
|
203
217
|
return f"[{_Prefixes.version.value}]{version}"
|
|
204
218
|
case YearMonth() as year_month:
|
|
205
219
|
return f"[{_Prefixes.year_month.value}]{year_month}"
|
|
206
|
-
case ZonedDateTime() as
|
|
207
|
-
return f"[{_Prefixes.zoned_date_time.value}]{
|
|
220
|
+
case ZonedDateTime() as date_time:
|
|
221
|
+
return f"[{_Prefixes.zoned_date_time.value}]{date_time}"
|
|
222
|
+
case ZonedDateTimePeriod() as period:
|
|
223
|
+
return f"[{_Prefixes.zoned_date_time_period.value}]{period.start.to_plain()},{period.end}"
|
|
208
224
|
case dt.datetime() as py_datetime:
|
|
209
225
|
match py_datetime.tzinfo:
|
|
210
226
|
case None:
|
|
@@ -357,8 +373,12 @@ def deserialize(
|
|
|
357
373
|
redirects: Mapping[str, type[Any]] | None = None,
|
|
358
374
|
) -> Any:
|
|
359
375
|
"""Deserialize an object."""
|
|
376
|
+
try:
|
|
377
|
+
obj = loads(data)
|
|
378
|
+
except JSONDecodeError:
|
|
379
|
+
raise _DeserializeInvalidJSONError(data=data) from None
|
|
360
380
|
return _object_hook(
|
|
361
|
-
|
|
381
|
+
obj,
|
|
362
382
|
data=data,
|
|
363
383
|
dataclass_hook=dataclass_hook,
|
|
364
384
|
objects=objects,
|
|
@@ -366,9 +386,15 @@ def deserialize(
|
|
|
366
386
|
)
|
|
367
387
|
|
|
368
388
|
|
|
389
|
+
@dataclass(kw_only=True, slots=True)
|
|
390
|
+
class DeerializeError(Exception):
|
|
391
|
+
obj: Any
|
|
392
|
+
|
|
393
|
+
|
|
369
394
|
(
|
|
370
395
|
_DATE_PATTERN,
|
|
371
396
|
_DATE_DELTA_PATTERN,
|
|
397
|
+
_DATE_PERIOD_PATTERN,
|
|
372
398
|
_DATE_TIME_DELTA_PATTERN,
|
|
373
399
|
_FLOAT_PATTERN,
|
|
374
400
|
_MONTH_DAY_PATTERN,
|
|
@@ -381,15 +407,18 @@ def deserialize(
|
|
|
381
407
|
_PY_ZONED_DATE_TIME_PATTERN,
|
|
382
408
|
_TIME_PATTERN,
|
|
383
409
|
_TIME_DELTA_PATTERN,
|
|
410
|
+
_TIME_PERIOD_PATTERN,
|
|
384
411
|
_UUID_PATTERN,
|
|
385
412
|
_VERSION_PATTERN,
|
|
386
413
|
_YEAR_MONTH_PATTERN,
|
|
387
414
|
_ZONED_DATE_TIME_PATTERN,
|
|
415
|
+
_ZONED_DATE_TIME_PERIOD_PATTERN,
|
|
388
416
|
) = [
|
|
389
417
|
re.compile(r"^\[" + p.value + r"\](" + ".*" + ")$")
|
|
390
418
|
for p in [
|
|
391
419
|
_Prefixes.date,
|
|
392
420
|
_Prefixes.date_delta,
|
|
421
|
+
_Prefixes.date_period,
|
|
393
422
|
_Prefixes.date_time_delta,
|
|
394
423
|
_Prefixes.float_,
|
|
395
424
|
_Prefixes.month_day,
|
|
@@ -402,10 +431,12 @@ def deserialize(
|
|
|
402
431
|
_Prefixes.py_zoned_date_time,
|
|
403
432
|
_Prefixes.time,
|
|
404
433
|
_Prefixes.time_delta,
|
|
434
|
+
_Prefixes.time_period,
|
|
405
435
|
_Prefixes.uuid,
|
|
406
436
|
_Prefixes.version,
|
|
407
437
|
_Prefixes.year_month,
|
|
408
438
|
_Prefixes.zoned_date_time,
|
|
439
|
+
_Prefixes.zoned_date_time_period,
|
|
409
440
|
]
|
|
410
441
|
]
|
|
411
442
|
|
|
@@ -450,39 +481,50 @@ def _object_hook(
|
|
|
450
481
|
if match := _NONE_PATTERN.search(text):
|
|
451
482
|
return None
|
|
452
483
|
if match := _DATE_PATTERN.search(text):
|
|
453
|
-
return Date.
|
|
484
|
+
return Date.parse_iso(match.group(1))
|
|
454
485
|
if match := _DATE_DELTA_PATTERN.search(text):
|
|
455
|
-
return DateDelta.
|
|
486
|
+
return DateDelta.parse_iso(match.group(1))
|
|
487
|
+
if match := _DATE_PERIOD_PATTERN.search(text):
|
|
488
|
+
start, end = map(Date.parse_iso, match.group(1).split(","))
|
|
489
|
+
return DatePeriod(start, end)
|
|
456
490
|
if match := _DATE_TIME_DELTA_PATTERN.search(text):
|
|
457
|
-
return DateTimeDelta.
|
|
491
|
+
return DateTimeDelta.parse_iso(match.group(1))
|
|
458
492
|
if match := _FLOAT_PATTERN.search(text):
|
|
459
493
|
return float(match.group(1))
|
|
460
494
|
if match := _MONTH_DAY_PATTERN.search(text):
|
|
461
|
-
return MonthDay.
|
|
495
|
+
return MonthDay.parse_iso(match.group(1))
|
|
462
496
|
if match := _PATH_PATTERN.search(text):
|
|
463
497
|
return Path(match.group(1))
|
|
464
498
|
if match := _PLAIN_DATE_TIME_PATTERN.search(text):
|
|
465
|
-
return PlainDateTime.
|
|
499
|
+
return PlainDateTime.parse_iso(match.group(1))
|
|
466
500
|
if match := _PY_DATE_PATTERN.search(text):
|
|
467
|
-
return Date.
|
|
501
|
+
return Date.parse_iso(match.group(1)).py_date()
|
|
468
502
|
if match := _PY_PLAIN_DATE_TIME_PATTERN.search(text):
|
|
469
|
-
return PlainDateTime.
|
|
503
|
+
return PlainDateTime.parse_iso(match.group(1)).py_datetime()
|
|
470
504
|
if match := _PY_TIME_PATTERN.search(text):
|
|
471
|
-
return Time.
|
|
505
|
+
return Time.parse_iso(match.group(1)).py_time()
|
|
472
506
|
if match := _PY_ZONED_DATE_TIME_PATTERN.search(text):
|
|
473
|
-
return ZonedDateTime.
|
|
507
|
+
return ZonedDateTime.parse_iso(match.group(1)).py_datetime()
|
|
474
508
|
if match := _TIME_PATTERN.search(text):
|
|
475
|
-
return Time.
|
|
509
|
+
return Time.parse_iso(match.group(1))
|
|
476
510
|
if match := _TIME_DELTA_PATTERN.search(text):
|
|
477
|
-
return TimeDelta.
|
|
511
|
+
return TimeDelta.parse_iso(match.group(1))
|
|
512
|
+
if match := _TIME_PERIOD_PATTERN.search(text):
|
|
513
|
+
start, end = map(Time.parse_iso, match.group(1).split(","))
|
|
514
|
+
return TimePeriod(start, end)
|
|
478
515
|
if match := _UUID_PATTERN.search(text):
|
|
479
516
|
return UUID(match.group(1))
|
|
480
517
|
if match := _VERSION_PATTERN.search(text):
|
|
481
518
|
return parse_version(match.group(1))
|
|
482
519
|
if match := _YEAR_MONTH_PATTERN.search(text):
|
|
483
|
-
return YearMonth.
|
|
520
|
+
return YearMonth.parse_iso(match.group(1))
|
|
484
521
|
if match := _ZONED_DATE_TIME_PATTERN.search(text):
|
|
485
|
-
return ZonedDateTime.
|
|
522
|
+
return ZonedDateTime.parse_iso(match.group(1))
|
|
523
|
+
if match := _ZONED_DATE_TIME_PERIOD_PATTERN.search(text):
|
|
524
|
+
start, end = match.group(1).split(",")
|
|
525
|
+
end = ZonedDateTime.parse_iso(end)
|
|
526
|
+
start = PlainDateTime.parse_iso(start).assume_tz(end.tz)
|
|
527
|
+
return ZonedDateTimePeriod(start, end)
|
|
486
528
|
if (
|
|
487
529
|
exc_class := _object_hook_exception_class(
|
|
488
530
|
text, data=data, objects=objects, redirects=redirects
|
|
@@ -564,7 +606,7 @@ def _object_hook(
|
|
|
564
606
|
)
|
|
565
607
|
for k, v in mapping.items()
|
|
566
608
|
}
|
|
567
|
-
case
|
|
609
|
+
case never:
|
|
568
610
|
assert_never(never)
|
|
569
611
|
|
|
570
612
|
|
|
@@ -708,11 +750,19 @@ def _object_hook_get_object(
|
|
|
708
750
|
@dataclass(kw_only=True, slots=True)
|
|
709
751
|
class DeserializeError(Exception):
|
|
710
752
|
data: bytes
|
|
711
|
-
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
@dataclass(kw_only=True, slots=True)
|
|
756
|
+
class _DeserializeInvalidJSONError(DeserializeError):
|
|
757
|
+
@override
|
|
758
|
+
def __str__(self) -> str:
|
|
759
|
+
return f"Invalid JSON: {self.data!r}"
|
|
712
760
|
|
|
713
761
|
|
|
714
762
|
@dataclass(kw_only=True, slots=True)
|
|
715
763
|
class _DeserializeNoObjectsError(DeserializeError):
|
|
764
|
+
qualname: str
|
|
765
|
+
|
|
716
766
|
@override
|
|
717
767
|
def __str__(self) -> str:
|
|
718
768
|
return f"Objects required to deserialize {self.qualname!r} from {self.data!r}"
|
|
@@ -720,6 +770,8 @@ class _DeserializeNoObjectsError(DeserializeError):
|
|
|
720
770
|
|
|
721
771
|
@dataclass(kw_only=True, slots=True)
|
|
722
772
|
class _DeserializeObjectNotFoundError(DeserializeError):
|
|
773
|
+
qualname: str
|
|
774
|
+
|
|
723
775
|
@override
|
|
724
776
|
def __str__(self) -> str:
|
|
725
777
|
return (
|
|
@@ -1006,7 +1058,7 @@ class GetLogRecordsOutput:
|
|
|
1006
1058
|
for r in records
|
|
1007
1059
|
if (r.func_name is not None) and search(func_name, r.func_name)
|
|
1008
1060
|
]
|
|
1009
|
-
case
|
|
1061
|
+
case never:
|
|
1010
1062
|
assert_never(never)
|
|
1011
1063
|
if extra is not None:
|
|
1012
1064
|
match extra:
|
|
@@ -1019,7 +1071,7 @@ class GetLogRecordsOutput:
|
|
|
1019
1071
|
if (r.extra is not None)
|
|
1020
1072
|
and set(r.extra).issuperset(always_iterable(keys))
|
|
1021
1073
|
]
|
|
1022
|
-
case
|
|
1074
|
+
case never:
|
|
1023
1075
|
assert_never(never)
|
|
1024
1076
|
if log_file is not None:
|
|
1025
1077
|
match log_file:
|
|
@@ -1034,7 +1086,7 @@ class GetLogRecordsOutput:
|
|
|
1034
1086
|
if (r.log_file is not None)
|
|
1035
1087
|
and search(str(log_file), str(r.log_file))
|
|
1036
1088
|
]
|
|
1037
|
-
case
|
|
1089
|
+
case never:
|
|
1038
1090
|
assert_never(never)
|
|
1039
1091
|
if log_file_line_num is not None:
|
|
1040
1092
|
match log_file_line_num:
|
|
@@ -1048,7 +1100,7 @@ class GetLogRecordsOutput:
|
|
|
1048
1100
|
records = [
|
|
1049
1101
|
r for r in records if r.log_file_line_num == log_file_line_num
|
|
1050
1102
|
]
|
|
1051
|
-
case
|
|
1103
|
+
case never:
|
|
1052
1104
|
assert_never(never)
|
|
1053
1105
|
if min_log_file_line_num is not None:
|
|
1054
1106
|
records = [
|
|
@@ -1126,7 +1178,7 @@ def _get_log_records_one(
|
|
|
1126
1178
|
path = Path(path)
|
|
1127
1179
|
try:
|
|
1128
1180
|
lines = path.read_text().splitlines()
|
|
1129
|
-
except UnicodeDecodeError as error:
|
|
1181
|
+
except UnicodeDecodeError as error:
|
|
1130
1182
|
return _GetLogRecordsOneOutput(path=path, file_ok=False, other_errors=[error])
|
|
1131
1183
|
num_lines_blank, num_lines_error = 0, 0
|
|
1132
1184
|
missing: set[str] = set()
|
utilities/os.py
CHANGED
|
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Literal, assert_never, overload, override
|
|
|
7
7
|
|
|
8
8
|
from utilities.contextlib import enhanced_context_manager
|
|
9
9
|
from utilities.iterables import OneStrEmptyError, one_str
|
|
10
|
+
from utilities.platform import SYSTEM
|
|
10
11
|
|
|
11
12
|
if TYPE_CHECKING:
|
|
12
13
|
from collections.abc import Iterator, Mapping
|
|
@@ -48,7 +49,7 @@ def get_cpu_use(*, n: IntOrAll = "all") -> int:
|
|
|
48
49
|
raise GetCPUUseError(n=n)
|
|
49
50
|
case "all":
|
|
50
51
|
return CPU_COUNT
|
|
51
|
-
case
|
|
52
|
+
case never:
|
|
52
53
|
assert_never(never)
|
|
53
54
|
|
|
54
55
|
|
|
@@ -105,7 +106,7 @@ def get_env_var(
|
|
|
105
106
|
return None
|
|
106
107
|
case str(), _:
|
|
107
108
|
return default
|
|
108
|
-
case
|
|
109
|
+
case never:
|
|
109
110
|
assert_never(never)
|
|
110
111
|
return environ[key_use]
|
|
111
112
|
|
|
@@ -124,6 +125,39 @@ class GetEnvVarError(Exception):
|
|
|
124
125
|
##
|
|
125
126
|
|
|
126
127
|
|
|
128
|
+
def get_effective_group_id() -> int | None:
|
|
129
|
+
"""Get the effective group ID."""
|
|
130
|
+
match SYSTEM:
|
|
131
|
+
case "windows": # skipif-not-windows
|
|
132
|
+
return None
|
|
133
|
+
case "mac" | "linux": # skipif-windows
|
|
134
|
+
from os import getegid
|
|
135
|
+
|
|
136
|
+
return getegid()
|
|
137
|
+
case never:
|
|
138
|
+
assert_never(never)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def get_effective_user_id() -> int | None:
|
|
142
|
+
"""Get the effective user ID."""
|
|
143
|
+
match SYSTEM:
|
|
144
|
+
case "windows": # skipif-not-windows
|
|
145
|
+
return None
|
|
146
|
+
case "mac" | "linux": # skipif-windows
|
|
147
|
+
from os import geteuid
|
|
148
|
+
|
|
149
|
+
return geteuid()
|
|
150
|
+
case never:
|
|
151
|
+
assert_never(never)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
EFFECTIVE_USER_ID = get_effective_user_id()
|
|
155
|
+
EFFECTIVE_GROUP_ID = get_effective_group_id()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
##
|
|
159
|
+
|
|
160
|
+
|
|
127
161
|
def is_debug() -> bool:
|
|
128
162
|
"""Check if we are in `DEBUG` mode."""
|
|
129
163
|
return get_env_var("DEBUG", nullable=True) is not None
|
|
@@ -132,6 +166,14 @@ def is_debug() -> bool:
|
|
|
132
166
|
##
|
|
133
167
|
|
|
134
168
|
|
|
169
|
+
def is_pytest() -> bool:
|
|
170
|
+
"""Check if `pytest` is running."""
|
|
171
|
+
return get_env_var("PYTEST_VERSION", nullable=True) is not None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
##
|
|
175
|
+
|
|
176
|
+
|
|
135
177
|
@enhanced_context_manager
|
|
136
178
|
def temp_environ(
|
|
137
179
|
env: Mapping[str, str | None] | None = None, **env_kwargs: str | None
|
|
@@ -157,12 +199,17 @@ def temp_environ(
|
|
|
157
199
|
|
|
158
200
|
__all__ = [
|
|
159
201
|
"CPU_COUNT",
|
|
202
|
+
"EFFECTIVE_GROUP_ID",
|
|
203
|
+
"EFFECTIVE_USER_ID",
|
|
160
204
|
"GetCPUCountError",
|
|
161
205
|
"GetCPUUseError",
|
|
162
206
|
"IntOrAll",
|
|
163
207
|
"get_cpu_count",
|
|
164
208
|
"get_cpu_use",
|
|
209
|
+
"get_effective_group_id",
|
|
210
|
+
"get_effective_user_id",
|
|
165
211
|
"get_env_var",
|
|
166
212
|
"is_debug",
|
|
213
|
+
"is_pytest",
|
|
167
214
|
"temp_environ",
|
|
168
215
|
]
|
utilities/parse.py
CHANGED
|
@@ -204,7 +204,7 @@ def _parse_object_type(
|
|
|
204
204
|
),
|
|
205
205
|
):
|
|
206
206
|
try:
|
|
207
|
-
return cls.
|
|
207
|
+
return cls.parse_iso(text)
|
|
208
208
|
except ValueError:
|
|
209
209
|
raise _ParseObjectParseError(type_=cls, text=text) from None
|
|
210
210
|
if issubclass(cls, Path):
|
|
@@ -477,7 +477,7 @@ def serialize_object(
|
|
|
477
477
|
ZonedDateTime,
|
|
478
478
|
),
|
|
479
479
|
):
|
|
480
|
-
return obj.
|
|
480
|
+
return obj.format_iso()
|
|
481
481
|
if isinstance(obj, Enum):
|
|
482
482
|
return obj.name
|
|
483
483
|
if isinstance(obj, dict):
|
utilities/pathlib.py
CHANGED
|
@@ -12,7 +12,9 @@ from typing import TYPE_CHECKING, Literal, assert_never, overload, override
|
|
|
12
12
|
|
|
13
13
|
from utilities.contextlib import enhanced_context_manager
|
|
14
14
|
from utilities.errors import ImpossibleCaseError
|
|
15
|
-
from utilities.
|
|
15
|
+
from utilities.grp import get_gid_name
|
|
16
|
+
from utilities.pwd import get_uid_name
|
|
17
|
+
from utilities.sentinel import Sentinel
|
|
16
18
|
|
|
17
19
|
if TYPE_CHECKING:
|
|
18
20
|
from collections.abc import Iterator, Sequence
|
|
@@ -52,33 +54,22 @@ def expand_path(path: PathLike, /) -> Path:
|
|
|
52
54
|
##
|
|
53
55
|
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
match path:
|
|
64
|
-
case Path() | Sentinel():
|
|
65
|
-
return path
|
|
66
|
-
case str():
|
|
67
|
-
return Path(path)
|
|
68
|
-
case None:
|
|
69
|
-
return Path.cwd()
|
|
70
|
-
case Callable() as func:
|
|
71
|
-
return get_path(path=func())
|
|
72
|
-
case _ as never:
|
|
73
|
-
assert_never(never)
|
|
57
|
+
def get_file_group(path: PathLike, /) -> str | None:
|
|
58
|
+
"""Get the group of a file."""
|
|
59
|
+
return get_gid_name(to_path(path).stat().st_gid)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_file_owner(path: PathLike, /) -> str | None:
|
|
63
|
+
"""Get the owner of a file."""
|
|
64
|
+
return get_uid_name(to_path(path).stat().st_uid)
|
|
74
65
|
|
|
75
66
|
|
|
76
67
|
##
|
|
77
68
|
|
|
78
69
|
|
|
79
|
-
def get_package_root(
|
|
70
|
+
def get_package_root(path: MaybeCallablePathLike = Path.cwd, /) -> Path:
|
|
80
71
|
"""Get the package root."""
|
|
81
|
-
path =
|
|
72
|
+
path = to_path(path)
|
|
82
73
|
path_dir = path.parent if path.is_file() else path
|
|
83
74
|
all_paths = list(chain([path_dir], path_dir.parents))
|
|
84
75
|
try:
|
|
@@ -103,9 +94,9 @@ class GetPackageRootError(Exception):
|
|
|
103
94
|
##
|
|
104
95
|
|
|
105
96
|
|
|
106
|
-
def get_repo_root(
|
|
97
|
+
def get_repo_root(path: MaybeCallablePathLike = Path.cwd, /) -> Path:
|
|
107
98
|
"""Get the repo root."""
|
|
108
|
-
path =
|
|
99
|
+
path = to_path(path)
|
|
109
100
|
path_dir = path.parent if path.is_file() else path
|
|
110
101
|
try:
|
|
111
102
|
output = check_output(
|
|
@@ -118,15 +109,30 @@ def get_repo_root(*, path: MaybeCallablePathLike | None = None) -> Path:
|
|
|
118
109
|
# newer versions of git report "Not a git repository", whilst older
|
|
119
110
|
# versions report "not a git repository"
|
|
120
111
|
if search("fatal: not a git repository", error.stderr, flags=IGNORECASE):
|
|
121
|
-
raise
|
|
112
|
+
raise _GetRepoRootNotARepoError(path=path) from None
|
|
122
113
|
raise # pragma: no cover
|
|
114
|
+
except FileNotFoundError as error: # pragma: no cover
|
|
115
|
+
if search("No such file or directory: 'git'", str(error), flags=IGNORECASE):
|
|
116
|
+
raise _GetRepoRootGitNotFoundError from None
|
|
117
|
+
raise
|
|
123
118
|
else:
|
|
124
119
|
return Path(output.strip("\n"))
|
|
125
120
|
|
|
126
121
|
|
|
127
122
|
@dataclass(kw_only=True, slots=True)
|
|
128
|
-
class GetRepoRootError(Exception):
|
|
129
|
-
|
|
123
|
+
class GetRepoRootError(Exception): ...
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(kw_only=True, slots=True)
|
|
127
|
+
class _GetRepoRootGitNotFoundError(GetRepoRootError):
|
|
128
|
+
@override
|
|
129
|
+
def __str__(self) -> str:
|
|
130
|
+
return "'git' not found"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(kw_only=True, slots=True)
|
|
134
|
+
class _GetRepoRootNotARepoError(GetRepoRootError):
|
|
135
|
+
path: Path
|
|
130
136
|
|
|
131
137
|
@override
|
|
132
138
|
def __str__(self) -> str:
|
|
@@ -136,15 +142,15 @@ class GetRepoRootError(Exception):
|
|
|
136
142
|
##
|
|
137
143
|
|
|
138
144
|
|
|
139
|
-
def get_root(
|
|
145
|
+
def get_root(path: MaybeCallablePathLike = Path.cwd, /) -> Path:
|
|
140
146
|
"""Get the root of a path."""
|
|
141
|
-
path =
|
|
147
|
+
path = to_path(path)
|
|
142
148
|
try:
|
|
143
|
-
repo = get_repo_root(path
|
|
149
|
+
repo = get_repo_root(path)
|
|
144
150
|
except GetRepoRootError:
|
|
145
151
|
repo = None
|
|
146
152
|
try:
|
|
147
|
-
package = get_package_root(path
|
|
153
|
+
package = get_package_root(path)
|
|
148
154
|
except GetPackageRootError:
|
|
149
155
|
package = None
|
|
150
156
|
match repo, package:
|
|
@@ -164,7 +170,7 @@ def get_root(*, path: MaybeCallablePathLike | None = None) -> Path:
|
|
|
164
170
|
raise ImpossibleCaseError( # pragma: no cover
|
|
165
171
|
case=[f"{repo=}", f"{package=}"]
|
|
166
172
|
)
|
|
167
|
-
case
|
|
173
|
+
case never:
|
|
168
174
|
assert_never(never)
|
|
169
175
|
|
|
170
176
|
|
|
@@ -213,7 +219,7 @@ def get_tail(
|
|
|
213
219
|
return _get_tail_core(path, next(iter(matches)))
|
|
214
220
|
case _, "later":
|
|
215
221
|
return _get_tail_core(path, next(iter(reversed(matches))))
|
|
216
|
-
case
|
|
222
|
+
case never:
|
|
217
223
|
assert_never(never)
|
|
218
224
|
|
|
219
225
|
|
|
@@ -305,6 +311,30 @@ def temp_cwd(path: PathLike, /) -> Iterator[None]:
|
|
|
305
311
|
chdir(prev)
|
|
306
312
|
|
|
307
313
|
|
|
314
|
+
##
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@overload
|
|
318
|
+
def to_path(path: Sentinel, /) -> Sentinel: ...
|
|
319
|
+
@overload
|
|
320
|
+
def to_path(path: MaybeCallablePathLike | None = Path.cwd, /) -> Path: ...
|
|
321
|
+
def to_path(
|
|
322
|
+
path: MaybeCallablePathLike | None | Sentinel = Path.cwd, /
|
|
323
|
+
) -> Path | Sentinel:
|
|
324
|
+
"""Get the path."""
|
|
325
|
+
match path:
|
|
326
|
+
case Path() | Sentinel():
|
|
327
|
+
return path
|
|
328
|
+
case None:
|
|
329
|
+
return Path.cwd()
|
|
330
|
+
case str():
|
|
331
|
+
return Path(path)
|
|
332
|
+
case Callable() as func:
|
|
333
|
+
return to_path(func())
|
|
334
|
+
case never:
|
|
335
|
+
assert_never(never)
|
|
336
|
+
|
|
337
|
+
|
|
308
338
|
__all__ = [
|
|
309
339
|
"PWD",
|
|
310
340
|
"GetPackageRootError",
|
|
@@ -312,12 +342,14 @@ __all__ = [
|
|
|
312
342
|
"GetTailError",
|
|
313
343
|
"ensure_suffix",
|
|
314
344
|
"expand_path",
|
|
345
|
+
"get_file_group",
|
|
346
|
+
"get_file_owner",
|
|
315
347
|
"get_package_root",
|
|
316
|
-
"get_path",
|
|
317
348
|
"get_repo_root",
|
|
318
349
|
"get_tail",
|
|
319
350
|
"is_sub_path",
|
|
320
351
|
"list_dir",
|
|
321
352
|
"module_path",
|
|
322
353
|
"temp_cwd",
|
|
354
|
+
"to_path",
|
|
323
355
|
]
|