dateutilx 0.2.0__tar.gz → 0.4.0__tar.gz
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.
- {dateutilx-0.2.0 → dateutilx-0.4.0}/.gitignore +0 -3
- {dateutilx-0.2.0 → dateutilx-0.4.0}/PKG-INFO +2 -2
- {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/__init__.py +1 -1
- {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/parser/_parser.py +19 -19
- {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/parser/isoparser.py +30 -19
- {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/relativedelta.py +55 -37
- {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/rrule.py +25 -27
- {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/tz/__init__.py +5 -14
- dateutilx-0.4.0/dateutilx/tz/_factories.py +61 -0
- {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/tz/_win.py +5 -5
- {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/tz/tz.py +31 -28
- {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/tz/win.py +2 -5
- dateutilx-0.4.0/dateutilx/utils/__init__.py +10 -0
- dateutilx-0.2.0/src/utils.py → dateutilx-0.4.0/dateutilx/utils/funcs.py +7 -9
- {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx/utils}/weekday.py +6 -7
- {dateutilx-0.2.0 → dateutilx-0.4.0}/pyproject.toml +3 -3
- dateutilx-0.2.0/src/helper/__init__.py +0 -4
- dateutilx-0.2.0/src/helper/funcs.py +0 -5
- dateutilx-0.2.0/src/tz/_factories.py +0 -67
- dateutilx-0.2.0/src/tzwin.py +0 -4
- {dateutilx-0.2.0 → dateutilx-0.4.0}/.base_project/LICENSE +0 -0
- {dateutilx-0.2.0 → dateutilx-0.4.0}/LICENSE +0 -0
- {dateutilx-0.2.0 → dateutilx-0.4.0}/README.md +0 -0
- {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/easter.py +0 -0
- {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/errors.py +0 -0
- {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/info.py +0 -0
- {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/parser/__init__.py +0 -0
- {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/parser/utils.py +0 -0
- {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/tz/_common.py +0 -0
- {dateutilx-0.2.0/src/helper → dateutilx-0.4.0/dateutilx/utils}/constants.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: dateutilx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Modern extension to the standard Python datetime module
|
|
5
5
|
Project-URL: Homepage, https://github.com/rsb-23/dateutilx
|
|
6
6
|
Project-URL: Documentation, https://dateutilx.readthedocs.io/en/stable/
|
|
@@ -13,7 +13,7 @@ def __getattr__(name):
|
|
|
13
13
|
|
|
14
14
|
if name in __all__:
|
|
15
15
|
return importlib.import_module(f".{name}", __name__)
|
|
16
|
-
raise AttributeError(f"module {__name__!r} has
|
|
16
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def __dir__():
|
|
@@ -39,8 +39,8 @@ from decimal import Decimal
|
|
|
39
39
|
from io import StringIO
|
|
40
40
|
from warnings import warn
|
|
41
41
|
|
|
42
|
-
from
|
|
43
|
-
from
|
|
42
|
+
from dateutilx import relativedelta, tz
|
|
43
|
+
from dateutilx.errors import ParserError, UnknownTimezoneWarning
|
|
44
44
|
|
|
45
45
|
from .utils import standard_dt_parser
|
|
46
46
|
|
|
@@ -391,15 +391,16 @@ class _YMD(list):
|
|
|
391
391
|
return 1 <= value <= monthrange(year, month)[1]
|
|
392
392
|
|
|
393
393
|
def append(self, val, label=None):
|
|
394
|
+
_valid_labels = (None, "Y")
|
|
394
395
|
if hasattr(val, "__len__"):
|
|
395
396
|
if val.isdigit() and len(val) > 2:
|
|
396
397
|
self.century_specified = True
|
|
397
|
-
if label not in
|
|
398
|
+
if label not in _valid_labels: # pragma: no cover
|
|
398
399
|
raise ValueError(label)
|
|
399
400
|
label = "Y"
|
|
400
401
|
elif val > 100:
|
|
401
402
|
self.century_specified = True
|
|
402
|
-
if label not in
|
|
403
|
+
if label not in _valid_labels: # pragma: no cover
|
|
403
404
|
raise ValueError(label)
|
|
404
405
|
label = "Y"
|
|
405
406
|
|
|
@@ -426,7 +427,7 @@ class _YMD(list):
|
|
|
426
427
|
if len(self) == 3 and len(strids) == 2:
|
|
427
428
|
# we can back out the remaining stridx value
|
|
428
429
|
missing = [x for x in range(3) if x not in strids.values()]
|
|
429
|
-
key = [x for x in
|
|
430
|
+
key = [x for x in ("y", "m", "d") if x not in strids]
|
|
430
431
|
assert len(missing) == len(key) == 1
|
|
431
432
|
key = key[0]
|
|
432
433
|
val = missing[0]
|
|
@@ -562,8 +563,8 @@ class Parser:
|
|
|
562
563
|
.. doctest::
|
|
563
564
|
:options: +NORMALIZE_WHITESPACE
|
|
564
565
|
|
|
565
|
-
>>> from
|
|
566
|
-
>>> from
|
|
566
|
+
>>> from dateutilx.parser import parse
|
|
567
|
+
>>> from dateutilx.tz import gettz
|
|
567
568
|
>>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")}
|
|
568
569
|
>>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos)
|
|
569
570
|
datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200))
|
|
@@ -667,7 +668,7 @@ class Parser:
|
|
|
667
668
|
|
|
668
669
|
.. doctest::
|
|
669
670
|
|
|
670
|
-
>>> from
|
|
671
|
+
>>> from dateutilx.parser import parse
|
|
671
672
|
>>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True)
|
|
672
673
|
(datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at '))
|
|
673
674
|
|
|
@@ -1022,8 +1023,7 @@ class Parser:
|
|
|
1022
1023
|
def _could_be_tzname(self, hour, tzname, tzoffset, token):
|
|
1023
1024
|
return (
|
|
1024
1025
|
hour is not None
|
|
1025
|
-
and tzname is None
|
|
1026
|
-
and tzoffset is None
|
|
1026
|
+
and tzname is tzoffset is None
|
|
1027
1027
|
and len(token) <= 5
|
|
1028
1028
|
and (all(x in string.ascii_uppercase for x in token) or token in self.info.UTCZONE)
|
|
1029
1029
|
)
|
|
@@ -1132,9 +1132,9 @@ class Parser:
|
|
|
1132
1132
|
if isinstance(tzdata, datetime.tzinfo) or tzdata is None:
|
|
1133
1133
|
tzinfo = tzdata
|
|
1134
1134
|
elif isinstance(tzdata, str):
|
|
1135
|
-
tzinfo = tz.
|
|
1135
|
+
tzinfo = tz.TzStr(tzdata)
|
|
1136
1136
|
elif isinstance(tzdata, int):
|
|
1137
|
-
tzinfo = tz.
|
|
1137
|
+
tzinfo = tz.TzOffset(tzname, tzdata)
|
|
1138
1138
|
else:
|
|
1139
1139
|
raise TypeError("Offset must be tzinfo subclass, tz string, or int offset.")
|
|
1140
1140
|
return tzinfo
|
|
@@ -1146,7 +1146,7 @@ class Parser:
|
|
|
1146
1146
|
aware = self._assign_tzname(aware, res.tzname)
|
|
1147
1147
|
|
|
1148
1148
|
elif res.tzname and res.tzname in time.tzname:
|
|
1149
|
-
aware = naive.replace(tzinfo=tz.
|
|
1149
|
+
aware = naive.replace(tzinfo=tz.TzLocal())
|
|
1150
1150
|
|
|
1151
1151
|
# Handle ambiguous local datetime
|
|
1152
1152
|
aware = self._assign_tzname(aware, res.tzname)
|
|
@@ -1159,7 +1159,7 @@ class Parser:
|
|
|
1159
1159
|
aware = naive.replace(tzinfo=tz.UTC)
|
|
1160
1160
|
|
|
1161
1161
|
elif res.tzoffset:
|
|
1162
|
-
aware = naive.replace(tzinfo=tz.
|
|
1162
|
+
aware = naive.replace(tzinfo=tz.TzOffset(res.tzname, res.tzoffset))
|
|
1163
1163
|
|
|
1164
1164
|
elif not res.tzname:
|
|
1165
1165
|
# i.e. no timezone information was found.
|
|
@@ -1268,8 +1268,8 @@ def parse(timestr, parser_info=None, **kwargs):
|
|
|
1268
1268
|
.. doctest::
|
|
1269
1269
|
:options: +NORMALIZE_WHITESPACE
|
|
1270
1270
|
|
|
1271
|
-
>>> from
|
|
1272
|
-
>>> from
|
|
1271
|
+
>>> from dateutilx.parser import parse
|
|
1272
|
+
>>> from dateutilx.tz import gettz
|
|
1273
1273
|
>>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")}
|
|
1274
1274
|
>>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos)
|
|
1275
1275
|
datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200))
|
|
@@ -1305,7 +1305,7 @@ def parse(timestr, parser_info=None, **kwargs):
|
|
|
1305
1305
|
|
|
1306
1306
|
.. doctest::
|
|
1307
1307
|
|
|
1308
|
-
>>> from
|
|
1308
|
+
>>> from dateutilx.parser import parse
|
|
1309
1309
|
>>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True)
|
|
1310
1310
|
(datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at '))
|
|
1311
1311
|
|
|
@@ -1370,7 +1370,7 @@ class _TzParser:
|
|
|
1370
1370
|
|
|
1371
1371
|
used_idxs.extend([*range(j)])
|
|
1372
1372
|
i = j
|
|
1373
|
-
if i < len_l and (_tmp_list[i] in ("+", "-") or _tmp_list[i][0] in
|
|
1373
|
+
if i < len_l and (_tmp_list[i] in ("+", "-") or _tmp_list[i][0] in string.digits):
|
|
1374
1374
|
if _tmp_list[i] in ("+", "-"):
|
|
1375
1375
|
# Yes, that's right. See the TZ variable documentation.
|
|
1376
1376
|
signal = (1, -1)[_tmp_list[i] == "+"]
|
|
@@ -1462,7 +1462,7 @@ class _TzParser:
|
|
|
1462
1462
|
for x in _tmp_list[i:]
|
|
1463
1463
|
if x not in (",", "/", "J", "M", ".", "-", ":")
|
|
1464
1464
|
for y in x
|
|
1465
|
-
if y not in
|
|
1465
|
+
if y not in string.digits
|
|
1466
1466
|
]
|
|
1467
1467
|
):
|
|
1468
1468
|
for x in (res.start, res.end):
|
|
@@ -10,9 +10,10 @@ ISO-8601 specification.
|
|
|
10
10
|
import calendar
|
|
11
11
|
import datetime as dt
|
|
12
12
|
import re
|
|
13
|
+
import string
|
|
13
14
|
from functools import wraps
|
|
14
15
|
|
|
15
|
-
from
|
|
16
|
+
from dateutilx.tz import UTC, TzOffset
|
|
16
17
|
|
|
17
18
|
__all__ = ["isoparse", "isoparser"]
|
|
18
19
|
|
|
@@ -49,7 +50,7 @@ class IsoParser:
|
|
|
49
50
|
For strict ISO-8601 adherence, pass ``'T'``.
|
|
50
51
|
"""
|
|
51
52
|
if sep is not None:
|
|
52
|
-
if len(sep) != 1 or ord(sep) >= 128 or sep in
|
|
53
|
+
if len(sep) != 1 or ord(sep) >= 128 or sep in string.digits:
|
|
53
54
|
raise ValueError("Separator must be a single, non-numeric " + "ASCII character")
|
|
54
55
|
|
|
55
56
|
sep = sep.encode("ascii")
|
|
@@ -107,10 +108,10 @@ class IsoParser:
|
|
|
107
108
|
- `±HHMM`
|
|
108
109
|
- `±HH`
|
|
109
110
|
|
|
110
|
-
Offsets will be represented as :class:`dateutilx.tz.
|
|
111
|
+
Offsets will be represented as :class:`dateutilx.tz.TzOffset` objects,
|
|
111
112
|
with the exception of UTC, which will be represented as
|
|
112
|
-
:class:`dateutilx.tz.
|
|
113
|
-
as `+00:00`) will also be represented as :class:`dateutilx.tz.
|
|
113
|
+
:class:`dateutilx.tz.TzUTC`. Time zone offsets equivalent to UTC (such
|
|
114
|
+
as `+00:00`) will also be represented as :class:`dateutilx.tz.TzUTC`.
|
|
114
115
|
|
|
115
116
|
:param dt_str:
|
|
116
117
|
A string or stream containing only an ISO-8601 datetime string
|
|
@@ -188,11 +189,11 @@ class IsoParser:
|
|
|
188
189
|
A string representing an ISO time zone offset
|
|
189
190
|
|
|
190
191
|
:param zero_as_utc:
|
|
191
|
-
Whether to return :class:`dateutilx.tz.
|
|
192
|
+
Whether to return :class:`dateutilx.tz.TzUTC` for zero-offset zones
|
|
192
193
|
|
|
193
194
|
:return:
|
|
194
|
-
Returns :class:`dateutilx.tz.
|
|
195
|
-
:class:`dateutilx.tz.
|
|
195
|
+
Returns :class:`dateutilx.tz.TzOffset` for offsets and
|
|
196
|
+
:class:`dateutilx.tz.TzUTC` for ``Z`` and (if ``zero_as_utc`` is
|
|
196
197
|
specified) offsets equivalent to UTC.
|
|
197
198
|
"""
|
|
198
199
|
return self._parse_tzstr(tzstr, zero_as_utc=zero_as_utc)
|
|
@@ -202,6 +203,13 @@ class IsoParser:
|
|
|
202
203
|
_TIME_SEP = b":"
|
|
203
204
|
_FRACTION_REGEX = re.compile(b"[.,]([0-9]+)")
|
|
204
205
|
|
|
206
|
+
def _parse_digits(self, s, pos, n):
|
|
207
|
+
"""Parse exactly ``n`` ASCII digits starting at ``pos``."""
|
|
208
|
+
digits = s[pos : pos + n]
|
|
209
|
+
if len(digits) != n or not digits.isdigit():
|
|
210
|
+
raise ValueError("Expected {} digits, got {!r}".format(n, digits.decode("ascii")))
|
|
211
|
+
return int(digits)
|
|
212
|
+
|
|
205
213
|
def _parse_isodate(self, dt_str: str) -> tuple:
|
|
206
214
|
if len(dt_str) < 4:
|
|
207
215
|
raise ValueError("ISO string too short")
|
|
@@ -216,7 +224,7 @@ class IsoParser:
|
|
|
216
224
|
components = [1, 1, 1]
|
|
217
225
|
|
|
218
226
|
# Year
|
|
219
|
-
components[0] =
|
|
227
|
+
components[0] = self._parse_digits(dt_str, 0, 4)
|
|
220
228
|
pos = 4
|
|
221
229
|
if pos >= len_str:
|
|
222
230
|
return components, pos
|
|
@@ -229,7 +237,7 @@ class IsoParser:
|
|
|
229
237
|
if len_str - pos < 2:
|
|
230
238
|
raise ValueError("Invalid common month")
|
|
231
239
|
|
|
232
|
-
components[1] =
|
|
240
|
+
components[1] = self._parse_digits(dt_str, pos, 2)
|
|
233
241
|
pos += 2
|
|
234
242
|
|
|
235
243
|
if pos >= len_str:
|
|
@@ -245,12 +253,12 @@ class IsoParser:
|
|
|
245
253
|
# Day
|
|
246
254
|
if len_str - pos < 2:
|
|
247
255
|
raise ValueError("Invalid common day")
|
|
248
|
-
components[2] =
|
|
256
|
+
components[2] = self._parse_digits(dt_str, pos, 2)
|
|
249
257
|
return components, pos + 2
|
|
250
258
|
|
|
251
259
|
def _parse_isodate_uncommon(self, dt_str: str) -> tuple:
|
|
252
260
|
# All ISO formats start with the year
|
|
253
|
-
year =
|
|
261
|
+
year = self._parse_digits(dt_str, 0, 4)
|
|
254
262
|
|
|
255
263
|
has_sep = dt_str[4:5] == self._DATE_SEP
|
|
256
264
|
|
|
@@ -258,7 +266,7 @@ class IsoParser:
|
|
|
258
266
|
if dt_str[pos : pos + 1] == b"W":
|
|
259
267
|
# YYYY-?Www-?D?
|
|
260
268
|
pos += 1
|
|
261
|
-
weekno =
|
|
269
|
+
weekno = self._parse_digits(dt_str, pos, 2)
|
|
262
270
|
pos += 2
|
|
263
271
|
|
|
264
272
|
dayno = 1
|
|
@@ -268,7 +276,7 @@ class IsoParser:
|
|
|
268
276
|
|
|
269
277
|
pos += has_sep
|
|
270
278
|
|
|
271
|
-
dayno =
|
|
279
|
+
dayno = self._parse_digits(dt_str, pos, 1)
|
|
272
280
|
pos += 1
|
|
273
281
|
|
|
274
282
|
base_date = self._calculate_weekdate(year, weekno, dayno)
|
|
@@ -277,7 +285,7 @@ class IsoParser:
|
|
|
277
285
|
if len(dt_str) - pos < 3:
|
|
278
286
|
raise ValueError("Invalid ordinal day")
|
|
279
287
|
|
|
280
|
-
ordinal_day =
|
|
288
|
+
ordinal_day = self._parse_digits(dt_str, pos, 3)
|
|
281
289
|
pos += 3
|
|
282
290
|
|
|
283
291
|
if ordinal_day < 1 or ordinal_day > (365 + calendar.isleap(year)):
|
|
@@ -347,7 +355,7 @@ class IsoParser:
|
|
|
347
355
|
|
|
348
356
|
if comp < 3:
|
|
349
357
|
# Hour, minute, second
|
|
350
|
-
components[comp] =
|
|
358
|
+
components[comp] = self._parse_digits(timestr, pos, 2)
|
|
351
359
|
pos += 2
|
|
352
360
|
|
|
353
361
|
if comp == 3:
|
|
@@ -383,13 +391,16 @@ class IsoParser:
|
|
|
383
391
|
else:
|
|
384
392
|
raise ValueError("Time zone offset requires sign")
|
|
385
393
|
|
|
386
|
-
hours =
|
|
394
|
+
hours = self._parse_digits(tzstr, 1, 2)
|
|
387
395
|
if len(tzstr) == 3:
|
|
388
396
|
minutes = 0
|
|
389
397
|
else:
|
|
390
|
-
|
|
398
|
+
pos = 4 if tzstr[3:4] == self._TIME_SEP else 3
|
|
399
|
+
if len(tzstr) != pos + 2:
|
|
400
|
+
raise ValueError("Invalid minutes in time zone offset")
|
|
401
|
+
minutes = self._parse_digits(tzstr, pos, 2)
|
|
391
402
|
|
|
392
|
-
if zero_as_utc and hours ==
|
|
403
|
+
if zero_as_utc and hours == minutes == 0:
|
|
393
404
|
return UTC
|
|
394
405
|
|
|
395
406
|
if minutes > 59:
|
|
@@ -3,18 +3,13 @@ import datetime as dt
|
|
|
3
3
|
import operator
|
|
4
4
|
from math import copysign
|
|
5
5
|
from types import NotImplementedType
|
|
6
|
-
from typing import Self
|
|
6
|
+
from typing import Self
|
|
7
7
|
from warnings import warn
|
|
8
8
|
|
|
9
|
-
from
|
|
9
|
+
from dateutilx.utils import Weekday, weekdays
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
__all__ = ["RelativeDelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
|
|
14
|
-
Number: TypeAlias = float | int
|
|
15
|
-
|
|
16
|
-
REL_FIELDS = "years", "months", "days", "hours", "minutes", "seconds", "microseconds"
|
|
17
|
-
ABS_FIELDS = "year", "month", "day", "hour", "minute", "second", "microsecond"
|
|
11
|
+
__all__ = ["RelativeDelta"]
|
|
12
|
+
Number = float | int
|
|
18
13
|
|
|
19
14
|
|
|
20
15
|
class RelativeDelta:
|
|
@@ -91,7 +86,7 @@ class RelativeDelta:
|
|
|
91
86
|
For example
|
|
92
87
|
|
|
93
88
|
>>> from datetime import datetime
|
|
94
|
-
>>> from
|
|
89
|
+
>>> from dateutilx.relativedelta import RelativeDelta, MO
|
|
95
90
|
>>> dt_ = datetime(2018, 4, 9, 13, 37, 0)
|
|
96
91
|
>>> delta = RelativeDelta(hours=25, day=1, weekday=MO(1))
|
|
97
92
|
>>> dt_ + delta
|
|
@@ -217,25 +212,48 @@ class RelativeDelta:
|
|
|
217
212
|
|
|
218
213
|
yday = nlyearday or yearday
|
|
219
214
|
|
|
220
|
-
if yearday and not nlyearday and yearday
|
|
215
|
+
if yearday and not nlyearday and (59 < yearday < 366):
|
|
216
|
+
# Shifting for leapday except Dec 32/31
|
|
221
217
|
self.leapdays = -1
|
|
222
218
|
|
|
223
219
|
if yday:
|
|
224
220
|
if not 1 <= yday <= 366:
|
|
225
221
|
raise ValueError(f"invalid year day ({yday})")
|
|
226
222
|
|
|
227
|
-
resolved = dt.date(2001, 1, 1) + dt.timedelta(days=yday - 1)
|
|
223
|
+
resolved = dt.date(2001, 1, 1) + dt.timedelta(days=min(yday, 365) - 1)
|
|
228
224
|
self.day, self.month = resolved.day, resolved.month
|
|
229
225
|
|
|
226
|
+
def _absolutes(self) -> dict[str, Number]:
|
|
227
|
+
return {
|
|
228
|
+
"year": self.year,
|
|
229
|
+
"month": self.month,
|
|
230
|
+
"day": self.day,
|
|
231
|
+
"hour": self.hour,
|
|
232
|
+
"minute": self.minute,
|
|
233
|
+
"second": self.second,
|
|
234
|
+
"microsecond": self.microsecond,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
def _relatives(self) -> dict[str, Number]:
|
|
238
|
+
return {
|
|
239
|
+
"years": self.years,
|
|
240
|
+
"months": self.months,
|
|
241
|
+
"days": self.days,
|
|
242
|
+
"hours": self.hours,
|
|
243
|
+
"minutes": self.minutes,
|
|
244
|
+
"seconds": self.seconds,
|
|
245
|
+
"microseconds": self.microseconds,
|
|
246
|
+
}
|
|
247
|
+
|
|
230
248
|
def _reset(self):
|
|
231
249
|
self.leapdays = 0
|
|
232
250
|
self.weekday = None
|
|
233
251
|
self._has_time = 0
|
|
234
252
|
|
|
235
|
-
for attr in
|
|
253
|
+
for attr in ("years", "months", "days", "hours", "minutes", "seconds", "microseconds"):
|
|
236
254
|
setattr(self, attr, 0)
|
|
237
255
|
|
|
238
|
-
for attr in
|
|
256
|
+
for attr in ("year", "month", "day", "hour", "minute", "second", "microsecond"):
|
|
239
257
|
setattr(self, attr, None)
|
|
240
258
|
|
|
241
259
|
def _fix(self):
|
|
@@ -295,8 +313,8 @@ class RelativeDelta:
|
|
|
295
313
|
|
|
296
314
|
def _make_relativedelta(self, **kwargs) -> Self:
|
|
297
315
|
"""Factory method to create a new RelativeDelta with given overrides."""
|
|
298
|
-
rel_fields = {field: kwargs.get(field,
|
|
299
|
-
abs_fields = {field: kwargs.get(field,
|
|
316
|
+
rel_fields = {field: kwargs.get(field, val) for field, val in self._relatives().items()}
|
|
317
|
+
abs_fields = {field: kwargs.get(field, val) for field, val in self._absolutes().items()}
|
|
300
318
|
return self.__class__(
|
|
301
319
|
**rel_fields,
|
|
302
320
|
**abs_fields,
|
|
@@ -337,8 +355,8 @@ class RelativeDelta:
|
|
|
337
355
|
def __add__(self, other):
|
|
338
356
|
if isinstance(other, RelativeDelta):
|
|
339
357
|
kwargs = {
|
|
340
|
-
field:
|
|
341
|
-
for field in
|
|
358
|
+
field: val if getattr(other, field) is None else getattr(other, field)
|
|
359
|
+
for field, val in self._absolutes().items()
|
|
342
360
|
}
|
|
343
361
|
return self._make_relativedelta(
|
|
344
362
|
years=other.years + self.years,
|
|
@@ -376,7 +394,7 @@ class RelativeDelta:
|
|
|
376
394
|
month += 12
|
|
377
395
|
day = min(calendar.monthrange(year, month)[1], self.day or other.day)
|
|
378
396
|
repl = {"year": year, "month": month, "day": day}
|
|
379
|
-
for attr in
|
|
397
|
+
for attr in ("hour", "minute", "second", "microsecond"):
|
|
380
398
|
value = getattr(self, attr)
|
|
381
399
|
if value is not None:
|
|
382
400
|
repl[attr] = value
|
|
@@ -407,10 +425,7 @@ class RelativeDelta:
|
|
|
407
425
|
if not isinstance(other, RelativeDelta):
|
|
408
426
|
return NotImplemented # In case the other object defines __rsub__
|
|
409
427
|
|
|
410
|
-
kwargs = {
|
|
411
|
-
field: getattr(other, field) if getattr(self, field) is None else getattr(self, field)
|
|
412
|
-
for field in ABS_FIELDS
|
|
413
|
-
}
|
|
428
|
+
kwargs = {field: getattr(other, field) if val is None else val for field, val in self._absolutes().items()}
|
|
414
429
|
return self._make_relativedelta(
|
|
415
430
|
years=self.years - other.years,
|
|
416
431
|
months=self.months - other.months,
|
|
@@ -424,25 +439,28 @@ class RelativeDelta:
|
|
|
424
439
|
)
|
|
425
440
|
|
|
426
441
|
def __abs__(self):
|
|
427
|
-
fields = {field: abs(
|
|
442
|
+
fields = {field: abs(val) for field, val in self._relatives().items()}
|
|
428
443
|
return self._make_relativedelta(**fields)
|
|
429
444
|
|
|
430
445
|
def __neg__(self):
|
|
431
|
-
fields = {field: -
|
|
446
|
+
fields = {field: -val for field, val in self._relatives().items()}
|
|
432
447
|
return self._make_relativedelta(**fields)
|
|
433
448
|
|
|
434
|
-
def __bool__(self):
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
449
|
+
def __bool__(self) -> bool:
|
|
450
|
+
return (
|
|
451
|
+
any(value for value in self._relatives().values())
|
|
452
|
+
or bool(self.leapdays)
|
|
453
|
+
or bool(self.weekday)
|
|
454
|
+
or any(value is not None for value in self._absolutes().values())
|
|
455
|
+
)
|
|
438
456
|
|
|
439
|
-
def __mul__(self, other) ->
|
|
457
|
+
def __mul__(self, other) -> Self | NotImplementedType:
|
|
440
458
|
try:
|
|
441
459
|
f = float(other)
|
|
442
460
|
except TypeError:
|
|
443
461
|
return NotImplemented
|
|
444
462
|
|
|
445
|
-
fields = {field: int(
|
|
463
|
+
fields = {field: int(val * f) for field, val in self._relatives().items()}
|
|
446
464
|
return self._make_relativedelta(**fields)
|
|
447
465
|
|
|
448
466
|
__rmul__ = __mul__
|
|
@@ -463,8 +481,10 @@ class RelativeDelta:
|
|
|
463
481
|
return False
|
|
464
482
|
|
|
465
483
|
return (
|
|
466
|
-
all((
|
|
484
|
+
all((a == b for a, b in zip(self._relatives().values(), other._relatives().values())))
|
|
485
|
+
and all((a == b for a, b in zip(self._absolutes().values(), other._absolutes().values())))
|
|
467
486
|
and self.leapdays == other.leapdays
|
|
487
|
+
and self.weekday == other.weekday
|
|
468
488
|
)
|
|
469
489
|
|
|
470
490
|
def __hash__(self):
|
|
@@ -483,7 +503,7 @@ class RelativeDelta:
|
|
|
483
503
|
return NotImplemented
|
|
484
504
|
return not self.__eq__(other)
|
|
485
505
|
|
|
486
|
-
def
|
|
506
|
+
def __truediv__(self, other) -> NotImplementedType | Self:
|
|
487
507
|
try:
|
|
488
508
|
reciprocal = 1 / float(other)
|
|
489
509
|
except TypeError:
|
|
@@ -491,15 +511,13 @@ class RelativeDelta:
|
|
|
491
511
|
|
|
492
512
|
return self.__mul__(reciprocal)
|
|
493
513
|
|
|
494
|
-
__truediv__ = __div__
|
|
495
|
-
|
|
496
514
|
def __repr__(self):
|
|
497
515
|
_tmp_list = []
|
|
498
|
-
for attr in
|
|
516
|
+
for attr in ("years", "months", "days", "leapdays", "hours", "minutes", "seconds", "microseconds"):
|
|
499
517
|
value = getattr(self, attr)
|
|
500
518
|
if value:
|
|
501
519
|
_tmp_list.append(f"{attr}={value:+g}")
|
|
502
|
-
for attr in
|
|
520
|
+
for attr in ("year", "month", "day", "weekday", "hour", "minute", "second", "microsecond"):
|
|
503
521
|
value = getattr(self, attr)
|
|
504
522
|
if value is not None:
|
|
505
523
|
_tmp_list.append(f"{attr}={value!r}")
|
|
@@ -6,7 +6,6 @@ the recurrence rules documented in the
|
|
|
6
6
|
including support for caching of results.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
import _thread
|
|
10
9
|
import calendar
|
|
11
10
|
import contextlib
|
|
12
11
|
import datetime as dt
|
|
@@ -15,14 +14,14 @@ import itertools
|
|
|
15
14
|
import operator
|
|
16
15
|
import re
|
|
17
16
|
import sys
|
|
17
|
+
import threading
|
|
18
18
|
from functools import wraps
|
|
19
19
|
from itertools import product
|
|
20
20
|
from math import gcd
|
|
21
21
|
from typing import Any
|
|
22
22
|
from warnings import warn
|
|
23
23
|
|
|
24
|
-
from .
|
|
25
|
-
from .weekday import Weekday, weekdays
|
|
24
|
+
from dateutilx.utils import Day, Frequency, Weekday, weekdays
|
|
26
25
|
|
|
27
26
|
# fmt: off
|
|
28
27
|
__all__ = [
|
|
@@ -79,7 +78,7 @@ class RruleBase:
|
|
|
79
78
|
def __init__(self, cache=False):
|
|
80
79
|
if cache:
|
|
81
80
|
self._cache = []
|
|
82
|
-
self._cache_lock =
|
|
81
|
+
self._cache_lock = threading.Lock()
|
|
83
82
|
self._invalidate_cache()
|
|
84
83
|
else:
|
|
85
84
|
self._cache = None
|
|
@@ -107,21 +106,20 @@ class RruleBase:
|
|
|
107
106
|
i = 0
|
|
108
107
|
gen = self._cache_gen
|
|
109
108
|
cache = self._cache
|
|
110
|
-
|
|
111
|
-
release = self._cache_lock.release
|
|
109
|
+
|
|
112
110
|
while gen:
|
|
113
111
|
if i == len(cache):
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
112
|
+
with self._cache_lock:
|
|
113
|
+
if self._cache_complete:
|
|
114
|
+
break
|
|
115
|
+
try:
|
|
116
|
+
for _ in range(10):
|
|
117
|
+
cache.append(next(gen))
|
|
118
|
+
except StopIteration:
|
|
119
|
+
self._cache_gen = gen = None
|
|
120
|
+
self._cache_complete = True
|
|
121
|
+
break
|
|
122
|
+
|
|
125
123
|
yield cache[i]
|
|
126
124
|
i += 1
|
|
127
125
|
while i < self._len:
|
|
@@ -270,7 +268,7 @@ class Rrule(RruleBase):
|
|
|
270
268
|
This can lead to possibly surprising behavior when, for example, the
|
|
271
269
|
start date occurs at the end of the month:
|
|
272
270
|
|
|
273
|
-
>>> from
|
|
271
|
+
>>> from dateutilx.rrule import rrule, MONTHLY
|
|
274
272
|
>>> from datetime import datetime
|
|
275
273
|
>>> start_date = datetime(2014, 12, 31)
|
|
276
274
|
>>> list(rrule(freq=MONTHLY, count=4, dtstart=start_date))
|
|
@@ -460,7 +458,7 @@ class Rrule(RruleBase):
|
|
|
460
458
|
if self._bysetpos:
|
|
461
459
|
self._original_rule["bysetpos"] = self._bysetpos
|
|
462
460
|
|
|
463
|
-
if
|
|
461
|
+
if all(x is None for x in (byweekno, byyearday, bymonthday, byweekday, byeaster)):
|
|
464
462
|
if freq == YEARLY:
|
|
465
463
|
if bymonth is None:
|
|
466
464
|
bymonth = dtstart.month
|
|
@@ -498,7 +496,7 @@ class Rrule(RruleBase):
|
|
|
498
496
|
self._byeaster = None
|
|
499
497
|
if byeaster is not None:
|
|
500
498
|
if not easter:
|
|
501
|
-
from
|
|
499
|
+
from dateutilx import easter
|
|
502
500
|
if isinstance(byeaster, int):
|
|
503
501
|
self._byeaster = (byeaster,)
|
|
504
502
|
else:
|
|
@@ -661,7 +659,7 @@ class Rrule(RruleBase):
|
|
|
661
659
|
original_rule = self._original_rule
|
|
662
660
|
|
|
663
661
|
partfmt = "{name}={vals}"
|
|
664
|
-
for name, key in
|
|
662
|
+
for name, key in (
|
|
665
663
|
("BYSETPOS", "bysetpos"),
|
|
666
664
|
("BYMONTH", "bymonth"),
|
|
667
665
|
("BYMONTHDAY", "bymonthday"),
|
|
@@ -672,7 +670,7 @@ class Rrule(RruleBase):
|
|
|
672
670
|
("BYMINUTE", "byminute"),
|
|
673
671
|
("BYSECOND", "bysecond"),
|
|
674
672
|
("BYEASTER", "byeaster"),
|
|
675
|
-
|
|
673
|
+
):
|
|
676
674
|
value = original_rule.get(key)
|
|
677
675
|
if value:
|
|
678
676
|
parts.append(partfmt.format(name=name, vals=(",".join(str(v) for v in value))))
|
|
@@ -1001,7 +999,7 @@ class Rrule(RruleBase):
|
|
|
1001
999
|
|
|
1002
1000
|
return cset
|
|
1003
1001
|
|
|
1004
|
-
def __mod_distance(self, value, byxxx, base):
|
|
1002
|
+
def __mod_distance(self, value, byxxx, base) -> tuple[int, int]:
|
|
1005
1003
|
"""
|
|
1006
1004
|
Calculates the next value in a sequence where the `FREQ` parameter is
|
|
1007
1005
|
specified along with a `BYXXX` parameter at the same "level"
|
|
@@ -1032,7 +1030,7 @@ class Rrule(RruleBase):
|
|
|
1032
1030
|
accumulator += div
|
|
1033
1031
|
if value in byxxx:
|
|
1034
1032
|
return accumulator, value
|
|
1035
|
-
|
|
1033
|
+
raise ValueError("No valid values found")
|
|
1036
1034
|
|
|
1037
1035
|
|
|
1038
1036
|
# pylint: disable=E0203
|
|
@@ -1403,7 +1401,7 @@ class _RRuleStr:
|
|
|
1403
1401
|
def _handle_until(rrkwargs, name, value, **kwargs):
|
|
1404
1402
|
global parser
|
|
1405
1403
|
if not parser:
|
|
1406
|
-
from
|
|
1404
|
+
from dateutilx import parser
|
|
1407
1405
|
try:
|
|
1408
1406
|
rrkwargs["until"] = parser.parse(value, ignoretz=kwargs.get("ignoretz"), tzinfos=kwargs.get("tzinfos"))
|
|
1409
1407
|
except ValueError as e:
|
|
@@ -1465,7 +1463,7 @@ class _RRuleStr:
|
|
|
1465
1463
|
def _parse_date_value(self, date_value, parms, rule_tzids, *, ignoretz, tzids, tzinfos):
|
|
1466
1464
|
global parser
|
|
1467
1465
|
if not parser:
|
|
1468
|
-
from
|
|
1466
|
+
from dateutilx import parser
|
|
1469
1467
|
|
|
1470
1468
|
datevals = []
|
|
1471
1469
|
value_found = False
|
|
@@ -1597,7 +1595,7 @@ class _RRuleStr:
|
|
|
1597
1595
|
|
|
1598
1596
|
if forceset or len(rrulevals) > 1 or rdatevals or exrulevals or exdatevals:
|
|
1599
1597
|
if not parser and (rdatevals or exdatevals):
|
|
1600
|
-
from
|
|
1598
|
+
from dateutilx import parser
|
|
1601
1599
|
rset = RruleSet(cache=cache)
|
|
1602
1600
|
for value in rrulevals:
|
|
1603
1601
|
rset.rrule(self._parse_rfc_rrule(value, dtstart=dtstart, ignoretz=ignoretz, tzinfos=tzinfos))
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from .tz import (
|
|
2
|
-
UTC,
|
|
3
2
|
TzFile,
|
|
4
3
|
TzIcal,
|
|
5
4
|
TzLocal,
|
|
@@ -12,25 +11,17 @@ from .tz import (
|
|
|
12
11
|
gettz,
|
|
13
12
|
resolve_imaginary,
|
|
14
13
|
)
|
|
15
|
-
from .win import TzWin, TzWinLocal, tzwin, tzwinlocal
|
|
16
14
|
|
|
17
15
|
# fmt: off
|
|
18
16
|
__all__ = ["TzFile", "TzIcal", "TzLocal", "TzOffset", "TzRange", "TzStr", "TzUTC",
|
|
19
|
-
"
|
|
17
|
+
"gettz", "datetime_ambiguous", "datetime_exists",
|
|
20
18
|
"resolve_imaginary", "UTC", "DeprecatedTzFormatWarning",
|
|
21
|
-
|
|
19
|
+
]
|
|
22
20
|
# fmt: on
|
|
23
21
|
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
"""Warning raised when time zones are parsed from deprecated formats."""
|
|
23
|
+
UTC = TzUTC()
|
|
27
24
|
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
tzfile = TzFile
|
|
32
|
-
tzical = TzIcal
|
|
33
|
-
tzlocal = TzLocal
|
|
34
|
-
tzoffset = TzOffset
|
|
35
|
-
tzrange = TzRange
|
|
36
|
-
tzutc = TzUTC
|
|
26
|
+
class DeprecatedTzFormatWarning(Warning):
|
|
27
|
+
"""Warning raised when time zones are parsed from deprecated formats."""
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import weakref
|
|
2
|
+
from collections import OrderedDict
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _TzSingleton(type):
|
|
7
|
+
def __init__(cls, *args, **kwargs):
|
|
8
|
+
cls.__instance = None
|
|
9
|
+
super().__init__(*args, **kwargs)
|
|
10
|
+
|
|
11
|
+
def __call__(cls):
|
|
12
|
+
if cls.__instance is None:
|
|
13
|
+
cls.__instance = super().__call__()
|
|
14
|
+
return cls.__instance
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _TzFactory(type):
|
|
18
|
+
def instance(cls, *args, **kwargs):
|
|
19
|
+
"""Alternate constructor that returns a fresh instance"""
|
|
20
|
+
return type.__call__(cls, *args, **kwargs)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _TzCachedFactory(_TzFactory):
|
|
24
|
+
"""Factory with a weak identity cache plus a small strong LRU cache
|
|
25
|
+
to keep the most recently used instances alive."""
|
|
26
|
+
|
|
27
|
+
_strong_cache_size = 8
|
|
28
|
+
|
|
29
|
+
def __init__(cls, *args, **kwargs):
|
|
30
|
+
cls._instances = weakref.WeakValueDictionary()
|
|
31
|
+
cls._strong_cache = OrderedDict()
|
|
32
|
+
super().__init__(*args, **kwargs)
|
|
33
|
+
|
|
34
|
+
def _cache_key(cls, *args, **kwargs):
|
|
35
|
+
"""Subclasses define how constructor args map to a cache key."""
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
|
|
38
|
+
def __call__(cls, *args, **kwargs):
|
|
39
|
+
key = cls._cache_key(*args, **kwargs)
|
|
40
|
+
|
|
41
|
+
instance = cls._instances.get(key)
|
|
42
|
+
if instance is None:
|
|
43
|
+
instance = cls._instances.setdefault(key, cls.instance(*args, **kwargs))
|
|
44
|
+
|
|
45
|
+
cls._strong_cache[key] = cls._strong_cache.pop(key, instance)
|
|
46
|
+
if len(cls._strong_cache) > cls._strong_cache_size:
|
|
47
|
+
cls._strong_cache.popitem(last=False)
|
|
48
|
+
|
|
49
|
+
return instance
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class _TzOffsetFactory(_TzCachedFactory):
|
|
53
|
+
def _cache_key(cls, name, offset):
|
|
54
|
+
if isinstance(offset, timedelta):
|
|
55
|
+
offset = offset.total_seconds()
|
|
56
|
+
return name, offset
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class _TzStrFactory(_TzCachedFactory):
|
|
60
|
+
def _cache_key(cls, s, posix_offset=False):
|
|
61
|
+
return s, posix_offset
|
|
@@ -69,7 +69,7 @@ class TzRes:
|
|
|
69
69
|
"""
|
|
70
70
|
Load a timezone name from a DLL offset (integer).
|
|
71
71
|
|
|
72
|
-
>>> from
|
|
72
|
+
>>> from dateutilx.tzwin import TzRes
|
|
73
73
|
>>> tzr = TzRes()
|
|
74
74
|
>>> print(tzr.load_name(112))
|
|
75
75
|
'Eastern Standard Time'
|
|
@@ -93,7 +93,7 @@ class TzRes:
|
|
|
93
93
|
Parse strings as returned from the Windows registry into the time zone
|
|
94
94
|
name as defined in the registry.
|
|
95
95
|
|
|
96
|
-
>>> from
|
|
96
|
+
>>> from dateutilx.tzwin import TzRes
|
|
97
97
|
>>> tzr = TzRes()
|
|
98
98
|
>>> print(tzr.name_from_string('@tzres.dll,-251'))
|
|
99
99
|
'Dateline Daylight Time'
|
|
@@ -255,16 +255,16 @@ class TzWinLocal(TzWinBase):
|
|
|
255
255
|
"""
|
|
256
256
|
Class representing the local time zone information in the Windows registry
|
|
257
257
|
|
|
258
|
-
While :class:`dateutilx.tz.
|
|
258
|
+
While :class:`dateutilx.tz.TzLocal` makes system calls (via the :mod:`time`
|
|
259
259
|
module) to retrieve time zone information, ``tzwinlocal`` retrieves the
|
|
260
260
|
rules directly from the Windows registry and creates an object like
|
|
261
261
|
:class:`dateutilx.tz.tzwin`.
|
|
262
262
|
|
|
263
263
|
Because Windows does not have an equivalent of :func:`time.tzset`, on
|
|
264
|
-
Windows, :class:`dateutilx.tz.
|
|
264
|
+
Windows, :class:`dateutilx.tz.TzLocal` instances will always reflect the
|
|
265
265
|
time zone settings *at the time that the process was started*, meaning
|
|
266
266
|
changes to the machine's time zone settings during the run of a program
|
|
267
|
-
on Windows will **not** be reflected by :class:`dateutilx.tz.
|
|
267
|
+
on Windows will **not** be reflected by :class:`dateutilx.tz.TzLocal`.
|
|
268
268
|
Because ``tzwinlocal`` reads the registry directly, it is unaffected by
|
|
269
269
|
this issue.
|
|
270
270
|
"""
|
|
@@ -7,19 +7,20 @@ from relative deltas), local machine timezone, fixed offset timezone, and UTC
|
|
|
7
7
|
timezone.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
import _thread
|
|
11
10
|
import bisect
|
|
12
11
|
import contextlib
|
|
13
12
|
import datetime
|
|
14
13
|
import os
|
|
14
|
+
import string
|
|
15
15
|
import struct
|
|
16
|
+
import threading
|
|
16
17
|
import time
|
|
17
18
|
import weakref
|
|
18
19
|
from collections import OrderedDict
|
|
19
20
|
from dataclasses import dataclass
|
|
20
21
|
from typing import Any
|
|
21
22
|
|
|
22
|
-
from
|
|
23
|
+
from dateutilx.utils import Day, is_windows_os, weekdays
|
|
23
24
|
|
|
24
25
|
from ._common import TzRangeBase, _TzInfo, _validate_fromutc_inputs
|
|
25
26
|
from ._factories import _TzOffsetFactory, _TzSingleton, _TzStrFactory
|
|
@@ -29,6 +30,7 @@ rrule = None # pylint: disable=c0103
|
|
|
29
30
|
ZERO = datetime.timedelta(0)
|
|
30
31
|
EPOCH = datetime.datetime(1970, 1, 1, 0, 0)
|
|
31
32
|
EPOCHORDINAL = EPOCH.toordinal()
|
|
33
|
+
SU = weekdays[Day.SU]
|
|
32
34
|
|
|
33
35
|
|
|
34
36
|
class TzUTC(datetime.tzinfo, metaclass=_TzSingleton):
|
|
@@ -40,7 +42,7 @@ class TzUTC(datetime.tzinfo, metaclass=_TzSingleton):
|
|
|
40
42
|
.. doctest::
|
|
41
43
|
|
|
42
44
|
>>> import datetime as dt
|
|
43
|
-
>>> from
|
|
45
|
+
>>> from dateutilx.tz import *
|
|
44
46
|
|
|
45
47
|
>>> dt.datetime.now()
|
|
46
48
|
datetime.datetime(2003, 9, 27, 9, 40, 1, 521290)
|
|
@@ -57,7 +59,7 @@ class TzUTC(datetime.tzinfo, metaclass=_TzSingleton):
|
|
|
57
59
|
|
|
58
60
|
.. doctest::
|
|
59
61
|
|
|
60
|
-
>>> from
|
|
62
|
+
>>> from dateutilx.tz import tzutc, UTC
|
|
61
63
|
>>> tzutc() is tzutc()
|
|
62
64
|
True
|
|
63
65
|
>>> tzutc() is UTC
|
|
@@ -66,7 +68,9 @@ class TzUTC(datetime.tzinfo, metaclass=_TzSingleton):
|
|
|
66
68
|
|
|
67
69
|
__name__ = "tzutc"
|
|
68
70
|
__hash__ = None
|
|
69
|
-
|
|
71
|
+
|
|
72
|
+
def __reduce__(self):
|
|
73
|
+
return self.__class__, tuple()
|
|
70
74
|
|
|
71
75
|
def utcoffset(self, dt):
|
|
72
76
|
return ZERO
|
|
@@ -111,9 +115,6 @@ class TzUTC(datetime.tzinfo, metaclass=_TzSingleton):
|
|
|
111
115
|
return f"{self.__name__}()"
|
|
112
116
|
|
|
113
117
|
|
|
114
|
-
#: Convenience constant providing a :class:`tzutc()` instance
|
|
115
|
-
#:
|
|
116
|
-
#: .. versionadded:: 2.7.0
|
|
117
118
|
UTC = TzUTC()
|
|
118
119
|
|
|
119
120
|
|
|
@@ -174,7 +175,8 @@ class TzOffset(datetime.tzinfo, metaclass=_TzOffsetFactory):
|
|
|
174
175
|
def __repr__(self):
|
|
175
176
|
return f"{self.__class__.__name__}({self._name!r}, {int(self._offset.total_seconds())})"
|
|
176
177
|
|
|
177
|
-
|
|
178
|
+
def __reduce__(self):
|
|
179
|
+
return self.__class__, (self._name, self._offset)
|
|
178
180
|
|
|
179
181
|
|
|
180
182
|
class TzLocal(_TzInfo):
|
|
@@ -193,7 +195,7 @@ class TzLocal(_TzInfo):
|
|
|
193
195
|
|
|
194
196
|
self._dst_saved = self._dst_offset - self._std_offset
|
|
195
197
|
self._hasdst = bool(self._dst_saved)
|
|
196
|
-
self._tznames =
|
|
198
|
+
self._tznames = time.tzname
|
|
197
199
|
|
|
198
200
|
def utcoffset(self, dt):
|
|
199
201
|
if dt is None and self._hasdst:
|
|
@@ -242,7 +244,7 @@ class TzLocal(_TzInfo):
|
|
|
242
244
|
# The code above yields the following result:
|
|
243
245
|
#
|
|
244
246
|
# >>> import tz, datetime
|
|
245
|
-
# >>> t = tz.
|
|
247
|
+
# >>> t = tz.TzLocal()
|
|
246
248
|
# >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
|
|
247
249
|
# 'BRDT'
|
|
248
250
|
# >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname()
|
|
@@ -344,9 +346,6 @@ class _TzFile:
|
|
|
344
346
|
ttinfo_first: Any | None = None
|
|
345
347
|
|
|
346
348
|
|
|
347
|
-
_tzfile = _TzFile
|
|
348
|
-
|
|
349
|
-
|
|
350
349
|
class TzFile(_TzInfo):
|
|
351
350
|
"""
|
|
352
351
|
This is a ``tzinfo`` subclass that allows one to use the ``tzfile(5)``
|
|
@@ -384,7 +383,7 @@ class TzFile(_TzInfo):
|
|
|
384
383
|
|
|
385
384
|
.. testsetup:: tzfile
|
|
386
385
|
|
|
387
|
-
from
|
|
386
|
+
from dateutilx.tz import gettz
|
|
388
387
|
from datetime import datetime
|
|
389
388
|
|
|
390
389
|
.. doctest:: tzfile
|
|
@@ -446,7 +445,7 @@ class TzFile(_TzInfo):
|
|
|
446
445
|
def _set_tzdata(self, tzobj):
|
|
447
446
|
"""Set the time zone data of this object from a _tzfile object"""
|
|
448
447
|
# Copy the relevant attributes over as private attributes
|
|
449
|
-
for attr in
|
|
448
|
+
for attr in _TzFile.__slots__:
|
|
450
449
|
setattr(self, f"_{attr}", getattr(tzobj, attr))
|
|
451
450
|
|
|
452
451
|
def _read_tzfile(self, fileobj):
|
|
@@ -867,14 +866,14 @@ class TzRange(TzRangeBase):
|
|
|
867
866
|
|
|
868
867
|
.. testsetup:: tzrange
|
|
869
868
|
|
|
870
|
-
>>> from
|
|
869
|
+
>>> from dateutilx.tz import tzrange, tzstr
|
|
871
870
|
|
|
872
871
|
.. doctest:: tzrange
|
|
873
872
|
|
|
874
873
|
>>> tzstr('EST5EDT') == tzrange("EST", -18000, "EDT")
|
|
875
874
|
True
|
|
876
875
|
|
|
877
|
-
>>> from
|
|
876
|
+
>>> from dateutilx.relativedelta import *
|
|
878
877
|
>>> range1 = tzrange("EST", -18000, "EDT")
|
|
879
878
|
>>> range2 = tzrange("EST", -18000, "EDT", -14400,
|
|
880
879
|
... RelativeDelta(hours=+2, month=4, day=1,
|
|
@@ -888,7 +887,8 @@ class TzRange(TzRangeBase):
|
|
|
888
887
|
|
|
889
888
|
def __init__(self, stdabbr, stdoffset=None, dstabbr=None, dstoffset=None, *, start=None, end=None):
|
|
890
889
|
# global relativedelta
|
|
891
|
-
from
|
|
890
|
+
from dateutilx.relativedelta import RelativeDelta
|
|
891
|
+
from dateutilx.utils.weekday import SU
|
|
892
892
|
|
|
893
893
|
self._std_abbr = stdabbr
|
|
894
894
|
self._dst_abbr = dstabbr
|
|
@@ -1009,7 +1009,7 @@ class TzStr(TzRange, metaclass=_TzStrFactory):
|
|
|
1009
1009
|
|
|
1010
1010
|
def __init__(self, s, posix_offset=False):
|
|
1011
1011
|
global parser
|
|
1012
|
-
from
|
|
1012
|
+
from dateutilx.parser import _parser as parser
|
|
1013
1013
|
|
|
1014
1014
|
self._s = s
|
|
1015
1015
|
|
|
@@ -1038,7 +1038,7 @@ class TzStr(TzRange, metaclass=_TzStrFactory):
|
|
|
1038
1038
|
self.hasdst = bool(self._start_delta)
|
|
1039
1039
|
|
|
1040
1040
|
def _delta(self, x, isend=0):
|
|
1041
|
-
from
|
|
1041
|
+
from dateutilx import relativedelta
|
|
1042
1042
|
|
|
1043
1043
|
kwargs = {}
|
|
1044
1044
|
if x.month is not None:
|
|
@@ -1058,11 +1058,11 @@ class TzStr(TzRange, metaclass=_TzStrFactory):
|
|
|
1058
1058
|
if not isend:
|
|
1059
1059
|
kwargs["month"] = 4
|
|
1060
1060
|
kwargs["day"] = 1
|
|
1061
|
-
kwargs["weekday"] =
|
|
1061
|
+
kwargs["weekday"] = SU(+1)
|
|
1062
1062
|
else:
|
|
1063
1063
|
kwargs["month"] = 10
|
|
1064
1064
|
kwargs["day"] = 31
|
|
1065
|
-
kwargs["weekday"] =
|
|
1065
|
+
kwargs["weekday"] = SU(-1)
|
|
1066
1066
|
|
|
1067
1067
|
kwargs["seconds"] = x.time if x.time is not None else 7200 # Default is 2AM.
|
|
1068
1068
|
|
|
@@ -1094,7 +1094,7 @@ class _TzIcalVtz(_TzInfo):
|
|
|
1094
1094
|
self._tzid = tzid
|
|
1095
1095
|
self._comps = comps or []
|
|
1096
1096
|
self._cache = OrderedDict()
|
|
1097
|
-
self._cache_lock =
|
|
1097
|
+
self._cache_lock = threading.Lock()
|
|
1098
1098
|
|
|
1099
1099
|
def _find_comp(self, dt):
|
|
1100
1100
|
if len(self._comps) == 1:
|
|
@@ -1176,7 +1176,7 @@ class TzIcal:
|
|
|
1176
1176
|
def __init__(self, fileobj):
|
|
1177
1177
|
global rrule
|
|
1178
1178
|
if not rrule:
|
|
1179
|
-
from
|
|
1179
|
+
from dateutilx import rrule
|
|
1180
1180
|
|
|
1181
1181
|
if isinstance(fileobj, str):
|
|
1182
1182
|
self._s = fileobj
|
|
@@ -1235,6 +1235,9 @@ class TzIcal:
|
|
|
1235
1235
|
else:
|
|
1236
1236
|
signal = +1
|
|
1237
1237
|
|
|
1238
|
+
if not (s.isascii() and s.isdigit()):
|
|
1239
|
+
raise ValueError("invalid offset: " + s)
|
|
1240
|
+
|
|
1238
1241
|
if len(s) == 4:
|
|
1239
1242
|
return (int(s[:2]) * 3600 + int(s[2:]) * 60) * signal
|
|
1240
1243
|
if len(s) == 6:
|
|
@@ -1436,7 +1439,7 @@ def __get_gettz():
|
|
|
1436
1439
|
self.__instances = weakref.WeakValueDictionary()
|
|
1437
1440
|
self.__strong_cache_size = 8
|
|
1438
1441
|
self.__strong_cache = OrderedDict()
|
|
1439
|
-
self._cache_lock =
|
|
1442
|
+
self._cache_lock = threading.Lock()
|
|
1440
1443
|
|
|
1441
1444
|
def __call__(self, name=None):
|
|
1442
1445
|
with self._cache_lock:
|
|
@@ -1541,7 +1544,7 @@ def __get_gettz():
|
|
|
1541
1544
|
# one offset. For short values of "name", an
|
|
1542
1545
|
# explicit for loop seems to be the fastest way
|
|
1543
1546
|
# To determine if a string contains a digit
|
|
1544
|
-
if c in
|
|
1547
|
+
if c in string.digits:
|
|
1545
1548
|
with contextlib.suppress(ValueError):
|
|
1546
1549
|
tz = TzStr(name)
|
|
1547
1550
|
break
|
|
@@ -1651,7 +1654,7 @@ def resolve_imaginary(dt):
|
|
|
1651
1654
|
|
|
1652
1655
|
.. doctest::
|
|
1653
1656
|
|
|
1654
|
-
>>> from
|
|
1657
|
+
>>> from dateutilx import tz
|
|
1655
1658
|
>>> from datetime import datetime
|
|
1656
1659
|
>>> NYC = tz.gettz('America/New_York')
|
|
1657
1660
|
>>> print(tz.resolve_imaginary(datetime(2017, 3, 12, 2, 30, tzinfo=NYC)))
|
|
@@ -2,14 +2,11 @@
|
|
|
2
2
|
Wrapper for _win.py to handle platform dependent import
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from dateutilx.utils import is_windows_os
|
|
6
6
|
|
|
7
7
|
if is_windows_os():
|
|
8
8
|
from ._win import TzRes, TzWin, TzWinLocal
|
|
9
9
|
else:
|
|
10
10
|
TzRes = TzWin = TzWinLocal = None
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
tzres = TzRes
|
|
14
|
-
tzwin = TzWin
|
|
15
|
-
tzwinlocal = TzWinLocal
|
|
12
|
+
__all__ = ["TzRes", "TzWin", "TzWinLocal"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from .constants import Day, Frequency, Month
|
|
2
|
+
from .funcs import default_tzinfo, is_windows_os, today, within_delta
|
|
3
|
+
from .weekday import Weekday, weekdays
|
|
4
|
+
|
|
5
|
+
# fmt: off
|
|
6
|
+
__all__ = ["Day", "Frequency", "Month",
|
|
7
|
+
"default_tzinfo", "is_windows_os", "today", "within_delta",
|
|
8
|
+
"Weekday", "weekdays"
|
|
9
|
+
]
|
|
10
|
+
# fmt: on
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
datetimes.
|
|
1
|
+
import datetime as dt
|
|
2
|
+
import sys
|
|
4
3
|
|
|
5
|
-
.. versionadded:: 2.7.0
|
|
6
|
-
"""
|
|
7
4
|
|
|
8
|
-
|
|
5
|
+
def is_windows_os() -> bool:
|
|
6
|
+
return sys.platform.startswith("win")
|
|
9
7
|
|
|
10
8
|
|
|
11
9
|
def today(tzinfo=None):
|
|
@@ -33,9 +31,9 @@ def default_tzinfo(dt, tzinfo):
|
|
|
33
31
|
|
|
34
32
|
.. doctest::
|
|
35
33
|
|
|
36
|
-
>>> from
|
|
37
|
-
>>> from
|
|
38
|
-
>>> from
|
|
34
|
+
>>> from dateutilx.tz import tzoffset
|
|
35
|
+
>>> from dateutilx.parser import parse
|
|
36
|
+
>>> from dateutilx.utils import default_tzinfo
|
|
39
37
|
>>> dflt_tz = tzoffset("EST", -18000)
|
|
40
38
|
>>> print(default_tzinfo(parse('2014-01-01 12:30 UTC'), dflt_tz))
|
|
41
39
|
2014-01-01 12:30:00+00:00
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from .constants import Day
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class NthWeekday:
|
|
@@ -22,6 +22,9 @@ class NthWeekday:
|
|
|
22
22
|
except AttributeError:
|
|
23
23
|
return False
|
|
24
24
|
|
|
25
|
+
def __hash__(self):
|
|
26
|
+
return hash((self.weekday, self.n))
|
|
27
|
+
|
|
25
28
|
def __repr__(self) -> str:
|
|
26
29
|
name = self.weekday.name
|
|
27
30
|
return f"{name}({self.n:+d})" if self.n else name
|
|
@@ -30,13 +33,9 @@ class NthWeekday:
|
|
|
30
33
|
# pylint: disable=r0903
|
|
31
34
|
class Weekday(NthWeekday):
|
|
32
35
|
def __init__(self, wkday: int, n: int | None = None):
|
|
36
|
+
if not 0 <= wkday <= 6:
|
|
37
|
+
raise ValueError(f"Weekday must be between 0 and 6, got {wkday}")
|
|
33
38
|
super().__init__(wkday=Day(wkday), n=n)
|
|
34
39
|
|
|
35
|
-
def __hash__(self):
|
|
36
|
-
return hash((self.weekday, self.n))
|
|
37
|
-
|
|
38
40
|
|
|
39
41
|
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(Weekday(x) for x in range(7))
|
|
40
|
-
|
|
41
|
-
# Alias
|
|
42
|
-
weekday = Weekday
|
|
@@ -43,12 +43,12 @@ Source = "https://github.com/rsb-23/dateutilx"
|
|
|
43
43
|
[tool.hatch.version]
|
|
44
44
|
source = "vcs"
|
|
45
45
|
[tool.hatch.build.targets.sdist]
|
|
46
|
-
include = ["
|
|
46
|
+
include = ["dateutilx", "LICENSE", "README.md", "pyproject.toml"]
|
|
47
47
|
[tool.hatch.build.targets.wheel]
|
|
48
|
-
packages = ["
|
|
48
|
+
packages = ["dateutilx"]
|
|
49
49
|
|
|
50
50
|
[tool.coverage.run]
|
|
51
|
-
source = ["
|
|
51
|
+
source = ["dateutilx"]
|
|
52
52
|
[tool.coverage.report]
|
|
53
53
|
fail_under = 85.0
|
|
54
54
|
skip_covered = true
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import weakref
|
|
2
|
-
from collections import OrderedDict
|
|
3
|
-
from datetime import timedelta
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class _TzSingleton(type):
|
|
7
|
-
def __init__(cls, *args, **kwargs):
|
|
8
|
-
cls.__instance = None
|
|
9
|
-
super().__init__(*args, **kwargs)
|
|
10
|
-
|
|
11
|
-
def __call__(cls):
|
|
12
|
-
if cls.__instance is None:
|
|
13
|
-
cls.__instance = super().__call__()
|
|
14
|
-
return cls.__instance
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class _TzFactory(type):
|
|
18
|
-
def instance(cls, *args, **kwargs):
|
|
19
|
-
"""Alternate constructor that returns a fresh instance"""
|
|
20
|
-
return type.__call__(cls, *args, **kwargs)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class _TzOffsetFactory(_TzFactory):
|
|
24
|
-
def __init__(cls, *args, **kwargs):
|
|
25
|
-
cls.__instances = weakref.WeakValueDictionary()
|
|
26
|
-
cls.__strong_cache = OrderedDict()
|
|
27
|
-
cls.__strong_cache_size = 8
|
|
28
|
-
|
|
29
|
-
def __call__(cls, name, offset):
|
|
30
|
-
if isinstance(offset, timedelta):
|
|
31
|
-
key = (name, offset.total_seconds())
|
|
32
|
-
else:
|
|
33
|
-
key = (name, offset)
|
|
34
|
-
|
|
35
|
-
instance = cls.__instances.get(key, None)
|
|
36
|
-
if instance is None:
|
|
37
|
-
instance = cls.__instances.setdefault(key, cls.instance(name, offset))
|
|
38
|
-
|
|
39
|
-
cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
|
|
40
|
-
|
|
41
|
-
# Remove an item if the strong cache is overpopulated
|
|
42
|
-
if len(cls.__strong_cache) > cls.__strong_cache_size:
|
|
43
|
-
cls.__strong_cache.popitem(last=False)
|
|
44
|
-
|
|
45
|
-
return instance
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class _TzStrFactory(_TzFactory):
|
|
49
|
-
def __init__(cls, *args, **kwargs):
|
|
50
|
-
cls.__instances = weakref.WeakValueDictionary()
|
|
51
|
-
cls.__strong_cache = OrderedDict()
|
|
52
|
-
cls.__strong_cache_size = 8
|
|
53
|
-
|
|
54
|
-
def __call__(cls, s, posix_offset=False):
|
|
55
|
-
key = (s, posix_offset)
|
|
56
|
-
instance = cls.__instances.get(key, None)
|
|
57
|
-
|
|
58
|
-
if instance is None:
|
|
59
|
-
instance = cls.__instances.setdefault(key, cls.instance(s, posix_offset))
|
|
60
|
-
|
|
61
|
-
cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
|
|
62
|
-
|
|
63
|
-
# Remove an item if the strong cache is overpopulated
|
|
64
|
-
if len(cls.__strong_cache) > cls.__strong_cache_size:
|
|
65
|
-
cls.__strong_cache.popitem(last=False)
|
|
66
|
-
|
|
67
|
-
return instance
|
dateutilx-0.2.0/src/tzwin.py
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|