dycw-utilities 0.129.10__py3-none-any.whl → 0.175.17__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 (103) hide show
  1. dycw_utilities-0.175.17.dist-info/METADATA +34 -0
  2. dycw_utilities-0.175.17.dist-info/RECORD +103 -0
  3. dycw_utilities-0.175.17.dist-info/WHEEL +4 -0
  4. dycw_utilities-0.175.17.dist-info/entry_points.txt +4 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +14 -14
  7. utilities/asyncio.py +350 -819
  8. utilities/atomicwrites.py +18 -6
  9. utilities/atools.py +77 -22
  10. utilities/cachetools.py +24 -29
  11. utilities/click.py +393 -237
  12. utilities/concurrent.py +8 -11
  13. utilities/contextlib.py +216 -17
  14. utilities/contextvars.py +20 -1
  15. utilities/cryptography.py +3 -3
  16. utilities/dataclasses.py +83 -118
  17. utilities/docker.py +293 -0
  18. utilities/enum.py +26 -23
  19. utilities/errors.py +17 -3
  20. utilities/fastapi.py +29 -65
  21. utilities/fpdf2.py +3 -3
  22. utilities/functions.py +169 -416
  23. utilities/functools.py +18 -19
  24. utilities/git.py +9 -30
  25. utilities/grp.py +28 -0
  26. utilities/gzip.py +31 -0
  27. utilities/http.py +3 -2
  28. utilities/hypothesis.py +738 -589
  29. utilities/importlib.py +17 -1
  30. utilities/inflect.py +25 -0
  31. utilities/iterables.py +194 -262
  32. utilities/jinja2.py +148 -0
  33. utilities/json.py +70 -0
  34. utilities/libcst.py +38 -17
  35. utilities/lightweight_charts.py +5 -9
  36. utilities/logging.py +345 -543
  37. utilities/math.py +18 -13
  38. utilities/memory_profiler.py +11 -15
  39. utilities/more_itertools.py +200 -131
  40. utilities/operator.py +33 -29
  41. utilities/optuna.py +6 -6
  42. utilities/orjson.py +272 -137
  43. utilities/os.py +61 -4
  44. utilities/parse.py +59 -61
  45. utilities/pathlib.py +281 -40
  46. utilities/permissions.py +298 -0
  47. utilities/pickle.py +2 -2
  48. utilities/platform.py +24 -5
  49. utilities/polars.py +1214 -430
  50. utilities/polars_ols.py +1 -1
  51. utilities/postgres.py +408 -0
  52. utilities/pottery.py +113 -26
  53. utilities/pqdm.py +10 -11
  54. utilities/psutil.py +6 -57
  55. utilities/pwd.py +28 -0
  56. utilities/pydantic.py +4 -54
  57. utilities/pydantic_settings.py +240 -0
  58. utilities/pydantic_settings_sops.py +76 -0
  59. utilities/pyinstrument.py +8 -10
  60. utilities/pytest.py +227 -121
  61. utilities/pytest_plugins/__init__.py +1 -0
  62. utilities/pytest_plugins/pytest_randomly.py +23 -0
  63. utilities/pytest_plugins/pytest_regressions.py +56 -0
  64. utilities/pytest_regressions.py +26 -46
  65. utilities/random.py +13 -9
  66. utilities/re.py +58 -28
  67. utilities/redis.py +401 -550
  68. utilities/scipy.py +1 -1
  69. utilities/sentinel.py +10 -0
  70. utilities/shelve.py +4 -1
  71. utilities/shutil.py +25 -0
  72. utilities/slack_sdk.py +36 -106
  73. utilities/sqlalchemy.py +502 -473
  74. utilities/sqlalchemy_polars.py +38 -94
  75. utilities/string.py +2 -3
  76. utilities/subprocess.py +1572 -0
  77. utilities/tempfile.py +86 -4
  78. utilities/testbook.py +50 -0
  79. utilities/text.py +165 -42
  80. utilities/timer.py +37 -65
  81. utilities/traceback.py +158 -929
  82. utilities/types.py +146 -116
  83. utilities/typing.py +531 -71
  84. utilities/tzdata.py +1 -53
  85. utilities/tzlocal.py +6 -23
  86. utilities/uuid.py +43 -5
  87. utilities/version.py +27 -26
  88. utilities/whenever.py +1776 -386
  89. utilities/zoneinfo.py +84 -22
  90. dycw_utilities-0.129.10.dist-info/METADATA +0 -241
  91. dycw_utilities-0.129.10.dist-info/RECORD +0 -96
  92. dycw_utilities-0.129.10.dist-info/WHEEL +0 -4
  93. dycw_utilities-0.129.10.dist-info/licenses/LICENSE +0 -21
  94. utilities/datetime.py +0 -1409
  95. utilities/eventkit.py +0 -402
  96. utilities/loguru.py +0 -144
  97. utilities/luigi.py +0 -228
  98. utilities/period.py +0 -324
  99. utilities/pyrsistent.py +0 -89
  100. utilities/python_dotenv.py +0 -105
  101. utilities/streamlit.py +0 -105
  102. utilities/sys.py +0 -87
  103. utilities/tenacity.py +0 -145
