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/luigi.py
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from abc import ABC, abstractmethod
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import TYPE_CHECKING, Any, Literal, cast, overload, override
|
|
6
|
-
|
|
7
|
-
import luigi
|
|
8
|
-
from luigi import Parameter, PathParameter, Target, Task
|
|
9
|
-
from luigi import build as _build
|
|
10
|
-
|
|
11
|
-
from utilities.datetime import EPOCH_UTC
|
|
12
|
-
|
|
13
|
-
if TYPE_CHECKING:
|
|
14
|
-
import datetime as dt
|
|
15
|
-
from collections.abc import Iterable
|
|
16
|
-
|
|
17
|
-
from luigi.execution_summary import LuigiRunResult
|
|
18
|
-
|
|
19
|
-
from utilities.types import LogLevel, MaybeStr, PathLike
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
# parameters
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class DateHourParameter(luigi.DateHourParameter):
|
|
26
|
-
"""A parameter which takes the value of an hourly `dt.datetime`."""
|
|
27
|
-
|
|
28
|
-
def __init__(self, interval: int = 1, **kwargs: Any) -> None:
|
|
29
|
-
super().__init__(interval, EPOCH_UTC, **kwargs)
|
|
30
|
-
|
|
31
|
-
@override
|
|
32
|
-
def normalize(self, dt: MaybeStr[dt.datetime]) -> dt.datetime:
|
|
33
|
-
from utilities.whenever import ensure_zoned_datetime
|
|
34
|
-
|
|
35
|
-
return ensure_zoned_datetime(dt)
|
|
36
|
-
|
|
37
|
-
@override
|
|
38
|
-
def parse(self, s: str) -> dt.datetime:
|
|
39
|
-
from utilities.whenever import parse_zoned_datetime
|
|
40
|
-
|
|
41
|
-
return parse_zoned_datetime(s)
|
|
42
|
-
|
|
43
|
-
@override
|
|
44
|
-
def serialize(self, dt: dt.datetime) -> str:
|
|
45
|
-
from utilities.whenever import serialize_zoned_datetime
|
|
46
|
-
|
|
47
|
-
return serialize_zoned_datetime(dt)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class DateMinuteParameter(luigi.DateMinuteParameter):
|
|
51
|
-
"""A parameter which takes the value of a minutely `dt.datetime`."""
|
|
52
|
-
|
|
53
|
-
def __init__(self, interval: int = 1, **kwargs: Any) -> None:
|
|
54
|
-
super().__init__(interval=interval, start=EPOCH_UTC, **kwargs)
|
|
55
|
-
|
|
56
|
-
@override
|
|
57
|
-
def normalize(self, dt: MaybeStr[dt.datetime]) -> dt.datetime:
|
|
58
|
-
from utilities.whenever import ensure_zoned_datetime
|
|
59
|
-
|
|
60
|
-
return ensure_zoned_datetime(dt)
|
|
61
|
-
|
|
62
|
-
@override
|
|
63
|
-
def parse(self, s: str) -> dt.datetime:
|
|
64
|
-
from utilities.whenever import parse_zoned_datetime
|
|
65
|
-
|
|
66
|
-
return parse_zoned_datetime(s)
|
|
67
|
-
|
|
68
|
-
@override
|
|
69
|
-
def serialize(self, dt: dt.datetime) -> str:
|
|
70
|
-
from utilities.whenever import serialize_zoned_datetime
|
|
71
|
-
|
|
72
|
-
return serialize_zoned_datetime(dt)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
class DateSecondParameter(luigi.DateSecondParameter):
|
|
76
|
-
"""A parameter which takes the value of a secondly `dt.datetime`."""
|
|
77
|
-
|
|
78
|
-
def __init__(self, interval: int = 1, **kwargs: Any) -> None:
|
|
79
|
-
super().__init__(interval, EPOCH_UTC, **kwargs)
|
|
80
|
-
|
|
81
|
-
@override
|
|
82
|
-
def normalize(self, dt: MaybeStr[dt.datetime]) -> dt.datetime:
|
|
83
|
-
from utilities.whenever import ensure_zoned_datetime
|
|
84
|
-
|
|
85
|
-
return ensure_zoned_datetime(dt)
|
|
86
|
-
|
|
87
|
-
@override
|
|
88
|
-
def parse(self, s: str) -> dt.datetime:
|
|
89
|
-
from utilities.whenever import parse_zoned_datetime
|
|
90
|
-
|
|
91
|
-
return parse_zoned_datetime(s)
|
|
92
|
-
|
|
93
|
-
@override
|
|
94
|
-
def serialize(self, dt: dt.datetime) -> str:
|
|
95
|
-
from utilities.whenever import serialize_zoned_datetime
|
|
96
|
-
|
|
97
|
-
return serialize_zoned_datetime(dt)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
class TimeParameter(Parameter):
|
|
101
|
-
"""A parameter which takes the value of a `dt.time`."""
|
|
102
|
-
|
|
103
|
-
@override
|
|
104
|
-
def normalize(self, x: MaybeStr[dt.time]) -> dt.time:
|
|
105
|
-
from utilities.whenever import ensure_time
|
|
106
|
-
|
|
107
|
-
return ensure_time(x)
|
|
108
|
-
|
|
109
|
-
@override
|
|
110
|
-
def parse(self, x: str) -> dt.time:
|
|
111
|
-
from utilities.whenever import parse_time
|
|
112
|
-
|
|
113
|
-
return parse_time(x)
|
|
114
|
-
|
|
115
|
-
@override
|
|
116
|
-
def serialize(self, x: dt.time) -> str:
|
|
117
|
-
from utilities.whenever import serialize_time
|
|
118
|
-
|
|
119
|
-
return serialize_time(x)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
# targets
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
class PathTarget(Target):
|
|
126
|
-
"""A local target whose `path` attribute is a Pathlib instance."""
|
|
127
|
-
|
|
128
|
-
def __init__(self, path: PathLike, /) -> None:
|
|
129
|
-
super().__init__()
|
|
130
|
-
self.path = Path(path)
|
|
131
|
-
|
|
132
|
-
@override
|
|
133
|
-
def exists(self) -> bool: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
134
|
-
"""Check if the target exists."""
|
|
135
|
-
return self.path.exists()
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
# tasks
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
class ExternalTask(ABC, luigi.ExternalTask):
|
|
142
|
-
"""An external task with `exists()` defined here."""
|
|
143
|
-
|
|
144
|
-
@abstractmethod
|
|
145
|
-
def exists(self) -> bool:
|
|
146
|
-
"""Predicate on which the external task is deemed to exist."""
|
|
147
|
-
msg = f"{self=}" # pragma: no cover
|
|
148
|
-
raise NotImplementedError(msg) # pragma: no cover
|
|
149
|
-
|
|
150
|
-
@override
|
|
151
|
-
def output(self) -> _ExternalTaskDummyTarget: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
152
|
-
return _ExternalTaskDummyTarget(self)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
class _ExternalTaskDummyTarget(Target):
|
|
156
|
-
"""Dummy target for `ExternalTask`."""
|
|
157
|
-
|
|
158
|
-
def __init__(self, task: ExternalTask, /) -> None:
|
|
159
|
-
super().__init__()
|
|
160
|
-
self._task = task
|
|
161
|
-
|
|
162
|
-
@override
|
|
163
|
-
def exists(self) -> bool: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
164
|
-
return self._task.exists()
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
class ExternalFile(ExternalTask):
|
|
168
|
-
"""Await an external file on the local disk."""
|
|
169
|
-
|
|
170
|
-
path: Path = cast("Any", PathParameter())
|
|
171
|
-
|
|
172
|
-
@override
|
|
173
|
-
def exists(self) -> bool:
|
|
174
|
-
return self.path.exists()
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
# functions
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
@overload
|
|
181
|
-
def build(
|
|
182
|
-
task: Iterable[Task],
|
|
183
|
-
/,
|
|
184
|
-
*,
|
|
185
|
-
detailed_summary: Literal[False] = False,
|
|
186
|
-
local_scheduler: bool = False,
|
|
187
|
-
log_level: LogLevel | None = None,
|
|
188
|
-
workers: int | None = None,
|
|
189
|
-
) -> bool: ...
|
|
190
|
-
@overload
|
|
191
|
-
def build(
|
|
192
|
-
task: Iterable[Task],
|
|
193
|
-
/,
|
|
194
|
-
*,
|
|
195
|
-
detailed_summary: Literal[True],
|
|
196
|
-
local_scheduler: bool = False,
|
|
197
|
-
log_level: LogLevel | None = None,
|
|
198
|
-
workers: int | None = None,
|
|
199
|
-
) -> LuigiRunResult: ...
|
|
200
|
-
def build(
|
|
201
|
-
task: Iterable[Task],
|
|
202
|
-
/,
|
|
203
|
-
*,
|
|
204
|
-
detailed_summary: bool = False,
|
|
205
|
-
local_scheduler: bool = False,
|
|
206
|
-
log_level: LogLevel | None = None,
|
|
207
|
-
workers: int | None = None,
|
|
208
|
-
) -> bool | LuigiRunResult:
|
|
209
|
-
"""Build a set of tasks."""
|
|
210
|
-
return _build(
|
|
211
|
-
task,
|
|
212
|
-
detailed_summary=detailed_summary,
|
|
213
|
-
local_scheduler=local_scheduler,
|
|
214
|
-
**({} if log_level is None else {"log_level": log_level}),
|
|
215
|
-
**({} if workers is None else {"workers": workers}),
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
__all__ = [
|
|
220
|
-
"DateHourParameter",
|
|
221
|
-
"DateMinuteParameter",
|
|
222
|
-
"DateSecondParameter",
|
|
223
|
-
"ExternalFile",
|
|
224
|
-
"ExternalTask",
|
|
225
|
-
"PathTarget",
|
|
226
|
-
"TimeParameter",
|
|
227
|
-
"build",
|
|
228
|
-
]
|
utilities/period.py
DELETED
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import datetime as dt
|
|
4
|
-
from dataclasses import dataclass, field
|
|
5
|
-
from functools import cached_property
|
|
6
|
-
from itertools import permutations
|
|
7
|
-
from typing import (
|
|
8
|
-
TYPE_CHECKING,
|
|
9
|
-
Generic,
|
|
10
|
-
Literal,
|
|
11
|
-
Self,
|
|
12
|
-
TypedDict,
|
|
13
|
-
TypeVar,
|
|
14
|
-
assert_never,
|
|
15
|
-
cast,
|
|
16
|
-
override,
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
from utilities.datetime import ZERO_TIME
|
|
20
|
-
from utilities.functions import get_class_name
|
|
21
|
-
from utilities.iterables import OneUniqueNonUniqueError, always_iterable, one_unique
|
|
22
|
-
from utilities.sentinel import Sentinel, sentinel
|
|
23
|
-
from utilities.typing import is_instance_gen
|
|
24
|
-
from utilities.whenever import (
|
|
25
|
-
serialize_date,
|
|
26
|
-
serialize_plain_datetime,
|
|
27
|
-
serialize_zoned_datetime,
|
|
28
|
-
)
|
|
29
|
-
from utilities.zoneinfo import EnsureTimeZoneError, ensure_time_zone
|
|
30
|
-
|
|
31
|
-
if TYPE_CHECKING:
|
|
32
|
-
from zoneinfo import ZoneInfo
|
|
33
|
-
|
|
34
|
-
from utilities.iterables import MaybeIterable
|
|
35
|
-
from utilities.types import DateOrDateTime
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
type _DateOrDateTime = Literal["date", "datetime"]
|
|
39
|
-
_TPeriod = TypeVar("_TPeriod", dt.date, dt.datetime)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
class _PeriodAsDict(TypedDict, Generic[_TPeriod]):
|
|
43
|
-
start: _TPeriod
|
|
44
|
-
end: _TPeriod
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@dataclass(repr=False, order=True, unsafe_hash=True)
|
|
48
|
-
class Period(Generic[_TPeriod]):
|
|
49
|
-
"""A period of time."""
|
|
50
|
-
|
|
51
|
-
start: _TPeriod
|
|
52
|
-
end: _TPeriod
|
|
53
|
-
req_duration: MaybeIterable[dt.timedelta] | None = field(
|
|
54
|
-
default=None, repr=False, kw_only=True
|
|
55
|
-
)
|
|
56
|
-
min_duration: dt.timedelta | None = field(default=None, repr=False, kw_only=True)
|
|
57
|
-
max_duration: dt.timedelta | None = field(default=None, repr=False, kw_only=True)
|
|
58
|
-
|
|
59
|
-
def __post_init__(self) -> None:
|
|
60
|
-
if any(
|
|
61
|
-
is_instance_gen(left, cls) is not is_instance_gen(right, cls)
|
|
62
|
-
for left, right in permutations([self.start, self.end], 2)
|
|
63
|
-
for cls in [dt.date, dt.datetime]
|
|
64
|
-
):
|
|
65
|
-
raise _PeriodDateAndDateTimeMixedError(start=self.start, end=self.end)
|
|
66
|
-
for date in [self.start, self.end]:
|
|
67
|
-
if isinstance(date, dt.datetime):
|
|
68
|
-
try:
|
|
69
|
-
_ = ensure_time_zone(date)
|
|
70
|
-
except EnsureTimeZoneError:
|
|
71
|
-
raise _PeriodNaiveDateTimeError(
|
|
72
|
-
start=self.start, end=self.end
|
|
73
|
-
) from None
|
|
74
|
-
duration = self.end - self.start
|
|
75
|
-
if duration < ZERO_TIME:
|
|
76
|
-
raise _PeriodInvalidError(start=self.start, end=self.end)
|
|
77
|
-
if (self.req_duration is not None) and (
|
|
78
|
-
duration not in always_iterable(self.req_duration)
|
|
79
|
-
):
|
|
80
|
-
raise _PeriodReqDurationError(
|
|
81
|
-
start=self.start,
|
|
82
|
-
end=self.end,
|
|
83
|
-
duration=duration,
|
|
84
|
-
req_duration=self.req_duration,
|
|
85
|
-
)
|
|
86
|
-
if (self.min_duration is not None) and (duration < self.min_duration):
|
|
87
|
-
raise _PeriodMinDurationError(
|
|
88
|
-
start=self.start,
|
|
89
|
-
end=self.end,
|
|
90
|
-
duration=duration,
|
|
91
|
-
min_duration=self.min_duration,
|
|
92
|
-
)
|
|
93
|
-
if (self.max_duration is not None) and (duration > self.max_duration):
|
|
94
|
-
raise _PeriodMaxDurationError(
|
|
95
|
-
start=self.start,
|
|
96
|
-
end=self.end,
|
|
97
|
-
duration=duration,
|
|
98
|
-
max_duration=self.max_duration,
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
def __add__(self, other: dt.timedelta, /) -> Self:
|
|
102
|
-
"""Offset the period."""
|
|
103
|
-
return self.replace(start=self.start + other, end=self.end + other)
|
|
104
|
-
|
|
105
|
-
def __contains__(self, other: DateOrDateTime, /) -> bool:
|
|
106
|
-
"""Check if a date/datetime lies in the period."""
|
|
107
|
-
match self.kind:
|
|
108
|
-
case "date":
|
|
109
|
-
if isinstance(other, dt.datetime):
|
|
110
|
-
raise _PeriodDateContainsDateTimeError(
|
|
111
|
-
start=self.start, end=self.end
|
|
112
|
-
)
|
|
113
|
-
case "datetime":
|
|
114
|
-
if not isinstance(other, dt.datetime):
|
|
115
|
-
raise _PeriodDateTimeContainsDateError(
|
|
116
|
-
start=self.start, end=self.end
|
|
117
|
-
)
|
|
118
|
-
case _ as never:
|
|
119
|
-
assert_never(never)
|
|
120
|
-
return self.start <= other <= self.end
|
|
121
|
-
|
|
122
|
-
@override
|
|
123
|
-
def __repr__(self) -> str:
|
|
124
|
-
cls = get_class_name(self)
|
|
125
|
-
match self.kind:
|
|
126
|
-
case "date":
|
|
127
|
-
result = cast("Period[dt.date]", self)
|
|
128
|
-
start, end = map(serialize_date, [result.start, result.end])
|
|
129
|
-
return f"{cls}({start}, {end})"
|
|
130
|
-
case "datetime":
|
|
131
|
-
result = cast("Period[dt.datetime]", self)
|
|
132
|
-
try:
|
|
133
|
-
time_zone = result.time_zone
|
|
134
|
-
except _PeriodTimeZoneNonUniqueError:
|
|
135
|
-
start, end = map(
|
|
136
|
-
serialize_zoned_datetime, [result.start, result.end]
|
|
137
|
-
)
|
|
138
|
-
return f"{cls}({start}, {end})"
|
|
139
|
-
start, end = (
|
|
140
|
-
serialize_plain_datetime(t.replace(tzinfo=None))
|
|
141
|
-
for t in [result.start, result.end]
|
|
142
|
-
)
|
|
143
|
-
return f"{cls}({start}, {end}, {time_zone})"
|
|
144
|
-
case _ as never:
|
|
145
|
-
assert_never(never)
|
|
146
|
-
|
|
147
|
-
def __sub__(self, other: dt.timedelta, /) -> Self:
|
|
148
|
-
"""Offset the period."""
|
|
149
|
-
return self.replace(start=self.start - other, end=self.end - other)
|
|
150
|
-
|
|
151
|
-
def astimezone(self, time_zone: ZoneInfo, /) -> Self:
|
|
152
|
-
"""Convert the timezone of the period, if it is a datetime period."""
|
|
153
|
-
match self.kind:
|
|
154
|
-
case "date":
|
|
155
|
-
raise _PeriodAsTimeZoneInapplicableError(start=self.start, end=self.end)
|
|
156
|
-
case "datetime":
|
|
157
|
-
result = cast("Period[dt.datetime]", self)
|
|
158
|
-
result = result.replace(
|
|
159
|
-
start=result.start.astimezone(time_zone),
|
|
160
|
-
end=result.end.astimezone(time_zone),
|
|
161
|
-
)
|
|
162
|
-
return cast("Self", result)
|
|
163
|
-
case _ as never:
|
|
164
|
-
assert_never(never)
|
|
165
|
-
|
|
166
|
-
@cached_property
|
|
167
|
-
def duration(self) -> dt.timedelta:
|
|
168
|
-
"""The duration of the period."""
|
|
169
|
-
return self.end - self.start
|
|
170
|
-
|
|
171
|
-
@cached_property
|
|
172
|
-
def kind(self) -> _DateOrDateTime:
|
|
173
|
-
"""The kind of the period."""
|
|
174
|
-
return "date" if is_instance_gen(self.start, dt.date) else "datetime"
|
|
175
|
-
|
|
176
|
-
def replace(
|
|
177
|
-
self,
|
|
178
|
-
*,
|
|
179
|
-
start: _TPeriod | None = None,
|
|
180
|
-
end: _TPeriod | None = None,
|
|
181
|
-
req_duration: MaybeIterable[dt.timedelta] | None | Sentinel = sentinel,
|
|
182
|
-
min_duration: dt.timedelta | None | Sentinel = sentinel,
|
|
183
|
-
max_duration: dt.timedelta | None | Sentinel = sentinel,
|
|
184
|
-
) -> Self:
|
|
185
|
-
"""Replace elements of the period."""
|
|
186
|
-
return type(self)(
|
|
187
|
-
self.start if start is None else start,
|
|
188
|
-
self.end if end is None else end,
|
|
189
|
-
req_duration=self.req_duration
|
|
190
|
-
if isinstance(req_duration, Sentinel)
|
|
191
|
-
else req_duration,
|
|
192
|
-
min_duration=self.min_duration
|
|
193
|
-
if isinstance(min_duration, Sentinel)
|
|
194
|
-
else min_duration,
|
|
195
|
-
max_duration=self.max_duration
|
|
196
|
-
if isinstance(max_duration, Sentinel)
|
|
197
|
-
else max_duration,
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
@cached_property
|
|
201
|
-
def time_zone(self) -> ZoneInfo:
|
|
202
|
-
"""The time zone of the period."""
|
|
203
|
-
match self.kind:
|
|
204
|
-
case "date":
|
|
205
|
-
raise _PeriodTimeZoneInapplicableError(
|
|
206
|
-
start=self.start, end=self.end
|
|
207
|
-
) from None
|
|
208
|
-
case "datetime":
|
|
209
|
-
result = cast("Period[dt.datetime]", self)
|
|
210
|
-
try:
|
|
211
|
-
return one_unique(map(ensure_time_zone, [result.start, result.end]))
|
|
212
|
-
except OneUniqueNonUniqueError as error:
|
|
213
|
-
raise _PeriodTimeZoneNonUniqueError(
|
|
214
|
-
start=self.start,
|
|
215
|
-
end=self.end,
|
|
216
|
-
first=error.first,
|
|
217
|
-
second=error.second,
|
|
218
|
-
) from None
|
|
219
|
-
case _ as never:
|
|
220
|
-
assert_never(never)
|
|
221
|
-
|
|
222
|
-
def to_dict(self) -> _PeriodAsDict:
|
|
223
|
-
"""Convert the period to a dictionary."""
|
|
224
|
-
return {"start": self.start, "end": self.end}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
@dataclass(kw_only=True, slots=True)
|
|
228
|
-
class PeriodError(Generic[_TPeriod], Exception):
|
|
229
|
-
start: _TPeriod
|
|
230
|
-
end: _TPeriod
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
@dataclass(kw_only=True, slots=True)
|
|
234
|
-
class _PeriodDateAndDateTimeMixedError(PeriodError[_TPeriod]):
|
|
235
|
-
@override
|
|
236
|
-
def __str__(self) -> str:
|
|
237
|
-
return f"Invalid period; got date and datetime mix ({self.start}, {self.end})"
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
@dataclass(kw_only=True, slots=True)
|
|
241
|
-
class _PeriodNaiveDateTimeError(PeriodError[_TPeriod]):
|
|
242
|
-
@override
|
|
243
|
-
def __str__(self) -> str:
|
|
244
|
-
return f"Invalid period; got naive datetime(s) ({self.start}, {self.end})"
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
@dataclass(kw_only=True, slots=True)
|
|
248
|
-
class _PeriodInvalidError(PeriodError[_TPeriod]):
|
|
249
|
-
@override
|
|
250
|
-
def __str__(self) -> str:
|
|
251
|
-
return f"Invalid period; got {self.start} > {self.end}"
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
@dataclass(kw_only=True, slots=True)
|
|
255
|
-
class _PeriodReqDurationError(PeriodError[_TPeriod]):
|
|
256
|
-
duration: dt.timedelta
|
|
257
|
-
req_duration: MaybeIterable[dt.timedelta]
|
|
258
|
-
|
|
259
|
-
@override
|
|
260
|
-
def __str__(self) -> str:
|
|
261
|
-
return f"Period must have duration {self.req_duration}; got {self.duration})"
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
@dataclass(kw_only=True, slots=True)
|
|
265
|
-
class _PeriodMinDurationError(PeriodError[_TPeriod]):
|
|
266
|
-
duration: dt.timedelta
|
|
267
|
-
min_duration: dt.timedelta
|
|
268
|
-
|
|
269
|
-
@override
|
|
270
|
-
def __str__(self) -> str:
|
|
271
|
-
return (
|
|
272
|
-
f"Period must have min duration {self.min_duration}; got {self.duration})"
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
@dataclass(kw_only=True, slots=True)
|
|
277
|
-
class _PeriodMaxDurationError(PeriodError[_TPeriod]):
|
|
278
|
-
duration: dt.timedelta
|
|
279
|
-
max_duration: dt.timedelta
|
|
280
|
-
|
|
281
|
-
@override
|
|
282
|
-
def __str__(self) -> str:
|
|
283
|
-
return f"Period must have duration at most {self.max_duration}; got {self.duration})"
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
@dataclass(kw_only=True, slots=True)
|
|
287
|
-
class _PeriodAsTimeZoneInapplicableError(PeriodError[_TPeriod]):
|
|
288
|
-
@override
|
|
289
|
-
def __str__(self) -> str:
|
|
290
|
-
return "Period of dates does not have a timezone attribute"
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
@dataclass(kw_only=True, slots=True)
|
|
294
|
-
class _PeriodDateContainsDateTimeError(PeriodError[_TPeriod]):
|
|
295
|
-
@override
|
|
296
|
-
def __str__(self) -> str:
|
|
297
|
-
return "Period of dates cannot contain datetimes"
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
@dataclass(kw_only=True, slots=True)
|
|
301
|
-
class _PeriodDateTimeContainsDateError(PeriodError[_TPeriod]):
|
|
302
|
-
@override
|
|
303
|
-
def __str__(self) -> str:
|
|
304
|
-
return "Period of datetimes cannot contain dates"
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
@dataclass(kw_only=True, slots=True)
|
|
308
|
-
class _PeriodTimeZoneInapplicableError(PeriodError[_TPeriod]):
|
|
309
|
-
@override
|
|
310
|
-
def __str__(self) -> str:
|
|
311
|
-
return "Period of dates does not have a timezone attribute"
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
@dataclass(kw_only=True, slots=True)
|
|
315
|
-
class _PeriodTimeZoneNonUniqueError(PeriodError[_TPeriod]):
|
|
316
|
-
first: ZoneInfo
|
|
317
|
-
second: ZoneInfo
|
|
318
|
-
|
|
319
|
-
@override
|
|
320
|
-
def __str__(self) -> str:
|
|
321
|
-
return f"Period must contain exactly one time zone; got {self.first} and {self.second}"
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
__all__ = ["Period", "PeriodError"]
|
utilities/pyrsistent.py
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import TYPE_CHECKING, Any, TypeVar, dataclass_transform, overload
|
|
4
|
-
|
|
5
|
-
from pyrsistent import PRecord as _PRecord
|
|
6
|
-
from pyrsistent import field as _field
|
|
7
|
-
from pyrsistent._field_common import (
|
|
8
|
-
PFIELD_NO_FACTORY,
|
|
9
|
-
PFIELD_NO_INITIAL,
|
|
10
|
-
PFIELD_NO_INVARIANT,
|
|
11
|
-
PFIELD_NO_SERIALIZER,
|
|
12
|
-
PFIELD_NO_TYPE,
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
if TYPE_CHECKING:
|
|
16
|
-
from collections.abc import Callable
|
|
17
|
-
|
|
18
|
-
from utilities.types import TypeLike
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
_T = TypeVar("_T")
|
|
22
|
-
_U = TypeVar("_U")
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@overload
|
|
26
|
-
def field(
|
|
27
|
-
*,
|
|
28
|
-
type: type[_T],
|
|
29
|
-
invariant: Callable[[Any], tuple[bool, Any]] = ...,
|
|
30
|
-
default: Any = ...,
|
|
31
|
-
mandatory: bool = ...,
|
|
32
|
-
factory: Callable[[_U], _U] = ...,
|
|
33
|
-
serializer: Callable[[Any, Any], Any] = ...,
|
|
34
|
-
) -> _T: ...
|
|
35
|
-
@overload
|
|
36
|
-
def field(
|
|
37
|
-
*,
|
|
38
|
-
type: tuple[type[_T]],
|
|
39
|
-
invariant: Callable[[Any], tuple[bool, Any]] = ...,
|
|
40
|
-
default: Any = ...,
|
|
41
|
-
mandatory: bool = ...,
|
|
42
|
-
factory: Callable[[_U], _U] = ...,
|
|
43
|
-
serializer: Callable[[Any, Any], Any] = ...,
|
|
44
|
-
) -> _T: ...
|
|
45
|
-
@overload
|
|
46
|
-
def field(
|
|
47
|
-
*,
|
|
48
|
-
type: tuple[type[_T], type[_U]],
|
|
49
|
-
invariant: Callable[[Any], tuple[bool, Any]] = ...,
|
|
50
|
-
default: Any = ...,
|
|
51
|
-
mandatory: bool = ...,
|
|
52
|
-
factory: Callable[[_U], _U] = ...,
|
|
53
|
-
serializer: Callable[[Any, Any], Any] = ...,
|
|
54
|
-
) -> _T | _U: ...
|
|
55
|
-
@overload
|
|
56
|
-
def field(
|
|
57
|
-
*,
|
|
58
|
-
type: tuple[Any, ...] = ...,
|
|
59
|
-
invariant: Callable[[Any], tuple[bool, Any]] = ...,
|
|
60
|
-
default: Any = ...,
|
|
61
|
-
mandatory: bool = ...,
|
|
62
|
-
factory: Callable[[_U], _U] = ...,
|
|
63
|
-
serializer: Callable[[Any, Any], Any] = ...,
|
|
64
|
-
) -> Any: ...
|
|
65
|
-
def field(
|
|
66
|
-
*,
|
|
67
|
-
type: TypeLike[_T] = PFIELD_NO_TYPE, # noqa: A002
|
|
68
|
-
invariant: Callable[[Any], tuple[bool, Any]] = PFIELD_NO_INVARIANT,
|
|
69
|
-
default: Any = PFIELD_NO_INITIAL,
|
|
70
|
-
mandatory: bool = False,
|
|
71
|
-
factory: Callable[[_U], _U] = PFIELD_NO_FACTORY,
|
|
72
|
-
serializer: Callable[[Any, Any], Any] = PFIELD_NO_SERIALIZER,
|
|
73
|
-
) -> Any:
|
|
74
|
-
"""Field specification factory for :py:class:`PRecord`."""
|
|
75
|
-
return _field(
|
|
76
|
-
type=type,
|
|
77
|
-
invariant=invariant,
|
|
78
|
-
initial=default,
|
|
79
|
-
mandatory=mandatory,
|
|
80
|
-
factory=factory,
|
|
81
|
-
serializer=serializer,
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
@dataclass_transform(kw_only_default=True, field_specifiers=(field,))
|
|
86
|
-
class PRecord(_PRecord): ...
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
__all__ = ["PRecord", "field"]
|