dycw-utilities 0.131.20__py3-none-any.whl → 0.132.0__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.20.dist-info → dycw_utilities-0.132.0.dist-info}/METADATA +1 -1
- {dycw_utilities-0.131.20.dist-info → dycw_utilities-0.132.0.dist-info}/RECORD +22 -23
- utilities/__init__.py +1 -1
- utilities/asyncio.py +1 -1
- utilities/click.py +6 -6
- utilities/fastapi.py +1 -1
- utilities/fpdf2.py +1 -1
- utilities/hypothesis.py +49 -53
- utilities/logging.py +1 -6
- utilities/orjson.py +1 -1
- utilities/pottery.py +1 -1
- utilities/psutil.py +1 -1
- utilities/pyinstrument.py +1 -1
- utilities/pytest.py +1 -1
- utilities/redis.py +1 -1
- utilities/slack_sdk.py +1 -1
- utilities/sqlalchemy.py +1 -1
- utilities/timer.py +1 -1
- utilities/traceback.py +1 -1
- utilities/{whenever2.py → whenever.py} +150 -12
- utilities/datetime.py +0 -205
- {dycw_utilities-0.131.20.dist-info → dycw_utilities-0.132.0.dist-info}/WHEEL +0 -0
- {dycw_utilities-0.131.20.dist-info → dycw_utilities-0.132.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,30 +1,29 @@
|
|
1
|
-
utilities/__init__.py,sha256=
|
1
|
+
utilities/__init__.py,sha256=rp9t0gUqAH2TAe2FasaswzrPMlJ2l6RH0WIc6dPt-DM,60
|
2
2
|
utilities/aiolimiter.py,sha256=mD0wEiqMgwpty4XTbawFpnkkmJS6R4JRsVXFUaoitSU,628
|
3
3
|
utilities/altair.py,sha256=HeZBVUocjkrTNwwKrClppsIqgNFF-ykv05HfZSoHYno,9104
|
4
|
-
utilities/asyncio.py,sha256=
|
4
|
+
utilities/asyncio.py,sha256=USWMMrHqPVRr20vlIn_n5JLimyqa-5xLhuqDYWJed8A,37586
|
5
5
|
utilities/atomicwrites.py,sha256=geFjn9Pwn-tTrtoGjDDxWli9NqbYfy3gGL6ZBctiqSo,5393
|
6
6
|
utilities/atools.py,sha256=-bFGIrwYMFR7xl39j02DZMsO_u5x5_Ph7bRlBUFVYyw,1048
|
7
7
|
utilities/cachetools.py,sha256=uBtEv4hD-TuCPX_cQy1lOpLF-QqfwnYGSf0o4Soqydc,2826
|
8
|
-
utilities/click.py,sha256=
|
8
|
+
utilities/click.py,sha256=jLyep_czA3k-3XWHWrKFEEN79ZbMhVT2H7TGnTa8i44,13314
|
9
9
|
utilities/concurrent.py,sha256=s2scTEd2AhXVTW4hpASU2qxV_DiVLALfms55cCQzCvM,2886
|
10
10
|
utilities/contextlib.py,sha256=lpaLJBy3X0UGLWjM98jkQZZq8so4fRmoK-Bheq0uOW4,1027
|
11
11
|
utilities/contextvars.py,sha256=RsSGGrbQqqZ67rOydnM7WWIsM2lIE31UHJLejnHJPWY,505
|
12
12
|
utilities/cryptography.py,sha256=_CiK_K6c_-uQuUhsUNjNjTL-nqxAh4_1zTfS11Xe120,972
|
13
13
|
utilities/cvxpy.py,sha256=Rv1-fD-XYerosCavRF8Pohop2DBkU3AlFaGTfD8AEAA,13776
|
14
14
|
utilities/dataclasses.py,sha256=iiC1wpGXWhaocIikzwBt8bbLWyImoUlOlcDZJGejaIg,33011
|
15
|
-
utilities/datetime.py,sha256=awFnwPswr6kfi76Y4AtOLxiNyV7HtURQS8NYUGe5Jz8,5308
|
16
15
|
utilities/enum.py,sha256=HoRwVCWzsnH0vpO9ZEcAAIZLMv0Sn2vJxxA4sYMQgDs,5793
|
17
16
|
utilities/errors.py,sha256=nC7ZYtxxDBMfrTHtT_MByBfup_wfGQFRo3eDt-0ZPe8,1045
|
18
17
|
utilities/eventkit.py,sha256=6M5Xu1SzN-juk9PqBHwy5dS-ta7T0qA6SMpDsakOJ0E,13039
|
19
|
-
utilities/fastapi.py,sha256=
|
20
|
-
utilities/fpdf2.py,sha256=
|
18
|
+
utilities/fastapi.py,sha256=E8T2J1-N_RbpkN4czthU6NPIxAZDzxy-k_WGJaxeJ48,2671
|
19
|
+
utilities/fpdf2.py,sha256=PmPj8ugr_SlxFEpw-9OsI8--mteLQ4MaXv_Cbmf7XXs,1852
|
21
20
|
utilities/functions.py,sha256=1GMHO3PJUPil9cpDNI656RN9aGWoJS2grzPFk3HWgJg,28267
|
22
21
|
utilities/functools.py,sha256=WrpHt7NLNWSUn9A1Q_ZIWlNaYZOEI4IFKyBG9HO3BC4,1643
|
23
22
|
utilities/getpass.py,sha256=DfN5UgMAtFCqS3dSfFHUfqIMZX2shXvwphOz_6J6f6A,103
|
24
23
|
utilities/git.py,sha256=oi7-_l5e9haSANSCvQw25ufYGoNahuUPHAZ6114s3JQ,1191
|
25
24
|
utilities/hashlib.py,sha256=SVTgtguur0P4elppvzOBbLEjVM3Pea0eWB61yg2ilxo,309
|
26
25
|
utilities/http.py,sha256=WcahTcKYRtZ04WXQoWt5EGCgFPcyHD3EJdlMfxvDt-0,946
|
27
|
-
utilities/hypothesis.py,sha256=
|
26
|
+
utilities/hypothesis.py,sha256=MS0UgZjevC9QuJAUlGa8ozcbAhlq1qZnSRiSk_1KsXg,35204
|
28
27
|
utilities/importlib.py,sha256=mV1xT_O_zt_GnZZ36tl3xOmMaN_3jErDWY54fX39F6Y,429
|
29
28
|
utilities/inflect.py,sha256=DbqB5Q9FbRGJ1NbvEiZBirRMxCxgrz91zy5jCO9ZIs0,347
|
30
29
|
utilities/ipython.py,sha256=V2oMYHvEKvlNBzxDXdLvKi48oUq2SclRg5xasjaXStw,763
|
@@ -32,7 +31,7 @@ utilities/iterables.py,sha256=cuebB4ivKlZuKm8S3PQIfjavn9h-5mBGmvYq4FpSxFg,43812
|
|
32
31
|
utilities/jupyter.py,sha256=ft5JA7fBxXKzP-L9W8f2-wbF0QeYc_2uLQNFDVk4Z-M,2917
|
33
32
|
utilities/libcst.py,sha256=Jto5ppzRzsxn4AD32IS8n0lbgLYXwsVJB6EY8giNZyY,4974
|
34
33
|
utilities/lightweight_charts.py,sha256=JrkrAZMo6JID2Eoc9QCc05Y_pK4l2zsApIhmii1z2Ig,2764
|
35
|
-
utilities/logging.py,sha256=
|
34
|
+
utilities/logging.py,sha256=j0xS7bNdZcMAobWSRahpg_d7GWewd_99oXvexrjWm6k,17841
|
36
35
|
utilities/luigi.py,sha256=UAt4TDMtinLAN7sipX0jSvH-aZzHUTQbHB3Rwtbq994,4840
|
37
36
|
utilities/math.py,sha256=_6vrDyjtaqE_OFE-F2DNWrDG_J_kMl3nFAJsok9v_bY,26862
|
38
37
|
utilities/memory_profiler.py,sha256=tf2C51P2lCujPGvRt2Rfc7VEw5LDXmVPCG3z_AvBmbU,962
|
@@ -41,7 +40,7 @@ utilities/more_itertools.py,sha256=tBbjjKx8_Uv_TCjxhPwrGfAx_jRHtvLIZqXVWAsjzqA,8
|
|
41
40
|
utilities/numpy.py,sha256=Xn23sA2ZbVNqwUYEgNJD3XBYH6IbCri_WkHSNhg3NkY,26122
|
42
41
|
utilities/operator.py,sha256=DuiWdkmK0D-ddvFqOayDkazTQE1Ysvtl6-UiBN5gns8,3857
|
43
42
|
utilities/optuna.py,sha256=loyJGWTzljgdJaoLhP09PT8Jz6o_pwBOwehY33lHkhw,1923
|
44
|
-
utilities/orjson.py,sha256=
|
43
|
+
utilities/orjson.py,sha256=y5ynSGhQjX7vikfvsHh_AklLnH7gjvsSkIC3h5tnx98,36474
|
45
44
|
utilities/os.py,sha256=D_FyyT-6TtqiN9KSS7c9g1fnUtgxmyMtzAjmYLkk46A,3587
|
46
45
|
utilities/parse.py,sha256=YE2VWYKDm9WYf5wcOWrOxVfM6UWvhkSxSkwdxRQNsA0,17633
|
47
46
|
utilities/pathlib.py,sha256=PK41rf1c9Wqv7h8f5R7H3_Lhq_gQZTUJD5tu3gMHVaU,3247
|
@@ -50,26 +49,26 @@ utilities/pickle.py,sha256=MBT2xZCsv0pH868IXLGKnlcqNx2IRVKYNpRcqiQQqxw,653
|
|
50
49
|
utilities/platform.py,sha256=5uCKRf_ij7ukJDcbnNfhY2ay9fbrpiNLRO1t2QvcwqQ,2825
|
51
50
|
utilities/polars.py,sha256=BYTYniVSW9SrBWdmoTy8RqUgqBW4y07mlRPEWXPUyYg,63357
|
52
51
|
utilities/polars_ols.py,sha256=Uc9V5kvlWZ5cU93lKZ-cfAKdVFFw81tqwLW9PxtUvMs,5618
|
53
|
-
utilities/pottery.py,sha256=
|
52
|
+
utilities/pottery.py,sha256=RN3XwOEsVAPXvEfsRPmn3ZSKgTzK_c182PNrtksq-bg,3429
|
54
53
|
utilities/pqdm.py,sha256=foRytQybmOQ05pjt5LF7ANyzrIa--4ScDE3T2wd31a4,3118
|
55
|
-
utilities/psutil.py,sha256=
|
54
|
+
utilities/psutil.py,sha256=0j4YxtVb8VjaaKKiHg6UEK95SUPkEcENgPtLgPJsNv0,3760
|
56
55
|
utilities/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
57
56
|
utilities/pydantic.py,sha256=aP6OKowg2Md4rgQuQ5qTSF4bTbBuq7WtRb7zS3JSRGY,1841
|
58
|
-
utilities/pyinstrument.py,sha256=
|
57
|
+
utilities/pyinstrument.py,sha256=_Rfq6Gg4NKV2NF0dRYOpK2IRyyePxI7-3UmHIQLYrlQ,852
|
59
58
|
utilities/pyrsistent.py,sha256=wVOVIe_68AAaa-lUE9y-TEzDawVp1uEIc_zfoDgr5ww,2287
|
60
|
-
utilities/pytest.py,sha256=
|
59
|
+
utilities/pytest.py,sha256=xSDybvkvdj7Ix-Tpc1INctKZV07qwrQvJlQonSimB7o,8273
|
61
60
|
utilities/pytest_regressions.py,sha256=YI55B7EtLjhz7zPJZ6NK9bWrxrKCKabWZJe1cwcbA5o,5082
|
62
61
|
utilities/python_dotenv.py,sha256=edXsvHZhZnYeqfMfrsRRpj7_9eJI6uizh3xLx8Q9B3w,3228
|
63
62
|
utilities/random.py,sha256=lYdjgxB7GCfU_fwFVl5U-BIM_HV3q6_urL9byjrwDM8,4157
|
64
63
|
utilities/re.py,sha256=6qxeV0rQZaBDKWcB7apSBmxtg_XzoGY-EdegTkMn-ZY,4578
|
65
|
-
utilities/redis.py,sha256=
|
64
|
+
utilities/redis.py,sha256=u1nYu3ccGni8u3AFg5aXDdpW4ZmT6lGAHIk-wKeZPq4,35761
|
66
65
|
utilities/reprlib.py,sha256=ssYTcBW-TeRh3fhCJv57sopTZHF5FrPyyUg9yp5XBlo,3953
|
67
66
|
utilities/scipy.py,sha256=wZJM7fEgBAkLSYYvSmsg5ac-QuwAI0BGqHVetw1_Hb0,947
|
68
67
|
utilities/sentinel.py,sha256=3jIwgpMekWgDAxPDA_hXMP2St43cPhciKN3LWiZ7kv0,1248
|
69
68
|
utilities/shelve.py,sha256=HZsMwK4tcIfg3sh0gApx4-yjQnrY4o3V3ZRimvRhoW0,738
|
70
|
-
utilities/slack_sdk.py,sha256=
|
69
|
+
utilities/slack_sdk.py,sha256=SsRMJD2HuPUjAFg-2JxOQ9IhKViu4f66cN5kt-C2a7M,4245
|
71
70
|
utilities/socket.py,sha256=K77vfREvzoVTrpYKo6MZakol0EYu2q1sWJnnZqL0So0,118
|
72
|
-
utilities/sqlalchemy.py,sha256=
|
71
|
+
utilities/sqlalchemy.py,sha256=2rApf8NNGdpT827ep19LFt2zCe_oHgF__0WYVk_svtw,38014
|
73
72
|
utilities/sqlalchemy_polars.py,sha256=bDiKqHxOWu0Dj4ZDuGcVgR7ulm7sB90iVNINAKaeaKc,14290
|
74
73
|
utilities/statsmodels.py,sha256=koyiBHvpMcSiBfh99wFUfSggLNx7cuAw3rwyfAhoKpQ,3410
|
75
74
|
utilities/streamlit.py,sha256=U9PJBaKP1IdSykKhPZhIzSPTZsmLsnwbEPZWzNhJPKk,2955
|
@@ -77,8 +76,8 @@ utilities/string.py,sha256=XmU-s04qIV_tODnKl2pQiwmHaxzgOqRKU-RyzdrfvSE,620
|
|
77
76
|
utilities/tempfile.py,sha256=VqmZJAhTJ1OaVywFzk5eqROV8iJbW9XQ_QYAV0bpdRo,1384
|
78
77
|
utilities/text.py,sha256=ymBFlP_cA8OgNnZRVNs7FAh7OG8HxE6YkiLEMZv5g_A,11297
|
79
78
|
utilities/threading.py,sha256=GvBOp4CyhHfN90wGXZuA2VKe9fGzMaEa7oCl4f3nnPU,1009
|
80
|
-
utilities/timer.py,sha256=
|
81
|
-
utilities/traceback.py,sha256=
|
79
|
+
utilities/timer.py,sha256=oYqRQ-G-DMOOHB6a4yP5-PJDVimLnbNkMnkOj_jUmFg,2474
|
80
|
+
utilities/traceback.py,sha256=i-790AQbTrDA8MiYyOcYPFpm48I558VR_kL_7x4ypfY,8503
|
82
81
|
utilities/typed_settings.py,sha256=zUA0_CmVJT5rwrm3e-dZO83OdPXEel4NfVK24NAD5Vk,1779
|
83
82
|
utilities/types.py,sha256=fuJQiVjKYKL9g3F5H7oW_98Xm1-R5Xq4t4kU-aHxW0M,18826
|
84
83
|
utilities/typing.py,sha256=kVWK6ciV8T0MKxnFQcMSEr_XlRisspH5aBTTosMUh30,13872
|
@@ -87,10 +86,10 @@ utilities/tzlocal.py,sha256=xbBBzVIUKMk8AkhuIp1qxGRNBioIa5I09dpeoBnIOOU,662
|
|
87
86
|
utilities/uuid.py,sha256=jJTFxz-CWgltqNuzmythB7iEQ-Q1mCwPevUfKthZT3c,611
|
88
87
|
utilities/version.py,sha256=ufhJMmI6KPs1-3wBI71aj5wCukd3sP_m11usLe88DNA,5117
|
89
88
|
utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
|
90
|
-
utilities/
|
89
|
+
utilities/whenever.py,sha256=tArX9unVEKhRYdvbUFa83e4hrzdtMKKCEN4QWTaYd8c,19524
|
91
90
|
utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
|
92
91
|
utilities/zoneinfo.py,sha256=oEH-nL3t4h9uawyZqWDtNtDAl6M-CLpLYGI_nI6DulM,1971
|
93
|
-
dycw_utilities-0.
|
94
|
-
dycw_utilities-0.
|
95
|
-
dycw_utilities-0.
|
96
|
-
dycw_utilities-0.
|
92
|
+
dycw_utilities-0.132.0.dist-info/METADATA,sha256=uup-06B--FHZD8qwwZxSe2XuXJvBLf6F5QPidKDvHCg,1584
|
93
|
+
dycw_utilities-0.132.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
94
|
+
dycw_utilities-0.132.0.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
|
95
|
+
dycw_utilities-0.132.0.dist-info/RECORD,,
|
utilities/__init__.py
CHANGED
utilities/asyncio.py
CHANGED
utilities/click.py
CHANGED
@@ -8,7 +8,7 @@ import whenever
|
|
8
8
|
from click import Choice, Context, Parameter, ParamType
|
9
9
|
from click.types import StringParamType
|
10
10
|
|
11
|
-
|
11
|
+
import utilities.whenever
|
12
12
|
from utilities.enum import EnsureEnumError, ensure_enum
|
13
13
|
from utilities.functions import EnsureStrError, ensure_str, get_class_name
|
14
14
|
from utilities.iterables import is_iterable_not_str
|
@@ -25,12 +25,12 @@ from utilities.types import (
|
|
25
25
|
TimeLike,
|
26
26
|
ZonedDateTimeLike,
|
27
27
|
)
|
28
|
+
from utilities.whenever import _MonthParseCommonISOError
|
28
29
|
|
29
30
|
if TYPE_CHECKING:
|
30
31
|
from collections.abc import Iterable, Sequence
|
31
32
|
|
32
|
-
|
33
|
-
from utilities.datetime import MonthLike
|
33
|
+
from utilities.whenever import MonthLike
|
34
34
|
|
35
35
|
|
36
36
|
_T = TypeVar("_T")
|
@@ -185,11 +185,11 @@ class Month(ParamType):
|
|
185
185
|
@override
|
186
186
|
def convert(
|
187
187
|
self, value: MonthLike, param: Parameter | None, ctx: Context | None
|
188
|
-
) -> utilities.
|
188
|
+
) -> utilities.whenever.Month:
|
189
189
|
"""Convert a value into the `Month` type."""
|
190
190
|
try:
|
191
|
-
return
|
192
|
-
except
|
191
|
+
return utilities.whenever.Month.ensure(value)
|
192
|
+
except _MonthParseCommonISOError as error:
|
193
193
|
self.fail(str(error), param, ctx)
|
194
194
|
|
195
195
|
|
utilities/fastapi.py
CHANGED
@@ -8,7 +8,7 @@ from fastapi import FastAPI
|
|
8
8
|
from uvicorn import Config, Server
|
9
9
|
|
10
10
|
from utilities.asyncio import Looper
|
11
|
-
from utilities.
|
11
|
+
from utilities.whenever import SECOND, get_now_local
|
12
12
|
|
13
13
|
if TYPE_CHECKING:
|
14
14
|
from types import TracebackType
|
utilities/fpdf2.py
CHANGED
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, override
|
|
6
6
|
from fpdf import FPDF
|
7
7
|
from fpdf.enums import XPos, YPos
|
8
8
|
|
9
|
-
from utilities.
|
9
|
+
from utilities.whenever import format_compact, get_now
|
10
10
|
|
11
11
|
if TYPE_CHECKING:
|
12
12
|
from collections.abc import Iterator
|
utilities/hypothesis.py
CHANGED
@@ -31,7 +31,6 @@ from hypothesis.strategies import (
|
|
31
31
|
booleans,
|
32
32
|
characters,
|
33
33
|
composite,
|
34
|
-
dates,
|
35
34
|
datetimes,
|
36
35
|
floats,
|
37
36
|
integers,
|
@@ -57,14 +56,6 @@ from whenever import (
|
|
57
56
|
ZonedDateTime,
|
58
57
|
)
|
59
58
|
|
60
|
-
from utilities.datetime import (
|
61
|
-
MAX_DATE_TWO_DIGIT_YEAR,
|
62
|
-
MAX_MONTH,
|
63
|
-
MIN_DATE_TWO_DIGIT_YEAR,
|
64
|
-
MIN_MONTH,
|
65
|
-
Month,
|
66
|
-
date_to_month,
|
67
|
-
)
|
68
59
|
from utilities.functions import ensure_int, ensure_str
|
69
60
|
from utilities.math import (
|
70
61
|
MAX_FLOAT32,
|
@@ -87,7 +78,7 @@ from utilities.platform import IS_WINDOWS
|
|
87
78
|
from utilities.sentinel import Sentinel, sentinel
|
88
79
|
from utilities.tempfile import TEMP_DIR, TemporaryDirectory
|
89
80
|
from utilities.version import Version
|
90
|
-
from utilities.
|
81
|
+
from utilities.whenever import (
|
91
82
|
DATE_DELTA_MAX,
|
92
83
|
DATE_DELTA_MIN,
|
93
84
|
DATE_DELTA_PARSABLE_MAX,
|
@@ -98,13 +89,18 @@ from utilities.whenever2 import (
|
|
98
89
|
DATE_TIME_DELTA_MIN,
|
99
90
|
DATE_TIME_DELTA_PARSABLE_MAX,
|
100
91
|
DATE_TIME_DELTA_PARSABLE_MIN,
|
92
|
+
DATE_TWO_DIGIT_YEAR_MAX,
|
93
|
+
DATE_TWO_DIGIT_YEAR_MIN,
|
101
94
|
DAY,
|
95
|
+
MONTH_MAX,
|
96
|
+
MONTH_MIN,
|
102
97
|
PLAIN_DATE_TIME_MAX,
|
103
98
|
PLAIN_DATE_TIME_MIN,
|
104
99
|
TIME_DELTA_MAX,
|
105
100
|
TIME_DELTA_MIN,
|
106
101
|
TIME_MAX,
|
107
102
|
TIME_MIN,
|
103
|
+
Month,
|
108
104
|
to_date_time_delta,
|
109
105
|
to_days,
|
110
106
|
to_nanos,
|
@@ -180,7 +176,7 @@ def bool_arrays(
|
|
180
176
|
|
181
177
|
|
182
178
|
@composite
|
183
|
-
def
|
179
|
+
def date_deltas(
|
184
180
|
draw: DrawFn,
|
185
181
|
/,
|
186
182
|
*,
|
@@ -217,7 +213,7 @@ def date_deltas_whenever(
|
|
217
213
|
|
218
214
|
|
219
215
|
@composite
|
220
|
-
def
|
216
|
+
def date_time_deltas(
|
221
217
|
draw: DrawFn,
|
222
218
|
/,
|
223
219
|
*,
|
@@ -253,30 +249,13 @@ def date_time_deltas_whenever(
|
|
253
249
|
|
254
250
|
|
255
251
|
@composite
|
256
|
-
def
|
257
|
-
draw: DrawFn,
|
258
|
-
/,
|
259
|
-
*,
|
260
|
-
min_value: MaybeSearchStrategy[dt.date] = MIN_DATE_TWO_DIGIT_YEAR,
|
261
|
-
max_value: MaybeSearchStrategy[dt.date] = MAX_DATE_TWO_DIGIT_YEAR,
|
262
|
-
) -> dt.date:
|
263
|
-
"""Strategy for generating dates with valid 2 digit years."""
|
264
|
-
min_value_, max_value_ = [draw2(draw, v) for v in [min_value, max_value]]
|
265
|
-
min_value_ = max(min_value_, MIN_DATE_TWO_DIGIT_YEAR)
|
266
|
-
max_value_ = min(max_value_, MAX_DATE_TWO_DIGIT_YEAR)
|
267
|
-
return draw(dates(min_value=min_value_, max_value=max_value_))
|
268
|
-
|
269
|
-
|
270
|
-
##
|
271
|
-
|
272
|
-
|
273
|
-
@composite
|
274
|
-
def dates_whenever(
|
252
|
+
def dates(
|
275
253
|
draw: DrawFn,
|
276
254
|
/,
|
277
255
|
*,
|
278
256
|
min_value: MaybeSearchStrategy[Date | None] = None,
|
279
257
|
max_value: MaybeSearchStrategy[Date | None] = None,
|
258
|
+
two_digit: MaybeSearchStrategy[bool] = False,
|
280
259
|
) -> Date:
|
281
260
|
"""Strategy for generating dates."""
|
282
261
|
min_value_, max_value_ = [draw2(draw, v) for v in [min_value, max_value]]
|
@@ -294,9 +273,11 @@ def dates_whenever(
|
|
294
273
|
...
|
295
274
|
case _ as never:
|
296
275
|
assert_never(never)
|
297
|
-
|
298
|
-
|
299
|
-
|
276
|
+
if draw2(draw, two_digit):
|
277
|
+
min_value_ = max(min_value_, DATE_TWO_DIGIT_YEAR_MIN)
|
278
|
+
max_value_ = min(max_value_, DATE_TWO_DIGIT_YEAR_MAX)
|
279
|
+
min_date, max_date = [d.py_date() for d in [min_value_, max_value_]]
|
280
|
+
py_date = draw(hypothesis.strategies.dates(min_value=min_date, max_value=max_date))
|
300
281
|
return Date.from_py_date(py_date)
|
301
282
|
|
302
283
|
|
@@ -639,13 +620,29 @@ def months(
|
|
639
620
|
draw: DrawFn,
|
640
621
|
/,
|
641
622
|
*,
|
642
|
-
min_value: MaybeSearchStrategy[Month] =
|
643
|
-
max_value: MaybeSearchStrategy[Month] =
|
623
|
+
min_value: MaybeSearchStrategy[Month | None] = None,
|
624
|
+
max_value: MaybeSearchStrategy[Month | None] = None,
|
625
|
+
two_digit: MaybeSearchStrategy[bool] = False,
|
644
626
|
) -> Month:
|
645
|
-
"""Strategy for generating
|
646
|
-
min_value_, max_value_ = [draw2(draw, v)
|
647
|
-
|
648
|
-
|
627
|
+
"""Strategy for generating months."""
|
628
|
+
min_value_, max_value_ = [draw2(draw, v) for v in [min_value, max_value]]
|
629
|
+
match min_value_:
|
630
|
+
case None:
|
631
|
+
min_value_ = MONTH_MIN
|
632
|
+
case Month():
|
633
|
+
...
|
634
|
+
case _ as never:
|
635
|
+
assert_never(never)
|
636
|
+
match max_value_:
|
637
|
+
case None:
|
638
|
+
max_value_ = MONTH_MAX
|
639
|
+
case Month():
|
640
|
+
...
|
641
|
+
case _ as never:
|
642
|
+
assert_never(never)
|
643
|
+
min_date, max_date = [m.to_date() for m in [min_value_, max_value_]]
|
644
|
+
date = draw(dates(min_value=min_date, max_value=max_date, two_digit=two_digit))
|
645
|
+
return Month.from_date(date)
|
649
646
|
|
650
647
|
|
651
648
|
##
|
@@ -735,7 +732,7 @@ def paths() -> SearchStrategy[Path]:
|
|
735
732
|
|
736
733
|
|
737
734
|
@composite
|
738
|
-
def
|
735
|
+
def plain_datetimes(
|
739
736
|
draw: DrawFn,
|
740
737
|
/,
|
741
738
|
*,
|
@@ -1076,7 +1073,7 @@ def text_printable(
|
|
1076
1073
|
|
1077
1074
|
|
1078
1075
|
@composite
|
1079
|
-
def
|
1076
|
+
def time_deltas(
|
1080
1077
|
draw: DrawFn,
|
1081
1078
|
/,
|
1082
1079
|
*,
|
@@ -1111,7 +1108,7 @@ def time_deltas_whenever(
|
|
1111
1108
|
|
1112
1109
|
|
1113
1110
|
@composite
|
1114
|
-
def
|
1111
|
+
def times(
|
1115
1112
|
draw: DrawFn,
|
1116
1113
|
/,
|
1117
1114
|
*,
|
@@ -1212,7 +1209,7 @@ def versions(draw: DrawFn, /, *, suffix: MaybeSearchStrategy[bool] = False) -> V
|
|
1212
1209
|
|
1213
1210
|
|
1214
1211
|
@composite
|
1215
|
-
def
|
1212
|
+
def zoned_datetimes(
|
1216
1213
|
draw: DrawFn,
|
1217
1214
|
/,
|
1218
1215
|
*,
|
@@ -1239,7 +1236,7 @@ def zoned_datetimes_whenever(
|
|
1239
1236
|
max_value_ = max_value_.to_tz(time_zone_.key).to_plain()
|
1240
1237
|
case _ as never:
|
1241
1238
|
assert_never(never)
|
1242
|
-
plain = draw(
|
1239
|
+
plain = draw(plain_datetimes(min_value=min_value_, max_value=max_value_))
|
1243
1240
|
with (
|
1244
1241
|
assume_does_not_raise(RepeatedTime),
|
1245
1242
|
assume_does_not_raise(SkippedTime),
|
@@ -1259,10 +1256,9 @@ __all__ = [
|
|
1259
1256
|
"Shape",
|
1260
1257
|
"assume_does_not_raise",
|
1261
1258
|
"bool_arrays",
|
1262
|
-
"
|
1263
|
-
"
|
1264
|
-
"
|
1265
|
-
"dates_whenever",
|
1259
|
+
"date_deltas",
|
1260
|
+
"date_time_deltas",
|
1261
|
+
"dates",
|
1266
1262
|
"draw2",
|
1267
1263
|
"float32s",
|
1268
1264
|
"float64s",
|
@@ -1279,7 +1275,7 @@ __all__ = [
|
|
1279
1275
|
"numbers",
|
1280
1276
|
"pairs",
|
1281
1277
|
"paths",
|
1282
|
-
"
|
1278
|
+
"plain_datetimes",
|
1283
1279
|
"random_states",
|
1284
1280
|
"sentinels",
|
1285
1281
|
"sets_fixed_length",
|
@@ -1294,11 +1290,11 @@ __all__ = [
|
|
1294
1290
|
"text_clean",
|
1295
1291
|
"text_digits",
|
1296
1292
|
"text_printable",
|
1297
|
-
"
|
1298
|
-
"
|
1293
|
+
"time_deltas",
|
1294
|
+
"times",
|
1299
1295
|
"triples",
|
1300
1296
|
"uint32s",
|
1301
1297
|
"uint64s",
|
1302
1298
|
"versions",
|
1303
|
-
"
|
1299
|
+
"zoned_datetimes",
|
1304
1300
|
]
|
utilities/logging.py
CHANGED
@@ -45,12 +45,7 @@ from utilities.re import (
|
|
45
45
|
)
|
46
46
|
from utilities.sentinel import Sentinel, sentinel
|
47
47
|
from utilities.tzlocal import LOCAL_TIME_ZONE_NAME
|
48
|
-
from utilities.
|
49
|
-
WheneverLogRecord,
|
50
|
-
format_compact,
|
51
|
-
get_now,
|
52
|
-
get_now_local,
|
53
|
-
)
|
48
|
+
from utilities.whenever import WheneverLogRecord, format_compact, get_now, get_now_local
|
54
49
|
|
55
50
|
if TYPE_CHECKING:
|
56
51
|
from collections.abc import Callable, Iterable, Mapping
|
utilities/orjson.py
CHANGED
@@ -47,7 +47,7 @@ from utilities.math import MAX_INT64, MIN_INT64
|
|
47
47
|
from utilities.types import Dataclass, LogLevel, MaybeIterable, PathLike, StrMapping
|
48
48
|
from utilities.tzlocal import LOCAL_TIME_ZONE
|
49
49
|
from utilities.version import Version, parse_version
|
50
|
-
from utilities.
|
50
|
+
from utilities.whenever import from_timestamp
|
51
51
|
|
52
52
|
if TYPE_CHECKING:
|
53
53
|
from collections.abc import Set as AbstractSet
|
utilities/pottery.py
CHANGED
@@ -10,7 +10,7 @@ from redis.asyncio import Redis
|
|
10
10
|
|
11
11
|
from utilities.asyncio import sleep_td, timeout_td
|
12
12
|
from utilities.iterables import always_iterable
|
13
|
-
from utilities.
|
13
|
+
from utilities.whenever import MILLISECOND, SECOND
|
14
14
|
|
15
15
|
if TYPE_CHECKING:
|
16
16
|
from collections.abc import AsyncIterator, Iterable
|
utilities/psutil.py
CHANGED
@@ -11,7 +11,7 @@ from psutil import swap_memory, virtual_memory
|
|
11
11
|
|
12
12
|
from utilities.asyncio import Looper
|
13
13
|
from utilities.contextlib import suppress_super_object_attribute_error
|
14
|
-
from utilities.
|
14
|
+
from utilities.whenever import SECOND, get_now
|
15
15
|
|
16
16
|
if TYPE_CHECKING:
|
17
17
|
from logging import Logger
|
utilities/pyinstrument.py
CHANGED
@@ -8,7 +8,7 @@ from pyinstrument.profiler import Profiler
|
|
8
8
|
|
9
9
|
from utilities.atomicwrites import writer
|
10
10
|
from utilities.pathlib import get_path
|
11
|
-
from utilities.
|
11
|
+
from utilities.whenever import format_compact, get_now
|
12
12
|
|
13
13
|
if TYPE_CHECKING:
|
14
14
|
from collections.abc import Iterator
|
utilities/pytest.py
CHANGED
@@ -24,7 +24,7 @@ from utilities.platform import (
|
|
24
24
|
IS_WINDOWS,
|
25
25
|
)
|
26
26
|
from utilities.random import get_state
|
27
|
-
from utilities.
|
27
|
+
from utilities.whenever import SECOND, get_now_local
|
28
28
|
|
29
29
|
if TYPE_CHECKING:
|
30
30
|
from collections.abc import Callable, Iterable, Sequence
|
utilities/redis.py
CHANGED
@@ -29,7 +29,7 @@ from utilities.errors import ImpossibleCaseError
|
|
29
29
|
from utilities.functions import ensure_int, identity
|
30
30
|
from utilities.iterables import always_iterable, one
|
31
31
|
from utilities.orjson import deserialize, serialize
|
32
|
-
from utilities.
|
32
|
+
from utilities.whenever import MILLISECOND, SECOND
|
33
33
|
|
34
34
|
if TYPE_CHECKING:
|
35
35
|
from collections.abc import (
|
utilities/slack_sdk.py
CHANGED
@@ -10,7 +10,7 @@ from slack_sdk.webhook.async_client import AsyncWebhookClient
|
|
10
10
|
from utilities.asyncio import Looper, timeout_td
|
11
11
|
from utilities.functools import cache
|
12
12
|
from utilities.sentinel import Sentinel, sentinel
|
13
|
-
from utilities.
|
13
|
+
from utilities.whenever import MINUTE, SECOND
|
14
14
|
|
15
15
|
if TYPE_CHECKING:
|
16
16
|
from collections.abc import Callable
|
utilities/sqlalchemy.py
CHANGED
@@ -91,7 +91,7 @@ from utilities.iterables import (
|
|
91
91
|
from utilities.reprlib import get_repr
|
92
92
|
from utilities.text import snake_case
|
93
93
|
from utilities.types import MaybeIterable, MaybeType, StrMapping, TupleOrStrMapping
|
94
|
-
from utilities.
|
94
|
+
from utilities.whenever import SECOND
|
95
95
|
|
96
96
|
if TYPE_CHECKING:
|
97
97
|
from whenever import TimeDelta
|
utilities/timer.py
CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
3
3
|
from operator import add, eq, ge, gt, le, lt, mul, ne, sub, truediv
|
4
4
|
from typing import TYPE_CHECKING, Any, Self, override
|
5
5
|
|
6
|
-
from utilities.
|
6
|
+
from utilities.whenever import get_now_local
|
7
7
|
|
8
8
|
if TYPE_CHECKING:
|
9
9
|
from collections.abc import Callable
|
utilities/traceback.py
CHANGED
@@ -27,7 +27,7 @@ from utilities.reprlib import (
|
|
27
27
|
yield_mapping_repr,
|
28
28
|
)
|
29
29
|
from utilities.version import get_version
|
30
|
-
from utilities.
|
30
|
+
from utilities.whenever import format_compact, get_now, to_zoned_date_time
|
31
31
|
|
32
32
|
if TYPE_CHECKING:
|
33
33
|
from collections.abc import Callable, Iterator, Sequence
|
@@ -1,26 +1,34 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import datetime as dt
|
4
3
|
from collections.abc import Callable, Iterable
|
5
|
-
from dataclasses import dataclass
|
4
|
+
from dataclasses import dataclass, replace
|
6
5
|
from functools import cache
|
7
6
|
from logging import LogRecord
|
8
7
|
from statistics import fmean
|
9
|
-
from typing import
|
8
|
+
from typing import (
|
9
|
+
TYPE_CHECKING,
|
10
|
+
Any,
|
11
|
+
Self,
|
12
|
+
SupportsFloat,
|
13
|
+
assert_never,
|
14
|
+
overload,
|
15
|
+
override,
|
16
|
+
)
|
10
17
|
|
11
18
|
from whenever import (
|
12
19
|
Date,
|
13
20
|
DateDelta,
|
14
21
|
DateTimeDelta,
|
15
22
|
PlainDateTime,
|
16
|
-
Time,
|
17
23
|
TimeDelta,
|
18
24
|
ZonedDateTime,
|
19
25
|
)
|
20
26
|
|
21
27
|
from utilities.math import sign
|
22
28
|
from utilities.platform import get_strftime
|
29
|
+
from utilities.re import ExtractGroupsError, extract_groups
|
23
30
|
from utilities.sentinel import Sentinel, sentinel
|
31
|
+
from utilities.types import MaybeStr
|
24
32
|
from utilities.tzlocal import LOCAL_TIME_ZONE, LOCAL_TIME_ZONE_NAME
|
25
33
|
from utilities.zoneinfo import UTC, get_time_zone_name
|
26
34
|
|
@@ -37,14 +45,14 @@ if TYPE_CHECKING:
|
|
37
45
|
## bounds
|
38
46
|
|
39
47
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
+
PLAIN_DATE_TIME_MIN = PlainDateTime(1, 1, 1)
|
49
|
+
PLAIN_DATE_TIME_MAX = PlainDateTime(
|
50
|
+
9999, 12, 31, hour=23, minute=59, second=59, nanosecond=999999999
|
51
|
+
)
|
52
|
+
DATE_MIN = PLAIN_DATE_TIME_MIN.date()
|
53
|
+
DATE_MAX = PLAIN_DATE_TIME_MAX.date()
|
54
|
+
TIME_MIN = PLAIN_DATE_TIME_MIN.time()
|
55
|
+
TIME_MAX = PLAIN_DATE_TIME_MIN.time()
|
48
56
|
ZONED_DATE_TIME_MIN = PLAIN_DATE_TIME_MIN.assume_tz(UTC.key)
|
49
57
|
ZONED_DATE_TIME_MAX = PLAIN_DATE_TIME_MAX.assume_tz(UTC.key)
|
50
58
|
|
@@ -97,6 +105,10 @@ DATE_DELTA_PARSABLE_MIN = DateDelta(days=-999999)
|
|
97
105
|
DATE_DELTA_PARSABLE_MAX = DateDelta(days=999999)
|
98
106
|
|
99
107
|
|
108
|
+
DATE_TWO_DIGIT_YEAR_MIN = Date(1969, 1, 1)
|
109
|
+
DATE_TWO_DIGIT_YEAR_MAX = Date(DATE_TWO_DIGIT_YEAR_MIN.year + 99, 12, 31)
|
110
|
+
|
111
|
+
|
100
112
|
## common constants
|
101
113
|
|
102
114
|
|
@@ -313,6 +325,124 @@ class _MinMaxDatePeriodError(MinMaxDateError):
|
|
313
325
|
##
|
314
326
|
|
315
327
|
|
328
|
+
@dataclass(order=True, unsafe_hash=True, slots=True)
|
329
|
+
class Month:
|
330
|
+
"""Represents a month in time."""
|
331
|
+
|
332
|
+
year: int
|
333
|
+
month: int
|
334
|
+
|
335
|
+
def __post_init__(self) -> None:
|
336
|
+
try:
|
337
|
+
_ = Date(self.year, self.month, 1)
|
338
|
+
except ValueError:
|
339
|
+
raise _MonthInvalidError(year=self.year, month=self.month) from None
|
340
|
+
|
341
|
+
@override
|
342
|
+
def __repr__(self) -> str:
|
343
|
+
return self.format_common_iso()
|
344
|
+
|
345
|
+
@override
|
346
|
+
def __str__(self) -> str:
|
347
|
+
return repr(self)
|
348
|
+
|
349
|
+
def __add__(self, other: Any, /) -> Self:
|
350
|
+
if not isinstance(other, int): # pragma: no cover
|
351
|
+
return NotImplemented
|
352
|
+
years, month = divmod(self.month + other - 1, 12)
|
353
|
+
month += 1
|
354
|
+
year = self.year + years
|
355
|
+
return replace(self, year=year, month=month)
|
356
|
+
|
357
|
+
@overload
|
358
|
+
def __sub__(self, other: Self, /) -> int: ...
|
359
|
+
@overload
|
360
|
+
def __sub__(self, other: int, /) -> Self: ...
|
361
|
+
def __sub__(self, other: Self | int, /) -> Self | int:
|
362
|
+
if isinstance(other, int): # pragma: no cover
|
363
|
+
return self + (-other)
|
364
|
+
if isinstance(other, type(self)):
|
365
|
+
self_as_int = 12 * self.year + self.month
|
366
|
+
other_as_int = 12 * other.year + other.month
|
367
|
+
return self_as_int - other_as_int
|
368
|
+
return NotImplemented # pragma: no cover
|
369
|
+
|
370
|
+
@classmethod
|
371
|
+
def ensure(cls, obj: MonthLike, /) -> Month:
|
372
|
+
"""Ensure the object is a month."""
|
373
|
+
match obj:
|
374
|
+
case Month() as month:
|
375
|
+
return month
|
376
|
+
case str() as text:
|
377
|
+
return cls.parse_common_iso(text)
|
378
|
+
case _ as never:
|
379
|
+
assert_never(never)
|
380
|
+
|
381
|
+
def format_common_iso(self) -> str:
|
382
|
+
return f"{self.year:04}-{self.month:02}"
|
383
|
+
|
384
|
+
@classmethod
|
385
|
+
def from_date(cls, date: Date, /) -> Self:
|
386
|
+
return cls(year=date.year, month=date.month)
|
387
|
+
|
388
|
+
@classmethod
|
389
|
+
def parse_common_iso(cls, text: str, /) -> Self:
|
390
|
+
try:
|
391
|
+
year, month = extract_groups(r"^(\d{2,4})[\-\. ]?(\d{2})$", text)
|
392
|
+
except ExtractGroupsError:
|
393
|
+
raise _MonthParseCommonISOError(text=text) from None
|
394
|
+
return cls(year=cls._parse_year(year), month=int(month))
|
395
|
+
|
396
|
+
def to_date(self, /, *, day: int = 1) -> Date:
|
397
|
+
return Date(self.year, self.month, day)
|
398
|
+
|
399
|
+
@classmethod
|
400
|
+
def _parse_year(cls, year: str, /) -> int:
|
401
|
+
match len(year):
|
402
|
+
case 4:
|
403
|
+
return int(year)
|
404
|
+
case 2:
|
405
|
+
min_year = DATE_TWO_DIGIT_YEAR_MIN.year
|
406
|
+
max_year = DATE_TWO_DIGIT_YEAR_MAX.year
|
407
|
+
years = range(min_year, max_year + 1)
|
408
|
+
(result,) = (y for y in years if y % 100 == int(year))
|
409
|
+
return result
|
410
|
+
case _:
|
411
|
+
raise _MonthParseCommonISOError(text=year) from None
|
412
|
+
|
413
|
+
|
414
|
+
@dataclass(kw_only=True, slots=True)
|
415
|
+
class MonthError(Exception): ...
|
416
|
+
|
417
|
+
|
418
|
+
@dataclass(kw_only=True, slots=True)
|
419
|
+
class _MonthInvalidError(MonthError):
|
420
|
+
year: int
|
421
|
+
month: int
|
422
|
+
|
423
|
+
@override
|
424
|
+
def __str__(self) -> str:
|
425
|
+
return f"Invalid year and month: {self.year}, {self.month}"
|
426
|
+
|
427
|
+
|
428
|
+
@dataclass(kw_only=True, slots=True)
|
429
|
+
class _MonthParseCommonISOError(MonthError):
|
430
|
+
text: str
|
431
|
+
|
432
|
+
@override
|
433
|
+
def __str__(self) -> str:
|
434
|
+
return f"Unable to parse month; got {self.text!r}"
|
435
|
+
|
436
|
+
|
437
|
+
type DateOrMonth = Date | Month
|
438
|
+
type MonthLike = MaybeStr[Month]
|
439
|
+
MONTH_MIN = Month.from_date(DATE_MIN)
|
440
|
+
MONTH_MAX = Month.from_date(DATE_MAX)
|
441
|
+
|
442
|
+
|
443
|
+
##
|
444
|
+
|
445
|
+
|
316
446
|
@overload
|
317
447
|
def to_date(*, date: MaybeCallableDate) -> Date: ...
|
318
448
|
@overload
|
@@ -579,12 +709,16 @@ __all__ = [
|
|
579
709
|
"DATE_TIME_DELTA_MIN",
|
580
710
|
"DATE_TIME_DELTA_PARSABLE_MAX",
|
581
711
|
"DATE_TIME_DELTA_PARSABLE_MIN",
|
712
|
+
"DATE_TWO_DIGIT_YEAR_MAX",
|
713
|
+
"DATE_TWO_DIGIT_YEAR_MIN",
|
582
714
|
"DAY",
|
583
715
|
"HOUR",
|
584
716
|
"MICROSECOND",
|
585
717
|
"MILLISECOND",
|
586
718
|
"MINUTE",
|
587
719
|
"MONTH",
|
720
|
+
"MONTH_MAX",
|
721
|
+
"MONTH_MIN",
|
588
722
|
"NOW_LOCAL",
|
589
723
|
"PLAIN_DATE_TIME_MAX",
|
590
724
|
"PLAIN_DATE_TIME_MIN",
|
@@ -601,8 +735,12 @@ __all__ = [
|
|
601
735
|
"ZERO_TIME",
|
602
736
|
"ZONED_DATE_TIME_MAX",
|
603
737
|
"ZONED_DATE_TIME_MIN",
|
738
|
+
"DateOrMonth",
|
604
739
|
"MeanDateTimeError",
|
605
740
|
"MinMaxDateError",
|
741
|
+
"Month",
|
742
|
+
"MonthError",
|
743
|
+
"MonthLike",
|
606
744
|
"ToDaysError",
|
607
745
|
"ToNanosError",
|
608
746
|
"WheneverLogRecord",
|
utilities/datetime.py
DELETED
@@ -1,205 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import datetime as dt
|
4
|
-
from dataclasses import dataclass, replace
|
5
|
-
from re import search
|
6
|
-
from typing import Any, Self, assert_never, overload, override
|
7
|
-
|
8
|
-
from utilities.iterables import OneEmptyError, one
|
9
|
-
from utilities.types import MaybeStr
|
10
|
-
from utilities.zoneinfo import UTC
|
11
|
-
|
12
|
-
|
13
|
-
def date_to_month(date: dt.date, /) -> Month:
|
14
|
-
"""Collapse a date into a month."""
|
15
|
-
return Month(year=date.year, month=date.month)
|
16
|
-
|
17
|
-
|
18
|
-
##
|
19
|
-
|
20
|
-
|
21
|
-
def ensure_month(month: MonthLike, /) -> Month:
|
22
|
-
"""Ensure the object is a month."""
|
23
|
-
if isinstance(month, Month):
|
24
|
-
return month
|
25
|
-
try:
|
26
|
-
return parse_month(month)
|
27
|
-
except ParseMonthError as error:
|
28
|
-
raise EnsureMonthError(month=error.month) from None
|
29
|
-
|
30
|
-
|
31
|
-
@dataclass(kw_only=True, slots=True)
|
32
|
-
class EnsureMonthError(Exception):
|
33
|
-
month: str
|
34
|
-
|
35
|
-
@override
|
36
|
-
def __str__(self) -> str:
|
37
|
-
return f"Unable to ensure month; got {self.month!r}"
|
38
|
-
|
39
|
-
|
40
|
-
##
|
41
|
-
|
42
|
-
|
43
|
-
@dataclass(order=True, unsafe_hash=True, slots=True)
|
44
|
-
class Month:
|
45
|
-
"""Represents a month in time."""
|
46
|
-
|
47
|
-
year: int
|
48
|
-
month: int
|
49
|
-
|
50
|
-
def __post_init__(self) -> None:
|
51
|
-
try:
|
52
|
-
_ = dt.date(self.year, self.month, 1)
|
53
|
-
except ValueError:
|
54
|
-
raise MonthError(year=self.year, month=self.month) from None
|
55
|
-
|
56
|
-
@override
|
57
|
-
def __repr__(self) -> str:
|
58
|
-
return serialize_month(self)
|
59
|
-
|
60
|
-
@override
|
61
|
-
def __str__(self) -> str:
|
62
|
-
return repr(self)
|
63
|
-
|
64
|
-
def __add__(self, other: Any, /) -> Self:
|
65
|
-
if not isinstance(other, int): # pragma: no cover
|
66
|
-
return NotImplemented
|
67
|
-
years, month = divmod(self.month + other - 1, 12)
|
68
|
-
month += 1
|
69
|
-
year = self.year + years
|
70
|
-
return replace(self, year=year, month=month)
|
71
|
-
|
72
|
-
@overload
|
73
|
-
def __sub__(self, other: Self, /) -> int: ...
|
74
|
-
@overload
|
75
|
-
def __sub__(self, other: int, /) -> Self: ...
|
76
|
-
def __sub__(self, other: Self | int, /) -> Self | int:
|
77
|
-
if isinstance(other, int): # pragma: no cover
|
78
|
-
return self + (-other)
|
79
|
-
if isinstance(other, type(self)):
|
80
|
-
self_as_int = 12 * self.year + self.month
|
81
|
-
other_as_int = 12 * other.year + other.month
|
82
|
-
return self_as_int - other_as_int
|
83
|
-
return NotImplemented # pragma: no cover
|
84
|
-
|
85
|
-
@classmethod
|
86
|
-
def from_date(cls, date: dt.date, /) -> Self:
|
87
|
-
return cls(year=date.year, month=date.month)
|
88
|
-
|
89
|
-
def to_date(self, /, *, day: int = 1) -> dt.date:
|
90
|
-
return dt.date(self.year, self.month, day)
|
91
|
-
|
92
|
-
|
93
|
-
@dataclass(kw_only=True, slots=True)
|
94
|
-
class MonthError(Exception):
|
95
|
-
year: int
|
96
|
-
month: int
|
97
|
-
|
98
|
-
@override
|
99
|
-
def __str__(self) -> str:
|
100
|
-
return f"Invalid year and month: {self.year}, {self.month}"
|
101
|
-
|
102
|
-
|
103
|
-
type DateOrMonth = dt.date | Month
|
104
|
-
type MonthLike = MaybeStr[Month]
|
105
|
-
MIN_MONTH = Month(dt.date.min.year, dt.date.min.month)
|
106
|
-
MAX_MONTH = Month(dt.date.max.year, dt.date.max.month)
|
107
|
-
|
108
|
-
|
109
|
-
##
|
110
|
-
|
111
|
-
|
112
|
-
_TWO_DIGIT_YEAR_MIN = 1969
|
113
|
-
_TWO_DIGIT_YEAR_MAX = _TWO_DIGIT_YEAR_MIN + 99
|
114
|
-
MIN_DATE_TWO_DIGIT_YEAR = dt.date(
|
115
|
-
_TWO_DIGIT_YEAR_MIN, dt.date.min.month, dt.date.min.day
|
116
|
-
)
|
117
|
-
MAX_DATE_TWO_DIGIT_YEAR = dt.date(
|
118
|
-
_TWO_DIGIT_YEAR_MAX, dt.date.max.month, dt.date.max.day
|
119
|
-
)
|
120
|
-
|
121
|
-
|
122
|
-
def parse_two_digit_year(year: int | str, /) -> int:
|
123
|
-
"""Parse a 2-digit year into a year."""
|
124
|
-
match year:
|
125
|
-
case int():
|
126
|
-
years = range(_TWO_DIGIT_YEAR_MIN, _TWO_DIGIT_YEAR_MAX + 1)
|
127
|
-
try:
|
128
|
-
return one(y for y in years if y % 100 == year)
|
129
|
-
except OneEmptyError:
|
130
|
-
raise _ParseTwoDigitYearInvalidIntegerError(year=year) from None
|
131
|
-
case str():
|
132
|
-
if search(r"^\d{1,2}$", year):
|
133
|
-
return parse_two_digit_year(int(year))
|
134
|
-
raise _ParseTwoDigitYearInvalidStringError(year=year)
|
135
|
-
case _ as never:
|
136
|
-
assert_never(never)
|
137
|
-
|
138
|
-
|
139
|
-
@dataclass(kw_only=True, slots=True)
|
140
|
-
class ParseTwoDigitYearError(Exception):
|
141
|
-
year: int | str
|
142
|
-
|
143
|
-
|
144
|
-
@dataclass(kw_only=True, slots=True)
|
145
|
-
class _ParseTwoDigitYearInvalidIntegerError(Exception):
|
146
|
-
year: int | str
|
147
|
-
|
148
|
-
@override
|
149
|
-
def __str__(self) -> str:
|
150
|
-
return f"Unable to parse year; got {self.year!r}"
|
151
|
-
|
152
|
-
|
153
|
-
@dataclass(kw_only=True, slots=True)
|
154
|
-
class _ParseTwoDigitYearInvalidStringError(Exception):
|
155
|
-
year: int | str
|
156
|
-
|
157
|
-
@override
|
158
|
-
def __str__(self) -> str:
|
159
|
-
return f"Unable to parse year; got {self.year!r}"
|
160
|
-
|
161
|
-
|
162
|
-
##
|
163
|
-
|
164
|
-
|
165
|
-
def serialize_month(month: Month, /) -> str:
|
166
|
-
"""Serialize a month."""
|
167
|
-
return f"{month.year:04}-{month.month:02}"
|
168
|
-
|
169
|
-
|
170
|
-
def parse_month(month: str, /) -> Month:
|
171
|
-
"""Parse a string into a month."""
|
172
|
-
for fmt in ["%Y-%m", "%Y%m", "%Y %m"]:
|
173
|
-
try:
|
174
|
-
date = dt.datetime.strptime(month, fmt).replace(tzinfo=UTC).date()
|
175
|
-
except ValueError:
|
176
|
-
pass
|
177
|
-
else:
|
178
|
-
return Month(date.year, date.month)
|
179
|
-
raise ParseMonthError(month=month)
|
180
|
-
|
181
|
-
|
182
|
-
@dataclass(kw_only=True, slots=True)
|
183
|
-
class ParseMonthError(Exception):
|
184
|
-
month: str
|
185
|
-
|
186
|
-
@override
|
187
|
-
def __str__(self) -> str:
|
188
|
-
return f"Unable to parse month; got {self.month!r}"
|
189
|
-
|
190
|
-
|
191
|
-
__all__ = [
|
192
|
-
"MAX_DATE_TWO_DIGIT_YEAR",
|
193
|
-
"MAX_MONTH",
|
194
|
-
"MIN_DATE_TWO_DIGIT_YEAR",
|
195
|
-
"MIN_MONTH",
|
196
|
-
"DateOrMonth",
|
197
|
-
"EnsureMonthError",
|
198
|
-
"Month",
|
199
|
-
"MonthError",
|
200
|
-
"MonthLike",
|
201
|
-
"ParseMonthError",
|
202
|
-
"date_to_month",
|
203
|
-
"ensure_month",
|
204
|
-
"parse_two_digit_year",
|
205
|
-
]
|
File without changes
|
File without changes
|