utilities/jinja2.py ADDED
@@ -0,0 +1,148 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Any, Literal, assert_never, override
5
+
6
+ from jinja2 import BaseLoader, BytecodeCache, Environment, FileSystemLoader, Undefined
7
+ from jinja2.defaults import (
8
+ BLOCK_END_STRING,
9
+ BLOCK_START_STRING,
10
+ COMMENT_END_STRING,
11
+ COMMENT_START_STRING,
12
+ KEEP_TRAILING_NEWLINE,
13
+ LINE_COMMENT_PREFIX,
14
+ LINE_STATEMENT_PREFIX,
15
+ LSTRIP_BLOCKS,
16
+ NEWLINE_SEQUENCE,
17
+ TRIM_BLOCKS,
18
+ VARIABLE_END_STRING,
19
+ VARIABLE_START_STRING,
20
+ )
21
+
22
+ from utilities.atomicwrites import writer
23
+ from utilities.text import kebab_case, pascal_case, snake_case
24
+
25
+ if TYPE_CHECKING:
26
+ from collections.abc import Callable, Sequence
27
+ from pathlib import Path
28
+
29
+ from jinja2.ext import Extension
30
+
31
+ from utilities.types import StrMapping
32
+
33
+
34
+ class EnhancedEnvironment(Environment):
35
+ """Environment with enhanced features."""
36
+
37
+ @override
38
+ def __init__(
39
+ self,
40
+ block_start_string: str = BLOCK_START_STRING,
41
+ block_end_string: str = BLOCK_END_STRING,
42
+ variable_start_string: str = VARIABLE_START_STRING,
43
+ variable_end_string: str = VARIABLE_END_STRING,
44
+ comment_start_string: str = COMMENT_START_STRING,
45
+ comment_end_string: str = COMMENT_END_STRING,
46
+ line_statement_prefix: str | None = LINE_STATEMENT_PREFIX,
47
+ line_comment_prefix: str | None = LINE_COMMENT_PREFIX,
48
+ trim_blocks: bool = TRIM_BLOCKS,
49
+ lstrip_blocks: bool = LSTRIP_BLOCKS,
50
+ newline_sequence: Literal["\n", "\r\n", "\r"] = NEWLINE_SEQUENCE,
51
+ keep_trailing_newline: bool = KEEP_TRAILING_NEWLINE,
52
+ extensions: Sequence[str | type[Extension]] = (),
53
+ optimized: bool = True,
54
+ undefined: type[Undefined] = Undefined,
55
+ finalize: Callable[..., Any] | None = None,
56
+ autoescape: bool | Callable[[str | None], bool] = False,
57
+ loader: BaseLoader | None = None,
58
+ cache_size: int = 400,
59
+ auto_reload: bool = True,
60
+ bytecode_cache: BytecodeCache | None = None,
61
+ enable_async: bool = False,
62
+ ) -> None:
63
+ super().__init__(
64
+ block_start_string,
65
+ block_end_string,
66
+ variable_start_string,
67
+ variable_end_string,
68
+ comment_start_string,
69
+ comment_end_string,
70
+ line_statement_prefix,
71
+ line_comment_prefix,
72
+ trim_blocks,
73
+ lstrip_blocks,
74
+ newline_sequence,
75
+ keep_trailing_newline,
76
+ extensions,
77
+ optimized,
78
+ undefined,
79
+ finalize,
80
+ autoescape,
81
+ loader,
82
+ cache_size,
83
+ auto_reload,
84
+ bytecode_cache,
85
+ enable_async,
86
+ )
87
+ self.filters["kebab"] = kebab_case
88
+ self.filters["pascal"] = pascal_case
89
+ self.filters["snake"] = snake_case
90
+
91
+
92
+ @dataclass(order=True, unsafe_hash=True, kw_only=True, slots=True)
93
+ class TemplateJob:
94
+ """A template with an associated rendering job."""
95
+
96
+ template: Path
97
+ kwargs: StrMapping
98
+ target: Path
99
+ mode: Literal["write", "append"] = "write"
100
+
101
+ def __post_init__(self) -> None:
102
+ if not self.template.exists():
103
+ raise _TemplateJobTemplateDoesNotExistError(path=self.template)
104
+ if (self.mode == "append") and not self.target.exists():
105
+ raise _TemplateJobTargetDoesNotExistError(path=self.template)
106
+
107
+ def run(self) -> None:
108
+ """Run the job."""
109
+ match self.mode:
110
+ case "write":
111
+ with writer(self.target, overwrite=True) as temp:
112
+ _ = temp.write_text(self.rendered)
113
+ case "append":
114
+ with self.target.open(mode="a") as fh:
115
+ _ = fh.write(self.rendered)
116
+ case never:
117
+ assert_never(never)
118
+
119
+ @property
120
+ def rendered(self) -> str:
121
+ """The template, rendered."""
122
+ env = EnhancedEnvironment(loader=FileSystemLoader(self.template.parent))
123
+ return env.get_template(self.template.name).render(self.kwargs)
124
+
125
+
126
+ @dataclass(kw_only=True, slots=True)
127
+ class TemplateJobError(Exception): ...
128
+
129
+
130
+ @dataclass(kw_only=True, slots=True)
131
+ class _TemplateJobTemplateDoesNotExistError(TemplateJobError):
132
+ path: Path
133
+
134
+ @override
135
+ def __str__(self) -> str:
136
+ return f"Template {str(self.path)!r} does not exist"
137
+
138
+
139
+ @dataclass(kw_only=True, slots=True)
140
+ class _TemplateJobTargetDoesNotExistError(TemplateJobError):
141
+ path: Path
142
+
143
+ @override
144
+ def __str__(self) -> str:
145
+ return f"Target {str(self.path)!r} does not exist"
146
+
147
+
148
+ __all__ = ["EnhancedEnvironment", "TemplateJob", "TemplateJobError"]
utilities/json.py ADDED
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import suppress
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from subprocess import check_output
7
+ from typing import TYPE_CHECKING, assert_never, overload, override
8
+
9
+ from utilities.atomicwrites import writer
10
+ from utilities.gzip import write_binary
11
+
12
+ if TYPE_CHECKING:
13
+ from utilities.types import PathLike
14
+
15
+
16
+ ##
17
+
18
+
19
+ @overload
20
+ def run_prettier(source: bytes, /) -> bytes: ...
21
+ @overload
22
+ def run_prettier(source: str, /) -> str: ...
23
+ @overload
24
+ def run_prettier(source: Path, /) -> None: ...
25
+ def run_prettier(source: bytes | str | Path, /) -> bytes | str | None:
26
+ """Run `prettier` on a string/path."""
27
+ match source: # skipif-ci
28
+ case bytes() as data:
29
+ return _run_prettier_core(data, text=False)
30
+ case str() as text:
31
+ if (path := Path(text)).is_file():
32
+ return run_prettier(path)
33
+ return _run_prettier_core(text, text=True)
34
+ case Path() as path:
35
+ result = run_prettier(path.read_bytes())
36
+ with writer(path, overwrite=True) as temp:
37
+ _ = temp.write_bytes(result)
38
+ return None
39
+ case never:
40
+ assert_never(never)
41
+
42
+
43
+ def _run_prettier_core(data: bytes | str, /, *, text: bool) -> bytes | str:
44
+ """Run `prettier` on a string/path."""
45
+ try: # skipif-ci
46
+ return check_output(["prettier", "--parser=json"], input=data, text=text)
47
+ except FileNotFoundError: # pragma: no cover
48
+ raise RunPrettierError from None
49
+
50
+
51
+ @dataclass(kw_only=True, slots=True)
52
+ class RunPrettierError(Exception):
53
+ @override
54
+ def __str__(self) -> str:
55
+ return "Unable to find 'prettier'" # pragma: no cover
56
+
57
+
58
+ ##
59
+
60
+
61
+ def write_formatted_json(
62
+ data: bytes, path: PathLike, /, *, compress: bool = False, overwrite: bool = False
63
+ ) -> None:
64
+ """Write a formatted byte string to disk."""
65
+ with suppress(RunPrettierError):
66
+ data = run_prettier(data)
67
+ write_binary(data, path, compress=compress, overwrite=overwrite)
68
+
69
+
70
+ __all__ = ["RunPrettierError", "run_prettier", "write_formatted_json"]
utilities/libcst.py CHANGED
@@ -23,16 +23,6 @@ from libcst import (
23
23
  from utilities.errors import ImpossibleCaseError
24
24
 
25
25
 
26
- def generate_from_import(
27
- module: str, name: str, /, *, asname: str | None = None
28
- ) -> ImportFrom:
29
- """Generate an `ImportFrom` object."""
30
- alias = ImportAlias(
31
- name=Name(name), asname=AsName(Name(asname)) if asname else None
32
- )
33
- return ImportFrom(module=split_dotted_str(module), names=[alias])
34
-
35
-
36
26
  def generate_f_string(var: str, suffix: str, /) -> FormattedString:
37
27
  """Generate an f-string."""
38
28
  return FormattedString([
@@ -49,6 +39,36 @@ def generate_import(module: str, /, *, asname: str | None = None) -> Import:
49
39
  return Import(names=[alias])
50
40
 
51
41
 
42
+ def generate_import_from(
43
+ module: str, name: str, /, *, asname: str | None = None
44
+ ) -> ImportFrom:
45
+ """Generate an `ImportFrom` object."""
46
+ match name, asname:
47
+ case "*", None:
48
+ names = ImportStar()
49
+ case "*", str():
50
+ raise GenerateImportFromError(module=module, asname=asname)
51
+ case _, None:
52
+ alias = ImportAlias(name=Name(name))
53
+ names = [alias]
54
+ case _, str():
55
+ alias = ImportAlias(name=Name(name), asname=AsName(Name(asname)))
56
+ names = [alias]
57
+ case never:
58
+ assert_never(never)
59
+ return ImportFrom(module=split_dotted_str(module), names=names)
60
+
61
+
62
+ @dataclass(kw_only=True, slots=True)
63
+ class GenerateImportFromError(Exception):
64
+ module: str
65
+ asname: str | None = None
66
+
67
+ @override
68
+ def __str__(self) -> str:
69
+ return f"Invalid import: 'from {self.module} import * as {self.asname}'"
70
+
71
+
52
72
  ##
53
73
 
54
74
 
@@ -72,9 +92,9 @@ def parse_import(import_: Import | ImportFrom, /) -> Sequence[_ParseImportOutput
72
92
  return [_parse_import_from_one(module, n) for n in names]
73
93
  case ImportStar():
74
94
  return [_ParseImportOutput(module=module, name="*")]
75
- case _ as never:
95
+ case never:
76
96
  assert_never(never)
77
- case _ as never:
97
+ case never:
78
98
  assert_never(never)
79
99
 
80
100
 
@@ -88,7 +108,7 @@ def _parse_import_from_one(module: str, alias: ImportAlias, /) -> _ParseImportOu
88
108
  return _ParseImportOutput(module=module, name=name)
89
109
  case Attribute() as attr:
90
110
  raise _ParseImportAliasError(module=module, attr=attr)
91
- case _ as never:
111
+ case never:
92
112
  assert_never(never)
93
113
 
94
114
 
@@ -130,7 +150,7 @@ def split_dotted_str(dotted: str, /) -> Name | Attribute:
130
150
 
131
151
  def join_dotted_str(name_or_attr: Name | Attribute, /) -> str:
132
152
  """Join a dotted from from a name/attribute."""
133
- parts: Sequence[str] = []
153
+ parts: list[str] = []
134
154
  curr: BaseExpression | Name | Attribute = name_or_attr
135
155
  while True:
136
156
  match curr:
@@ -142,7 +162,7 @@ def join_dotted_str(name_or_attr: Name | Attribute, /) -> str:
142
162
  curr = value
143
163
  case BaseExpression(): # pragma: no cover
144
164
  raise ImpossibleCaseError(case=[f"{curr=}"])
145
- case _ as never:
165
+ case never:
146
166
  assert_never(never)
147
167
  return ".".join(reversed(parts))
148
168
 
@@ -160,7 +180,7 @@ def render_module(source: str | Module, /) -> str:
160
180
  return text
161
181
  case Module() as module:
162
182
  return render_module(module.code)
163
- case _ as never:
183
+ case never:
164
184
  assert_never(never)
165
185
 
166
186
 
@@ -168,10 +188,11 @@ def render_module(source: str | Module, /) -> str:
168
188
 
169
189
 
170
190
  __all__ = [
191
+ "GenerateImportFromError",
171
192
  "ParseImportError",
172
193
  "generate_f_string",
173
- "generate_from_import",
174
194
  "generate_import",
195
+ "generate_import_from",
175
196
  "join_dotted_str",
176
197
  "parse_import",
177
198
  "render_module",
@@ -1,9 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
- from contextlib import asynccontextmanager
4
3
  from dataclasses import dataclass
5
4
  from typing import TYPE_CHECKING, override
6
5
 
6
+ from utilities.atomicwrites import writer # pragma: no cover
7
+ from utilities.contextlib import enhanced_async_context_manager
7
8
  from utilities.iterables import OneEmptyError, OneNonUniqueError, one
8
9
  from utilities.reprlib import get_repr
9
10
 
@@ -23,14 +24,9 @@ if TYPE_CHECKING:
23
24
 
24
25
  def save_chart(chart: Chart, path: PathLike, /, *, overwrite: bool = False) -> None:
25
26
  """Atomically save a chart to disk."""
26
- from utilities.atomicwrites import writer # pragma: no cover
27
-
28
27
  chart.show(block=False) # pragma: no cover
29
- with ( # pragma: no cover
30
- writer(path, overwrite=overwrite) as temp,
31
- temp.open(mode="wb") as fh,
32
- ):
33
- _ = fh.write(chart.screenshot())
28
+ with writer(path, overwrite=overwrite) as temp: # pragma: no cover
29
+ _ = temp.write_bytes(chart.screenshot())
34
30
  chart.exit() # pragma: no cover
35
31
 
36
32
 
@@ -82,7 +78,7 @@ class _SetDataFrameNonUniqueError(SetDataFrameError):
82
78
  ##
83
79
 
84
80
 
85
- @asynccontextmanager
81
+ @enhanced_async_context_manager
86
82
  async def yield_chart(chart: Chart, /) -> AsyncIterator[None]:
87
83
  """Yield a chart for visualization in a notebook."""
88
84
  try: # pragma: no cover