dycw-utilities 0.148.5__py3-none-any.whl → 0.175.31__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (84) hide show
  1. dycw_utilities-0.175.31.dist-info/METADATA +34 -0
  2. dycw_utilities-0.175.31.dist-info/RECORD +103 -0
  3. dycw_utilities-0.175.31.dist-info/WHEEL +4 -0
  4. {dycw_utilities-0.148.5.dist-info → dycw_utilities-0.175.31.dist-info}/entry_points.txt +1 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +10 -7
  7. utilities/asyncio.py +113 -64
  8. utilities/atomicwrites.py +1 -1
  9. utilities/atools.py +64 -4
  10. utilities/cachetools.py +9 -6
  11. utilities/click.py +144 -49
  12. utilities/concurrent.py +1 -1
  13. utilities/contextlib.py +4 -2
  14. utilities/contextvars.py +20 -1
  15. utilities/cryptography.py +3 -3
  16. utilities/dataclasses.py +15 -28
  17. utilities/docker.py +381 -0
  18. utilities/enum.py +2 -2
  19. utilities/errors.py +1 -1
  20. utilities/fastapi.py +8 -3
  21. utilities/fpdf2.py +2 -2
  22. utilities/functions.py +20 -297
  23. utilities/git.py +19 -0
  24. utilities/grp.py +28 -0
  25. utilities/hypothesis.py +361 -79
  26. utilities/importlib.py +17 -1
  27. utilities/inflect.py +1 -1
  28. utilities/iterables.py +12 -58
  29. utilities/jinja2.py +148 -0
  30. utilities/json.py +1 -1
  31. utilities/libcst.py +7 -7
  32. utilities/logging.py +74 -85
  33. utilities/math.py +8 -4
  34. utilities/more_itertools.py +4 -6
  35. utilities/operator.py +1 -1
  36. utilities/orjson.py +86 -34
  37. utilities/os.py +49 -2
  38. utilities/parse.py +2 -2
  39. utilities/pathlib.py +66 -34
  40. utilities/permissions.py +298 -0
  41. utilities/platform.py +4 -4
  42. utilities/polars.py +934 -420
  43. utilities/polars_ols.py +1 -1
  44. utilities/postgres.py +296 -174
  45. utilities/pottery.py +8 -73
  46. utilities/pqdm.py +3 -3
  47. utilities/pwd.py +28 -0
  48. utilities/pydantic.py +11 -0
  49. utilities/pydantic_settings.py +240 -0
  50. utilities/pydantic_settings_sops.py +76 -0
  51. utilities/pyinstrument.py +5 -5
  52. utilities/pytest.py +155 -46
  53. utilities/pytest_plugins/pytest_randomly.py +1 -1
  54. utilities/pytest_plugins/pytest_regressions.py +7 -3
  55. utilities/pytest_regressions.py +27 -8
  56. utilities/random.py +11 -6
  57. utilities/re.py +1 -1
  58. utilities/redis.py +101 -64
  59. utilities/sentinel.py +10 -0
  60. utilities/shelve.py +4 -1
  61. utilities/shutil.py +25 -0
  62. utilities/slack_sdk.py +8 -3
  63. utilities/sqlalchemy.py +422 -352
  64. utilities/sqlalchemy_polars.py +28 -52
  65. utilities/string.py +1 -1
  66. utilities/subprocess.py +1947 -0
  67. utilities/tempfile.py +95 -4
  68. utilities/testbook.py +50 -0
  69. utilities/text.py +165 -42
  70. utilities/timer.py +2 -2
  71. utilities/traceback.py +46 -36
  72. utilities/types.py +62 -23
  73. utilities/typing.py +479 -19
  74. utilities/uuid.py +42 -5
  75. utilities/version.py +27 -26
  76. utilities/whenever.py +661 -151
  77. utilities/zoneinfo.py +80 -22
  78. dycw_utilities-0.148.5.dist-info/METADATA +0 -41
  79. dycw_utilities-0.148.5.dist-info/RECORD +0 -95
  80. dycw_utilities-0.148.5.dist-info/WHEEL +0 -4
  81. dycw_utilities-0.148.5.dist-info/licenses/LICENSE +0 -21
  82. utilities/eventkit.py +0 -388
  83. utilities/period.py +0 -237
  84. utilities/typed_settings.py +0 -144
