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.
Files changed (30) hide show
  1. {dateutilx-0.2.0 → dateutilx-0.4.0}/.gitignore +0 -3
  2. {dateutilx-0.2.0 → dateutilx-0.4.0}/PKG-INFO +2 -2
  3. {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/__init__.py +1 -1
  4. {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/parser/_parser.py +19 -19
  5. {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/parser/isoparser.py +30 -19
  6. {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/relativedelta.py +55 -37
  7. {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/rrule.py +25 -27
  8. {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/tz/__init__.py +5 -14
  9. dateutilx-0.4.0/dateutilx/tz/_factories.py +61 -0
  10. {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/tz/_win.py +5 -5
  11. {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/tz/tz.py +31 -28
  12. {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/tz/win.py +2 -5
  13. dateutilx-0.4.0/dateutilx/utils/__init__.py +10 -0
  14. dateutilx-0.2.0/src/utils.py → dateutilx-0.4.0/dateutilx/utils/funcs.py +7 -9
  15. {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx/utils}/weekday.py +6 -7
  16. {dateutilx-0.2.0 → dateutilx-0.4.0}/pyproject.toml +3 -3
  17. dateutilx-0.2.0/src/helper/__init__.py +0 -4
  18. dateutilx-0.2.0/src/helper/funcs.py +0 -5
  19. dateutilx-0.2.0/src/tz/_factories.py +0 -67
  20. dateutilx-0.2.0/src/tzwin.py +0 -4
  21. {dateutilx-0.2.0 → dateutilx-0.4.0}/.base_project/LICENSE +0 -0
  22. {dateutilx-0.2.0 → dateutilx-0.4.0}/LICENSE +0 -0
  23. {dateutilx-0.2.0 → dateutilx-0.4.0}/README.md +0 -0
  24. {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/easter.py +0 -0
  25. {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/errors.py +0 -0
  26. {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/info.py +0 -0
  27. {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/parser/__init__.py +0 -0
  28. {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/parser/utils.py +0 -0
  29. {dateutilx-0.2.0/src → dateutilx-0.4.0/dateutilx}/tz/_common.py +0 -0
  30. {dateutilx-0.2.0/src/helper → dateutilx-0.4.0/dateutilx/utils}/constants.py +0 -0
@@ -17,9 +17,6 @@ venv/
17
17
  .hypothesis/
18
18
  .coverage
19
19
 
20
- # Autogenerated version information
21
- src/dateutilx/_version.py
22
-
23
20
  # Sphinx documentation
24
21
  docs/_build/
25
22
  docs/docs_venv
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: dateutilx
3
- Version: 0.2.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 not attribute {name!r}")
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 src import relativedelta, tz
43
- from src.errors import ParserError, UnknownTimezoneWarning
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 [None, "Y"]: # pragma: no cover
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 [None, "Y"]: # pragma: no cover
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 ["y", "m", "d"] if x not in strids]
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 src.parser import parse
566
- >>> from src.tz import gettz
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 src.parser import parse
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.tzstr(tzdata)
1135
+ tzinfo = tz.TzStr(tzdata)
1136
1136
  elif isinstance(tzdata, int):
1137
- tzinfo = tz.tzoffset(tzname, tzdata)
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.tzlocal())
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.tzoffset(res.tzname, res.tzoffset))
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 src.parser import parse
1272
- >>> from src.tz import gettz
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 src.parser import parse
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 "0123456789"):
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 "0123456789"
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 src.tz import UTC, TzOffset
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 "0123456789":
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.tzoffset` objects,
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.tzutc`. Time zone offsets equivalent to UTC (such
113
- as `+00:00`) will also be represented as :class:`dateutilx.tz.tzutc`.
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.tzutc` for zero-offset zones
192
+ Whether to return :class:`dateutilx.tz.TzUTC` for zero-offset zones
192
193
 
193
194
  :return:
194
- Returns :class:`dateutilx.tz.tzoffset` for offsets and
195
- :class:`dateutilx.tz.tzutc` for ``Z`` and (if ``zero_as_utc`` is
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] = int(dt_str[:4])
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] = int(dt_str[pos : pos + 2])
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] = int(dt_str[pos : pos + 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 = int(dt_str[:4])
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 = int(dt_str[pos : pos + 2])
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 = int(dt_str[pos : pos + 1])
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 = int(dt_str[pos : pos + 3])
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] = int(timestr[pos : pos + 2])
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 = int(tzstr[1:3])
394
+ hours = self._parse_digits(tzstr, 1, 2)
387
395
  if len(tzstr) == 3:
388
396
  minutes = 0
389
397
  else:
390
- minutes = int(tzstr[(4 if tzstr[3:4] == self._TIME_SEP else 3) :])
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 == 0 and minutes == 0:
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, TypeAlias
6
+ from typing import Self
7
7
  from warnings import warn
8
8
 
9
- from src.weekday import Weekday, weekdays
9
+ from dateutilx.utils import Weekday, weekdays
10
10
 
11
- MO, TU, WE, TH, FR, SA, SU = weekdays
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 src.relativedelta import RelativeDelta, MO
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 > 59:
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 REL_FIELDS:
253
+ for attr in ("years", "months", "days", "hours", "minutes", "seconds", "microseconds"):
236
254
  setattr(self, attr, 0)
237
255
 
238
- for attr in ABS_FIELDS:
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, getattr(self, field)) for field in REL_FIELDS}
299
- abs_fields = {field: kwargs.get(field, getattr(self, field)) for field in ABS_FIELDS}
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: getattr(self, field) if getattr(other, field) is None else getattr(other, field)
341
- for field in ABS_FIELDS
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 ["hour", "minute", "second", "microsecond"]:
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(getattr(self, field)) for field in REL_FIELDS}
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: -getattr(self, field) for field in REL_FIELDS}
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
- s_fields = (getattr(self, field) for field in REL_FIELDS)
436
- attribs = (getattr(self, field) for field in ABS_FIELDS)
437
- return any((*s_fields, self.leapdays, self.weekday, *(x is not None for x in attribs)))
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) -> NotImplementedType | Self:
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(getattr(self, field) * f) for field in REL_FIELDS}
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((getattr(self, field) == getattr(other, field) for field in REL_FIELDS + ABS_FIELDS))
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 __div__(self, other) -> NotImplementedType | Self:
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 ["years", "months", "days", "leapdays", "hours", "minutes", "seconds", "microseconds"]:
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 ["year", "month", "day", "weekday", "hour", "minute", "second", "microsecond"]:
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 .helper import Day, Frequency
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 = _thread.allocate_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
- acquire = self._cache_lock.acquire
111
- release = self._cache_lock.release
109
+
112
110
  while gen:
113
111
  if i == len(cache):
114
- acquire()
115
- if self._cache_complete:
116
- break
117
- try:
118
- for _ in range(10):
119
- cache.append(next(gen))
120
- except StopIteration:
121
- self._cache_gen = gen = None
122
- self._cache_complete = True
123
- break
124
- release()
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 src.rrule import rrule, MONTHLY
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 byweekno is None and byyearday is None and bymonthday is None and byweekday is None and byeaster is None:
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 src import easter
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
- return None
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 src import parser
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 src import parser
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 src import parser
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
- "tzutc", "gettz", "datetime_ambiguous", "datetime_exists",
17
+ "gettz", "datetime_ambiguous", "datetime_exists",
20
18
  "resolve_imaginary", "UTC", "DeprecatedTzFormatWarning",
21
- "TzWin", "TzWinLocal"]
19
+ ]
22
20
  # fmt: on
23
21
 
24
22
 
25
- class DeprecatedTzFormatWarning(Warning):
26
- """Warning raised when time zones are parsed from deprecated formats."""
23
+ UTC = TzUTC()
27
24
 
28
25
 
29
- # Alias
30
- tzstr = TzStr
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 src.tzwin import TzRes
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 src.tzwin import TzRes
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.tzlocal` makes system calls (via the :mod:`time`
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.tzlocal` instances will always reflect the
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.tzlocal`.
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 src.helper import is_windows_os
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 src.tz import *
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 src.tz import tzutc, UTC
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
- __reduce__ = object.__reduce__
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
- __reduce__ = object.__reduce__
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 = tuple(time.tzname)
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.tzlocal()
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 src.tz import gettz
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 _tzfile.__slots__:
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 src.tz import tzrange, tzstr
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 src.relativedelta import *
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 src.relativedelta import SU, RelativeDelta
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 src.parser import _parser as parser
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 src import relativedelta
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"] = relativedelta.SU(+1)
1061
+ kwargs["weekday"] = SU(+1)
1062
1062
  else:
1063
1063
  kwargs["month"] = 10
1064
1064
  kwargs["day"] = 31
1065
- kwargs["weekday"] = relativedelta.SU(-1)
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 = _thread.allocate_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 src import rrule
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 = _thread.allocate_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 "0123456789":
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 src import tz
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 src.helper import is_windows_os
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
- # Alias
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
- This module offers general convenience and utility functions for dealing with
3
- datetimes.
1
+ import datetime as dt
2
+ import sys
4
3
 
5
- .. versionadded:: 2.7.0
6
- """
7
4
 
8
- import datetime as dt
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 src.tz import tzoffset
37
- >>> from src.parser import parse
38
- >>> from src.utils import default_tzinfo
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 src.helper import Day
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 = ["src", "LICENSE", "README.md", "pyproject.toml"]
46
+ include = ["dateutilx", "LICENSE", "README.md", "pyproject.toml"]
47
47
  [tool.hatch.build.targets.wheel]
48
- packages = ["src"]
48
+ packages = ["dateutilx"]
49
49
 
50
50
  [tool.coverage.run]
51
- source = ["src"]
51
+ source = ["dateutilx"]
52
52
  [tool.coverage.report]
53
53
  fail_under = 85.0
54
54
  skip_covered = true
@@ -1,4 +0,0 @@
1
- from .constants import Day, Frequency, Month
2
- from .funcs import is_windows_os
3
-
4
- __all__ = ["Day", "Frequency", "Month", "is_windows_os"]
@@ -1,5 +0,0 @@
1
- import sys
2
-
3
-
4
- def is_windows_os() -> bool:
5
- return sys.platform.startswith("win")
@@ -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
@@ -1,4 +0,0 @@
1
- # tzwin has moved to dateutilx.tz.win
2
- from .tz.win import TzRes, TzWin, TzWinLocal, tzres, tzwin, tzwinlocal # noqa
3
-
4
- __all__ = ["TzRes", "TzWin", "TzWinLocal", "tzres", "tzwin", "tzwinlocal"]
File without changes
File without changes