dycw-utilities 0.131.19__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.131.19
3
+ Version: 0.132.0
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -1,30 +1,29 @@
1
- utilities/__init__.py,sha256=R2vwqRIsE4_EtyBG_rcNMmBFLNOvTZ8ZPET3SOpv2QI,61
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=mHnlSA4KPeDaBRts8Rn4sNA_4urodj7gQzs-_8Z-F7A,37587
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=sdGAMoarAWJ4_A3o-UzM7hpe7R37yvC_X_Uh8vasa-A,13295
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=NwqxkOufzZyGrnX8YwksrEF9DAkVPf3HioLOwe9jMWA,5754
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=4PpToyT-xE8kjVJpFE1nMBzx2I1HcXaGTRzNV3JfZLQ,2672
20
- utilities/fpdf2.py,sha256=PQysO4BcSMRpg-h-t2Pm7yatyk2yFl1inkvtKs1NG4M,1853
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=OMuN6S_L_OjEOdXdDlyJ53li1vKZPJVUmUFYGtNM9-0,35277
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=zm5k0Cduxtx2H2o7odxUTJtPNkJS85mqHYN1cS5Kc1w,17863
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,35 +40,35 @@ 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=7CraMEtaJtKTDTTVS4fQU6PeuFcG0V8SGwD3sqZ8kSI,36475
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
48
47
  utilities/period.py,sha256=opqpBevBGSGXbA7NYfRJjtthi1JPxdMaZ7QV3xosnTc,4774
49
48
  utilities/pickle.py,sha256=MBT2xZCsv0pH868IXLGKnlcqNx2IRVKYNpRcqiQQqxw,653
50
- utilities/platform.py,sha256=48IOKx1IC6ZJXWG-b56ZQptITcNFhWRjELW72o2dGTA,2398
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=2w3YuoH1KmLaCVqkwSghHTOT8S4xiUskwRHSRqrUEQY,3430
52
+ utilities/pottery.py,sha256=RN3XwOEsVAPXvEfsRPmn3ZSKgTzK_c182PNrtksq-bg,3429
54
53
  utilities/pqdm.py,sha256=foRytQybmOQ05pjt5LF7ANyzrIa--4ScDE3T2wd31a4,3118
55
- utilities/psutil.py,sha256=ZkwBGfTqAv8hxCyJVVJFctIVQvxhYuB28re3mIPBodc,3761
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=3MPNDbZW_1Aj1aA1_f-yqPStgmjIFxPwIafYq2dsaTs,853
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=BQ3D3HjkmccTx9R9MahsNqtHEqB9L4rdjV0Xd-GCf5Y,8274
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=oA70YoFnSaSqTBzMHrbHkkGIBWU69-97MQEwH_0iX3g,35762
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=ZpPzV_oHpSJbontGo2_YVVjKH08PjLjsx5Me-nLy918,4246
69
+ utilities/slack_sdk.py,sha256=SsRMJD2HuPUjAFg-2JxOQ9IhKViu4f66cN5kt-C2a7M,4245
71
70
  utilities/socket.py,sha256=K77vfREvzoVTrpYKo6MZakol0EYu2q1sWJnnZqL0So0,118
72
- utilities/sqlalchemy.py,sha256=sTlxK-Is-XCj7srTRh3Nz68axzIdDYZWLQdTeXnOdkQ,38015
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=VeSl3ot8-f4D1d3HjjSsgKvjxHJGXd_sW4KcTExOR64,2475
81
- utilities/traceback.py,sha256=cMXrCD59CROnezAU8VW67CxZ8Igc5QmaxlV8qrBvNMs,8504
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/whenever2.py,sha256=dzIIyKjHLvS6_GkMnU1SCZCDRZ03YvbDaECc7_Zn4k4,15706
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.131.19.dist-info/METADATA,sha256=4wWvsi5Rc-2kqwdJGksTRAULcU83WQf3u2sBx6UIL20,1585
94
- dycw_utilities-0.131.19.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
95
- dycw_utilities-0.131.19.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
96
- dycw_utilities-0.131.19.dist-info/RECORD,,
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
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.131.19"
3
+ __version__ = "0.132.0"
utilities/asyncio.py CHANGED
@@ -56,7 +56,7 @@ from utilities.types import (
56
56
  THashable,
57
57
  TSupportsRichComparison,
58
58
  )
