dycw-utilities 0.131.18__py3-none-any.whl → 0.131.19__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.131.18.dist-info → dycw_utilities-0.131.19.dist-info}/METADATA +1 -1
- {dycw_utilities-0.131.18.dist-info → dycw_utilities-0.131.19.dist-info}/RECORD +15 -16
- utilities/__init__.py +1 -1
- utilities/datetime.py +3 -906
- utilities/fastapi.py +5 -7
- utilities/functions.py +78 -45
- utilities/hypothesis.py +20 -269
- utilities/polars.py +6 -3
- utilities/pytest.py +83 -63
- utilities/types.py +3 -24
- utilities/typing.py +2 -15
- utilities/whenever2.py +147 -3
- utilities/zoneinfo.py +4 -0
- utilities/whenever.py +0 -230
- {dycw_utilities-0.131.18.dist-info → dycw_utilities-0.131.19.dist-info}/WHEEL +0 -0
- {dycw_utilities-0.131.18.dist-info → dycw_utilities-0.131.19.dist-info}/licenses/LICENSE +0 -0
utilities/pytest.py
CHANGED
@@ -8,10 +8,11 @@ from pathlib import Path
|
|
8
8
|
from typing import TYPE_CHECKING, Any, ParamSpec, assert_never, cast, override
|
9
9
|
|
10
10
|
from pytest import fixture
|
11
|
+
from whenever import ZonedDateTime
|
11
12
|
|
12
13
|
from utilities.atomicwrites import writer
|
13
|
-
from utilities.datetime import datetime_duration_to_float, get_now
|
14
14
|
from utilities.functools import cache
|
15
|
+
from utilities.git import get_repo_root
|
15
16
|
from utilities.hashlib import md5_hash
|
16
17
|
from utilities.pathlib import ensure_suffix
|
17
18
|
from utilities.platform import (
|
@@ -23,17 +24,15 @@ from utilities.platform import (
|
|
23
24
|
IS_WINDOWS,
|
24
25
|
)
|
25
26
|
from utilities.random import get_state
|
27
|
+
from utilities.whenever2 import SECOND, get_now_local
|
26
28
|
|
27
29
|
if TYPE_CHECKING:
|
28
30
|
from collections.abc import Callable, Iterable, Sequence
|
29
31
|
from random import Random
|
30
32
|
|
31
|
-
from
|
32
|
-
|
33
|
-
|
34
|
-
PathLike,
|
35
|
-
TCallableMaybeCoroutine1None,
|
36
|
-
)
|
33
|
+
from whenever import TimeDelta
|
34
|
+
|
35
|
+
from utilities.types import Coroutine1, PathLike, TCallableMaybeCoroutine1None
|
37
36
|
|
38
37
|
try: # WARNING: this package cannot use unguarded `pytest` imports
|
39
38
|
from _pytest.config import Config
|
@@ -169,88 +168,109 @@ def random_state(*, seed: int) -> Random:
|
|
169
168
|
|
170
169
|
|
171
170
|
def throttle(
|
172
|
-
*, root: PathLike | None = None,
|
171
|
+
*, root: PathLike | None = None, delta: TimeDelta = SECOND, on_try: bool = False
|
173
172
|
) -> Callable[[TCallableMaybeCoroutine1None], TCallableMaybeCoroutine1None]:
|
174
173
|
"""Throttle a test. On success by default, on try otherwise."""
|
175
|
-
|
176
|
-
return cast(
|
177
|
-
"Any", partial(_throttle_inner, root=root_use, duration=duration, on_try=on_try)
|
178
|
-
)
|
174
|
+
return cast("Any", partial(_throttle_inner, root=root, delta=delta, on_try=on_try))
|
179
175
|
|
180
176
|
|
181
177
|
def _throttle_inner(
|
182
178
|
func: TCallableMaybeCoroutine1None,
|
183
179
|
/,
|
184
180
|
*,
|
185
|
-
root:
|
186
|
-
|
181
|
+
root: PathLike | None = None,
|
182
|
+
delta: TimeDelta = SECOND,
|
187
183
|
on_try: bool = False,
|
188
184
|
) -> TCallableMaybeCoroutine1None:
|
189
185
|
"""Throttle a test function/method."""
|
190
|
-
match bool(iscoroutinefunction(func)):
|
191
|
-
case False:
|
192
|
-
|
186
|
+
match bool(iscoroutinefunction(func)), on_try:
|
187
|
+
case False, False:
|
188
|
+
|
189
|
+
@wraps(func)
|
190
|
+
def throttle_sync_on_pass(*args: _P.args, **kwargs: _P.kwargs) -> None:
|
191
|
+
_skipif_recent(root=root, delta=delta)
|
192
|
+
cast("Callable[..., None]", func)(*args, **kwargs)
|
193
|
+
_write(root=root)
|
194
|
+
|
195
|
+
return cast("Any", throttle_sync_on_pass)
|
196
|
+
|
197
|
+
case False, True:
|
198
|
+
|
199
|
+
@wraps(func)
|
200
|
+
def throttle_sync_on_try(*args: _P.args, **kwargs: _P.kwargs) -> None:
|
201
|
+
_skipif_recent(root=root, delta=delta)
|
202
|
+
_write(root=root)
|
203
|
+
cast("Callable[..., None]", func)(*args, **kwargs)
|
204
|
+
|
205
|
+
return cast("Any", throttle_sync_on_try)
|
206
|
+
|
207
|
+
case True, False:
|
193
208
|
|
194
209
|
@wraps(func)
|
195
|
-
def
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
return cast("TCallableMaybeCoroutine1None", throttle_sync)
|
206
|
-
case True:
|
207
|
-
func_typed = cast("Callable[..., Coroutine1[None]]", func)
|
210
|
+
async def throttle_async_on_pass(
|
211
|
+
*args: _P.args, **kwargs: _P.kwargs
|
212
|
+
) -> None:
|
213
|
+
_skipif_recent(root=root, delta=delta)
|
214
|
+
await cast("Callable[..., Coroutine1[None]]", func)(*args, **kwargs)
|
215
|
+
_write(root=root)
|
216
|
+
|
217
|
+
return cast("Any", throttle_async_on_pass)
|
218
|
+
|
219
|
+
case True, True:
|
208
220
|
|
209
221
|
@wraps(func)
|
210
|
-
async def
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
return cast("TCallableMaybeCoroutine1None", throttle_async)
|
222
|
+
async def throttle_async_on_try(
|
223
|
+
*args: _P.args, **kwargs: _P.kwargs
|
224
|
+
) -> None:
|
225
|
+
_skipif_recent(root=root, delta=delta)
|
226
|
+
_write(root=root)
|
227
|
+
await cast("Callable[..., Coroutine1[None]]", func)(*args, **kwargs)
|
228
|
+
|
229
|
+
return cast("Any", throttle_async_on_try)
|
230
|
+
|
221
231
|
case _ as never:
|
222
232
|
assert_never(never)
|
223
233
|
|
224
234
|
|
225
|
-
def
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
235
|
+
def _skipif_recent(*, root: PathLike | None = None, delta: TimeDelta = SECOND) -> None:
|
236
|
+
if skip is None:
|
237
|
+
return # pragma: no cover
|
238
|
+
path = _get_path(root=root)
|
239
|
+
try:
|
240
|
+
contents = path.read_text()
|
241
|
+
except FileNotFoundError:
|
242
|
+
return
|
243
|
+
try:
|
244
|
+
last = ZonedDateTime.parse_common_iso(contents)
|
245
|
+
except ValueError:
|
246
|
+
return
|
247
|
+
if (age := (get_now_local() - last)) < delta:
|
248
|
+
_ = skip(reason=f"{_get_name()} throttled (age {age})")
|
249
|
+
|
250
|
+
|
251
|
+
def _get_path(*, root: PathLike | None = None) -> Path:
|
252
|
+
if root is None:
|
253
|
+
root_use = get_repo_root().joinpath( # pragma: no cover
|
254
|
+
".pytest_cache", "throttle"
|
255
|
+
)
|
234
256
|
else:
|
235
|
-
|
236
|
-
|
237
|
-
if (
|
238
|
-
(skip is not None)
|
239
|
-
and (prev is not None)
|
240
|
-
and ((now - prev) < datetime_duration_to_float(duration))
|
241
|
-
):
|
242
|
-
_ = skip(reason=f"{test} throttled")
|
243
|
-
return path, now
|
257
|
+
root_use = root
|
258
|
+
return Path(root_use, _md5_hash_cached(_get_name()))
|
244
259
|
|
245
260
|
|
246
261
|
@cache
|
247
|
-
def
|
262
|
+
def _md5_hash_cached(text: str, /) -> str:
|
248
263
|
return md5_hash(text)
|
249
264
|
|
250
265
|
|
251
|
-
def
|
252
|
-
|
253
|
-
|
266
|
+
def _get_name() -> str:
|
267
|
+
return environ["PYTEST_CURRENT_TEST"]
|
268
|
+
|
269
|
+
|
270
|
+
def _write(*, root: PathLike | None = None) -> None:
|
271
|
+
path = _get_path(root=root)
|
272
|
+
with writer(path, overwrite=True) as temp:
|
273
|
+
_ = temp.write_text(get_now_local().format_common_iso())
|
254
274
|
|
255
275
|
|
256
276
|
__all__ = [
|
utilities/types.py
CHANGED
@@ -97,19 +97,6 @@ class Dataclass(Protocol):
|
|
97
97
|
TDataclass = TypeVar("TDataclass", bound=Dataclass)
|
98
98
|
|
99
99
|
|
100
|
-
# datetime
|
101
|
-
type DateOrDateTime = dt.date | dt.datetime
|
102
|
-
type DateTimeLike = MaybeStr[dt.datetime]
|
103
|
-
type Duration = Number | dt.timedelta
|
104
|
-
type DurationLike = MaybeStr[Duration]
|
105
|
-
type DurationOrEveryDuration = Duration | tuple[Literal["every"], Duration]
|
106
|
-
type MaybeCallablePyDate = MaybeCallable[dt.date]
|
107
|
-
type MaybeCallablePyDateTime = MaybeCallable[dt.datetime]
|
108
|
-
type PyDateLike = MaybeStr[dt.date]
|
109
|
-
type PyTimeDeltaLike = MaybeStr[dt.timedelta]
|
110
|
-
type PyTimeLike = MaybeStr[dt.time]
|
111
|
-
|
112
|
-
|
113
100
|
# enum
|
114
101
|
type EnumLike[_TEnum: Enum] = MaybeStr[_TEnum]
|
115
102
|
TEnum = TypeVar("TEnum", bound=Enum)
|
@@ -293,7 +280,9 @@ type TimeZone = Literal[
|
|
293
280
|
"Africa/Abidjan", "Africa/Accra", "Africa/Addis_Ababa", "Africa/Algiers", "Africa/Asmara", "Africa/Asmera", "Africa/Bamako", "Africa/Bangui", "Africa/Banjul", "Africa/Bissau", "Africa/Blantyre", "Africa/Brazzaville", "Africa/Bujumbura", "Africa/Cairo", "Africa/Casablanca", "Africa/Ceuta", "Africa/Conakry", "Africa/Dakar", "Africa/Dar_es_Salaam", "Africa/Djibouti", "Africa/Douala", "Africa/El_Aaiun", "Africa/Freetown", "Africa/Gaborone", "Africa/Harare", "Africa/Johannesburg", "Africa/Juba", "Africa/Kampala", "Africa/Khartoum", "Africa/Kigali", "Africa/Kinshasa", "Africa/Lagos", "Africa/Libreville", "Africa/Lome", "Africa/Luanda", "Africa/Lubumbashi", "Africa/Lusaka", "Africa/Malabo", "Africa/Maputo", "Africa/Maseru", "Africa/Mbabane", "Africa/Mogadishu", "Africa/Monrovia", "Africa/Nairobi", "Africa/Ndjamena", "Africa/Niamey", "Africa/Nouakchott", "Africa/Ouagadougou", "Africa/Porto-Novo", "Africa/Sao_Tome", "Africa/Timbuktu", "Africa/Tripoli", "Africa/Tunis", "Africa/Windhoek", "America/Adak", "America/Anchorage", "America/Anguilla", "America/Antigua", "America/Araguaina", "America/Argentina/Buenos_Aires", "America/Argentina/Catamarca", "America/Argentina/ComodRivadavia", "America/Argentina/Cordoba", "America/Argentina/Jujuy", "America/Argentina/La_Rioja", "America/Argentina/Mendoza", "America/Argentina/Rio_Gallegos", "America/Argentina/Salta", "America/Argentina/San_Juan", "America/Argentina/San_Luis", "America/Argentina/Tucuman", "America/Argentina/Ushuaia", "America/Aruba", "America/Asuncion", "America/Atikokan", "America/Atka", "America/Bahia", "America/Bahia_Banderas", "America/Barbados", "America/Belem", "America/Belize", "America/Blanc-Sablon", "America/Boa_Vista", "America/Bogota", "America/Boise", "America/Buenos_Aires", "America/Cambridge_Bay", "America/Campo_Grande", "America/Cancun", "America/Caracas", "America/Catamarca", "America/Cayenne", "America/Cayman", "America/Chicago", "America/Chihuahua", "America/Ciudad_Juarez", "America/Coral_Harbour", "America/Cordoba", "America/Costa_Rica", "America/Coyhaique", "America/Creston", "America/Cuiaba", "America/Curacao", "America/Danmarkshavn", "America/Dawson", "America/Dawson_Creek", "America/Denver", "America/Detroit", "America/Dominica", "America/Edmonton", "America/Eirunepe", "America/El_Salvador", "America/Ensenada", "America/Fort_Nelson", "America/Fort_Wayne", "America/Fortaleza", "America/Glace_Bay", "America/Godthab", "America/Goose_Bay", "America/Grand_Turk", "America/Grenada", "America/Guadeloupe", "America/Guatemala", "America/Guayaquil", "America/Guyana", "America/Halifax", "America/Havana", "America/Hermosillo", "America/Indiana/Indianapolis", "America/Indiana/Knox", "America/Indiana/Marengo", "America/Indiana/Petersburg", "America/Indiana/Tell_City", "America/Indiana/Vevay", "America/Indiana/Vincennes", "America/Indiana/Winamac", "America/Indianapolis", "America/Inuvik", "America/Iqaluit", "America/Jamaica", "America/Jujuy", "America/Juneau", "America/Kentucky/Louisville", "America/Kentucky/Monticello", "America/Knox_IN", "America/Kralendijk", "America/La_Paz", "America/Lima", "America/Los_Angeles", "America/Louisville", "America/Lower_Princes", "America/Maceio", "America/Managua", "America/Manaus", "America/Marigot", "America/Martinique", "America/Matamoros", "America/Mazatlan", "America/Mendoza", "America/Menominee", "America/Merida", "America/Metlakatla", "America/Mexico_City", "America/Miquelon", "America/Moncton", "America/Monterrey", "America/Montevideo", "America/Montreal", "America/Montserrat", "America/Nassau", "America/New_York", "America/Nipigon", "America/Nome", "America/Noronha", "America/North_Dakota/Beulah", "America/North_Dakota/Center", "America/North_Dakota/New_Salem", "America/Nuuk", "America/Ojinaga", "America/Panama", "America/Pangnirtung", "America/Paramaribo", "America/Phoenix", "America/Port-au-Prince", "America/Port_of_Spain", "America/Porto_Acre", "America/Porto_Velho", "America/Puerto_Rico", "America/Punta_Arenas", "America/Rainy_River", "America/Rankin_Inlet", "America/Recife", "America/Regina", "America/Resolute", "America/Rio_Branco", "America/Rosario", "America/Santa_Isabel", "America/Santarem", "America/Santiago", "America/Santo_Domingo", "America/Sao_Paulo", "America/Scoresbysund", "America/Shiprock", "America/Sitka", "America/St_Barthelemy", "America/St_Johns", "America/St_Kitts", "America/St_Lucia", "America/St_Thomas", "America/St_Vincent", "America/Swift_Current", "America/Tegucigalpa", "America/Thule", "America/Thunder_Bay", "America/Tijuana", "America/Toronto", "America/Tortola", "America/Vancouver", "America/Virgin", "America/Whitehorse", "America/Winnipeg", "America/Yakutat", "America/Yellowknife", "Antarctica/Casey", "Antarctica/Davis", "Antarctica/DumontDUrville", "Antarctica/Macquarie", "Antarctica/Mawson", "Antarctica/McMurdo", "Antarctica/Palmer", "Antarctica/Rothera", "Antarctica/South_Pole", "Antarctica/Syowa", "Antarctica/Troll", "Antarctica/Vostok", "Arctic/Longyearbyen", "Asia/Aden", "Asia/Almaty", "Asia/Amman", "Asia/Anadyr", "Asia/Aqtau", "Asia/Aqtobe", "Asia/Ashgabat", "Asia/Ashkhabad", "Asia/Atyrau", "Asia/Baghdad", "Asia/Bahrain", "Asia/Baku", "Asia/Bangkok", "Asia/Barnaul", "Asia/Beirut", "Asia/Bishkek", "Asia/Brunei", "Asia/Calcutta", "Asia/Chita", "Asia/Choibalsan", "Asia/Chongqing", "Asia/Chungking", "Asia/Colombo", "Asia/Dacca", "Asia/Damascus", "Asia/Dhaka", "Asia/Dili", "Asia/Dubai", "Asia/Dushanbe", "Asia/Famagusta", "Asia/Gaza", "Asia/Harbin", "Asia/Hebron", "Asia/Ho_Chi_Minh", "Asia/Hong_Kong", "Asia/Hovd", "Asia/Irkutsk", "Asia/Istanbul", "Asia/Jakarta", "Asia/Jayapura", "Asia/Jerusalem", "Asia/Kabul", "Asia/Kamchatka", "Asia/Karachi", "Asia/Kashgar", "Asia/Kathmandu", "Asia/Katmandu", "Asia/Khandyga", "Asia/Kolkata", "Asia/Krasnoyarsk", "Asia/Kuala_Lumpur", "Asia/Kuching", "Asia/Kuwait", "Asia/Macao", "Asia/Macau", "Asia/Magadan", "Asia/Makassar", "Asia/Manila", "Asia/Muscat", "Asia/Nicosia", "Asia/Novokuznetsk", "Asia/Novosibirsk", "Asia/Omsk", "Asia/Oral", "Asia/Phnom_Penh", "Asia/Pontianak", "Asia/Pyongyang", "Asia/Qatar", "Asia/Qostanay", "Asia/Qyzylorda", "Asia/Rangoon", "Asia/Riyadh", "Asia/Saigon", "Asia/Sakhalin", "Asia/Samarkand", "Asia/Seoul", "Asia/Shanghai", "Asia/Singapore", "Asia/Srednekolymsk", "Asia/Taipei", "Asia/Tashkent", "Asia/Tbilisi", "Asia/Tehran", "Asia/Tel_Aviv", "Asia/Thimbu", "Asia/Thimphu", "Asia/Tokyo", "Asia/Tomsk", "Asia/Ujung_Pandang", "Asia/Ulaanbaatar", "Asia/Ulan_Bator", "Asia/Urumqi", "Asia/Ust-Nera", "Asia/Vientiane", "Asia/Vladivostok", "Asia/Yakutsk", "Asia/Yangon", "Asia/Yekaterinburg", "Asia/Yerevan", "Atlantic/Azores", "Atlantic/Bermuda", "Atlantic/Canary", "Atlantic/Cape_Verde", "Atlantic/Faeroe", "Atlantic/Faroe", "Atlantic/Jan_Mayen", "Atlantic/Madeira", "Atlantic/Reykjavik", "Atlantic/South_Georgia", "Atlantic/St_Helena", "Atlantic/Stanley", "Australia/ACT", "Australia/Adelaide", "Australia/Brisbane", "Australia/Broken_Hill", "Australia/Canberra", "Australia/Currie", "Australia/Darwin", "Australia/Eucla", "Australia/Hobart", "Australia/LHI", "Australia/Lindeman", "Australia/Lord_Howe", "Australia/Melbourne", "Australia/NSW", "Australia/North", "Australia/Perth", "Australia/Queensland", "Australia/South", "Australia/Sydney", "Australia/Tasmania", "Australia/Victoria", "Australia/West", "Australia/Yancowinna", "Brazil/Acre", "Brazil/DeNoronha", "Brazil/East", "Brazil/West", "CET", "CST6CDT", "Canada/Atlantic", "Canada/Central", "Canada/Eastern", "Canada/Mountain", "Canada/Newfoundland", "Canada/Pacific", "Canada/Saskatchewan", "Canada/Yukon", "Chile/Continental", "Chile/EasterIsland", "Cuba", "EET", "EST", "EST5EDT", "Egypt", "Eire", "Etc/GMT", "Etc/GMT+0", "Etc/GMT+1", "Etc/GMT+10", "Etc/GMT+11", "Etc/GMT+12", "Etc/GMT+2", "Etc/GMT+3", "Etc/GMT+4", "Etc/GMT+5", "Etc/GMT+6", "Etc/GMT+7", "Etc/GMT+8", "Etc/GMT+9", "Etc/GMT-0", "Etc/GMT-1", "Etc/GMT-10", "Etc/GMT-11", "Etc/GMT-12", "Etc/GMT-13", "Etc/GMT-14", "Etc/GMT-2", "Etc/GMT-3", "Etc/GMT-4", "Etc/GMT-5", "Etc/GMT-6", "Etc/GMT-7", "Etc/GMT-8", "Etc/GMT-9", "Etc/GMT0", "Etc/Greenwich", "Etc/UCT", "Etc/UTC", "Etc/Universal", "Etc/Zulu", "Europe/Amsterdam", "Europe/Andorra", "Europe/Astrakhan", "Europe/Athens", "Europe/Belfast", "Europe/Belgrade", "Europe/Berlin", "Europe/Bratislava", "Europe/Brussels", "Europe/Bucharest", "Europe/Budapest", "Europe/Busingen", "Europe/Chisinau", "Europe/Copenhagen", "Europe/Dublin", "Europe/Gibraltar", "Europe/Guernsey", "Europe/Helsinki", "Europe/Isle_of_Man", "Europe/Istanbul", "Europe/Jersey", "Europe/Kaliningrad", "Europe/Kiev", "Europe/Kirov", "Europe/Kyiv", "Europe/Lisbon", "Europe/Ljubljana", "Europe/London", "Europe/Luxembourg", "Europe/Madrid", "Europe/Malta", "Europe/Mariehamn", "Europe/Minsk", "Europe/Monaco", "Europe/Moscow", "Europe/Nicosia", "Europe/Oslo", "Europe/Paris", "Europe/Podgorica", "Europe/Prague", "Europe/Riga", "Europe/Rome", "Europe/Samara", "Europe/San_Marino", "Europe/Sarajevo", "Europe/Saratov", "Europe/Simferopol", "Europe/Skopje", "Europe/Sofia", "Europe/Stockholm", "Europe/Tallinn", "Europe/Tirane", "Europe/Tiraspol", "Europe/Ulyanovsk", "Europe/Uzhgorod", "Europe/Vaduz", "Europe/Vatican", "Europe/Vienna", "Europe/Vilnius", "Europe/Volgograd", "Europe/Warsaw", "Europe/Zagreb", "Europe/Zaporozhye", "Europe/Zurich", "Factory", "GB", "GB-Eire", "GMT", "GMT+0", "GMT-0", "GMT0", "Greenwich", "HST", "Hongkong", "Iceland", "Indian/Antananarivo", "Indian/Chagos", "Indian/Christmas", "Indian/Cocos", "Indian/Comoro", "Indian/Kerguelen", "Indian/Mahe", "Indian/Maldives", "Indian/Mauritius", "Indian/Mayotte", "Indian/Reunion", "Iran", "Israel", "Jamaica", "Japan", "Kwajalein", "Libya", "MET", "MST", "MST7MDT", "Mexico/BajaNorte", "Mexico/BajaSur", "Mexico/General", "NZ", "NZ-CHAT", "Navajo", "PRC", "PST8PDT", "Pacific/Apia", "Pacific/Auckland", "Pacific/Bougainville", "Pacific/Chatham", "Pacific/Chuuk", "Pacific/Easter", "Pacific/Efate", "Pacific/Enderbury", "Pacific/Fakaofo", "Pacific/Fiji", "Pacific/Funafuti", "Pacific/Galapagos", "Pacific/Gambier", "Pacific/Guadalcanal", "Pacific/Guam", "Pacific/Honolulu", "Pacific/Johnston", "Pacific/Kanton", "Pacific/Kiritimati", "Pacific/Kosrae", "Pacific/Kwajalein", "Pacific/Majuro", "Pacific/Marquesas", "Pacific/Midway", "Pacific/Nauru", "Pacific/Niue", "Pacific/Norfolk", "Pacific/Noumea", "Pacific/Pago_Pago", "Pacific/Palau", "Pacific/Pitcairn", "Pacific/Pohnpei", "Pacific/Ponape", "Pacific/Port_Moresby", "Pacific/Rarotonga", "Pacific/Saipan", "Pacific/Samoa", "Pacific/Tahiti", "Pacific/Tarawa", "Pacific/Tongatapu", "Pacific/Truk", "Pacific/Wake", "Pacific/Wallis", "Pacific/Yap", "Poland", "Portugal", "ROC", "ROK", "Singapore", "Turkey", "UCT", "US/Alaska", "US/Aleutian", "US/Arizona", "US/Central", "US/East-Indiana", "US/Eastern", "US/Hawaii", "US/Indiana-Starke", "US/Michigan", "US/Mountain", "US/Pacific", "US/Samoa", "UTC", "Universal", "W-SU", "WET", "Zulu"
|
294
281
|
]
|
295
282
|
# fmt: on
|
296
|
-
type TimeZoneLike =
|
283
|
+
type TimeZoneLike = (
|
284
|
+
ZoneInfo | ZonedDateTime | Literal["local"] | TimeZone | dt.tzinfo | dt.datetime
|
285
|
+
)
|
297
286
|
|
298
287
|
|
299
288
|
__all__ = [
|
@@ -301,14 +290,9 @@ __all__ = [
|
|
301
290
|
"Dataclass",
|
302
291
|
"DateDeltaLike",
|
303
292
|
"DateLike",
|
304
|
-
"DateOrDateTime",
|
305
293
|
"DateTimeDeltaLike",
|
306
|
-
"DateTimeLike",
|
307
294
|
"DateTimeRoundMode",
|
308
295
|
"DateTimeRoundUnit",
|
309
|
-
"Duration",
|
310
|
-
"DurationLike",
|
311
|
-
"DurationOrEveryDuration",
|
312
296
|
"EnumLike",
|
313
297
|
"ExcInfo",
|
314
298
|
"IterableHashable",
|
@@ -320,8 +304,6 @@ __all__ = [
|
|
320
304
|
"MaybeCallableDate",
|
321
305
|
"MaybeCallableEvent",
|
322
306
|
"MaybeCallablePathLike",
|
323
|
-
"MaybeCallablePyDate",
|
324
|
-
"MaybeCallablePyDateTime",
|
325
307
|
"MaybeCallableZonedDateTime",
|
326
308
|
"MaybeCoroutine1",
|
327
309
|
"MaybeIterable",
|
@@ -336,9 +318,6 @@ __all__ = [
|
|
336
318
|
"PathLike",
|
337
319
|
"PatternLike",
|
338
320
|
"PlainDateTimeLike",
|
339
|
-
"PyDateLike",
|
340
|
-
"PyTimeDeltaLike",
|
341
|
-
"PyTimeLike",
|
342
321
|
"Seed",
|
343
322
|
"SerializeObjectExtra",
|
344
323
|
"Sign",
|
utilities/typing.py
CHANGED
@@ -11,7 +11,6 @@ from typing import (
|
|
11
11
|
Literal,
|
12
12
|
NamedTuple,
|
13
13
|
Optional, # pyright: ignore[reportDeprecated]
|
14
|
-
Self,
|
15
14
|
TypeAliasType,
|
16
15
|
TypeGuard,
|
17
16
|
TypeVar,
|
@@ -50,14 +49,6 @@ _T5 = TypeVar("_T5")
|
|
50
49
|
##
|
51
50
|
|
52
51
|
|
53
|
-
def contains_self(obj: Any, /) -> bool:
|
54
|
-
"""Check if an annotation contains `Self`."""
|
55
|
-
return (obj is Self) or any(map(contains_self, get_args(obj)))
|
56
|
-
|
57
|
-
|
58
|
-
##
|
59
|
-
|
60
|
-
|
61
52
|
def get_args(obj: Any, /, *, optional_drop_none: bool = False) -> tuple[Any, ...]:
|
62
53
|
"""Get the arguments of an annotation."""
|
63
54
|
if isinstance(obj, TypeAliasType):
|
@@ -183,12 +174,9 @@ def get_union_type_classes(obj: Any, /) -> tuple[type[Any], ...]:
|
|
183
174
|
raise _GetUnionTypeClassesUnionTypeError(obj=obj)
|
184
175
|
types_: Sequence[type[Any]] = []
|
185
176
|
for arg in get_args(obj):
|
186
|
-
if isinstance(arg, type):
|
187
|
-
types_.append(arg)
|
188
|
-
elif is_union_type(arg):
|
189
|
-
types_.extend(get_union_type_classes(arg))
|
190
|
-
else:
|
177
|
+
if not isinstance(arg, type):
|
191
178
|
raise _GetUnionTypeClassesInternalTypeError(obj=obj, inner=arg)
|
179
|
+
types_.append(arg)
|
192
180
|
return tuple(types_)
|
193
181
|
|
194
182
|
|
@@ -495,7 +483,6 @@ __all__ = [
|
|
495
483
|
"GetUnionTypeClassesError",
|
496
484
|
"IsInstanceGenError",
|
497
485
|
"IsSubclassGenError",
|
498
|
-
"contains_self",
|
499
486
|
"get_literal_elements",
|
500
487
|
"get_type_classes",
|
501
488
|
"get_type_hints",
|
utilities/whenever2.py
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import datetime as dt
|
4
|
-
from collections.abc import Callable
|
4
|
+
from collections.abc import Callable, Iterable
|
5
5
|
from dataclasses import dataclass
|
6
6
|
from functools import cache
|
7
7
|
from logging import LogRecord
|
8
|
-
from
|
8
|
+
from statistics import fmean
|
9
|
+
from typing import TYPE_CHECKING, Any, SupportsFloat, assert_never, overload, override
|
9
10
|
|
10
11
|
from whenever import (
|
11
12
|
Date,
|
@@ -99,6 +100,7 @@ DATE_DELTA_PARSABLE_MAX = DateDelta(days=999999)
|
|
99
100
|
## common constants
|
100
101
|
|
101
102
|
|
103
|
+
ZERO_DAYS = DateDelta()
|
102
104
|
ZERO_TIME = TimeDelta()
|
103
105
|
MICROSECOND = TimeDelta(microseconds=1)
|
104
106
|
MILLISECOND = TimeDelta(milliseconds=1)
|
@@ -107,6 +109,37 @@ MINUTE = TimeDelta(minutes=1)
|
|
107
109
|
HOUR = TimeDelta(hours=1)
|
108
110
|
DAY = DateDelta(days=1)
|
109
111
|
WEEK = DateDelta(weeks=1)
|
112
|
+
MONTH = DateDelta(months=1)
|
113
|
+
YEAR = DateDelta(years=1)
|
114
|
+
|
115
|
+
|
116
|
+
##
|
117
|
+
|
118
|
+
|
119
|
+
def datetime_utc(
|
120
|
+
year: int,
|
121
|
+
month: int,
|
122
|
+
day: int,
|
123
|
+
/,
|
124
|
+
hour: int = 0,
|
125
|
+
minute: int = 0,
|
126
|
+
second: int = 0,
|
127
|
+
millisecond: int = 0,
|
128
|
+
microsecond: int = 0,
|
129
|
+
nanosecond: int = 0,
|
130
|
+
) -> ZonedDateTime:
|
131
|
+
"""Create a UTC-zoned datetime."""
|
132
|
+
nanos = int(1e6) * millisecond + int(1e3) * microsecond + nanosecond
|
133
|
+
return ZonedDateTime(
|
134
|
+
year,
|
135
|
+
month,
|
136
|
+
day,
|
137
|
+
hour=hour,
|
138
|
+
minute=minute,
|
139
|
+
second=second,
|
140
|
+
nanosecond=nanos,
|
141
|
+
tz=UTC.key,
|
142
|
+
)
|
110
143
|
|
111
144
|
|
112
145
|
##
|
@@ -173,6 +206,110 @@ def get_today_local() -> Date:
|
|
173
206
|
|
174
207
|
TODAY_LOCAL = get_today_local()
|
175
208
|
|
209
|
+
|
210
|
+
##
|
211
|
+
|
212
|
+
|
213
|
+
def mean_datetime(
|
214
|
+
datetimes: Iterable[ZonedDateTime],
|
215
|
+
/,
|
216
|
+
*,
|
217
|
+
weights: Iterable[SupportsFloat] | None = None,
|
218
|
+
) -> ZonedDateTime:
|
219
|
+
"""Compute the mean of a set of datetimes."""
|
220
|
+
datetimes = list(datetimes)
|
221
|
+
match len(datetimes):
|
222
|
+
case 0:
|
223
|
+
raise MeanDateTimeError from None
|
224
|
+
case 1:
|
225
|
+
return datetimes[0]
|
226
|
+
case _:
|
227
|
+
timestamps = [d.timestamp_nanos() for d in datetimes]
|
228
|
+
timestamp = round(fmean(timestamps, weights=weights))
|
229
|
+
return ZonedDateTime.from_timestamp_nanos(timestamp, tz=datetimes[0].tz)
|
230
|
+
|
231
|
+
|
232
|
+
@dataclass(kw_only=True, slots=True)
|
233
|
+
class MeanDateTimeError(Exception):
|
234
|
+
@override
|
235
|
+
def __str__(self) -> str:
|
236
|
+
return "Mean requires at least 1 datetime"
|
237
|
+
|
238
|
+
|
239
|
+
##
|
240
|
+
|
241
|
+
|
242
|
+
def min_max_date(
|
243
|
+
*,
|
244
|
+
min_date: Date | None = None,
|
245
|
+
max_date: Date | None = None,
|
246
|
+
min_age: DateDelta | None = None,
|
247
|
+
max_age: DateDelta | None = None,
|
248
|
+
time_zone: TimeZoneLike = UTC,
|
249
|
+
) -> tuple[Date | None, Date | None]:
|
250
|
+
"""Ucompute the min/max date given a combination of dates/ages."""
|
251
|
+
today = get_today(time_zone=time_zone)
|
252
|
+
min_parts: list[Date] = []
|
253
|
+
if min_date is not None:
|
254
|
+
if min_date > today:
|
255
|
+
raise _MinMaxDateMinDateError(min_date=min_date, today=today)
|
256
|
+
min_parts.append(min_date)
|
257
|
+
if max_age is not None:
|
258
|
+
min_parts.append(today - max_age)
|
259
|
+
min_date_use = max(min_parts, default=None)
|
260
|
+
max_parts: list[Date] = []
|
261
|
+
if max_date is not None:
|
262
|
+
if max_date > today:
|
263
|
+
raise _MinMaxDateMaxDateError(max_date=max_date, today=today)
|
264
|
+
max_parts.append(max_date)
|
265
|
+
if min_age is not None:
|
266
|
+
max_parts.append(today - min_age)
|
267
|
+
max_date_use = min(max_parts, default=None)
|
268
|
+
if (
|
269
|
+
(min_date_use is not None)
|
270
|
+
and (max_date_use is not None)
|
271
|
+
and (min_date_use > max_date_use)
|
272
|
+
):
|
273
|
+
raise _MinMaxDatePeriodError(min_date=min_date_use, max_date=max_date_use)
|
274
|
+
return min_date_use, max_date_use
|
275
|
+
|
276
|
+
|
277
|
+
@dataclass(kw_only=True, slots=True)
|
278
|
+
class MinMaxDateError(Exception): ...
|
279
|
+
|
280
|
+
|
281
|
+
@dataclass(kw_only=True, slots=True)
|
282
|
+
class _MinMaxDateMinDateError(MinMaxDateError):
|
283
|
+
min_date: Date
|
284
|
+
today: Date
|
285
|
+
|
286
|
+
@override
|
287
|
+
def __str__(self) -> str:
|
288
|
+
return f"Min date must be at most today; got {self.min_date} > {self.today}"
|
289
|
+
|
290
|
+
|
291
|
+
@dataclass(kw_only=True, slots=True)
|
292
|
+
class _MinMaxDateMaxDateError(MinMaxDateError):
|
293
|
+
max_date: Date
|
294
|
+
today: Date
|
295
|
+
|
296
|
+
@override
|
297
|
+
def __str__(self) -> str:
|
298
|
+
return f"Max date must be at most today; got {self.max_date} > {self.today}"
|
299
|
+
|
300
|
+
|
301
|
+
@dataclass(kw_only=True, slots=True)
|
302
|
+
class _MinMaxDatePeriodError(MinMaxDateError):
|
303
|
+
min_date: Date
|
304
|
+
max_date: Date
|
305
|
+
|
306
|
+
@override
|
307
|
+
def __str__(self) -> str:
|
308
|
+
return (
|
309
|
+
f"Min date must be at most max date; got {self.min_date} > {self.max_date}"
|
310
|
+
)
|
311
|
+
|
312
|
+
|
176
313
|
##
|
177
314
|
|
178
315
|
|
@@ -447,6 +584,7 @@ __all__ = [
|
|
447
584
|
"MICROSECOND",
|
448
585
|
"MILLISECOND",
|
449
586
|
"MINUTE",
|
587
|
+
"MONTH",
|
450
588
|
"NOW_LOCAL",
|
451
589
|
"PLAIN_DATE_TIME_MAX",
|
452
590
|
"PLAIN_DATE_TIME_MIN",
|
@@ -458,13 +596,17 @@ __all__ = [
|
|
458
596
|
"TODAY_LOCAL",
|
459
597
|
"TODAY_UTC",
|
460
598
|
"WEEK",
|
599
|
+
"YEAR",
|
600
|
+
"ZERO_DAYS",
|
461
601
|
"ZERO_TIME",
|
462
602
|
"ZONED_DATE_TIME_MAX",
|
463
603
|
"ZONED_DATE_TIME_MIN",
|
604
|
+
"MeanDateTimeError",
|
605
|
+
"MinMaxDateError",
|
464
606
|
"ToDaysError",
|
465
607
|
"ToNanosError",
|
466
608
|
"WheneverLogRecord",
|
467
|
-
"
|
609
|
+
"datetime_utc",
|
468
610
|
"format_compact",
|
469
611
|
"from_timestamp",
|
470
612
|
"from_timestamp_millis",
|
@@ -473,6 +615,8 @@ __all__ = [
|
|
473
615
|
"get_now_local",
|
474
616
|
"get_today",
|
475
617
|
"get_today_local",
|
618
|
+
"mean_datetime",
|
619
|
+
"min_max_date",
|
476
620
|
"to_date",
|
477
621
|
"to_date_time_delta",
|
478
622
|
"to_days",
|
utilities/zoneinfo.py
CHANGED
@@ -5,6 +5,8 @@ from dataclasses import dataclass
|
|
5
5
|
from typing import TYPE_CHECKING, assert_never, cast, override
|
6
6
|
from zoneinfo import ZoneInfo
|
7
7
|
|
8
|
+
from whenever import ZonedDateTime
|
9
|
+
|
8
10
|
from utilities.tzlocal import LOCAL_TIME_ZONE
|
9
11
|
|
10
12
|
if TYPE_CHECKING:
|
@@ -22,6 +24,8 @@ def ensure_time_zone(obj: TimeZoneLike, /) -> ZoneInfo:
|
|
22
24
|
match obj:
|
23
25
|
case ZoneInfo() as zone_info:
|
24
26
|
return zone_info
|
27
|
+
case ZonedDateTime() as datetime:
|
28
|
+
return ZoneInfo(datetime.tz)
|
25
29
|
case "local":
|
26
30
|
return LOCAL_TIME_ZONE
|
27
31
|
case str() as key:
|