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/os.py CHANGED
@@ -1,42 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
- from contextlib import suppress
4
3
  from dataclasses import dataclass
5
- from os import cpu_count, environ, getenv
6
- from typing import TYPE_CHECKING, Literal, assert_never, overload, override
4
+ from typing import TYPE_CHECKING, assert_never, override
7
5
 
8
- from utilities.contextlib import enhanced_context_manager
9
- from utilities.iterables import OneStrEmptyError, one_str
6
+ from utilities.constants import CPU_COUNT
10
7
 
11
8
  if TYPE_CHECKING:
12
- from collections.abc import Iterator, Mapping
13
-
14
-
15
- type IntOrAll = int | Literal["all"]
16
-
17
-
18
- ##
19
-
20
-
21
- def get_cpu_count() -> int:
22
- """Get the CPU count."""
23
- count = cpu_count()
24
- if count is None: # pragma: no cover
25
- raise GetCPUCountError
26
- return count
27
-
28
-
29
- @dataclass(kw_only=True, slots=True)
30
- class GetCPUCountError(Exception):
31
- @override
32
- def __str__(self) -> str:
33
- return "CPU count must not be None" # pragma: no cover
34
-
35
-
36
- CPU_COUNT = get_cpu_count()
37
-
38
-
39
- ##
9
+ from utilities.types import IntOrAll
40
10
 
41
11
 
42
12
  def get_cpu_use(*, n: IntOrAll = "all") -> int:
@@ -61,117 +31,4 @@ class GetCPUUseError(Exception):
61
31
  return f"Invalid number of CPUs to use: {self.n}"
62
32
 
63
33
 