59
- from utilities.whenever2 import SECOND, get_now
59
+ from utilities.whenever import SECOND, get_now
60
60
 
61
61
  if TYPE_CHECKING:
62
62
  from asyncio import _CoroutineLike
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
- from utilities.datetime import EnsureMonthError, ensure_month
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
- import utilities.datetime
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.datetime.Month:
188
+ ) -> utilities.whenever.Month:
189
189
  """Convert a value into the `Month` type."""
190
190
  try:
191
- return ensure_month(value)
192
- except EnsureMonthError as error:
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.whenever2 import SECOND, get_now_local
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.whenever2 import format_compact, get_now
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.whenever2 import (
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 date_deltas_whenever(
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 date_time_deltas_whenever(
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 dates_two_digit_year(
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
- py_date = draw(
298
- dates(min_value=min_value_.py_date(), max_value=max_value_.py_date())
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] = MIN_MONTH,
643
- max_value: MaybeSearchStrategy[Month] = MAX_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 datetimes with the UTC timezone."""
646
- min_value_, max_value_ = [draw2(draw, v).to_date() for v in [min_value, max_value]]
647
- date = draw(dates(min_value=min_value_, max_value=max_value_))
648
- return date_to_month(date)
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 plain_datetimes_whenever(
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 time_deltas_whenever(
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 times_whenever(
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 zoned_datetimes_whenever(
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(plain_datetimes_whenever(min_value=min_value_, max_value=max_value_))
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
- "date_deltas_whenever",
1263
- "date_time_deltas_whenever",
1264
- "dates_two_digit_year",
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
- "plain_datetimes_whenever",
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
- "time_deltas_whenever",
1298
- "times_whenever",
1293
+ "time_deltas",
1294
+ "times",
1299
1295
  "triples",
1300
1296
  "uint32s",
1301
1297
  "uint64s",
1302
1298
  "versions",
1303
- "zoned_datetimes_whenever",
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.whenever2 import (
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.whenever2 import from_timestamp
50
+ from utilities.whenever import from_timestamp
51
51
 
52
52
  if TYPE_CHECKING:
53
53
  from collections.abc import Set as AbstractSet
utilities/platform.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from pathlib import Path
5
5
  from platform import system
6
+ from re import sub
6
7
  from typing import TYPE_CHECKING, Literal, assert_never, override
7
8
 
8
9
  if TYPE_CHECKING:
@@ -69,6 +70,22 @@ MAX_PID = get_max_pid()
69
70
  ##
70
71
 
71
72
 
73
+ def get_strftime(text: str, /) -> str:
74
+ """Get a platform-specific format string."""
75
+ match SYSTEM:
76
+ case "windows": # skipif-not-windows
77
+ return text
78
+ case "mac": # skipif-not-macos
79
+ return text
80
+ case "linux": # skipif-not-linux
81
+ return sub("%Y", "%4Y", text)
82
+ case _ as never:
83
+ assert_never(never)
84
+
85
+
86
+ ##
87
+
88
+
72
89
  def maybe_yield_lower_case(text: Iterable[str], /) -> Iterator[str]:
73
90
  """Yield lower-cased text if the platform is case-insentive."""
74
91
  match SYSTEM:
@@ -94,6 +111,7 @@ __all__ = [
94
111
  "GetSystemError",
95
112
  "System",
96
113
  "get_max_pid",
114
+ "get_strftime",
97
115
  "get_system",
98
116
  "maybe_yield_lower_case",
99
117
  ]
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.whenever2 import MILLISECOND, SECOND
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.whenever2 import SECOND, get_now
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.whenever2 import format_compact, get_now
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.whenever2 import SECOND, get_now_local
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.whenever2 import MILLISECOND, SECOND
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.whenever2 import MINUTE, SECOND
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.whenever2 import SECOND
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.whenever2 import get_now_local
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.whenever2 import format_compact, get_now, to_zoned_date_time
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 TYPE_CHECKING, Any, SupportsFloat, assert_never, overload, override
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
- from utilities.datetime import maybe_sub_pct_y
22
27
  from utilities.math import sign
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
- DATE_MIN = Date.from_py_date(dt.date.min)
41
- DATE_MAX = Date.from_py_date(dt.date.max)
42
- TIME_MIN = Time.from_py_time(dt.time.min)
43
- TIME_MAX = Time.from_py_time(dt.time.max)
44
-
45
-
46
- PLAIN_DATE_TIME_MIN = PlainDateTime.from_py_datetime(dt.datetime.min) # noqa: DTZ901
47
- PLAIN_DATE_TIME_MAX = PlainDateTime.from_py_datetime(dt.datetime.max) # noqa: DTZ901
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
 
@@ -148,7 +160,7 @@ def datetime_utc(
148
160
  def format_compact(datetime: ZonedDateTime, /) -> str:
149
161
  """Convert a zoned datetime to the local time zone, then format."""
150
162
  py_datetime = datetime.round().to_tz(LOCAL_TIME_ZONE_NAME).to_plain().py_datetime()
151
- return py_datetime.strftime(maybe_sub_pct_y("%Y%m%dT%H%M%S"))
163
+ return py_datetime.strftime(get_strftime("%Y%m%dT%H%M%S"))
152
164
 
153
165
 
154
166
  ##
@@ -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,222 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import datetime as dt
4
- from dataclasses import dataclass, replace
5
- from re import search, sub
6
- from typing import Any, Self, assert_never, overload, override
7
-
8
- from utilities.iterables import OneEmptyError, one
9
- from utilities.platform import SYSTEM
10
- from utilities.types import MaybeStr
11
- from utilities.zoneinfo import UTC
12
-
13
-
14
- def date_to_month(date: dt.date, /) -> Month:
15
- """Collapse a date into a month."""
16
- return Month(year=date.year, month=date.month)
17
-
18
-
19
- ##
20
-
21
-
22
- def ensure_month(month: MonthLike, /) -> Month:
23
- """Ensure the object is a month."""
24
- if isinstance(month, Month):
25
- return month
26
- try:
27
- return parse_month(month)
28
- except ParseMonthError as error:
29
- raise EnsureMonthError(month=error.month) from None
30
-
31
-
32
- @dataclass(kw_only=True, slots=True)
33
- class EnsureMonthError(Exception):
34
- month: str
35
-
36
- @override
37
- def __str__(self) -> str:
38
- return f"Unable to ensure month; got {self.month!r}"
39
-
40
-
41
- ##
42
-
43
-
44
- def maybe_sub_pct_y(text: str, /) -> str:
45
- """Substitute the `%Y' token with '%4Y' if necessary."""
46
- match SYSTEM:
47
- case "windows": # skipif-not-windows
48
- return text
49
- case "mac": # skipif-not-macos
50
- return text
51
- case "linux": # skipif-not-linux
52
- return sub("%Y", "%4Y", text)
53
- case _ as never:
54
- assert_never(never)
55
-
56
-
57
- ##
58
-
59
-
60
- @dataclass(order=True, unsafe_hash=True, slots=True)
61
- class Month:
62
- """Represents a month in time."""
63
-
64
- year: int
65
- month: int
66
-
67
- def __post_init__(self) -> None:
68
- try:
69
- _ = dt.date(self.year, self.month, 1)
70
- except ValueError:
71
- raise MonthError(year=self.year, month=self.month) from None
72
-
73
- @override
74
- def __repr__(self) -> str:
75
- return serialize_month(self)
76
-
77
- @override
78
- def __str__(self) -> str:
79
- return repr(self)
80
-
81
- def __add__(self, other: Any, /) -> Self:
82
- if not isinstance(other, int): # pragma: no cover
83
- return NotImplemented
84
- years, month = divmod(self.month + other - 1, 12)
85
- month += 1
86
- year = self.year + years
87
- return replace(self, year=year, month=month)
88
-
89
- @overload
90
- def __sub__(self, other: Self, /) -> int: ...
91
- @overload
92
- def __sub__(self, other: int, /) -> Self: ...
93
- def __sub__(self, other: Self | int, /) -> Self | int:
94
- if isinstance(other, int): # pragma: no cover
95
- return self + (-other)
96
- if isinstance(other, type(self)):
97
- self_as_int = 12 * self.year + self.month
98
- other_as_int = 12 * other.year + other.month
99
- return self_as_int - other_as_int
100
- return NotImplemented # pragma: no cover
101
-
102
- @classmethod
103
- def from_date(cls, date: dt.date, /) -> Self:
104
- return cls(year=date.year, month=date.month)
105
-
106
- def to_date(self, /, *, day: int = 1) -> dt.date:
107
- return dt.date(self.year, self.month, day)
108
-
109
-
110
- @dataclass(kw_only=True, slots=True)
111
- class MonthError(Exception):
112
- year: int
113
- month: int
114
-
115
- @override
116
- def __str__(self) -> str:
117
- return f"Invalid year and month: {self.year}, {self.month}"
118
-
119
-
120
- type DateOrMonth = dt.date | Month
121
- type MonthLike = MaybeStr[Month]
122
- MIN_MONTH = Month(dt.date.min.year, dt.date.min.month)
123
- MAX_MONTH = Month(dt.date.max.year, dt.date.max.month)
124
-
125
-
126
- ##
127
-
128
-
129
- _TWO_DIGIT_YEAR_MIN = 1969
130
- _TWO_DIGIT_YEAR_MAX = _TWO_DIGIT_YEAR_MIN + 99
131
- MIN_DATE_TWO_DIGIT_YEAR = dt.date(
132
- _TWO_DIGIT_YEAR_MIN, dt.date.min.month, dt.date.min.day
133
- )
134
- MAX_DATE_TWO_DIGIT_YEAR = dt.date(
135
- _TWO_DIGIT_YEAR_MAX, dt.date.max.month, dt.date.max.day
136
- )
137
-
138
-
139
- def parse_two_digit_year(year: int | str, /) -> int:
140
- """Parse a 2-digit year into a year."""
141
- match year:
142
- case int():
143
- years = range(_TWO_DIGIT_YEAR_MIN, _TWO_DIGIT_YEAR_MAX + 1)
144
- try:
145
- return one(y for y in years if y % 100 == year)
146
- except OneEmptyError:
147
- raise _ParseTwoDigitYearInvalidIntegerError(year=year) from None
148
- case str():
149
- if search(r"^\d{1,2}$", year):
150
- return parse_two_digit_year(int(year))
151
- raise _ParseTwoDigitYearInvalidStringError(year=year)
152
- case _ as never:
153
- assert_never(never)
154
-
155
-
156
- @dataclass(kw_only=True, slots=True)
157
- class ParseTwoDigitYearError(Exception):
158
- year: int | str
159
-
160
-
161
- @dataclass(kw_only=True, slots=True)
162
- class _ParseTwoDigitYearInvalidIntegerError(Exception):
163
- year: int | str
164
-
165
- @override
166
- def __str__(self) -> str:
167
- return f"Unable to parse year; got {self.year!r}"
168
-
169
-
170
- @dataclass(kw_only=True, slots=True)
171
- class _ParseTwoDigitYearInvalidStringError(Exception):
172
- year: int | str
173
-
174
- @override
175
- def __str__(self) -> str:
176
- return f"Unable to parse year; got {self.year!r}"
177
-
178
-
179
- ##
180
-
181
-
182
- def serialize_month(month: Month, /) -> str:
183
- """Serialize a month."""
184
- return f"{month.year:04}-{month.month:02}"
185
-
186
-
187
- def parse_month(month: str, /) -> Month:
188
- """Parse a string into a month."""
189
- for fmt in ["%Y-%m", "%Y%m", "%Y %m"]:
190
- try:
191
- date = dt.datetime.strptime(month, fmt).replace(tzinfo=UTC).date()
192
- except ValueError:
193
- pass
194
- else:
195
- return Month(date.year, date.month)
196
- raise ParseMonthError(month=month)
197
-
198
-
199
- @dataclass(kw_only=True, slots=True)
200
- class ParseMonthError(Exception):
201
- month: str
202
-
203
- @override
204
- def __str__(self) -> str:
205
- return f"Unable to parse month; got {self.month!r}"
206
-
207
-
208
- __all__ = [
209
- "MAX_DATE_TWO_DIGIT_YEAR",
210
- "MAX_MONTH",
211
- "MIN_DATE_TWO_DIGIT_YEAR",
212
- "MIN_MONTH",
213
- "DateOrMonth",
214
- "EnsureMonthError",
215
- "Month",
216
- "MonthError",
217
- "MonthLike",
218
- "ParseMonthError",
219
- "date_to_month",
220
- "ensure_month",
221
- "parse_two_digit_year",
222
- ]