dycw-utilities 0.175.17__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 (94) 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.175.17.dist-info → dycw_utilities-0.185.8.dist-info}/WHEEL +2 -2
  4. utilities/__init__.py +1 -1
  5. utilities/altair.py +8 -6
  6. utilities/asyncio.py +40 -56
  7. utilities/atools.py +9 -11
  8. utilities/cachetools.py +8 -6
  9. utilities/click.py +4 -3
  10. utilities/concurrent.py +1 -1
  11. utilities/constants.py +492 -0
  12. utilities/contextlib.py +23 -30
  13. utilities/contextvars.py +1 -23
  14. utilities/core.py +2581 -0
  15. utilities/dataclasses.py +16 -119
  16. utilities/docker.py +139 -45
  17. utilities/enum.py +1 -1
  18. utilities/errors.py +2 -16
  19. utilities/fastapi.py +5 -5
  20. utilities/fpdf2.py +2 -1
  21. utilities/functions.py +33 -264
  22. utilities/http.py +2 -3
  23. utilities/hypothesis.py +48 -25
  24. utilities/iterables.py +39 -575
  25. utilities/jinja2.py +3 -6
  26. utilities/jupyter.py +5 -3
  27. utilities/libcst.py +1 -1
  28. utilities/lightweight_charts.py +4 -6
  29. utilities/logging.py +17 -15
  30. utilities/math.py +1 -36
  31. utilities/more_itertools.py +4 -6
  32. utilities/numpy.py +2 -1
  33. utilities/operator.py +2 -2
  34. utilities/orjson.py +24 -25
  35. utilities/os.py +4 -185
  36. utilities/packaging.py +129 -0
  37. utilities/parse.py +33 -13
  38. utilities/pathlib.py +2 -136
  39. utilities/platform.py +8 -90
  40. utilities/polars.py +34 -31
  41. utilities/postgres.py +9 -4
  42. utilities/pottery.py +20 -18
  43. utilities/pqdm.py +3 -4
  44. utilities/psutil.py +2 -3
  45. utilities/pydantic.py +18 -4
  46. utilities/pydantic_settings.py +7 -9
  47. utilities/pydantic_settings_sops.py +3 -3
  48. utilities/pyinstrument.py +4 -4
  49. utilities/pytest.py +49 -108
  50. utilities/pytest_plugins/pytest_regressions.py +2 -2
  51. utilities/pytest_regressions.py +8 -6
  52. utilities/random.py +2 -8
  53. utilities/redis.py +98 -94
  54. utilities/reprlib.py +11 -118
  55. utilities/shellingham.py +66 -0
  56. utilities/slack_sdk.py +13 -12
  57. utilities/sqlalchemy.py +42 -30
  58. utilities/sqlalchemy_polars.py +16 -25
  59. utilities/subprocess.py +1166 -148
  60. utilities/tabulate.py +32 -0
  61. utilities/testbook.py +8 -8
  62. utilities/text.py +24 -115
  63. utilities/throttle.py +159 -0
  64. utilities/time.py +18 -0
  65. utilities/timer.py +29 -12
  66. utilities/traceback.py +15 -22
  67. utilities/types.py +38 -3
  68. utilities/typing.py +18 -12
  69. utilities/uuid.py +1 -1
  70. utilities/version.py +202 -45
  71. utilities/whenever.py +22 -150
  72. dycw_utilities-0.175.17.dist-info/METADATA +0 -34
  73. dycw_utilities-0.175.17.dist-info/RECORD +0 -103
  74. utilities/atomicwrites.py +0 -182
  75. utilities/cryptography.py +0 -41
  76. utilities/getpass.py +0 -8
  77. utilities/git.py +0 -19
  78. utilities/grp.py +0 -28
  79. utilities/gzip.py +0 -31
  80. utilities/json.py +0 -70
  81. utilities/permissions.py +0 -298
  82. utilities/pickle.py +0 -25
  83. utilities/pwd.py +0 -28
  84. utilities/re.py +0 -156
  85. utilities/sentinel.py +0 -73
  86. utilities/socket.py +0 -8
  87. utilities/string.py +0 -20
  88. utilities/tempfile.py +0 -136
  89. utilities/tzdata.py +0 -11
  90. utilities/tzlocal.py +0 -28
  91. utilities/warnings.py +0 -65
  92. utilities/zipfile.py +0 -25
  93. utilities/zoneinfo.py +0 -133
  94. {dycw_utilities-0.175.17.dist-info → dycw_utilities-0.185.8.dist-info}/entry_points.txt +0 -0