64
- ##
65
-
66
-
67
- @overload
68
- def get_env_var(
69
- key: str, /, *, case_sensitive: bool = False, default: str, nullable: bool = False
70
- ) -> str: ...
71
- @overload
72
- def get_env_var(
73
- key: str,
74
- /,
75
- *,
76
- case_sensitive: bool = False,
77
- default: None = None,
78
- nullable: Literal[False] = False,
79
- ) -> str: ...
80
- @overload
81
- def get_env_var(
82
- key: str,
83
- /,
84
- *,
85
- case_sensitive: bool = False,
86
- default: str | None = None,
87
- nullable: bool = False,
88
- ) -> str | None: ...
89
- def get_env_var(
90
- key: str,
91
- /,
92
- *,
93
- case_sensitive: bool = False,
94
- default: str | None = None,
95
- nullable: bool = False,
96
- ) -> str | None:
97
- """Get an environment variable."""
98
- try:
99
- key_use = one_str(environ, key, case_sensitive=case_sensitive)
100
- except OneStrEmptyError:
101
- match default, nullable:
102
- case None, False:
103
- raise GetEnvVarError(key=key, case_sensitive=case_sensitive) from None
104
- case None, True:
105
- return None
106
- case str(), _:
107
- return default
108
- case never:
109
- assert_never(never)
110
- return environ[key_use]
111
-
112
-
113
- @dataclass(kw_only=True, slots=True)
114
- class GetEnvVarError(Exception):
115
- key: str
116
- case_sensitive: bool = False
117
-
118
- @override
119
- def __str__(self) -> str:
120
- desc = f"No environment variable {self.key!r}"
121
- return desc if self.case_sensitive else f"{desc} (modulo case)"
122
-
123
-
124
- ##
125
-
126
-
127
- def is_debug() -> bool:
128
- """Check if we are in `DEBUG` mode."""
129
- return get_env_var("DEBUG", nullable=True) is not None
130
-
131
-
132
- ##
133
-
134
-
135
- def is_pytest() -> bool:
136
- """Check if `pytest` is running."""
137
- return get_env_var("PYTEST_VERSION", nullable=True) is not None
138
-
139
-
140
- ##
141
-
142
-
143
- @enhanced_context_manager
144
- def temp_environ(
145
- env: Mapping[str, str | None] | None = None, **env_kwargs: str | None
146
- ) -> Iterator[None]:
147
- """Context manager with temporary environment variable set."""
148
- mapping: dict[str, str | None] = ({} if env is None else dict(env)) | env_kwargs
149
- prev = {key: getenv(key) for key in mapping}
150
-
151
- def apply(mapping: Mapping[str, str | None], /) -> None:
152
- for key, value in mapping.items():
153
- if value is None:
154
- with suppress(KeyError):
155
- del environ[key]
156
- else:
157
- environ[key] = value
158
-
159
- apply(mapping)
160
- try:
161
- yield
162
- finally:
163
- apply(prev)
164
-
165
-
166
- __all__ = [
167
- "CPU_COUNT",
168
- "GetCPUCountError",
169
- "GetCPUUseError",
170
- "IntOrAll",
171
- "get_cpu_count",
172
- "get_cpu_use",
173
- "get_env_var",
174
- "is_debug",
175
- "is_pytest",
176
- "temp_environ",
177
- ]
34
+ __all__ = ["GetCPUUseError", "get_cpu_use"]
utilities/packaging.py ADDED
@@ -0,0 +1,129 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import TYPE_CHECKING, Self, overload, override
5
+
6
+ import packaging._parser
7
+ import packaging.requirements
8
+ from packaging.requirements import _parse_requirement
9
+ from packaging.specifiers import Specifier, SpecifierSet
10
+
11
+ from utilities.core import OneEmptyError, one
12
+
13
+ if TYPE_CHECKING:
14
+ from packaging._parser import MarkerList
15
+
16
+
17
+ @dataclass(order=True, unsafe_hash=True, slots=True)
18
+ class Requirement:
19
+ requirement: str
20
+ _parsed_req: packaging._parser.ParsedRequirement = field(init=False, repr=False)
21
+ _custom_req: _CustomRequirement = field(init=False, repr=False)
22
+
23
+ def __getitem__(self, operator: str, /) -> str:
24
+ return self.specifier_set[operator]
25
+
26
+ def __post_init__(self) -> None:
27
+ self._parsed_req = _parse_requirement(self.requirement)
28
+ self._custom_req = _CustomRequirement(self.requirement)
29
+
30
+ @override
31
+ def __str__(self) -> str:
32
+ return str(self._custom_req)
33
+
34
+ def drop(self, operator: str, /) -> Self:
35
+ return type(self)(str(self._custom_req.drop(operator)))
36
+
37
+ @property
38
+ def extras(self) -> list[str]:
39
+ return self._parsed_req.extras
40
+
41
+ @overload
42
+ def get(self, operator: str, default: str, /) -> str: ...
43
+ @overload
44
+ def get(self, operator: str, default: None = None, /) -> str | None: ...
45
+ def get(self, operator: str, default: str | None = None, /) -> str | None:
46
+ return self.specifier_set.get(operator, default)
47
+
48
+ @property
49
+ def marker(self) -> MarkerList | None:
50
+ return self._parsed_req.marker
51
+
52
+ @property
53
+ def name(self) -> str:
54
+ return self._parsed_req.name
55
+
56
+ def replace(self, operator: str, version: str | None, /) -> Self:
57
+ return type(self)(str(self._custom_req.replace(operator, version)))
58
+
59
+ @property
60
+ def specifier(self) -> str:
61
+ return self._parsed_req.specifier
62
+
63
+ @property
64
+ def specifier_set(self) -> _CustomSpecifierSet:
65
+ return _CustomSpecifierSet(_parse_requirement(self.requirement).specifier)
66
+
67
+ @property
68
+ def url(self) -> str:
69
+ return self._parsed_req.url
70
+
71
+
72
+ class _CustomRequirement(packaging.requirements.Requirement):
73
+ specifier: _CustomSpecifierSet
74
+
75
+ @override
76
+ def __init__(self, requirement_string: str) -> None:
77
+ super().__init__(requirement_string)
78
+ parsed = _parse_requirement(requirement_string)
79
+ self.specifier = _CustomSpecifierSet(parsed.specifier) # pyright: ignore[reportIncompatibleVariableOverride]
80
+
81
+ def drop(self, operator: str, /) -> Self:
82
+ new = type(self)(super().__str__())
83
+ new.specifier = self.specifier.drop(operator)
84
+ return new
85
+
86
+ def replace(self, operator: str, version: str | None, /) -> Self:
87
+ new = type(self)(super().__str__())
88
+ new.specifier = self.specifier.replace(operator, version)
89
+ return new
90
+
91
+
92
+ class _CustomSpecifierSet(SpecifierSet):
93
+ def __getitem__(self, operator: str, /) -> str:
94
+ try:
95
+ return one(s.version for s in self if s.operator == operator)
96
+ except OneEmptyError:
97
+ raise KeyError(operator) from None
98
+
99
+ @override
100
+ def __str__(self) -> str:
101
+ specs = sorted(self._specs, key=self._sort_key)
102
+ return ", ".join(map(str, specs))
103
+
104
+ def drop(self, operator: str, /) -> Self:
105
+ if any(s.operator == operator for s in self):
106
+ return type(self)(s for s in self if s.operator != operator)
107
+ raise KeyError(operator)
108
+
109
+ @overload
110
+ def get(self, operator: str, default: str, /) -> str: ...
111
+ @overload
112
+ def get(self, operator: str, default: None = None, /) -> str | None: ...
113
+ def get(self, operator: str, default: str | None = None, /) -> str | None:
114
+ try:
115
+ return self[operator]
116
+ except KeyError:
117
+ return default
118
+
119
+ def replace(self, operator: str, version: str | None, /) -> Self:
120
+ specifiers = [s for s in self if s.operator != operator]
121
+ if version is not None:
122
+ specifiers.append(Specifier(spec=f"{operator}{version}"))
123
+ return type(self)(specifiers)
124
+
125
+ def _sort_key(self, spec: Specifier, /) -> int:
126
+ return ["==", "!=", "~=", ">", ">=", "<", "<="].index(spec.operator)
127
+
128
+
129
+ __all__ = ["Requirement"]
utilities/parse.py CHANGED
@@ -21,15 +21,24 @@ from whenever import (
21
21
  ZonedDateTime,
22
22
  )
