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.
- dycw_utilities-0.185.8.dist-info/METADATA +33 -0
- dycw_utilities-0.185.8.dist-info/RECORD +90 -0
- {dycw_utilities-0.175.17.dist-info → dycw_utilities-0.185.8.dist-info}/WHEEL +2 -2
- utilities/__init__.py +1 -1
- utilities/altair.py +8 -6
- utilities/asyncio.py +40 -56
- utilities/atools.py +9 -11
- utilities/cachetools.py +8 -6
- utilities/click.py +4 -3
- utilities/concurrent.py +1 -1
- utilities/constants.py +492 -0
- utilities/contextlib.py +23 -30
- utilities/contextvars.py +1 -23
- utilities/core.py +2581 -0
- utilities/dataclasses.py +16 -119
- utilities/docker.py +139 -45
- utilities/enum.py +1 -1
- utilities/errors.py +2 -16
- utilities/fastapi.py +5 -5
- utilities/fpdf2.py +2 -1
- utilities/functions.py +33 -264
- utilities/http.py +2 -3
- utilities/hypothesis.py +48 -25
- utilities/iterables.py +39 -575
- utilities/jinja2.py +3 -6
- utilities/jupyter.py +5 -3
- utilities/libcst.py +1 -1
- utilities/lightweight_charts.py +4 -6
- utilities/logging.py +17 -15
- utilities/math.py +1 -36
- utilities/more_itertools.py +4 -6
- utilities/numpy.py +2 -1
- utilities/operator.py +2 -2
- utilities/orjson.py +24 -25
- utilities/os.py +4 -185
- utilities/packaging.py +129 -0
- utilities/parse.py +33 -13
- utilities/pathlib.py +2 -136
- utilities/platform.py +8 -90
- utilities/polars.py +34 -31
- utilities/postgres.py +9 -4
- utilities/pottery.py +20 -18
- utilities/pqdm.py +3 -4
- utilities/psutil.py +2 -3
- utilities/pydantic.py +18 -4
- utilities/pydantic_settings.py +7 -9
- utilities/pydantic_settings_sops.py +3 -3
- utilities/pyinstrument.py +4 -4
- utilities/pytest.py +49 -108
- utilities/pytest_plugins/pytest_regressions.py +2 -2
- utilities/pytest_regressions.py +8 -6
- utilities/random.py +2 -8
- utilities/redis.py +98 -94
- utilities/reprlib.py +11 -118
- utilities/shellingham.py +66 -0
- utilities/slack_sdk.py +13 -12
- utilities/sqlalchemy.py +42 -30
- utilities/sqlalchemy_polars.py +16 -25
- utilities/subprocess.py +1166 -148
- utilities/tabulate.py +32 -0
- utilities/testbook.py +8 -8
- utilities/text.py +24 -115
- utilities/throttle.py +159 -0
- utilities/time.py +18 -0
- utilities/timer.py +29 -12
- utilities/traceback.py +15 -22
- utilities/types.py +38 -3
- utilities/typing.py +18 -12
- utilities/uuid.py +1 -1
- utilities/version.py +202 -45
- utilities/whenever.py +22 -150
- dycw_utilities-0.175.17.dist-info/METADATA +0 -34
- dycw_utilities-0.175.17.dist-info/RECORD +0 -103
- utilities/atomicwrites.py +0 -182
- utilities/cryptography.py +0 -41
- utilities/getpass.py +0 -8
- utilities/git.py +0 -19
- utilities/grp.py +0 -28
- utilities/gzip.py +0 -31
- utilities/json.py +0 -70
- utilities/permissions.py +0 -298
- utilities/pickle.py +0 -25
- utilities/pwd.py +0 -28
- utilities/re.py +0 -156
- utilities/sentinel.py +0 -73
- utilities/socket.py +0 -8
- utilities/string.py +0 -20
- utilities/tempfile.py +0 -136
- utilities/tzdata.py +0 -11
- utilities/tzlocal.py +0 -28
- utilities/warnings.py +0 -65
- utilities/zipfile.py +0 -25
- utilities/zoneinfo.py +0 -133
- {dycw_utilities-0.175.17.dist-info → dycw_utilities-0.185.8.dist-info}/entry_points.txt +0 -0
utilities/version.py
CHANGED
|
@@ -2,23 +2,152 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
from collections.abc import Callable
|
|
5
|
+
from contextlib import suppress
|
|
5
6
|
from dataclasses import dataclass, field, replace
|
|
6
7
|
from functools import total_ordering
|
|
7
8
|
from typing import Any, Self, assert_never, overload, override
|
|
8
9
|
|
|
9
|
-
from utilities.
|
|
10
|
+
from utilities.constants import Sentinel
|
|
10
11
|
from utilities.types import MaybeCallable, MaybeStr
|
|
11
12
|
|
|
12
|
-
type
|
|
13
|
-
type
|
|
13
|
+
type Version2Like = MaybeStr[Version2]
|
|
14
|
+
type Version3Like = MaybeStr[Version3]
|
|
15
|
+
type Version2Or3 = Version2 | Version3
|
|
16
|
+
type MaybeCallableVersion3Like = MaybeCallable[Version3Like]
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
##
|
|
17
20
|
|
|
18
21
|
|
|
22
|
+
_PARSE_VERSION2_PATTERN = re.compile(r"^(\d+)\.(\d+)(?!\.\d)(?:-(\w+))?")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(repr=False, frozen=True, slots=True)
|
|
26
|
+
@total_ordering
|
|
27
|
+
class Version2:
|
|
28
|
+
"""A version identifier."""
|
|
29
|
+
|
|
30
|
+
major: int = 0
|
|
31
|
+
minor: int = 1
|
|
32
|
+
suffix: str | None = field(default=None, kw_only=True)
|
|
33
|
+
|
|
34
|
+
def __post_init__(self) -> None:
|
|
35
|
+
if (self.major == 0) and (self.minor == 0):
|
|
36
|
+
raise _Version2ZeroError(major=self.major, minor=self.minor)
|
|
37
|
+
if self.major < 0:
|
|
38
|
+
raise _Version2NegativeMajorVersionError(major=self.major)
|
|
39
|
+
if self.minor < 0:
|
|
40
|
+
raise _Version2NegativeMinorVersionError(minor=self.minor)
|
|
41
|
+
if (self.suffix is not None) and (len(self.suffix) == 0):
|
|
42
|
+
raise _Version2EmptySuffixError(suffix=self.suffix)
|
|
43
|
+
|
|
44
|
+
def __le__(self, other: Any, /) -> bool:
|
|
45
|
+
if not isinstance(other, type(self)):
|
|
46
|
+
return NotImplemented
|
|
47
|
+
self_as_tuple = (
|
|
48
|
+
self.major,
|
|
49
|
+
self.minor,
|
|
50
|
+
"" if self.suffix is None else self.suffix,
|
|
51
|
+
)
|
|
52
|
+
other_as_tuple = (
|
|
53
|
+
other.major,
|
|
54
|
+
other.minor,
|
|
55
|
+
"" if other.suffix is None else other.suffix,
|
|
56
|
+
)
|
|
57
|
+
return self_as_tuple <= other_as_tuple
|
|
58
|
+
|
|
59
|
+
@override
|
|
60
|
+
def __repr__(self) -> str:
|
|
61
|
+
version = f"{self.major}.{self.minor}"
|
|
62
|
+
if self.suffix is not None:
|
|
63
|
+
version = f"{version}-{self.suffix}"
|
|
64
|
+
return version
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def parse(cls, text: str, /) -> Self:
|
|
68
|
+
"""Parse a string into a Version2 object."""
|
|
69
|
+
try:
|
|
70
|
+
((major, minor, suffix),) = _PARSE_VERSION2_PATTERN.findall(text)
|
|
71
|
+
except ValueError:
|
|
72
|
+
raise _Version2ParseError(text=text) from None
|
|
73
|
+
return cls(int(major), int(minor), suffix=None if suffix == "" else suffix)
|
|
74
|
+
|
|
75
|
+
def bump_major(self) -> Self:
|
|
76
|
+
"""Bump the major component."""
|
|
77
|
+
return type(self)(self.major + 1, 0)
|
|
78
|
+
|
|
79
|
+
def bump_minor(self) -> Self:
|
|
80
|
+
"""Bump the minor component."""
|
|
81
|
+
return type(self)(self.major, self.minor + 1)
|
|
82
|
+
|
|
83
|
+
def version3(self, *, patch: int = 0) -> Version3:
|
|
84
|
+
"""Convert to a Version3 object."""
|
|
85
|
+
return Version3(self.major, self.minor, patch, suffix=self.suffix)
|
|
86
|
+
|
|
87
|
+
def with_suffix(self, *, suffix: str | None = None) -> Self:
|
|
88
|
+
"""Replace the suffix."""
|
|
89
|
+
return replace(self, suffix=suffix)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(kw_only=True, slots=True)
|
|
93
|
+
class Version2Error(Exception): ...
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(kw_only=True, slots=True)
|
|
97
|
+
class _Version2ZeroError(Version2Error):
|
|
98
|
+
major: int
|
|
99
|
+
minor: int
|
|
100
|
+
|
|
101
|
+
@override
|
|
102
|
+
def __str__(self) -> str:
|
|
103
|
+
return f"Version must be greater than zero; got {self.major}.{self.minor}"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(kw_only=True, slots=True)
|
|
107
|
+
class _Version2NegativeMajorVersionError(Version2Error):
|
|
108
|
+
major: int
|
|
109
|
+
|
|
110
|
+
@override
|
|
111
|
+
def __str__(self) -> str:
|
|
112
|
+
return f"Major version must be non-negative; got {self.major}"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass(kw_only=True, slots=True)
|
|
116
|
+
class _Version2NegativeMinorVersionError(Version2Error):
|
|
117
|
+
minor: int
|
|
118
|
+
|
|
119
|
+
@override
|
|
120
|
+
def __str__(self) -> str:
|
|
121
|
+
return f"Minor version must be non-negative; got {self.minor}"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(kw_only=True, slots=True)
|
|
125
|
+
class _Version2EmptySuffixError(Version2Error):
|
|
126
|
+
suffix: str
|
|
127
|
+
|
|
128
|
+
@override
|
|
129
|
+
def __str__(self) -> str:
|
|
130
|
+
return f"Suffix must be non-empty; got {self.suffix!r}"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(kw_only=True, slots=True)
|
|
134
|
+
class _Version2ParseError(Version2Error):
|
|
135
|
+
text: str
|
|
136
|
+
|
|
137
|
+
@override
|
|
138
|
+
def __str__(self) -> str:
|
|
139
|
+
return f"Unable to parse version; got {self.text!r}"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
##
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
_PARSE_VERSION3_PATTERN = re.compile(r"^(\d+)\.(\d+)\.(\d+)(?:-(\w+))?")
|
|
146
|
+
|
|
147
|
+
|
|
19
148
|
@dataclass(repr=False, frozen=True, slots=True)
|
|
20
149
|
@total_ordering
|
|
21
|
-
class
|
|
150
|
+
class Version3:
|
|
22
151
|
"""A version identifier."""
|
|
23
152
|
|
|
24
153
|
major: int = 0
|
|
@@ -28,17 +157,17 @@ class Version:
|
|
|
28
157
|
|
|
29
158
|
def __post_init__(self) -> None:
|
|
30
159
|
if (self.major == 0) and (self.minor == 0) and (self.patch == 0):
|
|
31
|
-
raise
|
|
160
|
+
raise _Version3ZeroError(
|
|
32
161
|
major=self.major, minor=self.minor, patch=self.patch
|
|
33
162
|
)
|
|
34
163
|
if self.major < 0:
|
|
35
|
-
raise
|
|
164
|
+
raise _Version3NegativeMajorVersionError(major=self.major)
|
|
36
165
|
if self.minor < 0:
|
|
37
|
-
raise
|
|
166
|
+
raise _Version3NegativeMinorVersionError(minor=self.minor)
|
|
38
167
|
if self.patch < 0:
|
|
39
|
-
raise
|
|
168
|
+
raise _Version3NegativePatchVersionError(patch=self.patch)
|
|
40
169
|
if (self.suffix is not None) and (len(self.suffix) == 0):
|
|
41
|
-
raise
|
|
170
|
+
raise _Version3EmptySuffixError(suffix=self.suffix)
|
|
42
171
|
|
|
43
172
|
def __le__(self, other: Any, /) -> bool:
|
|
44
173
|
if not isinstance(other, type(self)):
|
|
@@ -64,25 +193,45 @@ class Version:
|
|
|
64
193
|
version = f"{version}-{self.suffix}"
|
|
65
194
|
return version
|
|
66
195
|
|
|
196
|
+
@classmethod
|
|
197
|
+
def parse(cls, text: str, /) -> Self:
|
|
198
|
+
"""Parse a string into a Version3 object."""
|
|
199
|
+
try:
|
|
200
|
+
((major, minor, patch, suffix),) = _PARSE_VERSION3_PATTERN.findall(text)
|
|
201
|
+
except ValueError:
|
|
202
|
+
raise _Version3ParseError(text=text) from None
|
|
203
|
+
return cls(
|
|
204
|
+
int(major), int(minor), int(patch), suffix=None if suffix == "" else suffix
|
|
205
|
+
)
|
|
206
|
+
|
|
67
207
|
def bump_major(self) -> Self:
|
|
208
|
+
"""Bump the major component."""
|
|
68
209
|
return type(self)(self.major + 1, 0, 0)
|
|
69
210
|
|
|
70
211
|
def bump_minor(self) -> Self:
|
|
212
|
+
"""Bump the minor component."""
|
|
71
213
|
return type(self)(self.major, self.minor + 1, 0)
|
|
72
214
|
|
|
73
215
|
def bump_patch(self) -> Self:
|
|
216
|
+
"""Bump the patch component."""
|
|
74
217
|
return type(self)(self.major, self.minor, self.patch + 1)
|
|
75
218
|
|
|
219
|
+
@property
|
|
220
|
+
def version2(self) -> Version2:
|
|
221
|
+
"""Return the major/minor components only."""
|
|
222
|
+
return Version2(self.major, self.minor, suffix=self.suffix)
|
|
223
|
+
|
|
76
224
|
def with_suffix(self, *, suffix: str | None = None) -> Self:
|
|
225
|
+
"""Replace the suffix."""
|
|
77
226
|
return replace(self, suffix=suffix)
|
|
78
227
|
|
|
79
228
|
|
|
80
229
|
@dataclass(kw_only=True, slots=True)
|
|
81
|
-
class
|
|
230
|
+
class Version3Error(Exception): ...
|
|
82
231
|
|
|
83
232
|
|
|
84
233
|
@dataclass(kw_only=True, slots=True)
|
|
85
|
-
class
|
|
234
|
+
class _Version3ZeroError(Version3Error):
|
|
86
235
|
major: int
|
|
87
236
|
minor: int
|
|
88
237
|
patch: int
|
|
@@ -93,7 +242,7 @@ class _VersionZeroError(VersionError):
|
|
|
93
242
|
|
|
94
243
|
|
|
95
244
|
@dataclass(kw_only=True, slots=True)
|
|
96
|
-
class
|
|
245
|
+
class _Version3NegativeMajorVersionError(Version3Error):
|
|
97
246
|
major: int
|
|
98
247
|
|
|
99
248
|
@override
|
|
@@ -102,7 +251,7 @@ class _VersionNegativeMajorVersionError(VersionError):
|
|
|
102
251
|
|
|
103
252
|
|
|
104
253
|
@dataclass(kw_only=True, slots=True)
|
|
105
|
-
class
|
|
254
|
+
class _Version3NegativeMinorVersionError(Version3Error):
|
|
106
255
|
minor: int
|
|
107
256
|
|
|
108
257
|
@override
|
|
@@ -111,7 +260,7 @@ class _VersionNegativeMinorVersionError(VersionError):
|
|
|
111
260
|
|
|
112
261
|
|
|
113
262
|
@dataclass(kw_only=True, slots=True)
|
|
114
|
-
class
|
|
263
|
+
class _Version3NegativePatchVersionError(Version3Error):
|
|
115
264
|
patch: int
|
|
116
265
|
|
|
117
266
|
@override
|
|
@@ -120,7 +269,7 @@ class _VersionNegativePatchVersionError(VersionError):
|
|
|
120
269
|
|
|
121
270
|
|
|
122
271
|
@dataclass(kw_only=True, slots=True)
|
|
123
|
-
class
|
|
272
|
+
class _Version3EmptySuffixError(Version3Error):
|
|
124
273
|
suffix: str
|
|
125
274
|
|
|
126
275
|
@override
|
|
@@ -128,63 +277,71 @@ class _VersionEmptySuffixError(VersionError):
|
|
|
128
277
|
return f"Suffix must be non-empty; got {self.suffix!r}"
|
|
129
278
|
|
|
130
279
|
|
|
131
|
-
|
|
280
|
+
@dataclass(kw_only=True, slots=True)
|
|
281
|
+
class _Version3ParseError(Version3Error):
|
|
282
|
+
text: str
|
|
283
|
+
|
|
284
|
+
@override
|
|
285
|
+
def __str__(self) -> str:
|
|
286
|
+
return f"Unable to parse version; got {self.text!r}"
|
|
132
287
|
|
|
133
288
|
|
|
134
|
-
|
|
135
|
-
"""Parse a string into a version object."""
|
|
136
|
-
try:
|
|
137
|
-
((major, minor, patch, suffix),) = _PARSE_VERSION_PATTERN.findall(version)
|
|
138
|
-
except ValueError:
|
|
139
|
-
raise ParseVersionError(version=version) from None
|
|
140
|
-
return Version(
|
|
141
|
-
int(major), int(minor), int(patch), suffix=None if suffix == "" else suffix
|
|
142
|
-
)
|
|
289
|
+
##
|
|
143
290
|
|
|
144
291
|
|
|
145
|
-
|
|
292
|
+
def parse_version_2_or_3(text: str, /) -> Version2Or3:
|
|
293
|
+
"""Parse a string into a Version2 or Version3 object."""
|
|
294
|
+
with suppress(_Version3ParseError):
|
|
295
|
+
return Version3.parse(text)
|
|
296
|
+
with suppress(_Version2ParseError):
|
|
297
|
+
return Version2.parse(text)
|
|
298
|
+
raise ParseVersion2Or3Error(text=text)
|
|
146
299
|
|
|
147
300
|
|
|
148
301
|
@dataclass(kw_only=True, slots=True)
|
|
149
|
-
class
|
|
150
|
-
|
|
302
|
+
class ParseVersion2Or3Error(Exception):
|
|
303
|
+
text: str
|
|
151
304
|
|
|
152
305
|
@override
|
|
153
306
|
def __str__(self) -> str:
|
|
154
|
-
return f"
|
|
307
|
+
return f"Unable to parse Version2 or Version3; got {self.text!r}"
|
|
155
308
|
|
|
156
309
|
|
|
157
310
|
##
|
|
158
311
|
|
|
159
312
|
|
|
160
313
|
@overload
|
|
161
|
-
def
|
|
314
|
+
def to_version3(version: MaybeCallableVersion3Like, /) -> Version3: ...
|
|
162
315
|
@overload
|
|
163
|
-
def
|
|
316
|
+
def to_version3(version: None, /) -> None: ...
|
|
164
317
|
@overload
|
|
165
|
-
def
|
|
166
|
-
def
|
|
167
|
-
version:
|
|
168
|
-
) ->
|
|
318
|
+
def to_version3(version: Sentinel, /) -> Sentinel: ...
|
|
319
|
+
def to_version3(
|
|
320
|
+
version: MaybeCallableVersion3Like | None | Sentinel, /
|
|
321
|
+
) -> Version3 | None | Sentinel:
|
|
169
322
|
"""Convert to a version."""
|
|
170
323
|
match version:
|
|
171
|
-
case
|
|
324
|
+
case Version3() | None | Sentinel():
|
|
172
325
|
return version
|
|
173
326
|
case str():
|
|
174
|
-
return
|
|
327
|
+
return Version3.parse(version)
|
|
175
328
|
case Callable() as func:
|
|
176
|
-
return
|
|
329
|
+
return to_version3(func())
|
|
177
330
|
case never:
|
|
178
331
|
assert_never(never)
|
|
179
332
|
|
|
180
333
|
|
|
181
334
|
##
|
|
182
335
|
__all__ = [
|
|
183
|
-
"
|
|
184
|
-
"
|
|
185
|
-
"
|
|
186
|
-
"
|
|
187
|
-
"
|
|
188
|
-
"
|
|
189
|
-
"
|
|
336
|
+
"MaybeCallableVersion3Like",
|
|
337
|
+
"ParseVersion2Or3Error",
|
|
338
|
+
"Version2",
|
|
339
|
+
"Version2Error",
|
|
340
|
+
"Version2Like",
|
|
341
|
+
"Version2Or3",
|
|
342
|
+
"Version3",
|
|
343
|
+
"Version3Error",
|
|
344
|
+
"Version3Like",
|
|
345
|
+
"parse_version_2_or_3",
|
|
346
|
+
"to_version3",
|
|
190
347
|
]
|
utilities/whenever.py
CHANGED
|
@@ -32,13 +32,18 @@ from whenever import (
|
|
|
32
32
|
ZonedDateTime,
|
|
33
33
|
)
|
|
34
34
|
|
|
35
|
-
from utilities.
|
|
35
|
+
from utilities.constants import LOCAL_TIME_ZONE_NAME, UTC, Sentinel, sentinel
|
|
36
|
+
from utilities.core import (
|
|
37
|
+
get_now,
|
|
38
|
+
get_now_local,
|
|
39
|
+
get_time,
|
|
40
|
+
get_today,
|
|
41
|
+
replace_non_sentinel,
|
|
42
|
+
to_time_zone_name,
|
|
43
|
+
)
|
|
36
44
|
from utilities.functions import get_class_name
|
|
37
45
|
from utilities.math import sign
|
|
38
46
|
from utilities.platform import get_strftime
|
|
39
|
-
from utilities.sentinel import Sentinel, sentinel
|
|
40
|
-
from utilities.tzlocal import LOCAL_TIME_ZONE, LOCAL_TIME_ZONE_NAME
|
|
41
|
-
from utilities.zoneinfo import UTC, to_time_zone_name
|
|
42
47
|
|
|
43
48
|
if TYPE_CHECKING:
|
|
44
49
|
from utilities.types import (
|
|
@@ -53,13 +58,6 @@ if TYPE_CHECKING:
|
|
|
53
58
|
)
|
|
54
59
|
|
|
55
60
|
|
|
56
|
-
# bounds
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
ZONED_DATE_TIME_MIN = PlainDateTime.MIN.assume_tz(UTC.key)
|
|
60
|
-
ZONED_DATE_TIME_MAX = PlainDateTime.MAX.assume_tz(UTC.key)
|
|
61
|
-
|
|
62
|
-
|
|
63
61
|
DATE_TIME_DELTA_MIN = DateTimeDelta(
|
|
64
62
|
weeks=-521722,
|
|
65
63
|
days=-5,
|
|
@@ -80,10 +78,6 @@ DATE_TIME_DELTA_MAX = DateTimeDelta(
|
|
|
80
78
|
microseconds=999,
|
|
81
79
|
nanoseconds=999,
|
|
82
80
|
)
|
|
83
|
-
DATE_DELTA_MIN = DATE_TIME_DELTA_MIN.date_part()
|
|
84
|
-
DATE_DELTA_MAX = DATE_TIME_DELTA_MAX.date_part()
|
|
85
|
-
TIME_DELTA_MIN = TimeDelta(hours=-87831216)
|
|
86
|
-
TIME_DELTA_MAX = TimeDelta(hours=87831216)
|
|
87
81
|
|
|
88
82
|
|
|
89
83
|
DATE_TIME_DELTA_PARSABLE_MIN = DateTimeDelta(
|
|
@@ -115,22 +109,6 @@ DATE_TWO_DIGIT_YEAR_MAX = Date(DATE_TWO_DIGIT_YEAR_MIN.year + 99, 12, 31)
|
|
|
115
109
|
## common constants
|
|
116
110
|
|
|
117
111
|
|
|
118
|
-
ZERO_DAYS = DateDelta()
|
|
119
|
-
ZERO_TIME = TimeDelta()
|
|
120
|
-
MICROSECOND = TimeDelta(microseconds=1)
|
|
121
|
-
MILLISECOND = TimeDelta(milliseconds=1)
|
|
122
|
-
SECOND = TimeDelta(seconds=1)
|
|
123
|
-
MINUTE = TimeDelta(minutes=1)
|
|
124
|
-
HOUR = TimeDelta(hours=1)
|
|
125
|
-
DAY = DateDelta(days=1)
|
|
126
|
-
WEEK = DateDelta(weeks=1)
|
|
127
|
-
MONTH = DateDelta(months=1)
|
|
128
|
-
YEAR = DateDelta(years=1)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
##
|
|
132
|
-
|
|
133
|
-
|
|
134
112
|
def add_year_month(x: YearMonth, /, *, years: int = 0, months: int = 0) -> YearMonth:
|
|
135
113
|
"""Add to a year-month."""
|
|
136
114
|
y = x.on_day(1) + DateDelta(years=years, months=months)
|
|
@@ -258,6 +236,7 @@ def datetime_utc(
|
|
|
258
236
|
month: int,
|
|
259
237
|
day: int,
|
|
260
238
|
/,
|
|
239
|
+
*,
|
|
261
240
|
hour: int = 0,
|
|
262
241
|
minute: int = 0,
|
|
263
242
|
second: int = 0,
|
|
@@ -360,79 +339,6 @@ def from_timestamp_nanos(i: int, /, *, time_zone: TimeZoneLike = UTC) -> ZonedDa
|
|
|
360
339
|
##
|
|
361
340
|
|
|
362
341
|
|
|
363
|
-
def get_now(time_zone: TimeZoneLike = UTC, /) -> ZonedDateTime:
|
|
364
|
-
"""Get the current zoned date-time."""
|
|
365
|
-
return ZonedDateTime.now(to_time_zone_name(time_zone))
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
NOW_UTC = get_now(UTC)
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
def get_now_local() -> ZonedDateTime:
|
|
372
|
-
"""Get the current zoned date-time in the local time-zone."""
|
|
373
|
-
return get_now(LOCAL_TIME_ZONE)
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
NOW_LOCAL = get_now_local()
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
def get_now_plain(time_zone: TimeZoneLike = UTC, /) -> PlainDateTime:
|
|
380
|
-
"""Get the current plain date-time."""
|
|
381
|
-
return get_now(time_zone).to_plain()
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
NOW_PLAIN = get_now_plain()
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
def get_now_local_plain() -> PlainDateTime:
|
|
388
|
-
"""Get the current plain date-time in the local time-zone."""
|
|
389
|
-
return get_now_local().to_plain()
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
NOW_LOCAL_PLAIN = get_now_local_plain()
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
##
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
def get_time(time_zone: TimeZoneLike = UTC, /) -> Time:
|
|
399
|
-
"""Get the current time."""
|
|
400
|
-
return get_now(time_zone).time()
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
TIME_UTC = get_time(UTC)
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
def get_time_local() -> Time:
|
|
407
|
-
"""Get the current time in the local time-zone."""
|
|
408
|
-
return get_time(LOCAL_TIME_ZONE)
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
TIME_LOCAL = get_time_local()
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
##
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
def get_today(time_zone: TimeZoneLike = UTC, /) -> Date:
|
|
418
|
-
"""Get the current, timezone-aware local date."""
|
|
419
|
-
return get_now(time_zone).date()
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
TODAY_UTC = get_today(UTC)
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
def get_today_local() -> Date:
|
|
426
|
-
"""Get the current, timezone-aware local date."""
|
|
427
|
-
return get_today(LOCAL_TIME_ZONE)
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
TODAY_LOCAL = get_today_local()
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
##
|
|
434
|
-
|
|
435
|
-
|
|
436
342
|
def is_weekend(
|
|
437
343
|
date_time: ZonedDateTime,
|
|
438
344
|
/,
|
|
@@ -618,7 +524,7 @@ def _round_datetime_decompose(delta: Delta, /) -> tuple[int, _RoundDateOrDateTim
|
|
|
618
524
|
if (0 < hours < 24) and (24 % hours == 0):
|
|
619
525
|
return hours, "H"
|
|
620
526
|
raise _RoundDateOrDateTimeIncrementError(
|
|
621
|
-
|
|
527
|
+
delta=delta, increment=hours, divisor=24
|
|
622
528
|
)
|
|
623
529
|
try:
|
|
624
530
|
minutes = to_minutes(delta)
|
|
@@ -628,7 +534,7 @@ def _round_datetime_decompose(delta: Delta, /) -> tuple[int, _RoundDateOrDateTim
|
|
|
628
534
|
if (0 < minutes < 60) and (60 % minutes == 0):
|
|
629
535
|
return minutes, "M"
|
|
630
536
|
raise _RoundDateOrDateTimeIncrementError(
|
|
631
|
-
|
|
537
|
+
delta=delta, increment=minutes, divisor=60
|
|
632
538
|
)
|
|
633
539
|
try:
|
|
634
540
|
seconds = to_seconds(delta)
|
|
@@ -638,7 +544,7 @@ def _round_datetime_decompose(delta: Delta, /) -> tuple[int, _RoundDateOrDateTim
|
|
|
638
544
|
if (0 < seconds < 60) and (60 % seconds == 0):
|
|
639
545
|
return seconds, "S"
|
|
640
546
|
raise _RoundDateOrDateTimeIncrementError(
|
|
641
|
-
|
|
547
|
+
delta=delta, increment=seconds, divisor=60
|
|
642
548
|
)
|
|
643
549
|
try:
|
|
644
550
|
milliseconds = to_milliseconds(delta)
|
|
@@ -648,7 +554,7 @@ def _round_datetime_decompose(delta: Delta, /) -> tuple[int, _RoundDateOrDateTim
|
|
|
648
554
|
if (0 < milliseconds < 1000) and (1000 % milliseconds == 0):
|
|
649
555
|
return milliseconds, "ms"
|
|
650
556
|
raise _RoundDateOrDateTimeIncrementError(
|
|
651
|
-
|
|
557
|
+
delta=delta, increment=milliseconds, divisor=1000
|
|
652
558
|
)
|
|
653
559
|
try:
|
|
654
560
|
microseconds = to_microseconds(delta)
|
|
@@ -658,16 +564,16 @@ def _round_datetime_decompose(delta: Delta, /) -> tuple[int, _RoundDateOrDateTim
|
|
|
658
564
|
if (0 < microseconds < 1000) and (1000 % microseconds == 0):
|
|
659
565
|
return microseconds, "us"
|
|
660
566
|
raise _RoundDateOrDateTimeIncrementError(
|
|
661
|
-
|
|
567
|
+
delta=delta, increment=microseconds, divisor=1000
|
|
662
568
|
)
|
|
663
569
|
try:
|
|
664
570
|
nanoseconds = to_nanoseconds(delta)
|
|
665
571
|
except ToNanosecondsError:
|
|
666
|
-
raise
|
|
572
|
+
raise _RoundDateOrDateTimeInvalidDeltaError(delta=delta) from None
|
|
667
573
|
if (0 < nanoseconds < 1000) and (1000 % nanoseconds == 0):
|
|
668
574
|
return nanoseconds, "ns"
|
|
669
575
|
raise _RoundDateOrDateTimeIncrementError(
|
|
670
|
-
|
|
576
|
+
delta=delta, increment=nanoseconds, divisor=1000
|
|
671
577
|
)
|
|
672
578
|
|
|
673
579
|
|
|
@@ -787,22 +693,22 @@ class RoundDateOrDateTimeError(Exception): ...
|
|
|
787
693
|
|
|
788
694
|
@dataclass(kw_only=True, slots=True)
|
|
789
695
|
class _RoundDateOrDateTimeIncrementError(RoundDateOrDateTimeError):
|
|
790
|
-
|
|
696
|
+
delta: Delta
|
|
791
697
|
increment: int
|
|
792
698
|
divisor: int
|
|
793
699
|
|
|
794
700
|
@override
|
|
795
701
|
def __str__(self) -> str:
|
|
796
|
-
return f"
|
|
702
|
+
return f"Delta {self.delta} increment must be a proper divisor of {self.divisor}; got {self.increment}"
|
|
797
703
|
|
|
798
704
|
|
|
799
705
|
@dataclass(kw_only=True, slots=True)
|
|
800
|
-
class
|
|
801
|
-
|
|
706
|
+
class _RoundDateOrDateTimeInvalidDeltaError(RoundDateOrDateTimeError):
|
|
707
|
+
delta: Delta
|
|
802
708
|
|
|
803
709
|
@override
|
|
804
710
|
def __str__(self) -> str:
|
|
805
|
-
return f"
|
|
711
|
+
return f"Delta must be valid; got {self.delta}"
|
|
806
712
|
|
|
807
713
|
|
|
808
714
|
@dataclass(kw_only=True, slots=True)
|
|
@@ -1997,38 +1903,12 @@ class _ZonedDateTimePeriodExactEqError(ZonedDateTimePeriodError):
|
|
|
1997
1903
|
|
|
1998
1904
|
|
|
1999
1905
|
__all__ = [
|
|
2000
|
-
"DATE_DELTA_MAX",
|
|
2001
|
-
"DATE_DELTA_MIN",
|
|
2002
1906
|
"DATE_DELTA_PARSABLE_MAX",
|
|
2003
1907
|
"DATE_DELTA_PARSABLE_MIN",
|
|
2004
|
-
"DATE_TIME_DELTA_MAX",
|
|
2005
|
-
"DATE_TIME_DELTA_MIN",
|
|
2006
1908
|
"DATE_TIME_DELTA_PARSABLE_MAX",
|
|
2007
1909
|
"DATE_TIME_DELTA_PARSABLE_MIN",
|
|
2008
1910
|
"DATE_TWO_DIGIT_YEAR_MAX",
|
|
2009
1911
|
"DATE_TWO_DIGIT_YEAR_MIN",
|
|
2010
|
-
"DAY",
|
|
2011
|
-
"HOUR",
|
|
2012
|
-
"MICROSECOND",
|
|
2013
|
-
"MILLISECOND",
|
|
2014
|
-
"MINUTE",
|
|
2015
|
-
"MONTH",
|
|
2016
|
-
"NOW_LOCAL",
|
|
2017
|
-
"NOW_LOCAL_PLAIN",
|
|
2018
|
-
"NOW_PLAIN",
|
|
2019
|
-
"SECOND",
|
|
2020
|
-
"TIME_DELTA_MAX",
|
|
2021
|
-
"TIME_DELTA_MIN",
|
|
2022
|
-
"TIME_LOCAL",
|
|
2023
|
-
"TIME_UTC",
|
|
2024
|
-
"TODAY_LOCAL",
|
|
2025
|
-
"TODAY_UTC",
|
|
2026
|
-
"WEEK",
|
|
2027
|
-
"YEAR",
|
|
2028
|
-
"ZERO_DAYS",
|
|
2029
|
-
"ZERO_TIME",
|
|
2030
|
-
"ZONED_DATE_TIME_MAX",
|
|
2031
|
-
"ZONED_DATE_TIME_MIN",
|
|
2032
1912
|
"DatePeriod",
|
|
2033
1913
|
"DatePeriodError",
|
|
2034
1914
|
"MeanDateTimeError",
|
|
@@ -2055,14 +1935,6 @@ __all__ = [
|
|
|
2055
1935
|
"from_timestamp",
|
|
2056
1936
|
"from_timestamp_millis",
|
|
2057
1937
|
"from_timestamp_nanos",
|
|
2058
|
-
"get_now",
|
|
2059
|
-
"get_now_local",
|
|
2060
|
-
"get_now_local_plain",
|
|
2061
|
-
"get_now_plain",
|
|
2062
|
-
"get_time",
|
|
2063
|
-
"get_time_local",
|
|
2064
|
-
"get_today",
|
|
2065
|
-
"get_today_local",
|
|
2066
1938
|
"is_weekend",
|
|
2067
1939
|
"mean_datetime",
|
|
2068
1940
|
"min_max_date",
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: dycw-utilities
|
|
3
|
-
Version: 0.175.17
|
|
4
|
-
Summary: Miscellaneous Python utilities
|
|
5
|
-
Author: Derek Wan
|
|
6
|
-
Author-email: Derek Wan <d.wan@icloud.com>
|
|
7
|
-
Requires-Dist: atomicwrites>=1.4.1,<1.5
|
|
8
|
-
Requires-Dist: typing-extensions>=4.15.0,<4.16
|
|
9
|
-
Requires-Dist: tzlocal>=5.3.1,<5.4
|
|
10
|
-
Requires-Dist: whenever>=0.9.4,<0.10
|
|
11
|
-
Requires-Dist: coloredlogs>=15.0.1,<15.1 ; extra == 'logging'
|
|
12
|
-
Requires-Dist: dycw-pytest-only>=2.1.1,<2.2 ; extra == 'test'
|
|
13
|
-
Requires-Dist: hypothesis>=6.148.8,<6.149 ; extra == 'test'
|
|
14
|
-
Requires-Dist: pytest>=9.0.2,<9.1 ; extra == 'test'
|
|
15
|
-
Requires-Dist: pytest-asyncio>=1.3.0,<1.4 ; extra == 'test'
|
|
16
|
-
Requires-Dist: pytest-cov>=7.0.0,<7.1 ; extra == 'test'
|
|
17
|
-
Requires-Dist: pytest-instafail>=0.5.0,<0.6 ; extra == 'test'
|
|
18
|
-
Requires-Dist: pytest-lazy-fixtures>=1.4.0,<1.5 ; extra == 'test'
|
|
19
|
-
Requires-Dist: pytest-randomly>=4.0.1,<4.1 ; extra == 'test'
|
|
20
|
-
Requires-Dist: pytest-regressions>=2.8.3,<2.9 ; extra == 'test'
|
|
21
|
-
Requires-Dist: pytest-repeat>=0.9.4,<0.10 ; extra == 'test'
|
|
22
|
-
Requires-Dist: pytest-rerunfailures>=16.1,<16.2 ; extra == 'test'
|
|
23
|
-
Requires-Dist: pytest-rng>=1.0.0,<1.1 ; extra == 'test'
|
|
24
|
-
Requires-Dist: pytest-timeout>=2.4.0,<2.5 ; extra == 'test'
|
|
25
|
-
Requires-Dist: pytest-xdist>=3.8.0,<3.9 ; extra == 'test'
|
|
26
|
-
Requires-Dist: testbook>=0.4.2,<0.5 ; extra == 'test'
|
|
27
|
-
Requires-Python: >=3.12
|
|
28
|
-
Provides-Extra: logging
|
|
29
|
-
Provides-Extra: test
|
|
30
|
-
Description-Content-Type: text/markdown
|
|
31
|
-
|
|
32
|
-
# `python-utilities`
|
|
33
|
-
|
|
34
|
-
Miscellaneous Python utilities
|