utilities/permissions.py DELETED
@@ -1,298 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from functools import reduce
5
- from operator import or_
6
- from pathlib import Path
7
- from stat import (
8
- S_IMODE,
9
- S_IRGRP,
10
- S_IROTH,
11
- S_IRUSR,
12
- S_IWGRP,
13
- S_IWOTH,
14
- S_IWUSR,
15
- S_IXGRP,
16
- S_IXOTH,
17
- S_IXUSR,
18
- )
19
- from typing import TYPE_CHECKING, Literal, Self, assert_never, override
20
-
21
- from utilities.dataclasses import replace_non_sentinel
22
- from utilities.re import ExtractGroupsError, extract_groups
23
- from utilities.sentinel import Sentinel, sentinel
24
-
25
- if TYPE_CHECKING:
26
- from utilities.types import PathLike
27
-
28
-
29
- type PermissionsLike = Permissions | int | str
30
-
31
-
32
- ##
33
-
34
-
35
- def ensure_perms(perms: PermissionsLike, /) -> Permissions:
36
- """Ensure a set of file permissions."""
37
- match perms:
38
- case Permissions():
39
- return perms
40
- case int():
41
- return Permissions.from_int(perms)
42
- case str():
43
- return Permissions.from_text(perms)
44
- case never:
45
- assert_never(never)
46
-
47
-
48
- ##
49
-
50
-
51
- @dataclass(order=True, unsafe_hash=True, kw_only=True, slots=True)
52
- class Permissions:
53
- """A set of file permissions."""
54
-
55
- user_read: bool = False
56
- user_write: bool = False
57
- user_execute: bool = False
58
- group_read: bool = False
59
- group_write: bool = False
60
- group_execute: bool = False
61
- others_read: bool = False
62
- others_write: bool = False
63
- others_execute: bool = False
64
-
65
- def __int__(self) -> int:
66
- flags: list[int] = [
67
- S_IRUSR if self.user_read else 0,
68
- S_IWUSR if self.user_write else 0,
69
- S_IXUSR if self.user_execute else 0,
70
- S_IRGRP if self.group_read else 0,
71
- S_IWGRP if self.group_write else 0,
72
- S_IXGRP if self.group_execute else 0,
73
- S_IROTH if self.others_read else 0,
74
- S_IWOTH if self.others_write else 0,
75
- S_IXOTH if self.others_execute else 0,
76
- ]
77
- return reduce(or_, flags)
78
-
79
- @override
80
- def __repr__(self) -> str:
81
- return ",".join([
82
- self._repr_parts(
83
- "u",
84
- read=self.user_read,
85
- write=self.user_write,
86
- execute=self.user_execute,
87
- ),
88
- self._repr_parts(
89
- "g",
90
- read=self.group_read,
91
- write=self.group_write,
92
- execute=self.group_execute,
93
- ),
94
- self._repr_parts(
95
- "o",
96
- read=self.others_read,
97
- write=self.others_write,
98
- execute=self.others_execute,
99
- ),
100
- ])
101
-
102
- def _repr_parts(
103
- self,
104
- prefix: Literal["u", "g", "o"],
105
- /,
106
- *,
107
- read: bool = False,
108
- write: bool = False,
109
- execute: bool = False,
110
- ) -> str:
111
- parts: list[str] = [
112
- "r" if read else "",
113
- "w" if write else "",
114
- "x" if execute else "",
115
- ]
116
- return f"{prefix}={''.join(parts)}"
117
-
118
- @override
119
- def __str__(self) -> str:
120
- return repr(self)
121
-
122
- @classmethod
123
- def from_human_int(cls, n: int, /) -> Self:
124
- if not (0 <= n <= 777):
125
- raise PermissionsFromHumanIntRangeError(n=n)
126
- user_read, user_write, user_execute = cls._from_human_int(n, (n // 100) % 10)
127
- group_read, group_write, group_execute = cls._from_human_int(n, (n // 10) % 10)
128
- others_read, others_write, others_execute = cls._from_human_int(n, n % 10)
129
- return cls(
130
- user_read=user_read,
131
- user_write=user_write,
132
- user_execute=user_execute,
133
- group_read=group_read,
134
- group_write=group_write,
135
- group_execute=group_execute,
136
- others_read=others_read,
137
- others_write=others_write,
138
- others_execute=others_execute,
139
- )
140
-
141
- @classmethod
142
- def _from_human_int(cls, n: int, digit: int, /) -> tuple[bool, bool, bool]:
143
- if not (0 <= digit <= 7):
144
- raise PermissionsFromHumanIntDigitError(n=n, digit=digit)
145
- return bool(4 & digit), bool(2 & digit), bool(1 & digit)
146
-
147
- @classmethod
148
- def from_int(cls, n: int, /) -> Self:
149
- if 0o0 <= n <= 0o777:
150
- return cls(
151
- user_read=bool(n & S_IRUSR),
152
- user_write=bool(n & S_IWUSR),
153
- user_execute=bool(n & S_IXUSR),
154
- group_read=bool(n & S_IRGRP),
155
- group_write=bool(n & S_IWGRP),
156
- group_execute=bool(n & S_IXGRP),
157
- others_read=bool(n & S_IROTH),
158
- others_write=bool(n & S_IWOTH),
159
- others_execute=bool(n & S_IXOTH),
160
- )
161
- raise PermissionsFromIntError(n=n)
162
-
163
- @classmethod
164
- def from_path(cls, path: PathLike, /) -> Self:
165
- return cls.from_int(S_IMODE(Path(path).stat().st_mode))
166
-
167
- @classmethod
168
- def from_text(cls, text: str, /) -> Self:
169
- try:
170
- user, group, others = extract_groups(
171
- r"^u=(r?w?x?),g=(r?w?x?),o=(r?w?x?)$", text
172
- )
173
- except ExtractGroupsError:
174
- raise PermissionsFromTextError(text=text) from None
175
- user_read, user_write, user_execute = cls._from_text_part(user)
176
- group_read, group_write, group_execute = cls._from_text_part(group)
177
- others_read, others_write, others_execute = cls._from_text_part(others)
178
- return cls(
179
- user_read=user_read,
180
- user_write=user_write,
181
- user_execute=user_execute,
182
- group_read=group_read,
183
- group_write=group_write,
184
- group_execute=group_execute,
185
- others_read=others_read,
186
- others_write=others_write,
187
- others_execute=others_execute,
188
- )
189
-
190
- @classmethod
191
- def _from_text_part(cls, text: str, /) -> tuple[bool, bool, bool]:
192
- read, write, execute = extract_groups("^(r?)(w?)(x?)$", text)
193
- return read != "", write != "", execute != ""
194
-
195
- @property
196
- def human_int(self) -> int:
197
- return (
198
- 100
199
- * self._human_int(
200
- read=self.user_read, write=self.user_write, execute=self.user_execute
201
- )
202
- + 10
203
- * self._human_int(
204
- read=self.group_read, write=self.group_write, execute=self.group_execute
205
- )
206
- + self._human_int(
207
- read=self.others_read,
208
- write=self.others_write,
209
- execute=self.others_execute,
210
- )
211
- )
212
-
213
- def _human_int(
214
- self, *, read: bool = False, write: bool = False, execute: bool = False
215
- ) -> int:
216
- return (4 if read else 0) + (2 if write else 0) + (1 if execute else 0)
217
-
218
- def replace(
219
- self,
220
- *,
221
- user_read: bool | Sentinel = sentinel,
222
- user_write: bool | Sentinel = sentinel,
223
- user_execute: bool | Sentinel = sentinel,
224
- group_read: bool | Sentinel = sentinel,
225
- group_write: bool | Sentinel = sentinel,
226
- group_execute: bool | Sentinel = sentinel,
227
- others_read: bool | Sentinel = sentinel,
228
- others_write: bool | Sentinel = sentinel,
229
- others_execute: bool | Sentinel = sentinel,
230
- ) -> Self:
231
- return replace_non_sentinel(
232
- self,
233
- user_read=user_read,
234
- user_write=user_write,
235
- user_execute=user_execute,
236
- group_read=group_read,
237
- group_write=group_write,
238
- group_execute=group_execute,
239
- others_read=others_read,
240
- others_write=others_write,
241
- others_execute=others_execute,
242
- )
243
-
244
-
245
- @dataclass(kw_only=True, slots=True)
246
- class PermissionsError(Exception): ...
247
-
248
-
249
- @dataclass(kw_only=True, slots=True)
250
- class PermissionsFromHumanIntError(PermissionsError):
251
- n: int
252
-
253
-
254
- @dataclass(kw_only=True, slots=True)
255
- class PermissionsFromHumanIntRangeError(PermissionsFromHumanIntError):
256
- @override
257
- def __str__(self) -> str:
258
- return f"Invalid human integer for permissions; got {self.n}"
259
-
260
-
261
- @dataclass(kw_only=True, slots=True)
262
- class PermissionsFromHumanIntDigitError(PermissionsFromHumanIntError):
263
- digit: int
264
-
265
- @override
266
- def __str__(self) -> str:
267
- return (
268
- f"Invalid human integer for permissions; got digit {self.digit} in {self.n}"
269
- )
270
-
271
-
272
- @dataclass(kw_only=True, slots=True)
273
- class PermissionsFromIntError(PermissionsError):
274
- n: int
275
-
276
- @override
277
- def __str__(self) -> str:
278
- return f"Invalid integer for permissions; got {self.n} = {oct(self.n)}"
279
-
280
-
281
- @dataclass(kw_only=True, slots=True)
282
- class PermissionsFromTextError(PermissionsError):
283
- text: str
284
-
285
- @override
286
- def __str__(self) -> str:
287
- return f"Invalid string for permissions; got {self.text!r}"
288
-
289
-
290
- __all__ = [
291
- "Permissions",
292
- "PermissionsError",
293
- "PermissionsFromHumanIntDigitError",
294
- "PermissionsFromHumanIntError",
295
- "PermissionsFromIntError",
296
- "PermissionsFromTextError",
297
- "ensure_perms",
298
- ]
utilities/pickle.py DELETED
@@ -1,25 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import gzip
4
- from pickle import dump, load
5
- from typing import TYPE_CHECKING, Any
6
-
7
- from utilities.atomicwrites import writer
8
-
9
- if TYPE_CHECKING:
10
- from utilities.types import PathLike
11
-
12
-
13
- def read_pickle(path: PathLike, /) -> Any:
14
- """Read an object from disk."""
15
- with gzip.open(path, mode="rb") as gz:
16
- return load(gz) # noqa: S301
17
-
18
-
19
- def write_pickle(obj: Any, path: PathLike, /, *, overwrite: bool = False) -> None:
20
- """Write an object to disk."""
21
- with writer(path, overwrite=overwrite) as temp, gzip.open(temp, mode="wb") as gz:
22
- dump(obj, gz)
23
-
24
-
25
- __all__ = ["read_pickle", "write_pickle"]
utilities/pwd.py DELETED
@@ -1,28 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import assert_never
4
-
5
- from utilities.os import EFFECTIVE_USER_ID
6
- from utilities.platform import SYSTEM
7
-
8
-
9
- def get_uid_name(uid: int, /) -> str | None:
10
- """Get the name of a user ID."""
11
- match SYSTEM:
12
- case "windows": # skipif-not-windows
13
- return None
14
- case "mac" | "linux": # skipif-windows
15
- from pwd import getpwuid
16
-
17
- return getpwuid(uid).pw_name
18
- case never:
19
- assert_never(never)
20
-
21
-
22
- ROOT_USER_NAME = get_uid_name(0)
23
- EFFECTIVE_USER_NAME = (
24
- None if EFFECTIVE_USER_ID is None else get_uid_name(EFFECTIVE_USER_ID)
25
- )
26
-
27
-
28
- __all__ = ["EFFECTIVE_USER_NAME", "ROOT_USER_NAME", "get_uid_name"]
utilities/re.py DELETED
@@ -1,156 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import re
4
- from dataclasses import dataclass
5
- from re import Pattern
6
- from typing import TYPE_CHECKING, assert_never, override
7
-
8
- if TYPE_CHECKING:
9
- from utilities.types import PatternLike
10
-
11
-
12
- def ensure_pattern(pattern: PatternLike, /, *, flags: int = 0) -> Pattern[str]:
13
- """Ensure a pattern is returned."""
14
- match pattern:
15
- case Pattern():
16
- return pattern
17
- case str():
18
- return re.compile(pattern, flags=flags)
19
- case never:
20
- assert_never(never)
21
-
22
-
23
- ##
24
-
25
-
26
- def extract_group(pattern: PatternLike, text: str, /, *, flags: int = 0) -> str:
27
- """Extract a group.
28
-
29
- The regex must have 1 capture group, and this must match exactly once.
30
- """
31
- pattern_use = ensure_pattern(pattern, flags=flags)
32
- match pattern_use.groups:
33
- case 0:
34
- raise _ExtractGroupNoCaptureGroupsError(pattern=pattern_use, text=text)
35
- case 1:
36
- matches: list[str] = pattern_use.findall(text)
37
- match len(matches):
38
- case 0:
39
- raise _ExtractGroupNoMatchesError(
40
- pattern=pattern_use, text=text
41
- ) from None
42
- case 1:
43
- return matches[0]
44
- case _:
45
- raise _ExtractGroupMultipleMatchesError(
46
- pattern=pattern_use, text=text, matches=matches
47
- ) from None
48
- case _:
49
- raise _ExtractGroupMultipleCaptureGroupsError(
50
- pattern=pattern_use, text=text
51
- )
52
-
53
-
54
- @dataclass(kw_only=True, slots=True)
55
- class ExtractGroupError(Exception):
56
- pattern: Pattern[str]
57
- text: str
58
-
59
-
60
- @dataclass(kw_only=True, slots=True)
61
- class _ExtractGroupMultipleCaptureGroupsError(ExtractGroupError):
62
- @override
63
- def __str__(self) -> str:
64
- return f"Pattern {self.pattern} must contain exactly one capture group; it had multiple"
65
-
66
-
67
- @dataclass(kw_only=True, slots=True)
68
- class _ExtractGroupMultipleMatchesError(ExtractGroupError):
69
- matches: list[str]
70
-
71
- @override
72
- def __str__(self) -> str:
73
- return f"Pattern {self.pattern} must match against {self.text} exactly once; matches were {self.matches}"
74
-
75
-
76
- @dataclass(kw_only=True, slots=True)
77
- class _ExtractGroupNoCaptureGroupsError(ExtractGroupError):
78
- @override
79
- def __str__(self) -> str:
80
- return f"Pattern {self.pattern} must contain exactly one capture group; it had none".format(
81
- self.pattern
82
- )
83
-
84
-
85
- @dataclass(kw_only=True, slots=True)
86
- class _ExtractGroupNoMatchesError(ExtractGroupError):
87
- @override
88
- def __str__(self) -> str:
89
- return f"Pattern {self.pattern} must match against {self.text}"
90
-
91
-
92
- ##
93
-
94
-
95
- def extract_groups(pattern: PatternLike, text: str, /, *, flags: int = 0) -> list[str]:
96
- """Extract multiple groups.
97
-
98
- The regex may have any number of capture groups, and they must collectively
99
- match exactly once.
100
- """
101
- pattern_use = ensure_pattern(pattern, flags=flags)
102
- if (n_groups := pattern_use.groups) == 0:
103
- raise _ExtractGroupsNoCaptureGroupsError(pattern=pattern_use, text=text)
104
- matches: list[str] = pattern_use.findall(text)
105
- match len(matches), n_groups:
106
- case 0, _:
107
- raise _ExtractGroupsNoMatchesError(pattern=pattern_use, text=text)
108
- case 1, 1:
109
- return matches
110
- case 1, _:
111
- return list(matches[0])
112
- case _:
113
- raise _ExtractGroupsMultipleMatchesError(
114
- pattern=pattern_use, text=text, matches=matches
115
- )
116
-
117
-
118
- @dataclass(kw_only=True, slots=True)
119
- class ExtractGroupsError(Exception):
120
- pattern: Pattern[str]
121
- text: str
122
-
123
-
124
- @dataclass(kw_only=True, slots=True)
125
- class _ExtractGroupsMultipleMatchesError(ExtractGroupsError):
126
- matches: list[str]
127
-
128
- @override
129
- def __str__(self) -> str:
130
- return f"Pattern {self.pattern} must match against {self.text} exactly once; matches were {self.matches}"
131
-
132
-
133
- @dataclass(kw_only=True, slots=True)
134
- class _ExtractGroupsNoCaptureGroupsError(ExtractGroupsError):
135
- pattern: Pattern[str]
136
- text: str
137
-
138
- @override
139
- def __str__(self) -> str:
140
- return f"Pattern {self.pattern} must contain at least one capture group"
141
-
142
-
143
- @dataclass(kw_only=True, slots=True)
144
- class _ExtractGroupsNoMatchesError(ExtractGroupsError):
145
- @override
146
- def __str__(self) -> str:
147
- return f"Pattern {self.pattern} must match against {self.text}"
148
-
149
-
150
- __all__ = [
151
- "ExtractGroupError",
152
- "ExtractGroupsError",
153
- "ensure_pattern",
154
- "extract_group",
155
- "extract_groups",
156
- ]
utilities/sentinel.py DELETED
@@ -1,73 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from re import IGNORECASE, search
5
- from typing import Any, override
6
-
7
- from typing_extensions import TypeIs
8
-
9
-
10
- class _Meta(type):
11
- """Metaclass for the sentinel."""
12
-
13
- instance: Any = None
14
-
15
- @override
16
- def __call__(cls, *args: Any, **kwargs: Any) -> Any:
17
- if cls.instance is None:
18
- cls.instance = super().__call__(*args, **kwargs)
19
- return cls.instance
20
-
21
-
22
- SENTINEL_REPR = "<sentinel>"
23
-
24
-
25
- class Sentinel(metaclass=_Meta):
26
- """Base class for the sentinel object."""
27
-
28
- @override
29
- def __repr__(self) -> str:
30
- return SENTINEL_REPR
31
-
32
- @override
33
- def __str__(self) -> str:
34
- return repr(self)
35
-
36
-
37
- sentinel = Sentinel()
38
-
39
- ##
40
-
41
-
42
- def is_sentinel(obj: Any, /) -> TypeIs[Sentinel]:
43
- """Check if an object is the sentinel."""
44
- return obj is sentinel
45
-
46
-
47
- ##
48
-
49
-
50
- def parse_sentinel(text: str, /) -> Sentinel:
51
- """Parse text into the Sentinel value."""
52
- if search("^(|sentinel|<sentinel>)$", text, flags=IGNORECASE):
53
- return sentinel
54
- raise ParseSentinelError(text=text)
55
-
56
-
57
- @dataclass(kw_only=True, slots=True)
58
- class ParseSentinelError(Exception):
59
- text: str
60
-
61
- @override
62
- def __str__(self) -> str:
63
- return f"Unable to parse sentinel value; got {self.text!r}"
64
-
65
-
66
- __all__ = [
67
- "SENTINEL_REPR",
68
- "ParseSentinelError",
69
- "Sentinel",
70
- "is_sentinel",
71
- "parse_sentinel",
72
- "sentinel",
73
- ]
utilities/socket.py DELETED
@@ -1,8 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from socket import gethostname
4
-
5
- HOSTNAME = gethostname()
6
-
7
-
8
- __all__ = ["HOSTNAME"]
utilities/string.py DELETED
@@ -1,20 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from os import environ
4
- from pathlib import Path
5
- from string import Template
6
- from typing import Any, assert_never
7
-
8
-
9
- def substitute_environ(path_or_text: Path | str, /, **kwargs: Any) -> str:
10
- """Substitute the environment variables in a file."""
11
- match path_or_text:
12
- case Path() as path:
13
- return substitute_environ(path.read_text(), **kwargs)
14
- case str() as text:
15
- return Template(text).substitute(environ, **kwargs)
16
- case never:
17
- assert_never(never)
18
-
19
-
20
- __all__ = ["substitute_environ"]