23
23
 
24
- from utilities.enum import ParseEnumError, parse_enum
25
- from utilities.iterables import OneEmptyError, OneNonUniqueError, one, one_str
26
- from utilities.math import ParseNumberError, parse_number
27
- from utilities.re import ExtractGroupError, extract_group
28
- from utilities.sentinel import ParseSentinelError, Sentinel, parse_sentinel
29
- from utilities.text import (
24
+ from utilities.constants import (
30
25
  BRACKETS,
31
26
  LIST_SEPARATOR,
32
27
  PAIR_SEPARATOR,
28
+ Sentinel,
29
+ SentinelParseError,
30
+ )
31
+ from utilities.core import (
32
+ ExtractGroupError,
33
+ OneEmptyError,
34
+ OneNonUniqueError,
35
+ extract_group,
36
+ one,
37
+ one_str,
38
+ )
39
+ from utilities.enum import ParseEnumError, parse_enum
40
+ from utilities.math import ParseNumberError, parse_number
41
+ from utilities.text import (
33
42
  ParseBoolError,
34
43
  ParseNoneError,
35
44
  join_strs,
@@ -52,7 +61,12 @@ from utilities.typing import (
52
61
  is_tuple_type,
53
62
  is_union_type,
54
63
  )
55
- from utilities.version import ParseVersionError, Version, parse_version
64
+ from utilities.version import (
65
+ Version2,
66
+ Version3,
67
+ _Version2ParseError,
68
+ _Version3ParseError,
69
+ )
56
70
 
57
71
  if TYPE_CHECKING:
58
72
  from collections.abc import Iterable, Mapping, Sequence
@@ -204,20 +218,25 @@ def _parse_object_type(
204
218
  ),
205
219
  ):
206
220
  try:
207
- return cls.parse_common_iso(text)
221
+ return cls.parse_iso(text)
208
222
  except ValueError:
209
223
  raise _ParseObjectParseError(type_=cls, text=text) from None
210
224
  if issubclass(cls, Path):
211
225
  return Path(text).expanduser()
212
226
  if issubclass(cls, Sentinel):
213
227
  try:
214
- return parse_sentinel(text)
215
- except ParseSentinelError:
228
+ return Sentinel.parse(text)
229
+ except SentinelParseError:
230
+ raise _ParseObjectParseError(type_=cls, text=text) from None
231
+ if issubclass(cls, Version2):
232
+ try:
233
+ return Version2.parse(text)
234
+ except _Version2ParseError:
216
235
  raise _ParseObjectParseError(type_=cls, text=text) from None
217
- if issubclass(cls, Version):
236
+ if issubclass(cls, Version3):
218
237
  try:
219
- return parse_version(text)
220
- except ParseVersionError:
238
+ return Version3.parse(text)
239
+ except _Version3ParseError:
221
240
  raise _ParseObjectParseError(type_=cls, text=text) from None
222
241
  raise _ParseObjectParseError(type_=cls, text=text)
223
242
 
@@ -460,7 +479,8 @@ def serialize_object(
460
479
  | IPv6Address
461
480
  | Path
462
481
  | Sentinel
463
- | Version,
482
+ | Version2
483
+ | Version3,
464
484
  ):
465
485
  return str(obj)
466
486
  if isinstance(
@@ -477,7 +497,7 @@ def serialize_object(
477
497
  ZonedDateTime,
478
498
  ),
479
499
  ):
480
- return obj.format_common_iso()
500
+ return obj.format_iso()
481
501
  if isinstance(obj, Enum):
482
502
  return obj.name
483
503
  if isinstance(obj, dict):
utilities/pathlib.py CHANGED
@@ -3,26 +3,19 @@ from __future__ import annotations
3
3
  from collections.abc import Callable
4
4
  from dataclasses import dataclass
5
5
  from itertools import chain
6
- from os import chdir
7
- from os.path import expandvars
8
6
  from pathlib import Path
9
7
  from re import IGNORECASE, search
10
8
  from subprocess import PIPE, CalledProcessError, check_output
11
9
  from typing import TYPE_CHECKING, Literal, assert_never, overload, override
12
10
 
13
- from utilities.contextlib import enhanced_context_manager
14
- from utilities.errors import ImpossibleCaseError
15
- from utilities.sentinel import Sentinel
11
+ from utilities.constants import Sentinel
16
12
 
17
13
  if TYPE_CHECKING:
18
- from collections.abc import Iterator, Sequence
14
+ from collections.abc import Sequence
19
15
 
20
16
  from utilities.types import MaybeCallablePathLike, PathLike
21
17
 
22
18
 
23
- PWD = Path.cwd()
24
-
25
-
26
19
  def ensure_suffix(path: PathLike, suffix: str, /) -> Path:
27
20
  """Ensure a path has a given suffix."""
28
21
  path = Path(path)
@@ -42,43 +35,6 @@ def ensure_suffix(path: PathLike, suffix: str, /) -> Path:
42
35
  ##
43
36
 
44
37
 
45
- def expand_path(path: PathLike, /) -> Path:
46
- """Expand a path."""
47
- path = str(path)
48
- path = expandvars(path)
49
- return Path(path).expanduser()
50
-
51
-
52
- ##
53
-
54
-
55
- def get_package_root(path: MaybeCallablePathLike = Path.cwd, /) -> Path:
56
- """Get the package root."""
57
- path = to_path(path)
58
- path_dir = path.parent if path.is_file() else path
59
- all_paths = list(chain([path_dir], path_dir.parents))
60
- try:
61
- return next(
62
- p.resolve()
63
- for p in all_paths
64
- if any(p_i.name == "pyproject.toml" for p_i in p.iterdir())
65
- )
66
- except StopIteration:
67
- raise GetPackageRootError(path=path) from None
68
-
69
-
70
- @dataclass(kw_only=True, slots=True)
71
- class GetPackageRootError(Exception):
72
- path: PathLike
73
-
74
- @override
75
- def __str__(self) -> str:
76
- return f"Path is not part of a package: {self.path}"
77
-
78
-
79
- ##
80
-
81
-
82
38
  def get_repo_root(path: MaybeCallablePathLike = Path.cwd, /) -> Path:
83
39
  """Get the repo root."""
84
40
  path = to_path(path)
@@ -112,7 +68,7 @@ class GetRepoRootError(Exception): ...
112
68
  class _GetRepoRootGitNotFoundError(GetRepoRootError):
113
69
  @override
114
70
  def __str__(self) -> str:
115
- return "'git' not found"
71
+ return "'git' not found" # pragma: no cover
116
72
 
117
73
 
118
74
  @dataclass(kw_only=True, slots=True)
@@ -127,50 +83,6 @@ class _GetRepoRootNotARepoError(GetRepoRootError):
127
83
  ##
128
84
 
129
85
 
130
- def get_root(path: MaybeCallablePathLike = Path.cwd, /) -> Path:
131
- """Get the root of a path."""
132
- path = to_path(path)
133
- try:
134
- repo = get_repo_root(path)
135
- except GetRepoRootError:
136
- repo = None
137
- try:
138
- package = get_package_root(path)
139
- except GetPackageRootError:
140
- package = None
141
- match repo, package:
142
- case None, None:
143
- raise GetRootError(path=path)
144
- case Path(), None:
145
- return repo
146
- case None, Path():
147
- return package
148
- case Path(), Path():
149
- if repo == package:
150
- return repo
151
- if is_sub_path(repo, package, strict=True):
152
- return repo
153
- if is_sub_path(package, repo, strict=True):
154
- return package
155
- raise ImpossibleCaseError( # pragma: no cover
156
- case=[f"{repo=}", f"{package=}"]
157
- )
158
- case never:
159
- assert_never(never)
160
-
161
-
162
- @dataclass(kw_only=True, slots=True)
163
- class GetRootError(Exception):
164
- path: PathLike
165
-
166
- @override
167
- def __str__(self) -> str:
168
- return f"Unable to determine root from {str(self.path)!r}"
169
-
170
-
171
- ##
172
-
173
-
174
86
  type _GetTailDisambiguate = Literal["raise", "earlier", "later"]
175
87
 
176
88
 
@@ -268,15 +180,6 @@ def module_path(
268
180
  ##
269
181
 
270
182
 
271
- def is_sub_path(x: PathLike, y: PathLike, /, *, strict: bool = False) -> bool:
272
- """Check if a path is a sub path of another."""
273
- x, y = [Path(i).resolve() for i in [x, y]]
274
- return x.is_relative_to(y) and not (strict and y.is_relative_to(x))
275
-
276
-
277
- ##
278
-
279
-
280
183
  def list_dir(path: PathLike, /) -> Sequence[Path]:
281
184
  """List the contents of a directory."""
282
185
  return sorted(Path(path).iterdir())
@@ -285,20 +188,6 @@ def list_dir(path: PathLike, /) -> Sequence[Path]:
285
188
  ##
286
189
 
287
190
 
288
- @enhanced_context_manager
289
- def temp_cwd(path: PathLike, /) -> Iterator[None]:
290
- """Context manager with temporary current working directory set."""
291
- prev = Path.cwd()
292
- chdir(path)
293
- try:
294
- yield
295
- finally:
296
- chdir(prev)
297
-
298
-
299
- ##
300
-
301
-
302
191
  @overload
303
192
  def to_path(path: Sentinel, /) -> Sentinel: ...
304
193
  @overload
@@ -321,18 +210,12 @@ def to_path(
321
210
 
322
211
 
323
212
  __all__ = [
324
- "PWD",
325
- "GetPackageRootError",
326
213
  "GetRepoRootError",
327
214
  "GetTailError",
328
215
  "ensure_suffix",
329
- "expand_path",
330
- "get_package_root",
331
216
  "get_repo_root",
332
217
  "get_tail",
333
- "is_sub_path",
334
218
  "list_dir",
335
219
  "module_path",
336
- "temp_cwd",
337
220
  "to_path",
338
221
  ]
utilities/platform.py CHANGED
@@ -1,74 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
- from dataclasses import dataclass
4
- from pathlib import Path
5
- from platform import system
6
3
  from re import sub
7
- from typing import TYPE_CHECKING, Literal, assert_never, override
4
+ from typing import assert_never
8
5
 
9
- if TYPE_CHECKING:
10
- from collections.abc import Iterable, Iterator
11
-
12
-
13
- System = Literal["windows", "mac", "linux"]
14
-
15
-
16
- def get_system() -> System:
17
- """Get the system/OS name."""
18
- sys = system()
19
- if sys == "Windows": # skipif-not-windows
20
- return "windows"
21
- if sys == "Darwin": # skipif-not-macos
22
- return "mac"
23
- if sys == "Linux": # skipif-not-linux
24
- return "linux"
25
- raise GetSystemError(sys=sys) # pragma: no cover
26
-
27
-
28
- @dataclass(kw_only=True, slots=True)
29
- class GetSystemError(Exception):
30
- sys: str
31
-
32
- @override
33
- def __str__(self) -> str:
34
- return ( # pragma: no cover
35
- f"System must be one of Windows, Darwin, Linux; got {self.sys!r} instead"
36
- )
37
-
38
-
39
- SYSTEM = get_system()
40
- IS_WINDOWS = SYSTEM == "windows"
41
- IS_MAC = SYSTEM == "mac"
42
- IS_LINUX = SYSTEM == "linux"
43
- IS_NOT_WINDOWS = not IS_WINDOWS
44
- IS_NOT_MAC = not IS_MAC
45
- IS_NOT_LINUX = not IS_LINUX
46
-
47
-
48
- ##
49
-
50
-
51
- def get_max_pid() -> int | None:
52
- """Get the maximum process ID."""
53
- match SYSTEM:
54
- case "windows": # pragma: no cover
55
- return None
56
- case "mac": # skipif-not-macos
57
- return 99999
58
- case "linux": # skipif-not-linux
59
- path = Path("/proc/sys/kernel/pid_max")
60
- try:
61
- return int(path.read_text())
62
- except FileNotFoundError: # pragma: no cover
63
- return None
64
- case never:
65
- assert_never(never)
66
-
67
-
68
- MAX_PID = get_max_pid()
69
-
70
-
71
- ##
6
+ from utilities.constants import SYSTEM
72
7
 
73
8
 
74
9
  def get_strftime(text: str, /) -> str:
@@ -87,32 +22,15 @@ def get_strftime(text: str, /) -> str:
87
22
  ##
88
23
 
89
24
 
90
- def maybe_yield_lower_case(text: Iterable[str], /) -> Iterator[str]:
91
- """Yield lower-cased text if the platform is case-insentive."""
25
+ def maybe_lower_case(text: str, /) -> str:
26
+ """Lower-case text if the platform is case-insensitive w.r.t. filenames."""
92
27
  match SYSTEM:
93
- case "windows": # skipif-not-windows
94
- yield from (t.lower() for t in text)
95
- case "mac": # skipif-not-macos
96
- yield from (t.lower() for t in text)
28
+ case "windows" | "mac": # skipif-linux
29
+ return text.lower()
97
30
  case "linux": # skipif-not-linux
98
- yield from text
31
+ return text
99
32
  case never:
100
33
  assert_never(never)
101
34
 
102
35
 
103
- __all__ = [
104
- "IS_LINUX",
105
- "IS_MAC",
106
- "IS_NOT_LINUX",
107
- "IS_NOT_MAC",
108
- "IS_NOT_WINDOWS",
109
- "IS_WINDOWS",
110
- "MAX_PID",
111
- "SYSTEM",
112
- "GetSystemError",
113
- "System",
114
- "get_max_pid",
115
- "get_strftime",
116
- "get_system",
117
- "maybe_yield_lower_case",
118
- ]
36
+ __all__ = ["get_strftime", "maybe_lower_case"]