utilities/logging.py CHANGED
@@ -8,6 +8,7 @@ from logging import (
8
8
  Formatter,
9
9
  Handler,
10
10
  Logger,
11
+ LoggerAdapter,
11
12
  LogRecord,
12
13
  StreamHandler,
13
14
  basicConfig,
@@ -22,6 +23,7 @@ from socket import gethostname
22
23
  from typing import (
23
24
  TYPE_CHECKING,
24
25
  Any,
26
+ Concatenate,
25
27
  Literal,
26
28
  NotRequired,
27
29
  Self,
@@ -31,13 +33,13 @@ from typing import (
31
33
  override,
32
34
  )
33
35
 
34
- from whenever import PlainDateTime, ZonedDateTime
36
+ from whenever import ZonedDateTime
35
37
 
36
38
  from utilities.atomicwrites import move_many
37
39
  from utilities.dataclasses import replace_non_sentinel
38
40
  from utilities.errors import ImpossibleCaseError
39
41
  from utilities.iterables import OneEmptyError, always_iterable, one
40
- from utilities.pathlib import ensure_suffix, get_path
42
+ from utilities.pathlib import ensure_suffix, to_path
41
43
  from utilities.re import (
42
44
  ExtractGroupError,
43
45
  ExtractGroupsError,
@@ -45,25 +47,25 @@ from utilities.re import (
45
47
  extract_groups,
46
48
  )
47
49
  from utilities.sentinel import Sentinel, sentinel
48
- from utilities.tzlocal import LOCAL_TIME_ZONE_NAME
49
50
  from utilities.whenever import (
50
51
  WheneverLogRecord,
51
52
  format_compact,
52
53
  get_now_local,
53
- to_local_plain,
54
+ to_zoned_date_time,
54
55
  )
55
56
 
56
57
  if TYPE_CHECKING:
57
- from collections.abc import Callable, Iterable, Mapping
58
+ from collections.abc import Callable, Iterable, Mapping, MutableMapping
58
59
  from datetime import time
59
60
  from logging import _FilterType
60
61
 
61
62
  from utilities.types import (
62
- LoggerOrName,
63
+ LoggerLike,
63
64
  LogLevel,
64
65
  MaybeCallablePathLike,
65
66
  MaybeIterable,
66
67
  PathLike,
68
+ StrMapping,
67
69
  )
68
70
 
69
71
 
@@ -76,6 +78,35 @@ _DEFAULT_WHEN: _When = "D"
76
78
  ##
77
79
 
78
80
 
81
+ def add_adapter[**P](
82
+ logger: Logger,
83
+ process: Callable[Concatenate[str, P], str],
84
+ /,
85
+ *args: P.args,
86
+ **kwargs: P.kwargs,
87
+ ) -> LoggerAdapter:
88
+ """Add an adapter to a logger."""
89
+
90
+ class CustomAdapter(LoggerAdapter):
91
+ @override
92
+ def process(
93
+ self, msg: str, kwargs: MutableMapping[str, Any]
94
+ ) -> tuple[str, MutableMapping[str, Any]]:
95
+ extra = cast("_ArgsAndKwargs", self.extra)
96
+ new_msg = process(msg, *extra["args"], **extra["kwargs"])
97
+ return new_msg, kwargs
98
+
99
+ return CustomAdapter(logger, extra=_ArgsAndKwargs(args=args, kwargs=kwargs))
100
+
101
+
102
+ class _ArgsAndKwargs(TypedDict):
103
+ args: tuple[Any, ...]
104
+ kwargs: StrMapping
105
+
106
+
107
+ ##
108
+
109
+
79
110
  def add_filters(handler: Handler, /, *filters: _FilterType) -> None:
80
111
  """Add a set of filters to a handler."""
81
112
  for filter_i in filters:
@@ -87,7 +118,7 @@ def add_filters(handler: Handler, /, *filters: _FilterType) -> None:
87
118
 
88
119
  def basic_config(
89
120
  *,
90
- obj: LoggerOrName | Handler | None = None,
121
+ obj: LoggerLike | Handler | None = None,
91
122
  format_: str | None = None,
92
123
  prefix: str | None = None,
93
124
  hostname: bool = False,
@@ -121,7 +152,7 @@ def basic_config(
121
152
  )
122
153
  case str() as name:
123
154
  basic_config(
124
- obj=get_logger(logger=name),
155
+ obj=to_logger(name),
125
156
  format_=format_,
126
157
  prefix=prefix,
127
158
  hostname=hostname,
@@ -144,49 +175,13 @@ def basic_config(
144
175
  color_field_styles=color_field_styles,
145
176
  )
146
177
  handler.setFormatter(formatter)
147
- case _ as never:
178
+ case never:
148
179
  assert_never(never)
149
180
 
150
181
 
151
182
  ##
152
183
 
153
184
 
154
- def filter_for_key(
155
- key: str, /, *, default: bool = False
156
- ) -> Callable[[LogRecord], bool]:
157
- """Make a filter for a given attribute."""
158
- if (key in _FILTER_FOR_KEY_BLACKLIST) or key.startswith("__"):
159
- raise FilterForKeyError(key=key)
160
-
161
- def filter_(record: LogRecord, /) -> bool:
162
- try:
163
- value = getattr(record, key)
164
- except AttributeError:
165
- return default
166
- return bool(value)
167
-
168
- return filter_
169
-
170
-
171
- # fmt: off
172
- _FILTER_FOR_KEY_BLACKLIST = {
173
- "args", "created", "exc_info", "exc_text", "filename", "funcName", "getMessage", "levelname", "levelno", "lineno", "module", "msecs", "msg", "name", "pathname", "process", "processName", "relativeCreated", "stack_info", "taskName", "thread", "threadName"
174
- }
175
- # fmt: on
176
-
177
-
178
- @dataclass(kw_only=True, slots=True)
179
- class FilterForKeyError(Exception):
180
- key: str
181
-
182
- @override
183
- def __str__(self) -> str:
184
- return f"Invalid key: {self.key!r}"
185
-
186
-
187
- ##
188
-
189
-
190
185
  def get_format_str(*, prefix: str | None = None, hostname: bool = False) -> str:
191
186
  """Generate a format string."""
192
187
  parts: list[str] = [
@@ -268,20 +263,6 @@ def _get_plain_formatter(
268
263
  ##
269
264
 
270
265
 
271
- def get_logger(*, logger: LoggerOrName | None = None) -> Logger:
272
- """Get a logger."""
273
- match logger:
274
- case Logger():
275
- return logger
276
- case str() | None:
277
- return getLogger(logger)
278
- case _ as never:
279
- assert_never(never)
280
-
281
-
282
- ##
283
-
284
-
285
266
  def get_logging_level_number(level: LogLevel, /) -> int:
286
267
  """Get the logging level number."""
287
268
  mapping = getLevelNamesMapping()
@@ -305,13 +286,13 @@ class GetLoggingLevelNumberError(Exception):
305
286
 
306
287
  def setup_logging(
307
288
  *,
308
- logger: LoggerOrName | None = None,
289
+ logger: LoggerLike | None = None,
309
290
  format_: str | None = None,
310
291
  datefmt: str = _DEFAULT_DATEFMT,
311
292
  console_level: LogLevel = "INFO",
312
293
  console_prefix: str = "❯", # noqa: RUF001
313
294
  console_filters: MaybeIterable[_FilterType] | None = None,
314
- files_dir: MaybeCallablePathLike | None = None,
295
+ files_dir: MaybeCallablePathLike = Path.cwd,
315
296
  files_max_bytes: int = _DEFAULT_MAX_BYTES,
316
297
  files_when: _When = _DEFAULT_WHEN,
317
298
  files_interval: int = 1,
@@ -327,15 +308,14 @@ def setup_logging(
327
308
  level=console_level,
328
309
  filters=console_filters,
329
310
  )
330
- logger_use = get_logger(logger=logger)
311
+ logger_use = to_logger(logger)
331
312
  name = logger_use.name
332
- dir_ = get_path(path=files_dir)
333
313
  levels: list[LogLevel] = ["DEBUG", "INFO", "ERROR"]
334
314
  for level in levels:
335
315
  lower = level.lower()
336
316
  for stem in [lower, f"{name}-{lower}"]:
337
317
  handler = SizeAndTimeRotatingFileHandler(
338
- dir_.joinpath(stem).with_suffix(".txt"),
318
+ to_path(files_dir).joinpath(stem).with_suffix(".txt"),
339
319
  maxBytes=files_max_bytes,
340
320
  when=files_when,
341
321
  interval=files_interval,
@@ -407,9 +387,7 @@ class SizeAndTimeRotatingFileHandler(BaseRotatingHandler):
407
387
  def emit(self, record: LogRecord) -> None:
408
388
  try:
409
389
  if (self._backup_count is not None) and self._should_rollover(record):
410
- self._do_rollover( # skipif-ci-and-windows
411
- backup_count=self._backup_count
412
- )
390
+ self._do_rollover(backup_count=self._backup_count)
413
391
  FileHandler.emit(self, record)
414
392
  except Exception: # noqa: BLE001 # pragma: no cover
415
393
  self.handleError(record)
@@ -419,23 +397,23 @@ class SizeAndTimeRotatingFileHandler(BaseRotatingHandler):
419
397
  self.stream.close()
420
398
  self.stream = None
421
399
 
422
- actions = _compute_rollover_actions( # skipif-ci-and-windows
400
+ actions = _compute_rollover_actions(
423
401
  self._directory,
424
402
  self._stem,
425
403
  self._suffix,
426
404
  patterns=self._patterns,
427
405
  backup_count=backup_count,
428
406
  )
429
- actions.do() # skipif-ci-and-windows
407
+ actions.do()
430
408
 
431
409
  if not self.delay: # pragma: no cover
432
410
  self.stream = self._open()
433
- self._time_handler.rolloverAt = ( # skipif-ci-and-windows
434
- self._time_handler.computeRollover(get_now_local().timestamp())
411
+ self._time_handler.rolloverAt = self._time_handler.computeRollover(
412
+ get_now_local().timestamp()
435
413
  )
436
414
 
437
415
  def _should_rollover(self, record: LogRecord, /) -> bool:
438
- if self._max_bytes is not None: # skipif-ci-and-windows
416
+ if self._max_bytes is not None:
439
417
  try:
440
418
  size = self._filename.stat().st_size
441
419
  except FileNotFoundError:
@@ -443,14 +421,14 @@ class SizeAndTimeRotatingFileHandler(BaseRotatingHandler):
443
421
  else:
444
422
  if size >= self._max_bytes:
445
423
  return True
446
- return bool(self._time_handler.shouldRollover(record)) # skipif-ci-and-windows
424
+ return bool(self._time_handler.shouldRollover(record))
447
425
 
448
426
 
449
427
  def _compute_rollover_patterns(stem: str, suffix: str, /) -> _RolloverPatterns:
450
428
  return _RolloverPatterns(
451
429
  pattern1=re.compile(rf"^{stem}\.(\d+){suffix}$"),
452
- pattern2=re.compile(rf"^{stem}\.(\d+)__([\dT]+?){suffix}$"),
453
- pattern3=re.compile(rf"^{stem}\.(\d+)__([\dT]+?)__([\dT]+?){suffix}$"),
430
+ pattern2=re.compile(rf"^{stem}\.(\d+)__(.+?){suffix}$"),
431
+ pattern3=re.compile(rf"^{stem}\.(\d+)__(.+?)__(.+?){suffix}$"),
454
432
  )
455
433
 
456
434
 
@@ -550,10 +528,8 @@ class _RotatingLogFile:
550
528
  stem=stem,
551
529
  suffix=suffix,
552
530
  index=int(index),
553
- start=PlainDateTime.parse_common_iso(start).assume_tz(
554
- LOCAL_TIME_ZONE_NAME
555
- ),
556
- end=PlainDateTime.parse_common_iso(end).assume_tz(LOCAL_TIME_ZONE_NAME),
531
+ start=to_zoned_date_time(start),
532
+ end=to_zoned_date_time(end),
557
533
  )
558
534
  try:
559
535
  index, end = extract_groups(patterns.pattern2, path.name)
@@ -565,7 +541,7 @@ class _RotatingLogFile:
565
541
  stem=stem,
566
542
  suffix=suffix,
567
543
  index=int(index),
568
- end=PlainDateTime.parse_common_iso(end).assume_tz(LOCAL_TIME_ZONE_NAME),
544
+ end=to_zoned_date_time(end),
569
545
  )
570
546
  try:
571
547
  index = extract_group(patterns.pattern1, path.name)
@@ -586,9 +562,9 @@ class _RotatingLogFile:
586
562
  case int() as index, None, None:
587
563
  tail = str(index)
588
564
  case int() as index, None, ZonedDateTime() as end:
589
- tail = f"{index}__{format_compact(to_local_plain(end))}"
565
+ tail = f"{index}__{format_compact(end, path=True)}"
590
566
  case int() as index, ZonedDateTime() as start, ZonedDateTime() as end:
591
- tail = f"{index}__{format_compact(to_local_plain(start))}__{format_compact(to_local_plain(end))}"
567
+ tail = f"{index}__{format_compact(start, path=True)}__{format_compact(end, path=True)}"
592
568
  case _: # pragma: no cover
593
569
  raise ImpossibleCaseError(
594
570
  case=[f"{self.index=}", f"{self.start=}", f"{self.end=}"]
@@ -626,15 +602,28 @@ class _Rotation:
626
602
  return self.file.replace(index=self.index, start=self.start, end=self.end).path
627
603
 
628
604
 
605
+ ##
606
+
607
+
608
+ def to_logger(logger: LoggerLike | None = None, /) -> Logger:
609
+ """Convert to a logger."""
610
+ match logger:
611
+ case Logger():
612
+ return logger
613
+ case str() | None:
614
+ return getLogger(logger)
615
+ case never:
616
+ assert_never(never)
617
+
618
+
629
619
  __all__ = [
630
- "FilterForKeyError",
631
620
  "GetLoggingLevelNumberError",
632
621
  "SizeAndTimeRotatingFileHandler",
622
+ "add_adapter",
633
623
  "add_filters",
634
624
  "basic_config",
635
- "filter_for_key",
636
625
  "get_format_str",
637
- "get_logger",
638
626
  "get_logging_level_number",
639
627
  "setup_logging",
628
+ "to_logger",
640
629
  ]
utilities/math.py CHANGED
@@ -641,7 +641,10 @@ def _is_close(
641
641
  ##
642
642
 
643
643
 
644
- def number_of_decimals(x: float, /, *, max_decimals: int = 20) -> int:
644
+ MAX_DECIMALS = 10
645
+
646
+
647
+ def number_of_decimals(x: float, /, *, max_decimals: int = MAX_DECIMALS) -> int:
645
648
  """Get the number of decimals."""
646
649
  _, frac = divmod(x, 1)
647
650
  results = (
@@ -731,7 +734,7 @@ def round_(
731
734
  return 0
732
735
  case -1:
733
736
  return floor(x)
734
- case _ as never:
737
+ case never:
735
738
  assert_never(never)
736
739
  case "standard-tie-floor":
737
740
  return _round_tie_standard(x, "floor", rel_tol=rel_tol, abs_tol=abs_tol)
@@ -743,7 +746,7 @@ def round_(
743
746
  )
744
747
  case "standard-tie-away-zero":
745
748
  return _round_tie_standard(x, "away-zero", rel_tol=rel_tol, abs_tol=abs_tol)
746
- case _ as never:
749
+ case never:
747
750
  assert_never(never)
748
751
 
749
752
 
@@ -876,7 +879,7 @@ def sign(
876
879
  if is_negative(x, rel_tol=rel_tol, abs_tol=abs_tol):
877
880
  return -1
878
881
  return 0
879
- case _ as never:
882
+ case never:
880
883
  assert_never(never)
881
884
 
882
885
 
@@ -889,6 +892,7 @@ def significant_figures(x: float, /, *, n: int = 2) -> str:
889
892
 
890
893
 
891
894
  __all__ = [
895
+ "MAX_DECIMALS",
892
896
  "MAX_FLOAT32",
893
897
  "MAX_FLOAT64",
894
898
  "MAX_INT8",
@@ -21,7 +21,7 @@ from more_itertools import peekable as _peekable
21
21
  from utilities.functions import get_class_name
22
22
  from utilities.iterables import OneNonUniqueError, one
23
23
  from utilities.reprlib import get_repr
24
- from utilities.sentinel import Sentinel, sentinel
24
+ from utilities.sentinel import Sentinel, is_sentinel, sentinel
25
25
 
26
26
  if TYPE_CHECKING:
27
27
  from collections.abc import Iterable, Iterator, Mapping, Sequence
@@ -206,7 +206,7 @@ def bucket_mapping[T, U, UH: Hashable](
206
206
  return {k: frozenset(map(pre, v)) for k, v in mapping.items()}
207
207
  case Callable(), "unique":
208
208
  return _bucket_mapping_unique({k: map(pre, v) for k, v in mapping.items()})
209
- case _ as never:
209
+ case never:
210
210
  assert_never(never)
211
211
 
212
212
 
@@ -290,9 +290,7 @@ class peekable[T](_peekable): # noqa: N801
290
290
  def peek[U](self, *, default: U) -> T | U: ...
291
291
  @override
292
292
  def peek(self, *, default: Any = sentinel) -> Any: # pyright: ignore[reportIncompatibleMethodOverride]
293
- if isinstance(default, Sentinel):
294
- return super().peek()
295
- return super().peek(default=default)
293
+ return super().peek() if is_sentinel(default) else super().peek(default=default)
296
294
 
297
295
  def takewhile(self, predicate: Callable[[T], bool], /) -> Iterator[T]:
298
296
  while bool(self) and predicate(self.peek()):
@@ -374,7 +372,7 @@ def _yield_splits2[T](
374
372
  len_tail = max(len_win - head, 0)
375
373
  if len_tail >= 1:
376
374
  yield window, head, len_tail
377
- case _ as never:
375
+ case never:
378
376
  assert_never(never)
379
377
 
380
378
 
utilities/operator.py CHANGED
@@ -6,9 +6,9 @@ from dataclasses import asdict, dataclass
6
6
  from typing import TYPE_CHECKING, Any, cast, override
7
7
 
8
8
  import utilities.math
9
- from utilities.functions import is_dataclass_instance
10
9
  from utilities.iterables import SortIterableError, sort_iterable
11
10
  from utilities.reprlib import get_repr
11
+ from utilities.typing import is_dataclass_instance
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from utilities.types import Dataclass, Number