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.
Files changed (96) hide show
  1. dycw_utilities-0.185.8.dist-info/METADATA +33 -0
  2. dycw_utilities-0.185.8.dist-info/RECORD +90 -0
  3. {dycw_utilities-0.166.30.dist-info → dycw_utilities-0.185.8.dist-info}/WHEEL +1 -1
  4. {dycw_utilities-0.166.30.dist-info → dycw_utilities-0.185.8.dist-info}/entry_points.txt +1 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +17 -10
  7. utilities/asyncio.py +50 -72
  8. utilities/atools.py +9 -11
  9. utilities/cachetools.py +16 -11
  10. utilities/click.py +76 -19
  11. utilities/concurrent.py +1 -1
  12. utilities/constants.py +492 -0
  13. utilities/contextlib.py +23 -30
  14. utilities/contextvars.py +1 -23
  15. utilities/core.py +2581 -0
  16. utilities/dataclasses.py +16 -119
  17. utilities/docker.py +387 -0
  18. utilities/enum.py +1 -1
  19. utilities/errors.py +2 -16
  20. utilities/fastapi.py +5 -5
  21. utilities/fpdf2.py +2 -1
  22. utilities/functions.py +34 -265
  23. utilities/http.py +2 -3
  24. utilities/hypothesis.py +84 -29
  25. utilities/importlib.py +17 -1
  26. utilities/iterables.py +39 -575
  27. utilities/jinja2.py +145 -0
  28. utilities/jupyter.py +5 -3
  29. utilities/libcst.py +1 -1
  30. utilities/lightweight_charts.py +4 -6
  31. utilities/logging.py +24 -24
  32. utilities/math.py +1 -36
  33. utilities/more_itertools.py +4 -6
  34. utilities/numpy.py +2 -1
  35. utilities/operator.py +2 -2
  36. utilities/orjson.py +42 -43
  37. utilities/os.py +4 -147
  38. utilities/packaging.py +129 -0
  39. utilities/parse.py +35 -15
  40. utilities/pathlib.py +3 -120
  41. utilities/platform.py +8 -90
  42. utilities/polars.py +38 -32
  43. utilities/postgres.py +37 -33
  44. utilities/pottery.py +20 -18
  45. utilities/pqdm.py +3 -4
  46. utilities/psutil.py +2 -3
  47. utilities/pydantic.py +25 -0
  48. utilities/pydantic_settings.py +87 -16
  49. utilities/pydantic_settings_sops.py +16 -3
  50. utilities/pyinstrument.py +4 -4
  51. utilities/pytest.py +96 -125
  52. utilities/pytest_plugins/pytest_regressions.py +2 -2
  53. utilities/pytest_regressions.py +32 -11
  54. utilities/random.py +2 -8
  55. utilities/redis.py +98 -94
  56. utilities/reprlib.py +11 -118
  57. utilities/shellingham.py +66 -0
  58. utilities/shutil.py +25 -0
  59. utilities/slack_sdk.py +13 -12
  60. utilities/sqlalchemy.py +57 -30
  61. utilities/sqlalchemy_polars.py +16 -25
  62. utilities/subprocess.py +2590 -0
  63. utilities/tabulate.py +32 -0
  64. utilities/testbook.py +8 -8
  65. utilities/text.py +24 -99
  66. utilities/throttle.py +159 -0
  67. utilities/time.py +18 -0
  68. utilities/timer.py +31 -14
  69. utilities/traceback.py +16 -23
  70. utilities/types.py +42 -2
  71. utilities/typing.py +26 -14
  72. utilities/uuid.py +1 -1
  73. utilities/version.py +202 -45
  74. utilities/whenever.py +53 -150
  75. dycw_utilities-0.166.30.dist-info/METADATA +0 -41
  76. dycw_utilities-0.166.30.dist-info/RECORD +0 -98
  77. dycw_utilities-0.166.30.dist-info/licenses/LICENSE +0 -21
  78. utilities/aeventkit.py +0 -388
  79. utilities/atomicwrites.py +0 -182
  80. utilities/cryptography.py +0 -41
  81. utilities/getpass.py +0 -8
  82. utilities/git.py +0 -19
  83. utilities/gzip.py +0 -31
  84. utilities/json.py +0 -70
  85. utilities/pickle.py +0 -25
  86. utilities/re.py +0 -156
  87. utilities/sentinel.py +0 -73
  88. utilities/socket.py +0 -8
  89. utilities/string.py +0 -20
  90. utilities/tempfile.py +0 -77
  91. utilities/typed_settings.py +0 -152
  92. utilities/tzdata.py +0 -11
  93. utilities/tzlocal.py +0 -28
  94. utilities/warnings.py +0 -65
  95. utilities/zipfile.py +0 -25
  96. 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.reprlib import get_repr
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 {get_repr(self.row)}"
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 {get_repr(self.extra)}"
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 {get_repr(self.missing)}"
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 {get_repr(self.columns)}"
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 {get_repr(self.columns)}; got {get_repr(self.df.columns)}:\n\n{self.df}"
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 {get_repr(self.dtypes)}; got {get_repr(self.df.dtypes)}:\n\n{self.df}"
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 {get_repr(self.missing)}"
656
+ yield f"missing columns were {repr_(self.missing)}"
654
657
  if len(self.failed) >= 1:
655
- yield f"failed predicates were {get_repr(self.failed)}"
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 {get_repr(self.schema)} (ordered); got {get_repr(self.df.schema)}:\n\n{self.df}"
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 {get_repr(self.schema)} (unordered); got {get_repr(self.df.schema)}:\n\n{self.df}"
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 {get_repr(self.schema)} (unordered); got {get_repr(self.df.schema)}:\n\n{self.df}"
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 {get_repr(self.by)}:\n\n{self.df}"
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 {get_repr(self.by)}:\n\n{self.df}"
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 {get_repr(self.objs)} must contain exactly 1 class; got {self.first}, {self.second} and perhaps more"
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: dict[str, Any] = {}
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: dict[str, Any] = {}
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 = read_binary(path, decompress=decompress)
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
- write_formatted_json(data, path, compress=compress, overwrite=overwrite)
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 = read_binary(path, decompress=decompress)
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
- write_formatted_json(data, path, compress=compress, overwrite=overwrite)
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.to_frame()
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 {get_repr(self.columns)} remaining"
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.iterables import always_iterable
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 temp_environ(PGPASSWORD=url.password), Timer() as timer: # pragma: no cover
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
- "pg_dump",
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
- docker: str | None = None,
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
- docker=docker,
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 temp_environ(PGPASSWORD=url.password), Timer() as timer: # pragma: no cover
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
- docker: str | None = None,
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, docker=docker)
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
- docker=docker,
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
- docker: str | None = None,
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
- "pg_restore",
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(url: URL, path: PathLike, /, *, docker: str | None = None) -> str:
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
- "psql",
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
- from utilities.asyncio import sleep_td, timeout_td
12
+ import utilities.asyncio
13
+ from utilities.constants import MILLISECOND, SECOND
13
14
  from utilities.contextlib import enhanced_async_context_manager
14
- from utilities.iterables import always_iterable
15
- from utilities.whenever import MILLISECOND, SECOND, to_nanoseconds
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 whenever import Delta
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: Delta = 10 * SECOND
26
- _SLEEP: Delta = MILLISECOND
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: Delta = _TIMEOUT_RELEASE,
50
+ timeout_release: Duration = _TIMEOUT_RELEASE,
51
51
  num_extensions: int | None = None,
52
- timeout_acquire: Delta | None = None,
53
- sleep: Delta = _SLEEP,
54
- throttle: Delta | None = None,
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=to_nanoseconds(timeout_release) / 1e9,
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 sleep_td(throttle)
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: Delta | None = None,
91
- sleep: Delta | None = _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 timeout_td(timeout, error=error): # skipif-ci-and-not-linux
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 sleep_td(sleep)
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: Delta | None
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.functions import get_func_name
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.os import IntOrAll
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.contextlib import suppress_super_object_attribute_error
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 suppress_super_object_attribute_error():
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"]