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.
- dycw_utilities-0.175.17.dist-info/METADATA +34 -0
- dycw_utilities-0.175.17.dist-info/RECORD +103 -0
- dycw_utilities-0.175.17.dist-info/WHEEL +4 -0
- dycw_utilities-0.175.17.dist-info/entry_points.txt +4 -0
- utilities/__init__.py +1 -1
- utilities/altair.py +14 -14
- utilities/asyncio.py +350 -819
- utilities/atomicwrites.py +18 -6
- utilities/atools.py +77 -22
- utilities/cachetools.py +24 -29
- utilities/click.py +393 -237
- utilities/concurrent.py +8 -11
- utilities/contextlib.py +216 -17
- utilities/contextvars.py +20 -1
- utilities/cryptography.py +3 -3
- utilities/dataclasses.py +83 -118
- utilities/docker.py +293 -0
- utilities/enum.py +26 -23
- utilities/errors.py +17 -3
- utilities/fastapi.py +29 -65
- utilities/fpdf2.py +3 -3
- utilities/functions.py +169 -416
- utilities/functools.py +18 -19
- utilities/git.py +9 -30
- utilities/grp.py +28 -0
- utilities/gzip.py +31 -0
- utilities/http.py +3 -2
- utilities/hypothesis.py +738 -589
- utilities/importlib.py +17 -1
- utilities/inflect.py +25 -0
- utilities/iterables.py +194 -262
- utilities/jinja2.py +148 -0
- utilities/json.py +70 -0
- utilities/libcst.py +38 -17
- utilities/lightweight_charts.py +5 -9
- utilities/logging.py +345 -543
- utilities/math.py +18 -13
- utilities/memory_profiler.py +11 -15
- utilities/more_itertools.py +200 -131
- utilities/operator.py +33 -29
- utilities/optuna.py +6 -6
- utilities/orjson.py +272 -137
- utilities/os.py +61 -4
- utilities/parse.py +59 -61
- utilities/pathlib.py +281 -40
- utilities/permissions.py +298 -0
- utilities/pickle.py +2 -2
- utilities/platform.py +24 -5
- utilities/polars.py +1214 -430
- utilities/polars_ols.py +1 -1
- utilities/postgres.py +408 -0
- utilities/pottery.py +113 -26
- utilities/pqdm.py +10 -11
- utilities/psutil.py +6 -57
- utilities/pwd.py +28 -0
- utilities/pydantic.py +4 -54
- utilities/pydantic_settings.py +240 -0
- utilities/pydantic_settings_sops.py +76 -0
- utilities/pyinstrument.py +8 -10
- utilities/pytest.py +227 -121
- utilities/pytest_plugins/__init__.py +1 -0
- utilities/pytest_plugins/pytest_randomly.py +23 -0
- utilities/pytest_plugins/pytest_regressions.py +56 -0
- utilities/pytest_regressions.py +26 -46
- utilities/random.py +13 -9
- utilities/re.py +58 -28
- utilities/redis.py +401 -550
- utilities/scipy.py +1 -1
- utilities/sentinel.py +10 -0
- utilities/shelve.py +4 -1
- utilities/shutil.py +25 -0
- utilities/slack_sdk.py +36 -106
- utilities/sqlalchemy.py +502 -473
- utilities/sqlalchemy_polars.py +38 -94
- utilities/string.py +2 -3
- utilities/subprocess.py +1572 -0
- utilities/tempfile.py +86 -4
- utilities/testbook.py +50 -0
- utilities/text.py +165 -42
- utilities/timer.py +37 -65
- utilities/traceback.py +158 -929
- utilities/types.py +146 -116
- utilities/typing.py +531 -71
- utilities/tzdata.py +1 -53
- utilities/tzlocal.py +6 -23
- utilities/uuid.py +43 -5
- utilities/version.py +27 -26
- utilities/whenever.py +1776 -386
- utilities/zoneinfo.py +84 -22
- dycw_utilities-0.129.10.dist-info/METADATA +0 -241
- dycw_utilities-0.129.10.dist-info/RECORD +0 -96
- dycw_utilities-0.129.10.dist-info/WHEEL +0 -4
- dycw_utilities-0.129.10.dist-info/licenses/LICENSE +0 -21
- utilities/datetime.py +0 -1409
- utilities/eventkit.py +0 -402
- utilities/loguru.py +0 -144
- utilities/luigi.py +0 -228
- utilities/period.py +0 -324
- utilities/pyrsistent.py +0 -89
- utilities/python_dotenv.py +0 -105
- utilities/streamlit.py +0 -105
- utilities/sys.py +0 -87
- 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
|
|
95
|
+
case never:
|
|
76
96
|
assert_never(never)
|
|
77
|
-
case
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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",
|
utilities/lightweight_charts.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
@
|
|
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
|