dycw-utilities 0.166.30__py3-none-any.whl → 0.185.8__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.
- dycw_utilities-0.185.8.dist-info/METADATA +33 -0
- dycw_utilities-0.185.8.dist-info/RECORD +90 -0
- {dycw_utilities-0.166.30.dist-info → dycw_utilities-0.185.8.dist-info}/WHEEL +1 -1
- {dycw_utilities-0.166.30.dist-info → dycw_utilities-0.185.8.dist-info}/entry_points.txt +1 -0
- utilities/__init__.py +1 -1
- utilities/altair.py +17 -10
- utilities/asyncio.py +50 -72
- utilities/atools.py +9 -11
- utilities/cachetools.py +16 -11
- utilities/click.py +76 -19
- utilities/concurrent.py +1 -1
- utilities/constants.py +492 -0
- utilities/contextlib.py +23 -30
- utilities/contextvars.py +1 -23
- utilities/core.py +2581 -0
- utilities/dataclasses.py +16 -119
- utilities/docker.py +387 -0
- utilities/enum.py +1 -1
- utilities/errors.py +2 -16
- utilities/fastapi.py +5 -5
- utilities/fpdf2.py +2 -1
- utilities/functions.py +34 -265
- utilities/http.py +2 -3
- utilities/hypothesis.py +84 -29
- utilities/importlib.py +17 -1
- utilities/iterables.py +39 -575
- utilities/jinja2.py +145 -0
- utilities/jupyter.py +5 -3
- utilities/libcst.py +1 -1
- utilities/lightweight_charts.py +4 -6
- utilities/logging.py +24 -24
- utilities/math.py +1 -36
- utilities/more_itertools.py +4 -6
- utilities/numpy.py +2 -1
- utilities/operator.py +2 -2
- utilities/orjson.py +42 -43
- utilities/os.py +4 -147
- utilities/packaging.py +129 -0
- utilities/parse.py +35 -15
- utilities/pathlib.py +3 -120
- utilities/platform.py +8 -90
- utilities/polars.py +38 -32
- utilities/postgres.py +37 -33
- utilities/pottery.py +20 -18
- utilities/pqdm.py +3 -4
- utilities/psutil.py +2 -3
- utilities/pydantic.py +25 -0
- utilities/pydantic_settings.py +87 -16
- utilities/pydantic_settings_sops.py +16 -3
- utilities/pyinstrument.py +4 -4
- utilities/pytest.py +96 -125
- utilities/pytest_plugins/pytest_regressions.py +2 -2
- utilities/pytest_regressions.py +32 -11
- utilities/random.py +2 -8
- utilities/redis.py +98 -94
- utilities/reprlib.py +11 -118
- utilities/shellingham.py +66 -0
- utilities/shutil.py +25 -0
- utilities/slack_sdk.py +13 -12
- utilities/sqlalchemy.py +57 -30
- utilities/sqlalchemy_polars.py +16 -25
- utilities/subprocess.py +2590 -0
- utilities/tabulate.py +32 -0
- utilities/testbook.py +8 -8
- utilities/text.py +24 -99
- utilities/throttle.py +159 -0
- utilities/time.py +18 -0
- utilities/timer.py +31 -14
- utilities/traceback.py +16 -23
- utilities/types.py +42 -2
- utilities/typing.py +26 -14
- utilities/uuid.py +1 -1
- utilities/version.py +202 -45
- utilities/whenever.py +53 -150
- dycw_utilities-0.166.30.dist-info/METADATA +0 -41
- dycw_utilities-0.166.30.dist-info/RECORD +0 -98
- dycw_utilities-0.166.30.dist-info/licenses/LICENSE +0 -21
- utilities/aeventkit.py +0 -388
- utilities/atomicwrites.py +0 -182
- utilities/cryptography.py +0 -41
- utilities/getpass.py +0 -8
- utilities/git.py +0 -19
- utilities/gzip.py +0 -31
- utilities/json.py +0 -70
- utilities/pickle.py +0 -25
- utilities/re.py +0 -156
- utilities/sentinel.py +0 -73
- utilities/socket.py +0 -8
- utilities/string.py +0 -20
- utilities/tempfile.py +0 -77
- utilities/typed_settings.py +0 -152
- utilities/tzdata.py +0 -11
- utilities/tzlocal.py +0 -28
- utilities/warnings.py +0 -65
- utilities/zipfile.py +0 -25
- utilities/zoneinfo.py +0 -133
utilities/polars.py
CHANGED
|
@@ -54,25 +54,31 @@ from polars.testing import assert_frame_equal, assert_series_equal
|
|
|
54
54
|
from whenever import DateDelta, DateTimeDelta, PlainDateTime, TimeDelta, ZonedDateTime
|
|
55
55
|
|
|
56
56
|
import utilities.math
|
|
57
|
+
from utilities.constants import UTC
|
|
58
|
+
from utilities.core import (
|
|
59
|
+
OneEmptyError,
|
|
60
|
+
OneNonUniqueError,
|
|
61
|
+
always_iterable,
|
|
62
|
+
one,
|
|
63
|
+
read_bytes,
|
|
64
|
+
repr_,
|
|
65
|
+
suppress_warnings,
|
|
66
|
+
to_time_zone_name,
|
|
67
|
+
write_bytes,
|
|
68
|
+
)
|
|
57
69
|
from utilities.dataclasses import yield_fields
|
|
58
70
|
from utilities.errors import ImpossibleCaseError
|
|
59
71
|
from utilities.functions import get_class_name
|
|
60
|
-
from utilities.gzip import read_binary
|
|
61
72
|
from utilities.iterables import (
|
|
62
73
|
CheckIterablesEqualError,
|
|
63
74
|
CheckMappingsEqualError,
|
|
64
75
|
CheckSuperMappingError,
|
|
65
|
-
OneEmptyError,
|
|
66
|
-
OneNonUniqueError,
|
|
67
|
-
always_iterable,
|
|
68
76
|
check_iterables_equal,
|
|
69
77
|
check_mappings_equal,
|
|
70
78
|
check_supermapping,
|
|
71
79
|
is_iterable_not_str,
|
|
72
|
-
one,
|
|
73
80
|
resolve_include_and_exclude,
|
|
74
81
|
)
|
|
75
|
-
from utilities.json import write_formatted_json
|
|
76
82
|
from utilities.math import (
|
|
77
83
|
MAX_DECIMALS,
|
|
78
84
|
CheckIntegerError,
|
|
@@ -81,8 +87,7 @@ from utilities.math import (
|
|
|
81
87
|
is_less_than,
|
|
82
88
|
is_non_negative,
|
|
83
89
|
)
|
|
84
|
-
from utilities.
|
|
85
|
-
from utilities.types import MaybeStr, Number, PathLike, WeekDay
|
|
90
|
+
from utilities.types import MaybeStr, Number, PathLike, StrDict, WeekDay
|
|
86
91
|
from utilities.typing import (
|
|
87
92
|
get_args,
|
|
88
93
|
is_dataclass_class,
|
|
@@ -94,14 +99,12 @@ from utilities.typing import (
|
|
|
94
99
|
is_set_type,
|
|
95
100
|
make_isinstance,
|
|
96
101
|
)
|
|
97
|
-
from utilities.warnings import suppress_warnings
|
|
98
102
|
from utilities.whenever import (
|
|
99
103
|
DatePeriod,
|
|
100
104
|
TimePeriod,
|
|
101
105
|
ZonedDateTimePeriod,
|
|
102
106
|
to_py_time_delta,
|
|
103
107
|
)
|
|
104
|
-
from utilities.zoneinfo import UTC, to_time_zone_name
|
|
105
108
|
|
|
106
109
|
if TYPE_CHECKING:
|
|
107
110
|
import datetime as dt
|
|
@@ -343,7 +346,7 @@ class AppendRowError(Exception):
|
|
|
343
346
|
class _AppendRowPredicateError(AppendRowError):
|
|
344
347
|
@override
|
|
345
348
|
def __str__(self) -> str:
|
|
346
|
-
return f"Predicate failed; got {
|
|
349
|
+
return f"Predicate failed; got {repr_(self.row)}"
|
|
347
350
|
|
|
348
351
|
|
|
349
352
|
@dataclass(kw_only=True, slots=True)
|
|
@@ -352,7 +355,7 @@ class _AppendRowExtraKeysError(AppendRowError):
|
|
|
352
355
|
|
|
353
356
|
@override
|
|
354
357
|
def __str__(self) -> str:
|
|
355
|
-
return f"Extra key(s) found; got {
|
|
358
|
+
return f"Extra key(s) found; got {repr_(self.extra)}"
|
|
356
359
|
|
|
357
360
|
|
|
358
361
|
@dataclass(kw_only=True, slots=True)
|
|
@@ -361,7 +364,7 @@ class _AppendRowMissingKeysError(AppendRowError):
|
|
|
361
364
|
|
|
362
365
|
@override
|
|
363
366
|
def __str__(self) -> str:
|
|
364
|
-
return f"Missing key(s) found; got {
|
|
367
|
+
return f"Missing key(s) found; got {repr_(self.missing)}"
|
|
365
368
|
|
|
366
369
|
|
|
367
370
|
@dataclass(kw_only=True, slots=True)
|
|
@@ -370,7 +373,7 @@ class _AppendRowNullColumnsError(AppendRowError):
|
|
|
370
373
|
|
|
371
374
|
@override
|
|
372
375
|
def __str__(self) -> str:
|
|
373
|
-
return f"Null column(s) found; got {
|
|
376
|
+
return f"Null column(s) found; got {repr_(self.columns)}"
|
|
374
377
|
|
|
375
378
|
|
|
376
379
|
##
|
|
@@ -569,7 +572,7 @@ class _CheckPolarsDataFrameColumnsError(CheckPolarsDataFrameError):
|
|
|
569
572
|
|
|
570
573
|
@override
|
|
571
574
|
def __str__(self) -> str:
|
|
572
|
-
return f"DataFrame must have columns {
|
|
575
|
+
return f"DataFrame must have columns {repr_(self.columns)}; got {repr_(self.df.columns)}:\n\n{self.df}"
|
|
573
576
|
|
|
574
577
|
|
|
575
578
|
def _check_polars_dataframe_dtypes(
|
|
@@ -587,7 +590,7 @@ class _CheckPolarsDataFrameDTypesError(CheckPolarsDataFrameError):
|
|
|
587
590
|
|
|
588
591
|
@override
|
|
589
592
|
def __str__(self) -> str:
|
|
590
|
-
return f"DataFrame must have dtypes {
|
|
593
|
+
return f"DataFrame must have dtypes {repr_(self.dtypes)}; got {repr_(self.df.dtypes)}:\n\n{self.df}"
|
|
591
594
|
|
|
592
595
|
|
|
593
596
|
def _check_polars_dataframe_height(
|
|
@@ -650,9 +653,9 @@ class _CheckPolarsDataFramePredicatesError(CheckPolarsDataFrameError):
|
|
|
650
653
|
|
|
651
654
|
def _yield_parts(self) -> Iterator[str]:
|
|
652
655
|
if len(self.missing) >= 1:
|
|
653
|
-
yield f"missing columns were {
|
|
656
|
+
yield f"missing columns were {repr_(self.missing)}"
|
|
654
657
|
if len(self.failed) >= 1:
|
|
655
|
-
yield f"failed predicates were {
|
|
658
|
+
yield f"failed predicates were {repr_(self.failed)}"
|
|
656
659
|
|
|
657
660
|
|
|
658
661
|
def _check_polars_dataframe_schema_list(df: DataFrame, schema: SchemaDict, /) -> None:
|
|
@@ -672,7 +675,7 @@ class _CheckPolarsDataFrameSchemaListError(CheckPolarsDataFrameError):
|
|
|
672
675
|
|
|
673
676
|
@override
|
|
674
677
|
def __str__(self) -> str:
|
|
675
|
-
return f"DataFrame must have schema {
|
|
678
|
+
return f"DataFrame must have schema {repr_(self.schema)} (ordered); got {repr_(self.df.schema)}:\n\n{self.df}"
|
|
676
679
|
|
|
677
680
|
|
|
678
681
|
def _check_polars_dataframe_schema_set(df: DataFrame, schema: SchemaDict, /) -> None:
|
|
@@ -688,7 +691,7 @@ class _CheckPolarsDataFrameSchemaSetError(CheckPolarsDataFrameError):
|
|
|
688
691
|
|
|
689
692
|
@override
|
|
690
693
|
def __str__(self) -> str:
|
|
691
|
-
return f"DataFrame must have schema {
|
|
694
|
+
return f"DataFrame must have schema {repr_(self.schema)} (unordered); got {repr_(self.df.schema)}:\n\n{self.df}"
|
|
692
695
|
|
|
693
696
|
|
|
694
697
|
def _check_polars_dataframe_schema_subset(df: DataFrame, schema: SchemaDict, /) -> None:
|
|
@@ -704,7 +707,7 @@ class _CheckPolarsDataFrameSchemaSubsetError(CheckPolarsDataFrameError):
|
|
|
704
707
|
|
|
705
708
|
@override
|
|
706
709
|
def __str__(self) -> str:
|
|
707
|
-
return f"DataFrame schema must include {
|
|
710
|
+
return f"DataFrame schema must include {repr_(self.schema)} (unordered); got {repr_(self.df.schema)}:\n\n{self.df}"
|
|
708
711
|
|
|
709
712
|
|
|
710
713
|
def _check_polars_dataframe_shape(df: DataFrame, shape: tuple[int, int], /) -> None:
|
|
@@ -742,7 +745,7 @@ class _CheckPolarsDataFrameSortedError(CheckPolarsDataFrameError):
|
|
|
742
745
|
|
|
743
746
|
@override
|
|
744
747
|
def __str__(self) -> str:
|
|
745
|
-
return f"DataFrame must be sorted on {
|
|
748
|
+
return f"DataFrame must be sorted on {repr_(self.by)}:\n\n{self.df}"
|
|
746
749
|
|
|
747
750
|
|
|
748
751
|
def _check_polars_dataframe_unique(
|
|
@@ -761,7 +764,7 @@ class _CheckPolarsDataFrameUniqueError(CheckPolarsDataFrameError):
|
|
|
761
764
|
|
|
762
765
|
@override
|
|
763
766
|
def __str__(self) -> str:
|
|
764
|
-
return f"DataFrame must be unique on {
|
|
767
|
+
return f"DataFrame must be unique on {repr_(self.by)}:\n\n{self.df}"
|
|
765
768
|
|
|
766
769
|
|
|
767
770
|
def _check_polars_dataframe_width(df: DataFrame, width: int, /) -> None:
|
|
@@ -1132,7 +1135,7 @@ class _DataClassToDataFrameNonUniqueError(DataClassToDataFrameError):
|
|
|
1132
1135
|
|
|
1133
1136
|
@override
|
|
1134
1137
|
def __str__(self) -> str:
|
|
1135
|
-
return f"Iterable {
|
|
1138
|
+
return f"Iterable {repr_(self.objs)} must contain exactly 1 class; got {self.first}, {self.second} and perhaps more"
|
|
1136
1139
|
|
|
1137
1140
|
|
|
1138
1141
|
##
|
|
@@ -1147,7 +1150,7 @@ def dataclass_to_schema(
|
|
|
1147
1150
|
warn_name_errors: bool = False,
|
|
1148
1151
|
) -> SchemaDict:
|
|
1149
1152
|
"""Cast a dataclass as a schema dict."""
|
|
1150
|
-
out:
|
|
1153
|
+
out: StrDict = {}
|
|
1151
1154
|
for field in yield_fields(
|
|
1152
1155
|
obj, globalns=globalns, localns=localns, warn_name_errors=warn_name_errors
|
|
1153
1156
|
):
|
|
@@ -1204,7 +1207,7 @@ def _dataclass_to_schema_one(
|
|
|
1204
1207
|
if issubclass(obj, enum.Enum):
|
|
1205
1208
|
return pl.Enum([e.name for e in obj])
|
|
1206
1209
|
if is_dataclass_class(obj):
|
|
1207
|
-
out:
|
|
1210
|
+
out: StrDict = {}
|
|
1208
1211
|
for field in yield_fields(obj, globalns=globalns, localns=localns):
|
|
1209
1212
|
out[field.name] = _dataclass_to_schema_one(
|
|
1210
1213
|
field.type_, globalns=globalns, localns=localns
|
|
@@ -2425,7 +2428,7 @@ def _replace_time_zone_one(
|
|
|
2425
2428
|
|
|
2426
2429
|
def read_series(path: PathLike, /, *, decompress: bool = False) -> Series:
|
|
2427
2430
|
"""Read a Series from disk."""
|
|
2428
|
-
data =
|
|
2431
|
+
data = read_bytes(path, decompress=decompress)
|
|
2429
2432
|
return deserialize_series(data)
|
|
2430
2433
|
|
|
2431
2434
|
|
|
@@ -2439,12 +2442,12 @@ def write_series(
|
|
|
2439
2442
|
) -> None:
|
|
2440
2443
|
"""Write a Series to disk."""
|
|
2441
2444
|
data = serialize_series(series)
|
|
2442
|
-
|
|
2445
|
+
write_bytes(path, data, compress=compress, overwrite=overwrite, json=True)
|
|
2443
2446
|
|
|
2444
2447
|
|
|
2445
2448
|
def read_dataframe(path: PathLike, /, *, decompress: bool = False) -> DataFrame:
|
|
2446
2449
|
"""Read a DataFrame from disk."""
|
|
2447
|
-
data =
|
|
2450
|
+
data = read_bytes(path, decompress=decompress)
|
|
2448
2451
|
return deserialize_dataframe(data)
|
|
2449
2452
|
|
|
2450
2453
|
|
|
@@ -2453,7 +2456,7 @@ def write_dataframe(
|
|
|
2453
2456
|
) -> None:
|
|
2454
2457
|
"""Write a DataFrame to disk."""
|
|
2455
2458
|
data = serialize_dataframe(df)
|
|
2456
|
-
|
|
2459
|
+
write_bytes(path, data, compress=compress, overwrite=overwrite, json=True)
|
|
2457
2460
|
|
|
2458
2461
|
|
|
2459
2462
|
def serialize_series(series: Series, /) -> bytes:
|
|
@@ -2571,7 +2574,8 @@ def round_to_float(
|
|
|
2571
2574
|
return z.round(decimals=utilities.math.number_of_decimals(y) + 1)
|
|
2572
2575
|
case Series(), Expr() | Series():
|
|
2573
2576
|
df = (
|
|
2574
|
-
x
|
|
2577
|
+
x
|
|
2578
|
+
.to_frame()
|
|
2575
2579
|
.with_columns(y)
|
|
2576
2580
|
.with_columns(number_of_decimals(y).alias("_decimals"))
|
|
2577
2581
|
.with_row_index(name="_index")
|
|
@@ -2638,6 +2642,8 @@ def search_period(
|
|
|
2638
2642
|
return None
|
|
2639
2643
|
item: dt.datetime = series[index]["start"]
|
|
2640
2644
|
return index if py_date_time > item else None
|
|
2645
|
+
case never:
|
|
2646
|
+
assert_never(never)
|
|
2641
2647
|
|
|
2642
2648
|
|
|
2643
2649
|
##
|
|
@@ -2662,7 +2668,7 @@ class SelectExactError(Exception):
|
|
|
2662
2668
|
|
|
2663
2669
|
@override
|
|
2664
2670
|
def __str__(self) -> str:
|
|
2665
|
-
return f"All columns must be selected; got {
|
|
2671
|
+
return f"All columns must be selected; got {repr_(self.columns)} remaining"
|
|
2666
2672
|
|
|
2667
2673
|
|
|
2668
2674
|
##
|
utilities/postgres.py
CHANGED
|
@@ -9,9 +9,9 @@ from sqlalchemy import Table
|
|
|
9
9
|
from sqlalchemy.orm import DeclarativeBase
|
|
10
10
|
|
|
11
11
|
from utilities.asyncio import stream_command
|
|
12
|
-
from utilities.
|
|
12
|
+
from utilities.core import always_iterable, yield_temp_environ
|
|
13
|
+
from utilities.docker import docker_exec_cmd
|
|
13
14
|
from utilities.logging import to_logger
|
|
14
|
-
from utilities.os import temp_environ
|
|
15
15
|
from utilities.pathlib import ensure_suffix
|
|
16
16
|
from utilities.sqlalchemy import extract_url, get_table_name
|
|
17
17
|
from utilities.timer import Timer
|
|
@@ -37,6 +37,7 @@ async def pg_dump(
|
|
|
37
37
|
path: PathLike,
|
|
38
38
|
/,
|
|
39
39
|
*,
|
|
40
|
+
docker_container: str | None = None,
|
|
40
41
|
format_: _PGDumpFormat = "plain",
|
|
41
42
|
jobs: int | None = None,
|
|
42
43
|
data_only: bool = False,
|
|
@@ -51,7 +52,6 @@ async def pg_dump(
|
|
|
51
52
|
inserts: bool = False,
|
|
52
53
|
on_conflict_do_nothing: bool = False,
|
|
53
54
|
role: str | None = None,
|
|
54
|
-
docker: str | None = None,
|
|
55
55
|
dry_run: bool = False,
|
|
56
56
|
logger: LoggerLike | None = None,
|
|
57
57
|
) -> bool:
|
|
@@ -61,6 +61,7 @@ async def pg_dump(
|
|
|
61
61
|
cmd = _build_pg_dump(
|
|
62
62
|
url,
|
|
63
63
|
path,
|
|
64
|
+
docker_container=docker_container,
|
|
64
65
|
format_=format_,
|
|
65
66
|
jobs=jobs,
|
|
66
67
|
data_only=data_only,
|
|
@@ -75,13 +76,15 @@ async def pg_dump(
|
|
|
75
76
|
inserts=inserts,
|
|
76
77
|
on_conflict_do_nothing=on_conflict_do_nothing,
|
|
77
78
|
role=role,
|
|
78
|
-
docker=docker,
|
|
79
79
|
)
|
|
80
80
|
if dry_run:
|
|
81
81
|
if logger is not None:
|
|
82
82
|
to_logger(logger).info("Would run:\n\t%r", str(cmd))
|
|
83
83
|
return True
|
|
84
|
-
with
|
|
84
|
+
with (
|
|
85
|
+
yield_temp_environ(PGPASSWORD=url.password),
|
|
86
|
+
Timer() as timer,
|
|
87
|
+
): # pragma: no cover
|
|
85
88
|
try:
|
|
86
89
|
output = await stream_command(cmd)
|
|
87
90
|
except KeyboardInterrupt:
|
|
@@ -111,6 +114,7 @@ def _build_pg_dump(
|
|
|
111
114
|
path: PathLike,
|
|
112
115
|
/,
|
|
113
116
|
*,
|
|
117
|
+
docker_container: str | None = None,
|
|
114
118
|
format_: _PGDumpFormat = "plain",
|
|
115
119
|
jobs: int | None = None,
|
|
116
120
|
data_only: bool = False,
|
|
@@ -125,12 +129,13 @@ def _build_pg_dump(
|
|
|
125
129
|
inserts: bool = False,
|
|
126
130
|
on_conflict_do_nothing: bool = False,
|
|
127
131
|
role: str | None = None,
|
|
128
|
-
docker: str | None = None,
|
|
129
132
|
) -> str:
|
|
130
133
|
extracted = extract_url(url)
|
|
131
134
|
path = _path_pg_dump(path, format_=format_)
|
|
132
|
-
parts: list[str] = [
|
|
133
|
-
|
|
135
|
+
parts: list[str] = ["pg_dump"]
|
|
136
|
+
if docker_container is not None:
|
|
137
|
+
parts = docker_exec_cmd(docker_container, *parts, PGPASSWORD=extracted.password)
|
|
138
|
+
parts.extend([
|
|
134
139
|
# general options
|
|
135
140
|
f"--file={str(path)!r}",
|
|
136
141
|
f"--format={format_}",
|
|
@@ -146,7 +151,7 @@ def _build_pg_dump(
|
|
|
146
151
|
f"--port={extracted.port}",
|
|
147
152
|
f"--username={extracted.username}",
|
|
148
153
|
"--no-password",
|
|
149
|
-
]
|
|
154
|
+
])
|
|
150
155
|
if (format_ == "directory") and (jobs is not None):
|
|
151
156
|
parts.append(f"--jobs={jobs}")
|
|
152
157
|
if create:
|
|
@@ -173,8 +178,6 @@ def _build_pg_dump(
|
|
|
173
178
|
parts.append("--on-conflict-do-nothing")
|
|
174
179
|
if role is not None:
|
|
175
180
|
parts.append(f"--role={role}")
|
|
176
|
-
if docker is not None:
|
|
177
|
-
parts = _wrap_docker(parts, docker)
|
|
178
181
|
return " ".join(parts)
|
|
179
182
|
|
|
180
183
|
|
|
@@ -213,7 +216,7 @@ async def restore(
|
|
|
213
216
|
schema_exc: MaybeCollectionStr | None = None,
|
|
214
217
|
table: MaybeCollection[TableOrORMInstOrClass | str] | None = None,
|
|
215
218
|
role: str | None = None,
|
|
216
|
-
|
|
219
|
+
docker_container: str | None = None,
|
|
217
220
|
dry_run: bool = False,
|
|
218
221
|
logger: LoggerLike | None = None,
|
|
219
222
|
) -> bool:
|
|
@@ -230,13 +233,16 @@ async def restore(
|
|
|
230
233
|
schema_exc=schema_exc,
|
|
231
234
|
table=table,
|
|
232
235
|
role=role,
|
|
233
|
-
|
|
236
|
+
docker_container=docker_container,
|
|
234
237
|
)
|
|
235
238
|
if dry_run:
|
|
236
239
|
if logger is not None:
|
|
237
240
|
to_logger(logger).info("Would run:\n\t%r", str(cmd))
|
|
238
241
|
return True
|
|
239
|
-
with
|
|
242
|
+
with (
|
|
243
|
+
yield_temp_environ(PGPASSWORD=url.password),
|
|
244
|
+
Timer() as timer,
|
|
245
|
+
): # pragma: no cover
|
|
240
246
|
try:
|
|
241
247
|
output = await stream_command(cmd)
|
|
242
248
|
except KeyboardInterrupt:
|
|
@@ -276,11 +282,11 @@ def _build_pg_restore_or_psql(
|
|
|
276
282
|
schema_exc: MaybeCollectionStr | None = None,
|
|
277
283
|
table: MaybeCollection[TableOrORMInstOrClass | str] | None = None,
|
|
278
284
|
role: str | None = None,
|
|
279
|
-
|
|
285
|
+
docker_container: str | None = None,
|
|
280
286
|
) -> str:
|
|
281
287
|
path = Path(path)
|
|
282
288
|
if (path.suffix == ".sql") or psql:
|
|
283
|
-
return _build_psql(url, path,
|
|
289
|
+
return _build_psql(url, path, docker_container=docker_container)
|
|
284
290
|
return _build_pg_restore(
|
|
285
291
|
url,
|
|
286
292
|
path,
|
|
@@ -292,7 +298,7 @@ def _build_pg_restore_or_psql(
|
|
|
292
298
|
schemas_exc=schema_exc,
|
|
293
299
|
tables=table,
|
|
294
300
|
role=role,
|
|
295
|
-
|
|
301
|
+
docker_container=docker_container,
|
|
296
302
|
)
|
|
297
303
|
|
|
298
304
|
|
|
@@ -309,12 +315,14 @@ def _build_pg_restore(
|
|
|
309
315
|
schemas_exc: MaybeCollectionStr | None = None,
|
|
310
316
|
tables: MaybeCollection[TableOrORMInstOrClass | str] | None = None,
|
|
311
317
|
role: str | None = None,
|
|
312
|
-
|
|
318
|
+
docker_container: str | None = None,
|
|
313
319
|
) -> str:
|
|
314
320
|
"""Run `pg_restore`."""
|
|
315
321
|
extracted = extract_url(url)
|
|
316
|
-
parts: list[str] = [
|
|
317
|
-
|
|
322
|
+
parts: list[str] = ["pg_restore"]
|
|
323
|
+
if docker_container is not None:
|
|
324
|
+
parts = docker_exec_cmd(docker_container, *parts, PGPASSWORD=extracted.password)
|
|
325
|
+
parts.extend([
|
|
318
326
|
# general options
|
|
319
327
|
"--verbose",
|
|
320
328
|
# restore options
|
|
@@ -328,7 +336,7 @@ def _build_pg_restore(
|
|
|
328
336
|
f"--username={extracted.username}",
|
|
329
337
|
f"--dbname={extracted.database}",
|
|
330
338
|
"--no-password",
|
|
331
|
-
]
|
|
339
|
+
])
|
|
332
340
|
if create:
|
|
333
341
|
parts.append("--create")
|
|
334
342
|
if jobs is not None:
|
|
@@ -341,17 +349,19 @@ def _build_pg_restore(
|
|
|
341
349
|
parts.extend([f"--table={_get_table_name(t)}" for t in always_iterable(tables)])
|
|
342
350
|
if role is not None:
|
|
343
351
|
parts.append(f"--role={role}")
|
|
344
|
-
if docker is not None:
|
|
345
|
-
parts = _wrap_docker(parts, docker)
|
|
346
352
|
parts.append(str(path))
|
|
347
353
|
return " ".join(parts)
|
|
348
354
|
|
|
349
355
|
|
|
350
|
-
def _build_psql(
|
|
356
|
+
def _build_psql(
|
|
357
|
+
url: URL, path: PathLike, /, *, docker_container: str | None = None
|
|
358
|
+
) -> str:
|
|
351
359
|
"""Run `psql`."""
|
|
352
360
|
extracted = extract_url(url)
|
|
353
|
-
parts: list[str] = [
|
|
354
|
-
|
|
361
|
+
parts: list[str] = ["psql"]
|
|
362
|
+
if docker_container is not None:
|
|
363
|
+
parts = docker_exec_cmd(docker_container, *parts, PGPASSWORD=extracted.password)
|
|
364
|
+
parts.extend([
|
|
355
365
|
# general options
|
|
356
366
|
f"--dbname={extracted.database}",
|
|
357
367
|
f"--file={str(path)!r}",
|
|
@@ -360,9 +370,7 @@ def _build_psql(url: URL, path: PathLike, /, *, docker: str | None = None) -> st
|
|
|
360
370
|
f"--port={extracted.port}",
|
|
361
371
|
f"--username={extracted.username}",
|
|
362
372
|
"--no-password",
|
|
363
|
-
]
|
|
364
|
-
if docker is not None:
|
|
365
|
-
parts = _wrap_docker(parts, docker)
|
|
373
|
+
])
|
|
366
374
|
return " ".join(parts)
|
|
367
375
|
|
|
368
376
|
|
|
@@ -402,8 +410,4 @@ class _ResolveDataOnlyAndCleanError(Exception):
|
|
|
402
410
|
return "Cannot use '--data-only' and '--clean' together"
|
|
403
411
|
|
|
404
412
|
|
|
405
|
-
def _wrap_docker(parts: list[str], container: str, /) -> list[str]:
|
|
406
|
-
return ["docker", "exec", "-it", container, *parts]
|
|
407
|
-
|
|
408
|
-
|
|
409
413
|
__all__ = ["pg_dump", "restore"]
|
utilities/pottery.py
CHANGED
|
@@ -9,21 +9,21 @@ from pottery import AIORedlock
|
|
|
9
9
|
from pottery.exceptions import ReleaseUnlockedLock
|
|
10
10
|
from redis.asyncio import Redis
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
import utilities.asyncio
|
|
13
|
+
from utilities.constants import MILLISECOND, SECOND
|
|
13
14
|
from utilities.contextlib import enhanced_async_context_manager
|
|
14
|
-
from utilities.
|
|
15
|
-
from utilities.
|
|
15
|
+
from utilities.core import always_iterable
|
|
16
|
+
from utilities.functions import in_seconds
|
|
16
17
|
|
|
17
18
|
if TYPE_CHECKING:
|
|
18
19
|
from collections.abc import AsyncIterator, Iterable
|
|
19
20
|
|
|
20
|
-
from
|
|
21
|
+
from utilities.types import Duration, MaybeIterable
|
|
21
22
|
|
|
22
|
-
from utilities.types import MaybeIterable
|
|
23
23
|
|
|
24
24
|
_NUM: int = 1
|
|
25
|
-
_TIMEOUT_RELEASE:
|
|
26
|
-
_SLEEP:
|
|
25
|
+
_TIMEOUT_RELEASE: Duration = 10 * SECOND
|
|
26
|
+
_SLEEP: Duration = MILLISECOND
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
##
|
|
@@ -47,11 +47,11 @@ async def yield_access(
|
|
|
47
47
|
/,
|
|
48
48
|
*,
|
|
49
49
|
num: int = _NUM,
|
|
50
|
-
timeout_release:
|
|
50
|
+
timeout_release: Duration = _TIMEOUT_RELEASE,
|
|
51
51
|
num_extensions: int | None = None,
|
|
52
|
-
timeout_acquire:
|
|
53
|
-
sleep:
|
|
54
|
-
throttle:
|
|
52
|
+
timeout_acquire: Duration | None = None,
|
|
53
|
+
sleep: Duration = _SLEEP,
|
|
54
|
+
throttle: Duration | None = None,
|
|
55
55
|
) -> AsyncIterator[AIORedlock]:
|
|
56
56
|
"""Acquire access to a locked resource."""
|
|
57
57
|
if num <= 0:
|
|
@@ -63,7 +63,7 @@ async def yield_access(
|
|
|
63
63
|
AIORedlock(
|
|
64
64
|
key=f"{key}_{i}_of_{num}",
|
|
65
65
|
masters=masters,
|
|
66
|
-
auto_release_time=
|
|
66
|
+
auto_release_time=in_seconds(timeout_release),
|
|
67
67
|
num_extensions=maxsize if num_extensions is None else num_extensions,
|
|
68
68
|
)
|
|
69
69
|
for i in range(1, num + 1)
|
|
@@ -75,7 +75,7 @@ async def yield_access(
|
|
|
75
75
|
)
|
|
76
76
|
yield lock
|
|
77
77
|
finally: # skipif-ci-and-not-linux
|
|
78
|
-
await
|
|
78
|
+
await utilities.asyncio.sleep(throttle)
|
|
79
79
|
if lock is not None:
|
|
80
80
|
with suppress(ReleaseUnlockedLock):
|
|
81
81
|
await lock.release()
|
|
@@ -87,18 +87,20 @@ async def _get_first_available_lock(
|
|
|
87
87
|
/,
|
|
88
88
|
*,
|
|
89
89
|
num: int = _NUM,
|
|
90
|
-
timeout:
|
|
91
|
-
sleep:
|
|
90
|
+
timeout: Duration | None = None,
|
|
91
|
+
sleep: Duration | None = _SLEEP,
|
|
92
92
|
) -> AIORedlock:
|
|
93
93
|
locks = list(locks) # skipif-ci-and-not-linux
|
|
94
94
|
error = _YieldAccessUnableToAcquireLockError( # skipif-ci-and-not-linux
|
|
95
95
|
key=key, num=num, timeout=timeout
|
|
96
96
|
)
|
|
97
|
-
async with
|
|
97
|
+
async with utilities.asyncio.timeout( # skipif-ci-and-not-linux
|
|
98
|
+
timeout, error=error
|
|
99
|
+
):
|
|
98
100
|
while True:
|
|
99
101
|
if (result := await _get_first_available_lock_if_any(locks)) is not None:
|
|
100
102
|
return result
|
|
101
|
-
await
|
|
103
|
+
await utilities.asyncio.sleep(sleep)
|
|
102
104
|
|
|
103
105
|
|
|
104
106
|
async def _get_first_available_lock_if_any(
|
|
@@ -127,7 +129,7 @@ class _YieldAccessNumLocksError(YieldAccessError):
|
|
|
127
129
|
@dataclass(kw_only=True, slots=True)
|
|
128
130
|
class _YieldAccessUnableToAcquireLockError(YieldAccessError):
|
|
129
131
|
num: int
|
|
130
|
-
timeout:
|
|
132
|
+
timeout: Duration | None
|
|
131
133
|
|
|
132
134
|
@override
|
|
133
135
|
def __str__(self) -> str:
|
utilities/pqdm.py
CHANGED
|
@@ -6,18 +6,17 @@ from typing import TYPE_CHECKING, Any, Literal, assert_never
|
|
|
6
6
|
from pqdm import processes, threads
|
|
7
7
|
from tqdm.auto import tqdm as tqdm_auto
|
|
8
8
|
|
|
9
|
-
from utilities.
|
|
9
|
+
from utilities.constants import Sentinel, sentinel
|
|
10
|
+
from utilities.core import get_func_name, is_sentinel
|
|
10
11
|
from utilities.iterables import apply_to_varargs
|
|
11
12
|
from utilities.os import get_cpu_use
|
|
12
|
-
from utilities.sentinel import Sentinel, is_sentinel, sentinel
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
15
|
from collections.abc import Callable, Iterable
|
|
16
16
|
|
|
17
17
|
from tqdm import tqdm as tqdm_type
|
|
18
18
|
|
|
19
|
-
from utilities.
|
|
20
|
-
from utilities.types import Parallelism
|
|
19
|
+
from utilities.types import IntOrAll, Parallelism
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
type _ExceptionBehaviour = Literal["ignore", "immediate", "deferred"]
|
utilities/psutil.py
CHANGED
|
@@ -6,8 +6,7 @@ from typing import TYPE_CHECKING, Self
|
|
|
6
6
|
|
|
7
7
|
from psutil import swap_memory, virtual_memory
|
|
8
8
|
|
|
9
|
-
from utilities.
|
|
10
|
-
from utilities.whenever import get_now
|
|
9
|
+
from utilities.core import get_now, suppress_super_attribute_error
|
|
11
10
|
|
|
12
11
|
if TYPE_CHECKING:
|
|
13
12
|
from whenever import ZonedDateTime
|
|
@@ -30,7 +29,7 @@ class MemoryUsage:
|
|
|
30
29
|
swap_pct: float = field(init=False)
|
|
31
30
|
|
|
32
31
|
def __post_init__(self) -> None:
|
|
33
|
-
with
|
|
32
|
+
with suppress_super_attribute_error():
|
|
34
33
|
super().__post_init__() # pyright: ignore[reportAttributeAccessIssue]
|
|
35
34
|
self.virtual_used_mb = self._to_mb(self.virtual_used)
|
|
36
35
|
self.virtual_total_mb = self._to_mb(self.virtual_total)
|
utilities/pydantic.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated, assert_never
|
|
5
|
+
|
|
6
|
+
from pydantic import BeforeValidator, SecretStr
|
|
7
|
+
|
|
8
|
+
from utilities.types import PathLike
|
|
9
|
+
|
|
10
|
+
type ExpandedPath = Annotated[PathLike, BeforeValidator(lambda p: Path(p).expanduser())]
|
|
11
|
+
type SecretLike = SecretStr | str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def extract_secret(value: SecretLike, /) -> str:
|
|
15
|
+
"""Given a secret, extract its value."""
|
|
16
|
+
match value:
|
|
17
|
+
case SecretStr():
|
|
18
|
+
return value.get_secret_value()
|
|
19
|
+
case str():
|
|
20
|
+
return value
|
|
21
|
+
case never:
|
|
22
|
+
assert_never(never)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__all__ = ["ExpandedPath", "SecretLike", "extract_secret"]
|