python-dateutil 2.9.0__py2.py3-none-any.whl → 3.0.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dateutil/tz/_tzfile.py ADDED
@@ -0,0 +1,1428 @@
1
+ # -*- coding: utf-8 -*-
2
+ import bisect
3
+ import calendar
4
+ import functools
5
+ import io
6
+ import os
7
+ import re
8
+ import stat
9
+ import struct
10
+ import sys
11
+ from datetime import MAXYEAR, MINYEAR, datetime, timedelta
12
+
13
+ import six
14
+
15
+ from ._common import _tzinfo, enfold, tzname_in_python2
16
+
17
+ EPOCH = datetime(1970, 1, 1)
18
+ EPOCHORDINAL = EPOCH.toordinal()
19
+
20
+ # TODO: Move this to _common.py
21
+ try:
22
+ # Python 3.7 feature
23
+ from contextlib import nullcontext as _nullcontext
24
+ except ImportError:
25
+ import contextlib
26
+
27
+ @contextlib.contextmanager
28
+ def _nullcontext(context):
29
+ yield context
30
+
31
+
32
+ # It is relatively expensive to construct new timedelta objects, and in most
33
+ # cases we're looking at the same deltas, like integer numbers of hours, etc.
34
+ # To improve speed and memory use, we'll keep a dictionary with references
35
+ # to the ones we've already used so far.
36
+ #
37
+ # Loading every time zone in the 2020a version of the time zone database
38
+ # requires 447 timedeltas, which requires approximately the amount of space
39
+ # that ZoneInfo("America/New_York") with 236 transitions takes up, so we will
40
+ # set the cache size to 512 so that in the common case we always get cache
41
+ # hits, but specifically crafted ZoneInfo objects don't leak arbitrary amounts
42
+ # of memory.
43
+ _TIMEDELTA_CACHE_SIZE = 512
44
+ if six.PY2:
45
+
46
+ def _cache_load_timedelta(f):
47
+ from collections import OrderedDict
48
+
49
+ from six.moves import _thread
50
+
51
+ CACHE_LOCK = _thread.allocate_lock()
52
+ TIMEDELTA_CACHE = OrderedDict()
53
+
54
+ @functools.wraps(f)
55
+ def cached_load_timedelta(seconds):
56
+ # OrderedDict is implemented in Python on Python 2, so every
57
+ # access has to hold the lock to keep its linked list intact.
58
+ with CACHE_LOCK:
59
+ rv = TIMEDELTA_CACHE.pop(seconds, None)
60
+ if rv is None:
61
+ rv = f(seconds)
62
+
63
+ TIMEDELTA_CACHE[seconds] = rv
64
+
65
+ if len(TIMEDELTA_CACHE) > _TIMEDELTA_CACHE_SIZE:
66
+ TIMEDELTA_CACHE.popitem(last=False)
67
+
68
+ return rv
69
+
70
+ return cached_load_timedelta
71
+
72
+ else:
73
+ _cache_load_timedelta = functools.lru_cache(maxsize=512)
74
+
75
+
76
+ @_cache_load_timedelta
77
+ def _load_timedelta(seconds):
78
+ return timedelta(seconds=seconds)
79
+
80
+
81
+ def _unpickle(cls, key, filename, data):
82
+ obj = cls.__new__(cls)
83
+ obj._key = key
84
+ obj._filename = filename
85
+
86
+ obj._load_from_data(data)
87
+
88
+ return obj
89
+
90
+
91
+ class tzfile(_tzinfo):
92
+ """
93
+ This is a ``tzinfo`` subclass that allows one to use the ``tzfile(5)``
94
+ format timezone files to extract current and historical zone information.
95
+
96
+ :param fileobj:
97
+ This can be an opened file stream or a file name that the time zone
98
+ information can be read from.
99
+
100
+ :param filename:
101
+ This is an optional parameter specifying the source of the time zone
102
+ information in the event that ``fileobj`` is a file object. If omitted
103
+ and ``fileobj`` is a file stream, this parameter will be set either to
104
+ ``fileobj``'s ``name`` attribute or to ``repr(fileobj)``.
105
+
106
+ This class is intended to be mostly equivalent to the standard library
107
+ :py:class:`zoneinfo.ZoneInfo` class. The primary differences are:
108
+
109
+ * All caching in ``tzfile`` is handled by :func:`tz.gettz` level (you
110
+ should generally not attempt to construct a ``tzfile`` directly).
111
+ * ``tzfile`` objects are pickled by value rather than by reference to
112
+ the IANA key.
113
+ * ``tzfile`` has equality-by-value semantics, whereas
114
+ :py:class:`~zoneinfo.ZoneInfo` has equality-by-identity semantics.
115
+
116
+ See `Sources for Time Zone and Daylight Saving Time Data
117
+ <https://data.iana.org/time-zones/tz-link.html>`_ for more information.
118
+ Time zone files can be compiled from the `IANA Time Zone database files
119
+ <https://www.iana.org/time-zones>`_ with the `zic time zone compiler
120
+ <https://www.freebsd.org/cgi/man.cgi?query=zic&sektion=8>`_
121
+
122
+ .. note::
123
+
124
+ Only construct a ``tzfile`` directly if you have a specific timezone
125
+ file on disk that you want to read into a Python ``tzinfo`` object.
126
+ If you want to get a ``tzfile`` representing a specific IANA zone,
127
+ (e.g. ``'America/New_York'``), you should call
128
+ :func:`dateutil.tz.gettz` with the zone identifier.
129
+
130
+ .. versionchanged:: 3.0.0
131
+ ``tzfile`` is now a partial backport of :py:class:`zoneinfo.ZoneInfo`,
132
+ which brings with it support for Version 2 and 3 ``tzfile(5)`` binaries.
133
+
134
+ **Examples:**
135
+
136
+ Using the US Eastern time zone as an example, we can see that a ``tzfile``
137
+ provides time zone information for the standard Daylight Saving offsets:
138
+
139
+ .. testsetup:: tzfile
140
+
141
+ from dateutil.tz import gettz
142
+ from datetime import datetime
143
+
144
+ .. doctest:: tzfile
145
+
146
+ >>> NYC = gettz('America/New_York')
147
+ >>> NYC
148
+ tzfile('/usr/share/zoneinfo/America/New_York')
149
+
150
+ >>> print(datetime(2016, 1, 3, tzinfo=NYC)) # EST
151
+ 2016-01-03 00:00:00-05:00
152
+
153
+ >>> print(datetime(2016, 7, 7, tzinfo=NYC)) # EDT
154
+ 2016-07-07 00:00:00-04:00
155
+
156
+
157
+ The ``tzfile`` structure contains a fully history of the time zone,
158
+ so historical dates will also have the right offsets. For example, before
159
+ the adoption of standard time on November 18, 1883, New York used local
160
+ mean time:
161
+
162
+ .. doctest:: tzfile
163
+
164
+ >>> print(datetime(1883, 11, 18, tzinfo=NYC)) # LMT
165
+ 1883-11-18 00:00:00-04:56:02
166
+
167
+ Python versions before 3.6 do not support sub-minute offsets, so there the
168
+ offset is rounded to ``-04:56``.
169
+
170
+ And during World War II, New York was on "Eastern War Time", which was a
171
+ state of permanent daylight saving time:
172
+
173
+ .. doctest:: tzfile
174
+
175
+ >>> print(datetime(1944, 2, 7, tzinfo=NYC)) # EWT
176
+ 1944-02-07 00:00:00-04:00
177
+
178
+ """
179
+
180
+ def __init__(self, fileobj, filename=None, key=None):
181
+ super(tzfile, self).__init__()
182
+
183
+ file_opened_here = False
184
+ if isinstance(fileobj, six.string_types):
185
+ self._filename = fileobj
186
+ fileobj = open(fileobj, "rb")
187
+ file_opened_here = True
188
+ elif filename is not None:
189
+ self._filename = filename
190
+ elif hasattr(fileobj, "name"):
191
+ self._filename = fileobj.name
192
+ elif key is not None:
193
+ self._filename = key
194
+ else:
195
+ self._filename = repr(fileobj)
196
+
197
+ if fileobj is not None:
198
+ if not file_opened_here:
199
+ fileobj = _nullcontext(fileobj)
200
+
201
+ with fileobj as file_stream:
202
+ self._load_file(file_stream)
203
+
204
+ self._key = key
205
+
206
+ @property
207
+ def key(self):
208
+ """
209
+ Read-only attribute returning the ``key`` constructor argument.
210
+
211
+ For zones generated from :func:`gettz`, this will be the IANA key
212
+ passed to ``gettz`` — no normalization is done, so although in most
213
+ distributions of the time zone database "US/Eastern" is a link to
214
+ "America/New_York", ``tz.gettz("US/Eastern").key`` should always return
215
+ ``"US/Eastern"``.
216
+
217
+ For zones constructed via another mechanism without specifying ``key``,
218
+ this returns :obj:`None`.
219
+ """
220
+ return self._key
221
+
222
+ def is_ambiguous(self, dt):
223
+ """
224
+ Whether or not the "wall time" of a given datetime is ambiguous in this
225
+ zone.
226
+
227
+ :param dt:
228
+ A :py:class:`datetime.datetime`, naive or time zone aware.
229
+
230
+
231
+ :return:
232
+ Returns ``True`` if ambiguous, ``False`` otherwise.
233
+
234
+ .. versionadded:: 2.6.0
235
+ """
236
+ return self._is_ambiguous_or_imaginary(dt) == 1
237
+
238
+ def _is_ambiguous_or_imaginary(self, dt):
239
+ if dt is None or self._fixed_offset:
240
+ return 0
241
+
242
+ timestamp = self._get_local_timestamp(dt)
243
+ tti_0 = self._find_trans(dt, fold=0, timestamp=timestamp)
244
+ tti_1 = self._find_trans(dt, fold=1, timestamp=timestamp)
245
+
246
+ # Returns 1 if ambiguous, -1 if imaginary and 0 otherwise.
247
+ return int(tti_0.utcoff > tti_1.utcoff) - int(
248
+ tti_1.utcoff > tti_0.utcoff
249
+ )
250
+
251
+ def utcoffset(self, dt):
252
+ return self._find_trans(dt).utcoff
253
+
254
+ def dst(self, dt):
255
+ return self._find_trans(dt).dstoff
256
+
257
+ @tzname_in_python2
258
+ def tzname(self, dt):
259
+ return self._find_trans(dt).tzname
260
+
261
+ def fromutc(self, dt):
262
+ """
263
+ The ``tzfile`` implementation of :py:func:`datetime.tzinfo.fromutc`.
264
+
265
+ :param dt:
266
+ A :py:class:`datetime.datetime` object.
267
+
268
+ :raises TypeError:
269
+ Raised if ``dt`` is not a :py:class:`datetime.datetime` object.
270
+
271
+ :raises ValueError:
272
+ Raised if this is called with a ``dt`` which does not have this
273
+ ``tzinfo`` attached.
274
+
275
+ :return:
276
+ Returns a :py:class:`datetime.datetime` object representing the
277
+ wall time in ``self``'s time zone.
278
+ """
279
+
280
+ if not isinstance(dt, datetime):
281
+ raise TypeError("fromutc() requires a datetime argument")
282
+ if dt.tzinfo is not self:
283
+ raise ValueError("dt.tzinfo is not self")
284
+
285
+ timestamp = self._get_local_timestamp(dt)
286
+ num_trans = len(self._trans_utc)
287
+
288
+ if num_trans >= 1 and timestamp < self._trans_utc[0]:
289
+ tti = self._tti_before
290
+ fold = 0
291
+ elif (
292
+ num_trans == 0 or timestamp > self._trans_utc[-1]
293
+ ) and not isinstance(self._tz_after, _ttinfo):
294
+ tti, fold = self._tz_after.get_trans_info_fromutc(
295
+ timestamp, dt.year
296
+ )
297
+
298
+ # The TZ string only knows about its own transitions, so check
299
+ # whether we are in a fold created by the last explicit one.
300
+ if num_trans and not fold:
301
+ if num_trans > 1:
302
+ tti_prev = self._ttinfos[-2]
303
+ else:
304
+ tti_prev = self._tti_before
305
+
306
+ shift = tti_prev.utcoff - tti.utcoff
307
+ fold = shift.total_seconds() > timestamp - self._trans_utc[-1]
308
+ elif num_trans == 0:
309
+ tti = self._tz_after
310
+ fold = 0
311
+ else:
312
+ idx = bisect.bisect_right(self._trans_utc, timestamp)
313
+
314
+ if num_trans > 1 and timestamp >= self._trans_utc[1]:
315
+ tti_prev, tti = self._ttinfos[idx - 2 : idx]
316
+ elif timestamp > self._trans_utc[-1]:
317
+ # Only reachable with exactly one transition, so the
318
+ # offset before it is _tti_before, not _ttinfos[-1].
319
+ tti_prev = self._tti_before
320
+ tti = self._tz_after
321
+ else:
322
+ tti_prev = self._tti_before
323
+ tti = self._ttinfos[0]
324
+
325
+ # Detect fold
326
+ shift = tti_prev.utcoff - tti.utcoff
327
+ fold = shift.total_seconds() > timestamp - self._trans_utc[idx - 1]
328
+ dt += tti.utcoff
329
+ if fold:
330
+ return enfold(dt, fold=1)
331
+ else:
332
+ return dt
333
+
334
+ def _find_trans(self, dt, fold=None, timestamp=None):
335
+ if dt is None:
336
+ if self._fixed_offset:
337
+ return self._tz_after
338
+ else:
339
+ return _NO_TTINFO
340
+
341
+ if timestamp is None:
342
+ ts = self._get_local_timestamp(dt)
343
+ else:
344
+ ts = timestamp
345
+
346
+ if fold is None:
347
+ fold = getattr(dt, "fold", 0)
348
+
349
+ lt = self._trans_local[fold]
350
+
351
+ num_trans = len(lt)
352
+
353
+ if num_trans and ts < lt[0]:
354
+ return self._tti_before
355
+ elif not num_trans or ts > lt[-1]:
356
+ if isinstance(self._tz_after, _TZStr):
357
+ tti = self._tz_after.get_trans_info(ts, dt.year, fold)
358
+
359
+ # The TZ string only describes times after the last explicit
360
+ # transition. If its answer for this local time is a different
361
+ # offset from the last transition's, and corresponds to a time
362
+ # before that transition (e.g. when the TZ string's own DST
363
+ # period for that year ends after the transition), the offset
364
+ # from the last explicit transition applies instead.
365
+ last_tti = self._ttinfos[-1] if num_trans else None
366
+ if (
367
+ last_tti is not None
368
+ and tti.utcoff != last_tti.utcoff
369
+ and ts - tti.utcoff.total_seconds() < self._trans_utc[-1]
370
+ ):
371
+ return last_tti
372
+
373
+ return tti
374
+ else:
375
+ return self._tz_after
376
+ else:
377
+ # idx is the transition that occurs after this timestamp, so we
378
+ # subtract off 1 to get the current ttinfo
379
+ idx = bisect.bisect_right(lt, ts) - 1
380
+ assert idx >= 0
381
+ return self._ttinfos[idx]
382
+
383
+ def _get_local_timestamp(self, dt):
384
+ return (
385
+ (dt.toordinal() - EPOCHORDINAL) * 86400
386
+ + dt.hour * 3600
387
+ + dt.minute * 60
388
+ + dt.second
389
+ )
390
+
391
+ # The attributes set by _unpickle and _load_from_data
392
+ _UNPICKLE_ATTRS = frozenset(
393
+ [
394
+ "_filename",
395
+ "_fixed_offset",
396
+ "_isdst",
397
+ "_key",
398
+ "_trans_idx",
399
+ "_trans_local",
400
+ "_trans_utc",
401
+ "_tti_before",
402
+ "_ttinfo_list",
403
+ "_ttinfos",
404
+ "_tz_after",
405
+ "_tz_str",
406
+ "_utcoff_raw",
407
+ ]
408
+ )
409
+
410
+ def __reduce__(self):
411
+ return self.__reduce_ex__(None)
412
+
413
+ def __reduce_ex__(self, protocol):
414
+ utcoff = [
415
+ int(ttinfo.utcoff.total_seconds()) for ttinfo in self._ttinfo_list
416
+ ]
417
+ utcoff = getattr(self, "_utcoff_raw", utcoff)
418
+ abbr = [ttinfo.tzname for ttinfo in self._ttinfo_list]
419
+
420
+ # dst() is inferred per transition, so the transitions don't map back
421
+ # to the local time types, and these are kept as they were in the file
422
+ trans_idx = self._trans_idx
423
+ isdst = self._isdst
424
+
425
+ trans_utc = self._trans_utc
426
+ tz_str = self._tz_str
427
+
428
+ data = (trans_idx, trans_utc, utcoff, isdst, abbr, tz_str)
429
+ args = (self.__class__, self._key, self._filename, data)
430
+
431
+ # Preserve any other instance state, such as attributes added by a
432
+ # subclass, but not the attributes that _unpickle rebuilds from data
433
+ state = {
434
+ k: v for k, v in vars(self).items() if k not in self._UNPICKLE_ATTRS
435
+ }
436
+ if state:
437
+ return (_unpickle, args, state)
438
+
439
+ return (_unpickle, args)
440
+
441
+ def _load_file(self, fobj):
442
+ # Reading the file requires seeking past the parts we don't use and
443
+ # reading the footer a line at a time, so read anything that doesn't
444
+ # support that (e.g. a pipe) into memory first; TZif files are small.
445
+ if not _is_seekable(fobj) or not hasattr(fobj, "readline"):
446
+ fobj = io.BytesIO(fobj.read())
447
+
448
+ # Retrieve all the data as it exists in the zoneinfo file
449
+ data = load_data(fobj)
450
+ self._load_from_data(data)
451
+
452
+ def _load_from_data(self, data):
453
+ trans_idx, trans_utc, utcoff, isdst, abbr, tz_str = data
454
+
455
+ # Infer the DST offsets (needed for .dst()) from the data, for each
456
+ # transition and for each local time type
457
+ trans_dstoff = self._utcoff_to_dstoff(
458
+ trans_idx, trans_utc, utcoff, isdst
459
+ )
460
+ dstoff = [3600 if dst else 0 for dst in isdst]
461
+ for idx, trans_off in zip(trans_idx, trans_dstoff):
462
+ dstoff[idx] = trans_off
463
+
464
+ if sys.version_info < (3, 6):
465
+ # For python pre-3.6, round to full-minutes if that's not the case.
466
+ # Python's datetime doesn't accept sub-minute timezones. Check
467
+ # http://python.org/sf/1447945 or https://bugs.python.org/issue5288
468
+ # for some information.
469
+ utcoff_adjusted = [60 * ((s + 30) // 60) for s in utcoff]
470
+ dstoff = [60 * ((s + 30) // 60) for s in dstoff]
471
+ trans_dstoff = [60 * ((s + 30) // 60) for s in trans_dstoff]
472
+ self._utcoff_raw = utcoff
473
+ else:
474
+ utcoff_adjusted = utcoff
475
+
476
+ # Convert all the transition times (UTC) into "seconds since 1970-01-01 local time"
477
+ trans_local = self._ts_to_local(trans_idx, trans_utc, utcoff_adjusted)
478
+
479
+ # Construct `_ttinfo` objects for each transition in the file
480
+ _ttinfo_list = [
481
+ _ttinfo(
482
+ _load_timedelta(utcoffset), _load_timedelta(dstoffset), tzname
483
+ )
484
+ for utcoffset, dstoffset, tzname in zip(
485
+ utcoff_adjusted, dstoff, abbr
486
+ )
487
+ ]
488
+
489
+ self._ttinfo_list = _ttinfo_list
490
+
491
+ self._trans_idx = trans_idx
492
+ self._isdst = isdst
493
+ self._trans_utc = trans_utc
494
+ self._trans_local = trans_local
495
+
496
+ # Transitions to the same local time type usually have the same DST
497
+ # offset, so share the _ttinfo objects between them where possible.
498
+ ttinfos = {}
499
+ self._ttinfos = []
500
+ for idx, trans_off in zip(trans_idx, trans_dstoff):
501
+ tti = _ttinfo_list[idx]
502
+ if tti.dstoff.total_seconds() != trans_off:
503
+ key = (idx, trans_off)
504
+ if key not in ttinfos:
505
+ ttinfos[key] = _ttinfo(
506
+ tti.utcoff, _load_timedelta(trans_off), tti.tzname
507
+ )
508
+ tti = ttinfos[key]
509
+ self._ttinfos.append(tti)
510
+
511
+ # Find the first non-DST transition
512
+ for i in range(len(isdst)):
513
+ if not isdst[i]:
514
+ self._tti_before = _ttinfo_list[i]
515
+ break
516
+ else:
517
+ if self._ttinfos:
518
+ self._tti_before = self._ttinfos[0]
519
+ else:
520
+ self._tti_before = None
521
+
522
+ # Set the "fallback" time zone
523
+ self._tz_str = tz_str
524
+ if tz_str is not None and tz_str != b"":
525
+ self._tz_after = _parse_tz_str(tz_str.decode())
526
+ else:
527
+ if not self._ttinfos and not _ttinfo_list:
528
+ raise ValueError("No time zone information found.")
529
+
530
+ if self._ttinfos:
531
+ self._tz_after = self._ttinfos[-1]
532
+ else:
533
+ # With no transitions and no TZ string, RFC 9636 Section 3.2
534
+ # says that local time is specified by time type 0
535
+ self._tz_after = _ttinfo_list[0]
536
+
537
+ # Determine if this is a "fixed offset" zone, meaning that the output
538
+ # of the utcoffset, dst and tzname functions does not depend on the
539
+ # specific datetime passed.
540
+ #
541
+ # We make three simplifying assumptions here:
542
+ #
543
+ # 1. If _tz_after is not a _ttinfo, it has transitions that might
544
+ # actually occur (it is possible to construct TZ strings that
545
+ # specify STD and DST but no transitions ever occur, such as
546
+ # AAA0BBB,0/0,J365/25).
547
+ # 2. If _ttinfo_list contains more than one _ttinfo object, the objects
548
+ # represent different offsets.
549
+ # 3. _ttinfo_list contains no unused _ttinfos (in which case an
550
+ # otherwise fixed-offset zone with extra _ttinfos defined may
551
+ # appear to *not* be a fixed offset zone).
552
+ #
553
+ # Violations to these assumptions would be fairly exotic, and exotic
554
+ # zones should almost certainly not be used with datetime.time (the
555
+ # only thing that would be affected by this).
556
+ if len(_ttinfo_list) > 1 or not isinstance(self._tz_after, _ttinfo):
557
+ self._fixed_offset = False
558
+ elif not _ttinfo_list:
559
+ self._fixed_offset = True
560
+ else:
561
+ self._fixed_offset = _ttinfo_list[0] == self._tz_after
562
+
563
+ @staticmethod
564
+ def _utcoff_to_dstoff(trans_idx, trans_utc, utcoffsets, isdsts):
565
+ """Infer the DST offset (the value of dst()) for each transition.
566
+
567
+ TZif files give the total UTC offset of each local time type and
568
+ whether it is DST, but not how much of the offset is DST, so this has
569
+ to be worked out from the standard time on either side of each DST
570
+ period. That is done for each transition rather than for each local
571
+ time type, because zic reuses a local time type for periods with
572
+ different standard offsets (e.g. UTC+0 DST in the Azores was double
573
+ summer time in the 1940s and summer time later).
574
+ """
575
+ n = len(trans_idx)
576
+
577
+ # The DST offset implied by the last standard time before and the
578
+ # first standard time after each transition. Before the first
579
+ # transition, it's the first standard time type, as for _tti_before.
580
+ first_std_utcoff = next(
581
+ (utcoff for utcoff, dst in zip(utcoffsets, isdsts) if not dst),
582
+ None,
583
+ )
584
+ before = [None] * n
585
+ after = [None] * n
586
+ for indices, implied, std_utcoff in (
587
+ (range(n), before, first_std_utcoff),
588
+ (range(n - 1, -1, -1), after, None),
589
+ ):
590
+ for i in indices:
591
+ idx = trans_idx[i]
592
+ if not isdsts[idx]:
593
+ std_utcoff = utcoffsets[idx]
594
+ elif std_utcoff is not None:
595
+ implied[i] = utcoffsets[idx] - std_utcoff
596
+
597
+ # DST offsets where the standard time is the same on both sides
598
+ clear_cut = [
599
+ (trans_utc[i], before[i])
600
+ for i in range(n)
601
+ if before[i] and before[i] == after[i]
602
+ ]
603
+
604
+ dstoffs = [0] * n
605
+ for i in range(n):
606
+ if not isdsts[trans_idx[i]]:
607
+ continue
608
+
609
+ candidates = {c for c in (before[i], after[i]) if c}
610
+ if len(candidates) > 1:
611
+ # The standard offset changed during the DST period. A change
612
+ # from local mean time can give something like 0:34:39, which
613
+ # is not a plausible amount of DST.
614
+ plausible = {c for c in candidates if c % 300 == 0}
615
+ candidates = plausible or candidates
616
+
617
+ if len(candidates) > 1:
618
+ # Prefer the amount of DST the zone used around the same
619
+ # time, e.g. Tehran used 1:00 and Rarotonga 0:30, though the
620
+ # TZif data for Tehran in 1977 and for Rarotonga in 1978 look
621
+ # the same. This has to be local in time, because some zones
622
+ # used different amounts in different eras (e.g. Montevideo
623
+ # used 0:30 in the 1920s and 1:00 later).
624
+ nearby = {
625
+ dstoff
626
+ for ts, dstoff in clear_cut
627
+ if abs(ts - trans_utc[i]) <= _DST_CONSENSUS_WINDOW
628
+ }
629
+ consensus = candidates & nearby
630
+ if len(consensus) == 1:
631
+ candidates = consensus
632
+
633
+ if candidates:
634
+ # Otherwise, prefer positive DST, then the smaller amount. In
635
+ # the 1942 occupation zones, e.g. Europe/Kyiv, the standard
636
+ # time before gives -1:00 and the one after +1:00.
637
+ dstoffs[i] = min(candidates, key=lambda c: (c < 0, abs(c)))
638
+ else:
639
+ # Without any standard time to compare to, one hour is a much
640
+ # better guess than 0
641
+ dstoffs[i] = 3600
642
+
643
+ return dstoffs
644
+
645
+ @staticmethod
646
+ def _ts_to_local(trans_idx, trans_list_utc, utcoffsets):
647
+ """Generate number of seconds since 1970 *in the local time*.
648
+
649
+ This is necessary to easily find the transition times in local time"""
650
+ if not trans_list_utc:
651
+ return [[], []]
652
+
653
+ # Start with the timestamps and modify in-place
654
+ trans_list_wall = [list(trans_list_utc), list(trans_list_utc)]
655
+
656
+ if len(utcoffsets) > 1:
657
+ offset_0 = utcoffsets[0]
658
+ offset_1 = utcoffsets[trans_idx[0]]
659
+ if offset_1 > offset_0:
660
+ offset_1, offset_0 = offset_0, offset_1
661
+ else:
662
+ offset_0 = offset_1 = utcoffsets[0]
663
+
664
+ trans_list_wall[0][0] += offset_0
665
+ trans_list_wall[1][0] += offset_1
666
+
667
+ for i in range(1, len(trans_idx)):
668
+ offset_0 = utcoffsets[trans_idx[i - 1]]
669
+ offset_1 = utcoffsets[trans_idx[i]]
670
+
671
+ if offset_1 > offset_0:
672
+ offset_1, offset_0 = offset_0, offset_1
673
+
674
+ trans_list_wall[0][i] += offset_0
675
+ trans_list_wall[1][i] += offset_1
676
+
677
+ return trans_list_wall
678
+
679
+ def __eq__(self, other):
680
+ if not isinstance(other, tzfile):
681
+ return NotImplemented
682
+
683
+ return (
684
+ self._trans_utc == other._trans_utc
685
+ and self._trans_local == other._trans_local
686
+ and self._ttinfos == other._ttinfos
687
+ and self._tti_before == other._tti_before
688
+ and self._fallback_key() == other._fallback_key()
689
+ )
690
+
691
+ def _fallback_key(self):
692
+ # The behavior after the last transition is determined either by the
693
+ # TZ string or, in its absence, by a single _ttinfo; _TZStr objects
694
+ # do not define equality, so compare the string they came from.
695
+ if isinstance(self._tz_after, _ttinfo):
696
+ return self._tz_after
697
+ return self._tz_str
698
+
699
+ __hash__ = None
700
+
701
+ def __ne__(self, other):
702
+ return not (self == other)
703
+
704
+ def __repr__(self):
705
+ return "%s(%s)" % (self.__class__.__name__, repr(self._filename))
706
+
707
+
708
+ # Before Python 3.16, pipes on Windows claim to be seekable, but seeking on
709
+ # them silently does nothing. See https://github.com/python/cpython/issues/86768
710
+ _SEEKABLE_PIPES_BUG = sys.platform == "win32" and sys.version_info < (3, 16)
711
+
712
+
713
+ def _is_seekable(fobj):
714
+ if not hasattr(fobj, "seek"):
715
+ return False
716
+
717
+ seekable = getattr(fobj, "seekable", None)
718
+ # Python 2 file objects can seek but have no seekable() method
719
+ if seekable is not None and not seekable():
720
+ return False
721
+
722
+ if not _SEEKABLE_PIPES_BUG:
723
+ return True
724
+
725
+ # For anything backed by a file descriptor, only trust regular files.
726
+ try:
727
+ fd = fobj.fileno()
728
+ except (AttributeError, OSError, ValueError):
729
+ # io.UnsupportedOperation (e.g. from BytesIO) is both of the latter
730
+ return True
731
+
732
+ try:
733
+ return stat.S_ISREG(os.fstat(fd).st_mode)
734
+ except OSError:
735
+ return True
736
+
737
+
738
+ def load_data(fobj):
739
+ try:
740
+ return _load_data(fobj)
741
+ except struct.error as e:
742
+ # struct.unpack fails when a read comes up short
743
+ six.raise_from(
744
+ ValueError("Invalid TZif file: unexpected end of file"), e
745
+ )
746
+
747
+
748
+ def _load_data(fobj):
749
+ header = _TZifHeader.from_file(fobj)
750
+
751
+ if header.version == 1:
752
+ time_size = 4
753
+ time_type = "l"
754
+ else:
755
+ # Version 2+ has 64-bit integer transition times
756
+ time_size = 8
757
+ time_type = "q"
758
+
759
+ # Version 2+ also starts with a Version 1 header and data, which
760
+ # we need to skip now
761
+ skip_bytes = (
762
+ header.timecnt * 5 # Transition times and types
763
+ + header.typecnt * 6 # Local time type records
764
+ + header.charcnt # Time zone designations
765
+ + header.leapcnt * 8 # Leap second records
766
+ + header.isstdcnt # Standard/wall indicators
767
+ + header.isutcnt # UT/local indicators
768
+ )
769
+
770
+ fobj.seek(skip_bytes, 1)
771
+
772
+ # Now we need to read the second header, which is not the same
773
+ # as the first
774
+ header = _TZifHeader.from_file(fobj)
775
+
776
+ typecnt = header.typecnt
777
+ timecnt = header.timecnt
778
+ charcnt = header.charcnt
779
+
780
+ # The data portion starts with timecnt transitions and indices
781
+ if timecnt:
782
+ trans_list_utc = struct.unpack(
783
+ ">%s%s" % (timecnt, time_type), fobj.read(timecnt * time_size)
784
+ )
785
+ trans_idx = struct.unpack(">%sB" % timecnt, fobj.read(timecnt))
786
+
787
+ if max(trans_idx) >= typecnt:
788
+ raise ValueError(
789
+ "Invalid transition index found while reading TZif: %s"
790
+ % max(trans_idx)
791
+ )
792
+ else:
793
+ trans_list_utc = ()
794
+ trans_idx = ()
795
+
796
+ # Read the ttinfo struct, (utoff, isdst, abbrind)
797
+ if typecnt:
798
+ utcoff, isdst, abbrind = zip(
799
+ *(struct.unpack(">lbb", fobj.read(6)) for i in range(typecnt))
800
+ )
801
+ else:
802
+ utcoff = ()
803
+ isdst = ()
804
+ abbrind = ()
805
+
806
+ # Now read the abbreviations. They are null-terminated strings, indexed
807
+ # not by position in the array but by position in the unsplit
808
+ # abbreviation string. I suppose this makes more sense in C, which uses
809
+ # null to terminate the strings, but it's inconvenient here...
810
+ abbr_vals = {}
811
+ abbr_chars = fobj.read(charcnt)
812
+
813
+ def get_abbr(idx):
814
+ # Gets a string starting at idx and running until the next \x00
815
+ #
816
+ # We cannot pre-populate abbr_vals by splitting on \x00 because there
817
+ # are some zones that use subsets of longer abbreviations, like so:
818
+ #
819
+ # LMT\x00AHST\x00HDT\x00
820
+ #
821
+ # Where the idx to abbr mapping should be:
822
+ #
823
+ # {0: "LMT", 4: "AHST", 5: "HST", 9: "HDT"}
824
+ if idx not in abbr_vals:
825
+ span_end = abbr_chars.find(b"\x00", idx)
826
+ abbr_vals[idx] = abbr_chars[idx:span_end].decode()
827
+
828
+ return abbr_vals[idx]
829
+
830
+ abbr = tuple(get_abbr(idx) for idx in abbrind)
831
+
832
+ # The remainder of the file consists of leap seconds (currently unused) and
833
+ # the standard/wall and ut/local indicators, which are metadata we don't need.
834
+ # In version 2 files, we need to skip the unnecessary data to get at the TZ string,
835
+ # and in version 1 files we'll just hopefully consume the rest of the file.
836
+
837
+ # Each leap second record has size (time_size + 4)
838
+ skip_bytes = (
839
+ header.isutcnt + header.isstdcnt + header.leapcnt * (time_size + 4)
840
+ )
841
+ fobj.seek(skip_bytes, 1)
842
+ if header.version >= 2:
843
+ c = fobj.read(1) # Should be \n
844
+ if c != b"\n":
845
+ raise ValueError("Invalid TZif file: expected newline, got %r" % c)
846
+
847
+ line = fobj.readline()
848
+ if not line.endswith(b"\n"):
849
+ raise ValueError("Invalid TZif file: unexpected end of file")
850
+ tz_str = line.rstrip(b"\n")
851
+ else:
852
+ tz_str = None
853
+
854
+ return trans_idx, trans_list_utc, utcoff, isdst, abbr, tz_str
855
+
856
+
857
+ # How close in time (in seconds) the DST periods that _utcoff_to_dstoff uses
858
+ # to choose between two possible DST offsets have to be
859
+ _DST_CONSENSUS_WINDOW = 10 * 365 * 86400
860
+
861
+
862
+ class _TZifHeader(object):
863
+ __slots__ = [
864
+ "version",
865
+ "isutcnt",
866
+ "isstdcnt",
867
+ "leapcnt",
868
+ "timecnt",
869
+ "typecnt",
870
+ "charcnt",
871
+ ]
872
+
873
+ def __init__(self, *args):
874
+ assert len(self.__slots__) == len(args)
875
+ for attr, val in zip(self.__slots__, args):
876
+ setattr(self, attr, val)
877
+
878
+ @classmethod
879
+ def from_file(cls, stream):
880
+ # The header starts with a 4-byte "magic" value
881
+ if stream.read(4) != b"TZif":
882
+ raise ValueError("Invalid TZif file: magic not found")
883
+
884
+ _version = stream.read(1)
885
+ if _version == b"\x00":
886
+ version = 1
887
+ else:
888
+ version = int(_version)
889
+ stream.read(15)
890
+
891
+ args = (version,)
892
+
893
+ # Slots are defined in the order that the bytes are arranged
894
+ args = args + struct.unpack(">6l", stream.read(24))
895
+
896
+ return cls(*args)
897
+
898
+
899
+ class _ttinfo(object):
900
+ __slots__ = ["utcoff", "dstoff", "tzname"]
901
+
902
+ def __init__(self, utcoff, dstoff, tzname):
903
+ self.utcoff = utcoff
904
+ self.dstoff = dstoff
905
+ self.tzname = tzname
906
+
907
+ def __eq__(self, other):
908
+ if not isinstance(other, _ttinfo):
909
+ return NotImplemented
910
+
911
+ return (
912
+ self.utcoff == other.utcoff
913
+ and self.dstoff == other.dstoff
914
+ and self.tzname == other.tzname
915
+ )
916
+
917
+ def __ne__(self, other):
918
+ # Python 2 does not derive __ne__ from __eq__
919
+ eq = self.__eq__(other)
920
+ if eq is NotImplemented:
921
+ return eq
922
+ return not eq
923
+
924
+ def __repr__(self): # pragma: nocover
925
+ return "%s(%s, %s, %s)" % (
926
+ self.__class__.__name__,
927
+ self.utcoff,
928
+ self.dstoff,
929
+ self.tzname,
930
+ )
931
+
932
+
933
+ _NO_TTINFO = _ttinfo(None, None, None)
934
+
935
+
936
+ def _unpack(t):
937
+ # Python 2-specific backport for x, *y = t
938
+ return t[0], t[1:]
939
+
940
+
941
+ # re.ASCII does not exist on Python 2, where str patterns are ASCII-only
942
+ # anyway (and unicode patterns are too unless re.UNICODE is passed).
943
+ _RE_ASCII = getattr(re, "ASCII", 0)
944
+
945
+
946
+ def _fullmatch(pattern, string, flags=0):
947
+ # Python 2-specific backport of re.fullmatch
948
+ return re.match(r"(?:%s)\Z" % pattern, string, flags)
949
+
950
+
951
+ def _round_offset(seconds):
952
+ # Python < 3.6 does not support sub-minute offsets, see the comment in
953
+ # tzfile._load_from_data.
954
+ if sys.version_info < (3, 6):
955
+ return 60 * ((seconds + 30) // 60)
956
+ return seconds
957
+
958
+
959
+ def _parse_tz_str(tz_str):
960
+ # The tz string has the format:
961
+ #
962
+ # std[offset[dst[offset],start[/time],end[/time]]]
963
+ #
964
+ # std and dst must be 3 or more characters long and must not contain
965
+ # a leading colon, embedded digits, commas, nor a plus or minus signs;
966
+ # The spaces between "std" and "offset" are only for display and are
967
+ # not actually present in the string.
968
+ #
969
+ # The format of the offset is ``[+|-]hh[:mm[:ss]]``
970
+
971
+ # TODO: When we drop Python 2, this should be offset_str, *start_end_str = ...
972
+ offset_str, start_end_str = _unpack(tz_str.split(",", 1))
973
+
974
+ parser_re = (
975
+ r"(?P<std>[a-zA-Z]+|<[a-zA-Z0-9+-]+>)"
976
+ r"(?:"
977
+ r"(?P<stdoff>[+-]?\d{1,3}(?::\d{2}(?::\d{2})?)?)"
978
+ r"(?:"
979
+ r"(?P<dst>[a-zA-Z]+|<[a-zA-Z0-9+-]+>)"
980
+ r"(?P<dstoff>[+-]?\d{1,3}(?::\d{2}(?::\d{2})?)?)?"
981
+ r")?" # dst
982
+ r")?" # stdoff
983
+ )
984
+
985
+ m = _fullmatch(parser_re, offset_str, _RE_ASCII)
986
+
987
+ if m is None:
988
+ raise ValueError("%s is not a valid TZ string" % tz_str)
989
+
990
+ std_abbr = m.group("std")
991
+ dst_abbr = m.group("dst")
992
+ dst_offset = None
993
+
994
+ std_abbr = std_abbr.strip("<>")
995
+
996
+ if dst_abbr:
997
+ dst_abbr = dst_abbr.strip("<>")
998
+
999
+ std_offset = m.group("stdoff")
1000
+ if std_offset:
1001
+ try:
1002
+ std_offset = _round_offset(_parse_tz_delta(std_offset))
1003
+ except ValueError as e:
1004
+ raise ValueError("Invalid STD offset in %s" % tz_str)
1005
+ else:
1006
+ # The STD offset is required
1007
+ raise ValueError("Invalid STD offset in %s" % tz_str)
1008
+
1009
+ if dst_abbr is not None:
1010
+ dst_offset = m.group("dstoff")
1011
+ if dst_offset:
1012
+ try:
1013
+ dst_offset = _round_offset(_parse_tz_delta(dst_offset))
1014
+ except ValueError as e:
1015
+ raise ValueError("Invalid DST offset in %s" % tz_str)
1016
+ else:
1017
+ dst_offset = std_offset + 3600
1018
+
1019
+ if not start_end_str:
1020
+ raise ValueError("Missing transition rules: %s" % tz_str)
1021
+
1022
+ start_end_strs = start_end_str[0].split(",", 1)
1023
+ try:
1024
+ start, end = (_parse_dst_start_end(x) for x in start_end_strs)
1025
+ except ValueError as e:
1026
+ raise ValueError("Invalid TZ string: %s" % tz_str)
1027
+
1028
+ return _TZStr(std_abbr, std_offset, dst_abbr, dst_offset, start, end)
1029
+ elif start_end_str:
1030
+ raise ValueError("Transition rule present without DST: %s" % tz_str)
1031
+ else:
1032
+ # This is a static ttinfo, don't return _TZStr
1033
+ return _ttinfo(
1034
+ _load_timedelta(std_offset), _load_timedelta(0), std_abbr
1035
+ )
1036
+
1037
+
1038
+ def _parse_dst_start_end(dststr):
1039
+ date, time = _unpack(dststr.split("/", 1))
1040
+ date_type = date[:1]
1041
+ if date_type == "M":
1042
+ n_is_julian = False
1043
+ m = _fullmatch(r"M(\d{1,2})\.(\d)\.(\d)", date, _RE_ASCII)
1044
+ if m is None:
1045
+ raise ValueError("Invalid dst start/end date: %s" % dststr)
1046
+ date_offset = tuple(map(int, m.groups()))
1047
+ offset = _CalendarOffset(*date_offset)
1048
+ else:
1049
+ if date_type == "J":
1050
+ n_is_julian = True
1051
+ date = date[1:]
1052
+ else:
1053
+ n_is_julian = False
1054
+
1055
+ if _fullmatch(r"\d{1,3}", date, _RE_ASCII) is None:
1056
+ raise ValueError("Invalid dst start/end date: %s" % dststr)
1057
+ doy = int(date)
1058
+ offset = _DayOffset(doy, n_is_julian)
1059
+
1060
+ if time:
1061
+ offset.hour, offset.minute, offset.second = _parse_transition_time(
1062
+ time[0]
1063
+ )
1064
+
1065
+ return offset
1066
+
1067
+
1068
+ def _parse_transition_time(time_str):
1069
+ match = _fullmatch(
1070
+ r"(?P<sign>[+-])?(?P<h>\d{1,3})(:(?P<m>\d{2})(:(?P<s>\d{2}))?)?",
1071
+ time_str,
1072
+ _RE_ASCII,
1073
+ )
1074
+ if match is None:
1075
+ raise ValueError("Invalid time: %s" % time_str)
1076
+
1077
+ h, m, s = (int(v or 0) for v in map(match.group, ("h", "m", "s")))
1078
+
1079
+ if h > 167:
1080
+ raise ValueError("Hour must be in [0, 167]: %s" % time_str)
1081
+
1082
+ if match.group("sign") == "-":
1083
+ h, m, s = -h, -m, -s
1084
+
1085
+ return h, m, s
1086
+
1087
+
1088
+ def _parse_tz_delta(tz_delta):
1089
+ match = _fullmatch(
1090
+ r"(?P<sign>[+-])?(?P<h>\d{1,3})(:(?P<m>\d{2})(:(?P<s>\d{2}))?)?",
1091
+ tz_delta,
1092
+ _RE_ASCII,
1093
+ )
1094
+ # Anything passed to this function should already have hit an equivalent
1095
+ # regular expression to find the section to parse.
1096
+ assert match is not None, tz_delta
1097
+
1098
+ h, m, s = (int(v or 0) for v in map(match.group, ("h", "m", "s")))
1099
+
1100
+ total = h * 3600 + m * 60 + s
1101
+
1102
+ if h > 24:
1103
+ raise ValueError("Offset hours must be in [0, 24]: %s" % tz_delta)
1104
+
1105
+ # Yes, +5 maps to an offset of -5h
1106
+ if match.group("sign") != "-":
1107
+ total *= -1
1108
+
1109
+ return total
1110
+
1111
+
1112
+ class _TZStr(object):
1113
+ __slots__ = (
1114
+ "std",
1115
+ "dst",
1116
+ "start",
1117
+ "end",
1118
+ "get_trans_info",
1119
+ "get_trans_info_fromutc",
1120
+ "dst_diff",
1121
+ )
1122
+
1123
+ def __init__(
1124
+ self, std_abbr, std_offset, dst_abbr, dst_offset, start=None, end=None
1125
+ ):
1126
+ self.dst_diff = dst_offset - std_offset
1127
+ std_offset = _load_timedelta(std_offset)
1128
+ self.std = _ttinfo(
1129
+ utcoff=std_offset, dstoff=_load_timedelta(0), tzname=std_abbr
1130
+ )
1131
+
1132
+ self.start = start
1133
+ self.end = end
1134
+
1135
+ dst_offset = _load_timedelta(dst_offset)
1136
+ delta = _load_timedelta(self.dst_diff)
1137
+ self.dst = _ttinfo(utcoff=dst_offset, dstoff=delta, tzname=dst_abbr)
1138
+
1139
+ # These are assertions because the constructor should only be called
1140
+ # by functions that would fail before passing start or end
1141
+ assert start is not None, "No transition start specified"
1142
+ assert end is not None, "No transition end specified"
1143
+
1144
+ self.get_trans_info = self._get_trans_info
1145
+ self.get_trans_info_fromutc = self._get_trans_info_fromutc
1146
+
1147
+ def transitions(self, year):
1148
+ start = self.start.year_to_epoch(year)
1149
+ end = self.end.year_to_epoch(year)
1150
+ return start, end
1151
+
1152
+ def _near_new_year(self, ts, year):
1153
+ """Whether ts is close enough to the new year that the rules for the
1154
+ previous or next year might matter."""
1155
+ year_start = _post_epoch_days_before_year(year) * 86400
1156
+ year_end = _post_epoch_days_before_year(year + 1) * 86400
1157
+ return (
1158
+ ts < year_start + _NEW_YEAR_MARGIN
1159
+ or ts >= year_end - _NEW_YEAR_MARGIN
1160
+ )
1161
+
1162
+ def _get_trans_info(self, ts, year, fold):
1163
+ """Get the information about the current transition - tti"""
1164
+ if self._near_new_year(ts, year):
1165
+ return self._get_trans_info_near_new_year(ts, fold)
1166
+
1167
+ start, end = self.transitions(year)
1168
+
1169
+ # With fold = 0, the period (denominated in local time) with the
1170
+ # smaller offset starts at the end of the gap and ends at the end of
1171
+ # the fold; with fold = 1, it runs from the start of the gap to the
1172
+ # beginning of the fold.
1173
+ #
1174
+ # So in order to determine the DST boundaries we need to know both
1175
+ # the fold and whether DST is positive or negative (rare), and it
1176
+ # turns out that this boils down to fold XOR is_positive.
1177
+ if fold == (self.dst_diff >= 0):
1178
+ end -= self.dst_diff
1179
+ else:
1180
+ start += self.dst_diff
1181
+
1182
+ if start < end:
1183
+ isdst = start <= ts < end
1184
+ else:
1185
+ isdst = not (end <= ts < start)
1186
+
1187
+ return self.dst if isdst else self.std
1188
+
1189
+ def _get_trans_info_near_new_year(self, ts, fold):
1190
+ # Answer the question in terms of UTC, where the transitions from the
1191
+ # adjacent years are taken into account: a local time is valid for
1192
+ # an offset if the rules give that offset at the corresponding UTC
1193
+ # time.
1194
+ valid = []
1195
+ for tti in (self.std, self.dst):
1196
+ utc_ts = ts - tti.utcoff.total_seconds()
1197
+ try:
1198
+ utc_year = (EPOCH + timedelta(seconds=utc_ts)).year
1199
+ except OverflowError:
1200
+ # Before year 1 or after year 9999
1201
+ utc_year = MINYEAR if utc_ts < 0 else MAXYEAR
1202
+ actual, _ = self._get_trans_info_fromutc_near_new_year(
1203
+ utc_ts, utc_year
1204
+ )
1205
+ if actual is tti:
1206
+ valid.append(tti)
1207
+
1208
+ if len(valid) == 1:
1209
+ return valid[0]
1210
+
1211
+ smaller, larger = sorted((self.std, self.dst), key=lambda t: t.utcoff)
1212
+ if valid:
1213
+ # Ambiguous: fold=0 is the first occurrence, with the larger offset
1214
+ return larger if fold == 0 else smaller
1215
+ else:
1216
+ # Imaginary: fold=0 uses the offset from before the transition
1217
+ return smaller if fold == 0 else larger
1218
+
1219
+ def _get_trans_info_fromutc_near_new_year(self, ts, year):
1220
+ # The rules are in terms of the local year, which near the new year
1221
+ # is not always the UTC year, and a rule can put a transition up to a
1222
+ # week into the previous or next year (e.g. "J365/25"), so look at the
1223
+ # transitions for the years on either side as well and use the last
1224
+ # one before ts.
1225
+ std_utcoff = self.std.utcoff.total_seconds()
1226
+ dst_utcoff = self.dst.utcoff.total_seconds()
1227
+
1228
+ transitions = []
1229
+ for y in (year - 1, year, year + 1):
1230
+ start, end = self.transitions(y)
1231
+ transitions.append((start - std_utcoff, True))
1232
+ transitions.append((end - dst_utcoff, False))
1233
+
1234
+ # When one DST period ends at the same instant as the next one starts,
1235
+ # as with "DST all year" rules like "0/0,J365/25", sorting puts the
1236
+ # end first, so these stay in DST without a transition in between.
1237
+ transitions.sort()
1238
+
1239
+ isdst = not transitions[0][1]
1240
+ last_change = None
1241
+ i = 0
1242
+ while i < len(transitions) and transitions[i][0] <= ts:
1243
+ trans_ts = transitions[i][0]
1244
+ new_isdst = isdst
1245
+ while i < len(transitions) and transitions[i][0] == trans_ts:
1246
+ new_isdst = transitions[i][1]
1247
+ i += 1
1248
+
1249
+ if new_isdst != isdst:
1250
+ isdst = new_isdst
1251
+ last_change = trans_ts
1252
+
1253
+ if isdst:
1254
+ tti, tti_prev = self.dst, self.std
1255
+ else:
1256
+ tti, tti_prev = self.std, self.dst
1257
+
1258
+ # A transition to a smaller offset creates a fold just after it.
1259
+ fold = False
1260
+ if last_change is not None:
1261
+ shift = (tti_prev.utcoff - tti.utcoff).total_seconds()
1262
+ fold = shift > ts - last_change
1263
+
1264
+ return (tti, fold)
1265
+
1266
+ def _get_trans_info_fromutc(self, ts, year):
1267
+ if self._near_new_year(ts, year):
1268
+ return self._get_trans_info_fromutc_near_new_year(ts, year)
1269
+
1270
+ start, end = self.transitions(year)
1271
+ start -= self.std.utcoff.total_seconds()
1272
+ end -= self.dst.utcoff.total_seconds()
1273
+
1274
+ if start < end:
1275
+ isdst = start <= ts < end
1276
+ else:
1277
+ isdst = not (end <= ts < start)
1278
+
1279
+ # For positive DST, the ambiguous period is one dst_diff after the end
1280
+ # of DST; for negative DST, the ambiguous period is one dst_diff before
1281
+ # the start of DST.
1282
+ if self.dst_diff > 0:
1283
+ ambig_start = end
1284
+ ambig_end = end + self.dst_diff
1285
+ else:
1286
+ ambig_start = start
1287
+ ambig_end = start - self.dst_diff
1288
+
1289
+ fold = ambig_start <= ts < ambig_end
1290
+
1291
+ return (self.dst if isdst else self.std, fold)
1292
+
1293
+
1294
+ # A rule can put a transition up to 167 hours (just under a week) away from
1295
+ # the day it names, and local time is at most a day or so away from UTC, so
1296
+ # only times within this distance of the new year can be affected by the
1297
+ # rules for the previous or next year.
1298
+ _NEW_YEAR_MARGIN = 10 * 86400
1299
+
1300
+
1301
+ def _post_epoch_days_before_year(year):
1302
+ """Get the number of days between 1970-01-01 and YEAR-01-01"""
1303
+ y = year - 1
1304
+ return y * 365 + y // 4 - y // 100 + y // 400 + 1 - EPOCHORDINAL
1305
+
1306
+
1307
+ class _DayOffset(object):
1308
+ __slots__ = ["d", "julian", "hour", "minute", "second"]
1309
+
1310
+ def __init__(self, d, julian, hour=2, minute=0, second=0):
1311
+ min_day = 0 + julian # convert bool to int
1312
+ if not min_day <= d <= 365:
1313
+ raise ValueError("d must be in [%s, 365], not: %s" % (min_day, d))
1314
+
1315
+ self.d = d
1316
+ self.julian = julian
1317
+ self.hour = hour
1318
+ self.minute = minute
1319
+ self.second = second
1320
+
1321
+ def year_to_epoch(self, year):
1322
+ days_before_year = _post_epoch_days_before_year(year)
1323
+
1324
+ # "n" counts days from 0 and includes February 29; "Jn" counts days
1325
+ # from 1 and never counts February 29, so in leap years, J60 (March 1)
1326
+ # and every day after it is one day later than its number suggests.
1327
+ d = self.d
1328
+ if self.julian:
1329
+ d -= 1
1330
+ if d >= 59 and calendar.isleap(year):
1331
+ d += 1
1332
+
1333
+ epoch = (days_before_year + d) * 86400
1334
+ epoch += self.hour * 3600 + self.minute * 60 + self.second
1335
+
1336
+ return epoch
1337
+
1338
+
1339
+ class _CalendarOffset(object):
1340
+ __slots__ = ["m", "w", "d", "hour", "minute", "second"]
1341
+
1342
+ _DAYS_BEFORE_MONTH = (
1343
+ -1,
1344
+ 0,
1345
+ 31,
1346
+ 59,
1347
+ 90,
1348
+ 120,
1349
+ 151,
1350
+ 181,
1351
+ 212,
1352
+ 243,
1353
+ 273,
1354
+ 304,
1355
+ 334,
1356
+ )
1357
+
1358
+ def __init__(self, m, w, d, hour=2, minute=0, second=0):
1359
+ if not 1 <= m <= 12:
1360
+ raise ValueError("m must be in [1, 12]")
1361
+
1362
+ if not 1 <= w <= 5:
1363
+ raise ValueError("w must be in [1, 5]")
1364
+
1365
+ if not 0 <= d <= 6:
1366
+ raise ValueError("d must be in [0, 6]")
1367
+
1368
+ self.m = m
1369
+ self.w = w
1370
+ self.d = d
1371
+ self.hour = hour
1372
+ self.minute = minute
1373
+ self.second = second
1374
+
1375
+ @classmethod
1376
+ def _ymd2ord(cls, year, month, day):
1377
+ return (
1378
+ _post_epoch_days_before_year(year)
1379
+ + cls._DAYS_BEFORE_MONTH[month]
1380
+ + (month > 2 and calendar.isleap(year))
1381
+ + day
1382
+ - 1
1383
+ )
1384
+
1385
+ # TODO: These are not actually epoch dates as they are expressed in local time
1386
+ def year_to_epoch(self, year):
1387
+ """Calculates the datetime of the occurrence from the year"""
1388
+ # We know year and month, we need to convert w, d into day of month
1389
+ #
1390
+ # Week 1 is the first week in which day `d` (where 0 = Sunday) appears.
1391
+ # Week 5 represents the last occurrence of day `d`, so we need to know
1392
+ # the range of the month.
1393
+ #
1394
+ # This doesn't use calendar.monthrange because the rules for years 0
1395
+ # and 10000 can affect times that datetime can represent, and
1396
+ # calendar.monthrange constructs a datetime.date. Instead, get the
1397
+ # weekday (0 = Monday) from the ordinal: 1970-01-01 was a Thursday.
1398
+ first_ordinal = self._ymd2ord(year, self.m, 1)
1399
+ first_day = (first_ordinal + 3) % 7
1400
+ if self.m == 12:
1401
+ next_ordinal = self._ymd2ord(year + 1, 1, 1)
1402
+ else:
1403
+ next_ordinal = self._ymd2ord(year, self.m + 1, 1)
1404
+ days_in_month = next_ordinal - first_ordinal
1405
+
1406
+ # This equation seems magical, so I'll break it down:
1407
+ # 1. calendar says 0 = Monday, POSIX says 0 = Sunday
1408
+ # so we need first_day + 1 to get 1 = Monday -> 7 = Sunday,
1409
+ # which is still equivalent because this math is mod 7
1410
+ # 2. Get first day - desired day mod 7: -1 % 7 = 6, so we don't need
1411
+ # to do anything to adjust negative numbers.
1412
+ # 3. Add 1 because month days are a 1-based index.
1413
+ month_day = (self.d - (first_day + 1)) % 7 + 1
1414
+
1415
+ # Now use a 0-based index version of `w` to calculate the w-th
1416
+ # occurrence of `d`
1417
+ month_day += (self.w - 1) * 7
1418
+
1419
+ # month_day will only be > days_in_month if w was 5, and `w` means
1420
+ # "last occurrence of `d`", so now we just check if we over-shot the
1421
+ # end of the month and if so knock off 1 week.
1422
+ if month_day > days_in_month:
1423
+ month_day -= 7
1424
+
1425
+ ordinal = self._ymd2ord(year, self.m, month_day)
1426
+ epoch = ordinal * 86400
1427
+ epoch += self.hour * 3600 + self.minute * 60 + self.second
1428
+ return epoch