Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/dateutil/tz/tz.py: 21%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2"""
3This module offers timezone implementations subclassing the abstract
4:py:class:`datetime.tzinfo` type. There are classes to handle tzfile format
5files (usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`,
6etc), TZ environment string (in all known formats), given ranges (with help
7from relative deltas), local machine timezone, fixed offset timezone, and UTC
8timezone.
9"""
11import bisect
12import datetime
13import os
14import struct
15import sys
16import time
17import warnings
18import weakref
19from collections import OrderedDict
21import six
22from six import string_types
23from six.moves import _thread
25from .. import _tzdata_impl
26from . import _tzpath
27from ._common import (
28 _tzinfo,
29 _validate_fromutc_inputs,
30 enfold,
31 tzname_in_python2,
32 tzrangebase,
33)
34from ._factories import _TzOffsetFactory, _TzSingleton, _TzStrFactory
35from ._tzfile import tzfile
37try:
38 from .win import tzwin, tzwinlocal
39except ImportError:
40 tzwin = tzwinlocal = None
42# For warning about rounding tzinfo
43from warnings import warn
45ZERO = datetime.timedelta(0)
46EPOCH = datetime.datetime(1970, 1, 1, 0, 0)
49@six.add_metaclass(_TzSingleton)
50class tzutc(datetime.tzinfo):
51 """
52 This is a tzinfo object that represents the UTC time zone.
54 **Examples:**
56 .. doctest::
58 >>> from datetime import *
59 >>> from dateutil.tz import *
61 >>> datetime.now()
62 datetime.datetime(2003, 9, 27, 9, 40, 1, 521290)
64 >>> datetime.now(tzutc())
65 datetime.datetime(2003, 9, 27, 12, 40, 12, 156379, tzinfo=tzutc())
67 >>> datetime.now(tzutc()).tzname()
68 'UTC'
70 .. versionchanged:: 2.7.0
71 ``tzutc()`` is now a singleton, so the result of ``tzutc()`` will
72 always return the same object.
74 .. doctest::
76 >>> from dateutil.tz import tzutc, UTC
77 >>> tzutc() is tzutc()
78 True
79 >>> tzutc() is UTC
80 True
81 """
82 def utcoffset(self, dt):
83 return ZERO
85 def dst(self, dt):
86 return ZERO
88 @tzname_in_python2
89 def tzname(self, dt):
90 return "UTC"
92 def is_ambiguous(self, dt):
93 """
94 Whether or not the "wall time" of a given datetime is ambiguous in this
95 zone.
97 :param dt:
98 A :py:class:`datetime.datetime`, naive or time zone aware.
101 :return:
102 Returns ``True`` if ambiguous, ``False`` otherwise.
104 .. versionadded:: 2.6.0
105 """
106 return False
108 @_validate_fromutc_inputs
109 def fromutc(self, dt):
110 """
111 Fast track version of fromutc() returns the original ``dt`` object for
112 any valid :py:class:`datetime.datetime` object.
113 """
114 return dt
116 def __eq__(self, other):
117 if not isinstance(other, (tzutc, tzoffset)):
118 return NotImplemented
120 return (isinstance(other, tzutc) or
121 (isinstance(other, tzoffset) and other._offset == ZERO))
123 __hash__ = None
125 def __ne__(self, other):
126 return not (self == other)
128 def __repr__(self):
129 return "%s()" % self.__class__.__name__
131 __reduce__ = object.__reduce__
134#: Convenience constant providing a :class:`tzutc()` instance
135#:
136#: .. versionadded:: 2.7.0
137UTC = tzutc()
140@six.add_metaclass(_TzOffsetFactory)
141class tzoffset(datetime.tzinfo):
142 """
143 A simple class for representing a fixed offset from UTC.
145 :param name:
146 The timezone name, to be returned when ``tzname()`` is called.
147 :param offset:
148 The time zone offset in seconds, or (since version 2.6.0, represented
149 as a :py:class:`datetime.timedelta` object).
150 """
151 def __init__(self, name, offset):
152 self._name = name
154 try:
155 # Allow a timedelta
156 offset = offset.total_seconds()
157 except (TypeError, AttributeError):
158 pass
160 self._offset = datetime.timedelta(seconds=_get_supported_offset(offset))
162 def utcoffset(self, dt):
163 return self._offset
165 def dst(self, dt):
166 return ZERO
168 @tzname_in_python2
169 def tzname(self, dt):
170 return self._name
172 @_validate_fromutc_inputs
173 def fromutc(self, dt):
174 return dt + self._offset
176 def is_ambiguous(self, dt):
177 """
178 Whether or not the "wall time" of a given datetime is ambiguous in this
179 zone.
181 :param dt:
182 A :py:class:`datetime.datetime`, naive or time zone aware.
183 :return:
184 Returns ``True`` if ambiguous, ``False`` otherwise.
186 .. versionadded:: 2.6.0
187 """
188 return False
190 def __eq__(self, other):
191 if not isinstance(other, tzoffset):
192 return NotImplemented
194 return self._offset == other._offset
196 __hash__ = None
198 def __ne__(self, other):
199 return not (self == other)
201 def __repr__(self):
202 return "%s(%s, %s)" % (self.__class__.__name__,
203 repr(self._name),
204 int(self._offset.total_seconds()))
206 __reduce__ = object.__reduce__
209class tzlocal(_tzinfo):
210 """
211 A :class:`tzinfo` subclass built around the ``time`` timezone functions.
212 """
213 def __init__(self):
214 super(tzlocal, self).__init__()
216 self._std_offset = datetime.timedelta(seconds=-time.timezone)
217 if time.daylight:
218 self._dst_offset = datetime.timedelta(seconds=-time.altzone)
219 else:
220 self._dst_offset = self._std_offset
222 self._dst_saved = self._dst_offset - self._std_offset
223 self._hasdst = bool(self._dst_saved)
224 self._tznames = tuple(time.tzname)
226 def utcoffset(self, dt):
227 if dt is None and self._hasdst:
228 return None
230 if self._isdst(dt):
231 return self._dst_offset
232 else:
233 return self._std_offset
235 def dst(self, dt):
236 if dt is None and self._hasdst:
237 return None
239 if self._isdst(dt):
240 return self._dst_offset - self._std_offset
241 else:
242 return ZERO
244 @tzname_in_python2
245 def tzname(self, dt):
246 return self._tznames[self._isdst(dt)]
248 def is_ambiguous(self, dt):
249 """
250 Whether or not the "wall time" of a given datetime is ambiguous in this
251 zone.
253 :param dt:
254 A :py:class:`datetime.datetime`, naive or time zone aware.
257 :return:
258 Returns ``True`` if ambiguous, ``False`` otherwise.
260 .. versionadded:: 2.6.0
261 """
262 naive_dst = self._naive_is_dst(dt)
263 return (not naive_dst and
264 (naive_dst != self._naive_is_dst(dt - self._dst_saved)))
266 def _naive_is_dst(self, dt):
267 timestamp = _datetime_to_timestamp(dt)
268 return time.localtime(timestamp + time.timezone).tm_isdst
270 def _isdst(self, dt, fold_naive=True):
271 # We can't use mktime here. It is unstable when deciding if
272 # the hour near to a change is DST or not.
273 #
274 # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour,
275 # dt.minute, dt.second, dt.weekday(), 0, -1))
276 # return time.localtime(timestamp).tm_isdst
277 #
278 # The code above yields the following result:
279 #
280 # >>> import tz, datetime
281 # >>> t = tz.tzlocal()
282 # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
283 # 'BRDT'
284 # >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname()
285 # 'BRST'
286 # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
287 # 'BRST'
288 # >>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname()
289 # 'BRDT'
290 # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
291 # 'BRDT'
292 #
293 # Here is a more stable implementation:
294 #
295 if not self._hasdst:
296 return False
298 # Check for ambiguous times:
299 dstval = self._naive_is_dst(dt)
300 fold = getattr(dt, 'fold', None)
302 if self.is_ambiguous(dt):
303 if fold is not None:
304 return not self._fold(dt)
305 else:
306 return True
308 return dstval
310 def __eq__(self, other):
311 if isinstance(other, tzlocal):
312 return (self._std_offset == other._std_offset and
313 self._dst_offset == other._dst_offset)
314 elif isinstance(other, tzutc):
315 return (not self._hasdst and
316 self._tznames[0] in {'UTC', 'GMT'} and
317 self._std_offset == ZERO)
318 elif isinstance(other, tzoffset):
319 return (not self._hasdst and
320 self._tznames[0] == other._name and
321 self._std_offset == other._offset)
322 else:
323 return NotImplemented
325 __hash__ = None
327 def __ne__(self, other):
328 return not (self == other)
330 def __repr__(self):
331 return "%s()" % self.__class__.__name__
333 __reduce__ = object.__reduce__
336class tzrange(tzrangebase):
337 """
338 The ``tzrange`` object is a time zone specified by a set of offsets and
339 abbreviations, equivalent to the way the ``TZ`` variable can be specified
340 in POSIX-like systems, but using Python delta objects to specify DST
341 start, end and offsets.
343 :param stdabbr:
344 The abbreviation for standard time (e.g. ``'EST'``).
346 :param stdoffset:
347 An integer or :class:`datetime.timedelta` object or equivalent
348 specifying the base offset from UTC.
350 If unspecified, +00:00 is used.
352 :param dstabbr:
353 The abbreviation for DST / "Summer" time (e.g. ``'EDT'``).
355 If specified, with no other DST information, DST is assumed to occur
356 and the default behavior or ``dstoffset``, ``start`` and ``end`` is
357 used. If unspecified and no other DST information is specified, it
358 is assumed that this zone has no DST.
360 If this is unspecified and other DST information is *is* specified,
361 DST occurs in the zone but the time zone abbreviation is left
362 unchanged.
364 :param dstoffset:
365 A an integer or :class:`datetime.timedelta` object or equivalent
366 specifying the UTC offset during DST. If unspecified and any other DST
367 information is specified, it is assumed to be the STD offset +1 hour.
369 :param start:
370 A :class:`relativedelta.relativedelta` object or equivalent specifying
371 the time and time of year that daylight savings time starts. To
372 specify, for example, that DST starts at 2AM on the 2nd Sunday in
373 March, pass:
375 ``relativedelta(hours=2, month=3, day=1, weekday=SU(+2))``
377 If unspecified and any other DST information is specified, the default
378 value is 2 AM on the first Sunday in April.
380 :param end:
381 A :class:`relativedelta.relativedelta` object or equivalent
382 representing the time and time of year that daylight savings time
383 ends, with the same specification method as in ``start``. One note is
384 that this should point to the first time in the *standard* zone, so if
385 a transition occurs at 2AM in the DST zone and the clocks are set back
386 1 hour to 1AM, set the ``hours`` parameter to +1.
389 **Examples:**
391 .. testsetup:: tzrange
393 from dateutil.tz import tzrange, tzstr
395 .. doctest:: tzrange
397 >>> tzstr('EST5EDT') == tzrange("EST", -18000, "EDT")
398 True
400 >>> from dateutil.relativedelta import *
401 >>> range1 = tzrange("EST", -18000, "EDT")
402 >>> range2 = tzrange("EST", -18000, "EDT", -14400,
403 ... relativedelta(hours=+2, month=4, day=1,
404 ... weekday=SU(+1)),
405 ... relativedelta(hours=+1, month=10, day=31,
406 ... weekday=SU(-1)))
407 >>> tzstr('EST5EDT') == range1 == range2
408 True
410 """
411 def __init__(self, stdabbr, stdoffset=None,
412 dstabbr=None, dstoffset=None,
413 start=None, end=None):
415 global relativedelta
416 from dateutil import relativedelta
418 self._std_abbr = stdabbr
419 self._dst_abbr = dstabbr
421 try:
422 stdoffset = stdoffset.total_seconds()
423 except (TypeError, AttributeError):
424 pass
426 try:
427 dstoffset = dstoffset.total_seconds()
428 except (TypeError, AttributeError):
429 pass
431 if stdoffset is not None:
432 self._std_offset = datetime.timedelta(seconds=stdoffset)
433 else:
434 self._std_offset = ZERO
436 if dstoffset is not None:
437 self._dst_offset = datetime.timedelta(seconds=dstoffset)
438 elif dstabbr and stdoffset is not None:
439 self._dst_offset = self._std_offset + datetime.timedelta(hours=+1)
440 else:
441 self._dst_offset = ZERO
443 if dstabbr and start is None:
444 self._start_delta = relativedelta.relativedelta(
445 hours=+2, month=4, day=1, weekday=relativedelta.SU(+1))
446 else:
447 self._start_delta = start
449 if dstabbr and end is None:
450 self._end_delta = relativedelta.relativedelta(
451 hours=+1, month=10, day=31, weekday=relativedelta.SU(-1))
452 else:
453 self._end_delta = end
455 self._dst_base_offset_ = self._dst_offset - self._std_offset
456 self.hasdst = bool(self._start_delta)
458 def transitions(self, year):
459 """
460 For a given year, get the DST on and off transition times, expressed
461 always on the standard time side. For zones with no transitions, this
462 function returns ``None``.
464 :param year:
465 The year whose transitions you would like to query.
467 :return:
468 Returns a :class:`tuple` of :class:`datetime.datetime` objects,
469 ``(dston, dstoff)`` for zones with an annual DST transition, or
470 ``None`` for fixed offset zones.
471 """
472 if not self.hasdst:
473 return None
475 base_year = datetime.datetime(year, 1, 1)
477 start = base_year + self._start_delta
478 end = base_year + self._end_delta
480 return (start, end)
482 def __eq__(self, other):
483 if not isinstance(other, tzrange):
484 return NotImplemented
486 return (self._std_abbr == other._std_abbr and
487 self._dst_abbr == other._dst_abbr and
488 self._std_offset == other._std_offset and
489 self._dst_offset == other._dst_offset and
490 self._start_delta == other._start_delta and
491 self._end_delta == other._end_delta)
493 @property
494 def _dst_base_offset(self):
495 return self._dst_base_offset_
498@six.add_metaclass(_TzStrFactory)
499class tzstr(tzrange):
500 """
501 ``tzstr`` objects are time zone objects specified by a time-zone string as
502 it would be passed to a ``TZ`` variable on POSIX-style systems (see
503 the `GNU C Library: TZ Variable`_ for more details).
505 There is one notable exception, which is that POSIX-style time zones use an
506 inverted offset format, so normally ``GMT+3`` would be parsed as an offset
507 3 hours *behind* GMT. The ``tzstr`` time zone object will parse this as an
508 offset 3 hours *ahead* of GMT. If you would like to maintain the POSIX
509 behavior, pass a ``True`` value to ``posix_offset``.
511 The :class:`tzrange` object provides the same functionality, but is
512 specified using :class:`relativedelta.relativedelta` objects. rather than
513 strings.
515 :param s:
516 A time zone string in ``TZ`` variable format. This can be a
517 :class:`bytes` (2.x: :class:`str`), :class:`str` (2.x:
518 :class:`unicode`) or a stream emitting unicode characters
519 (e.g. :class:`StringIO`).
521 :param posix_offset:
522 Optional. If set to ``True``, interpret strings such as ``GMT+3`` or
523 ``UTC+3`` as being 3 hours *behind* UTC rather than ahead, per the
524 POSIX standard.
526 .. caution::
528 Prior to version 2.7.0, this function also supported time zones
529 in the format:
531 * ``EST5EDT,4,0,6,7200,10,0,26,7200,3600``
532 * ``EST5EDT,4,1,0,7200,10,-1,0,7200,3600``
534 This format is non-standard and has been deprecated; this function
535 will raise a :class:`DeprecatedTZFormatWarning` until
536 support is removed in a future version.
538 .. _`GNU C Library: TZ Variable`:
539 https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
540 """
541 def __init__(self, s, posix_offset=False):
542 global parser
543 from dateutil.parser import _parser as parser
545 self._s = s
547 res = parser._parsetz(s)
548 if res is None or res.any_unused_tokens:
549 raise ValueError("unknown string format")
551 # Here we break the compatibility with the TZ variable handling.
552 # GMT-3 actually *means* the timezone -3.
553 if res.stdabbr in ("GMT", "UTC") and not posix_offset:
554 res.stdoffset *= -1
556 # We must initialize it first, since _delta() needs
557 # _std_offset and _dst_offset set. Use False in start/end
558 # to avoid building it two times.
559 tzrange.__init__(self, res.stdabbr, res.stdoffset,
560 res.dstabbr, res.dstoffset,
561 start=False, end=False)
563 if not res.dstabbr:
564 self._start_delta = None
565 self._end_delta = None
566 else:
567 self._start_delta = self._delta(res.start)
568 if self._start_delta:
569 self._end_delta = self._delta(res.end, isend=1)
571 self.hasdst = bool(self._start_delta)
573 def _delta(self, x, isend=0):
574 from dateutil import relativedelta
575 kwargs = {}
576 if x.month is not None:
577 kwargs["month"] = x.month
578 if x.weekday is not None:
579 kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week)
580 if x.week > 0:
581 kwargs["day"] = 1
582 else:
583 kwargs["day"] = 31
584 elif x.day:
585 kwargs["day"] = x.day
586 elif x.yday is not None:
587 kwargs["yearday"] = x.yday
588 elif x.jyday is not None:
589 kwargs["nlyearday"] = x.jyday
590 if not kwargs:
591 # Default is to start on first sunday of april, and end
592 # on last sunday of october.
593 if not isend:
594 kwargs["month"] = 4
595 kwargs["day"] = 1
596 kwargs["weekday"] = relativedelta.SU(+1)
597 else:
598 kwargs["month"] = 10
599 kwargs["day"] = 31
600 kwargs["weekday"] = relativedelta.SU(-1)
601 if x.time is not None:
602 kwargs["seconds"] = x.time
603 else:
604 # Default is 2AM.
605 kwargs["seconds"] = 7200
606 if isend:
607 # Convert to standard time, to follow the documented way
608 # of working with the extra hour. See the documentation
609 # of the tzinfo class.
610 delta = self._dst_offset - self._std_offset
611 kwargs["seconds"] -= delta.seconds + delta.days * 86400
612 return relativedelta.relativedelta(**kwargs)
614 def __repr__(self):
615 return "%s(%s)" % (self.__class__.__name__, repr(self._s))
618class _tzicalvtzcomp(object):
619 def __init__(self, tzoffsetfrom, tzoffsetto, isdst,
620 tzname=None, rrule=None):
621 self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom)
622 self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto)
623 self.tzoffsetdiff = self.tzoffsetto - self.tzoffsetfrom
624 self.isdst = isdst
625 self.tzname = tzname
626 self.rrule = rrule
629class _tzicalvtz(_tzinfo):
630 def __init__(self, tzid, comps=[]):
631 super(_tzicalvtz, self).__init__()
633 self._tzid = tzid
634 self._comps = comps
635 self._cachedate = []
636 self._cachecomp = []
637 self._cache_lock = _thread.allocate_lock()
639 def _find_comp(self, dt):
640 if len(self._comps) == 1:
641 return self._comps[0]
643 dt = dt.replace(tzinfo=None)
645 try:
646 with self._cache_lock:
647 return self._cachecomp[self._cachedate.index(
648 (dt, self._fold(dt)))]
649 except ValueError:
650 pass
652 lastcompdt = None
653 lastcomp = None
655 for comp in self._comps:
656 compdt = self._find_compdt(comp, dt)
658 if compdt and (not lastcompdt or lastcompdt < compdt):
659 lastcompdt = compdt
660 lastcomp = comp
662 if not lastcomp:
663 # RFC says nothing about what to do when a given
664 # time is before the first onset date. We'll look for the
665 # first standard component, or the first component, if
666 # none is found.
667 for comp in self._comps:
668 if not comp.isdst:
669 lastcomp = comp
670 break
671 else:
672 lastcomp = comp[0]
674 with self._cache_lock:
675 self._cachedate.insert(0, (dt, self._fold(dt)))
676 self._cachecomp.insert(0, lastcomp)
678 if len(self._cachedate) > 10:
679 self._cachedate.pop()
680 self._cachecomp.pop()
682 return lastcomp
684 def _find_compdt(self, comp, dt):
685 if comp.tzoffsetdiff < ZERO and self._fold(dt):
686 dt -= comp.tzoffsetdiff
688 compdt = comp.rrule.before(dt, inc=True)
690 return compdt
692 def utcoffset(self, dt):
693 if dt is None:
694 return None
696 return self._find_comp(dt).tzoffsetto
698 def dst(self, dt):
699 comp = self._find_comp(dt)
700 if comp.isdst:
701 return comp.tzoffsetdiff
702 else:
703 return ZERO
705 @tzname_in_python2
706 def tzname(self, dt):
707 return self._find_comp(dt).tzname
709 def __repr__(self):
710 return "<tzicalvtz %s>" % repr(self._tzid)
712 __reduce__ = object.__reduce__
715class tzical(object):
716 """
717 This object is designed to parse an iCalendar-style ``VTIMEZONE`` structure
718 as set out in `RFC 5545`_ Section 4.6.5 into one or more `tzinfo` objects.
720 :param `fileobj`:
721 A file or stream in iCalendar format, which should be UTF-8 encoded
722 with CRLF endings.
724 .. _`RFC 5545`: https://tools.ietf.org/html/rfc5545
725 """
726 def __init__(self, fileobj):
727 global rrule
728 from dateutil import rrule
730 if isinstance(fileobj, string_types):
731 self._s = fileobj
732 # ical should be encoded in UTF-8 with CRLF
733 fileobj = open(fileobj, 'r')
734 else:
735 self._s = getattr(fileobj, 'name', repr(fileobj))
736 fileobj = _nullcontext(fileobj)
738 self._vtz = {}
740 with fileobj as fobj:
741 self._parse_rfc(fobj.read())
743 def keys(self):
744 """
745 Retrieves the available time zones as a list.
746 """
747 return list(self._vtz.keys())
749 def get(self, tzid=None):
750 """
751 Retrieve a :py:class:`datetime.tzinfo` object by its ``tzid``.
753 :param tzid:
754 If there is exactly one time zone available, omitting ``tzid``
755 or passing :py:const:`None` value returns it. Otherwise a valid
756 key (which can be retrieved from :func:`keys`) is required.
758 :raises ValueError:
759 Raised if ``tzid`` is not specified but there are either more
760 or fewer than 1 zone defined.
762 :returns:
763 Returns either a :py:class:`datetime.tzinfo` object representing
764 the relevant time zone or :py:const:`None` if the ``tzid`` was
765 not found.
766 """
767 if tzid is None:
768 if len(self._vtz) == 0:
769 raise ValueError("no timezones defined")
770 elif len(self._vtz) > 1:
771 raise ValueError("more than one timezone available")
772 tzid = next(iter(self._vtz))
774 return self._vtz.get(tzid)
776 def _parse_offset(self, s):
777 s = s.strip()
778 if not s:
779 raise ValueError("empty offset")
780 if s[0] in ('+', '-'):
781 signal = (-1, +1)[s[0] == '+']
782 s = s[1:]
783 else:
784 signal = +1
785 if len(s) == 4:
786 return (int(s[:2]) * 3600 + int(s[2:]) * 60) * signal
787 elif len(s) == 6:
788 return (int(s[:2]) * 3600 + int(s[2:4]) * 60 + int(s[4:])) * signal
789 else:
790 raise ValueError("invalid offset: " + s)
792 def _parse_rfc(self, s):
793 lines = s.splitlines()
794 if not lines:
795 raise ValueError("empty string")
797 # Unfold
798 i = 0
799 while i < len(lines):
800 line = lines[i].rstrip()
801 if not line:
802 del lines[i]
803 elif i > 0 and line[0] == " ":
804 lines[i-1] += line[1:]
805 del lines[i]
806 else:
807 i += 1
809 tzid = None
810 comps = []
811 invtz = False
812 comptype = None
813 for line in lines:
814 if not line:
815 continue
816 name, value = line.split(':', 1)
817 parms = name.split(';')
818 if not parms:
819 raise ValueError("empty property name")
820 name = parms[0].upper()
821 parms = parms[1:]
822 if invtz:
823 if name == "BEGIN":
824 if value in ("STANDARD", "DAYLIGHT"):
825 # Process component
826 pass
827 else:
828 raise ValueError("unknown component: "+value)
829 comptype = value
830 founddtstart = False
831 tzoffsetfrom = None
832 tzoffsetto = None
833 rrulelines = []
834 tzname = None
835 elif name == "END":
836 if value == "VTIMEZONE":
837 if comptype:
838 raise ValueError("component not closed: "+comptype)
839 if not tzid:
840 raise ValueError("mandatory TZID not found")
841 if not comps:
842 raise ValueError(
843 "at least one component is needed")
844 # Process vtimezone
845 self._vtz[tzid] = _tzicalvtz(tzid, comps)
846 invtz = False
847 elif value == comptype:
848 if not founddtstart:
849 raise ValueError("mandatory DTSTART not found")
850 if tzoffsetfrom is None:
851 raise ValueError(
852 "mandatory TZOFFSETFROM not found")
853 if tzoffsetto is None:
854 raise ValueError(
855 "mandatory TZOFFSETFROM not found")
856 # Process component
857 rr = None
858 if rrulelines:
859 rr = rrule.rrulestr("\n".join(rrulelines),
860 compatible=True,
861 ignoretz=True,
862 cache=True)
863 comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto,
864 (comptype == "DAYLIGHT"),
865 tzname, rr)
866 comps.append(comp)
867 comptype = None
868 else:
869 raise ValueError("invalid component end: "+value)
870 elif comptype:
871 if name == "DTSTART":
872 # DTSTART in VTIMEZONE takes a subset of valid RRULE
873 # values under RFC 5545.
874 for parm in parms:
875 if parm != 'VALUE=DATE-TIME':
876 msg = ('Unsupported DTSTART param in ' +
877 'VTIMEZONE: ' + parm)
878 raise ValueError(msg)
879 rrulelines.append(line)
880 founddtstart = True
881 elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"):
882 rrulelines.append(line)
883 elif name == "TZOFFSETFROM":
884 if parms:
885 raise ValueError(
886 "unsupported %s parm: %s " % (name, parms[0]))
887 tzoffsetfrom = self._parse_offset(value)
888 elif name == "TZOFFSETTO":
889 if parms:
890 raise ValueError(
891 "unsupported TZOFFSETTO parm: "+parms[0])
892 tzoffsetto = self._parse_offset(value)
893 elif name == "TZNAME":
894 if parms:
895 raise ValueError(
896 "unsupported TZNAME parm: "+parms[0])
897 tzname = value
898 elif name == "COMMENT":
899 pass
900 else:
901 raise ValueError("unsupported property: "+name)
902 else:
903 if name == "TZID":
904 if parms:
905 raise ValueError(
906 "unsupported TZID parm: "+parms[0])
907 tzid = value
908 elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"):
909 pass
910 else:
911 raise ValueError("unsupported property: "+name)
912 elif name == "BEGIN" and value == "VTIMEZONE":
913 tzid = None
914 comps = []
915 invtz = True
917 def __repr__(self):
918 return "%s(%s)" % (self.__class__.__name__, repr(self._s))
921TZFILES = ["/etc/localtime", "localtime"]
923def __get_gettz():
924 tzlocal_classes = (tzlocal,)
925 if tzwinlocal is not None:
926 tzlocal_classes += (tzwinlocal,)
928 class GettzFunc(object):
929 """
930 Retrieve a time zone object from a string representation
932 This function is intended to retrieve the :py:class:`tzinfo` subclass
933 that best represents the time zone that would be used if a POSIX
934 `TZ variable`_ were set to the same value.
936 If no argument or an empty string is passed to ``gettz``, local time
937 is returned:
939 .. code-block:: python3
941 >>> gettz()
942 tzfile('/etc/localtime')
944 This function is also the preferred way to map IANA tz database keys
945 to :class:`tzfile` objects. It searches for these keys in the
946 following order:
948 1. On the search path defined in :data:`TZPATH`.
949 2. In the `tzdata <https://pypi.org/project/tzdata/>`_ Python package.
950 3. Using the system-specific mechanisms (e.g. the Windows registry).
952 .. code-block:: python3
954 >>> gettz('Pacific/Kiritimati')
955 tzfile('/usr/share/zoneinfo/Pacific/Kiritimati')
957 On Windows, the standard is extended to include the Windows-specific
958 zone names provided by the operating system:
960 .. code-block:: python3
962 >>> gettz('Egypt Standard Time')
963 tzwin('Egypt Standard Time')
965 Passing a GNU ``TZ`` style string time zone specification returns a
966 :class:`tzstr` object:
968 .. code-block:: python3
970 >>> gettz('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3')
971 tzstr('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3')
973 :param name:
974 A time zone name (IANA, or, on Windows, Windows keys), location of
975 a ``tzfile(5)`` zoneinfo file or ``TZ`` variable style time zone
976 specifier. An empty string, no argument or ``None`` is interpreted
977 as local time.
979 :return:
980 Returns an instance of one of ``dateutil``'s :py:class:`tzinfo`
981 subclasses.
983 .. versionchanged:: 2.7.0
985 After version 2.7.0, any two calls to ``gettz`` using the same
986 input strings will return the same object:
988 .. code-block:: python3
990 >>> tz.gettz('America/Chicago') is tz.gettz('America/Chicago')
991 True
993 In addition to improving performance, this ensures that
994 `"same zone" semantics`_ are used for datetimes in the same zone.
997 .. _`TZ variable`:
998 https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
1000 .. _`"same zone" semantics`:
1001 https://blog.ganssle.io/articles/2018/02/aware-datetime-arithmetic.html
1002 """
1003 def __init__(self):
1005 self.__instances = weakref.WeakValueDictionary()
1006 self.__strong_cache_size = 8
1007 self.__strong_cache = OrderedDict()
1008 self._cache_lock = _thread.allocate_lock()
1010 def __call__(self, name=None):
1011 with self._cache_lock:
1012 rv = self.__instances.get(name, None)
1014 if rv is None:
1015 # TODO: This holds the cache lock while nocache() reads
1016 # the file, so a slow lookup (e.g. walking TZPATH on a
1017 # network file system) blocks every other gettz call,
1018 # including cache hits for unrelated keys.
1019 rv = self.nocache(name=name)
1020 if not (name is None
1021 or isinstance(rv, tzlocal_classes)
1022 or rv is None):
1023 # tzlocal is slightly more complicated than the other
1024 # time zone providers because it depends on environment
1025 # at construction time, so don't cache that.
1026 #
1027 # We also cannot store weak references to None, so we
1028 # will also not store that.
1029 self.__instances[name] = rv
1030 else:
1031 # No need for strong caching, return immediately
1032 return rv
1034 self.__strong_cache[name] = self.__strong_cache.pop(name, rv)
1036 if len(self.__strong_cache) > self.__strong_cache_size:
1037 self.__strong_cache.popitem(last=False)
1039 return rv
1041 def set_cache_size(self, size):
1042 with self._cache_lock:
1043 self.__strong_cache_size = size
1044 while len(self.__strong_cache) > size:
1045 self.__strong_cache.popitem(last=False)
1047 def cache_clear(self):
1048 with self._cache_lock:
1049 self.__instances = weakref.WeakValueDictionary()
1050 self.__strong_cache.clear()
1052 @staticmethod
1053 def nocache(name=None):
1054 """A non-cached version of gettz"""
1055 tz = None
1056 if not name:
1057 try:
1058 name = os.environ["TZ"]
1059 except KeyError:
1060 pass
1061 if name is None or name in ("", ":"):
1062 for filepath in TZFILES:
1063 if not os.path.isabs(filepath):
1064 filename = filepath
1065 for path in _tzpath.TZPATH:
1066 filepath = os.path.join(path, filename)
1067 if _isfile(filepath):
1068 break
1069 else:
1070 continue
1071 if _isfile(filepath):
1072 try:
1073 tz = tzfile(filepath)
1074 break
1075 except (IOError, OSError, ValueError):
1076 pass
1077 else:
1078 tz = tzlocal()
1079 else:
1080 try:
1081 if name.startswith(":"):
1082 name = name[1:]
1083 except TypeError as e:
1084 if isinstance(name, bytes):
1085 new_msg = "gettz argument should be str, not bytes"
1086 six.raise_from(TypeError(new_msg), e)
1087 else:
1088 raise
1090 # A NUL byte can never be part of a key or a path, and
1091 # depending on the platform and Python version it raises
1092 # ValueError from os.path, winreg or open(), so reject it
1093 # up front rather than catching it in every branch.
1094 if "\x00" in name:
1095 return None
1097 if os.path.isabs(name):
1098 if _isfile(name):
1099 tz = tzfile(name)
1100 else:
1101 tz = None
1102 else:
1103 if _escapes_tzpath(name):
1104 _warn_tzpath_escape(name)
1106 for path in _tzpath.TZPATH:
1107 filepath = os.path.join(path, name)
1108 if not _isfile(filepath):
1109 filepath = filepath.replace(' ', '_')
1110 if not _isfile(filepath):
1111 continue
1112 try:
1113 tz = tzfile(filepath, key=name)
1114 break
1115 except (IOError, OSError, ValueError):
1116 # TODO: A file that exists but can't be read or
1117 # isn't a valid TZif file is silently skipped,
1118 # and the key is looked up in the next TZPATH
1119 # entry and then in tzdata. Consider warning
1120 # here, since this usually means the system time
1121 # zone data is broken.
1122 pass
1123 else:
1124 tz = None
1125 # As with the search path lookup above, also try
1126 # the name with spaces replaced by underscores.
1127 candidates = [name]
1128 if " " in name:
1129 candidates.append(name.replace(" ", "_"))
1131 for candidate in candidates:
1132 try:
1133 with _tzdata_impl._load_tzdata(candidate) as f:
1134 return tzfile(f, key=name)
1135 except ValueError:
1136 # TZFileNotFound is a ValueError, and so is
1137 # the error raised for files in tzdata that
1138 # are not TZif files (e.g. "zone.tab"),
1139 # which are skipped just as on TZPATH.
1140 pass
1142 if tzwin is not None:
1143 try:
1144 tz = tzwin(name)
1145 except (WindowsError, UnicodeEncodeError):
1146 # UnicodeEncodeError is for Python 2.7 compat
1147 tz = None
1149 if not tz:
1150 for c in name:
1151 # name is not a tzstr unless it has at least
1152 # one offset. For short values of "name", an
1153 # explicit for loop seems to be the fastest way
1154 # To determine if a string contains a digit
1155 if c in "0123456789":
1156 try:
1157 tz = tzstr(name)
1158 except ValueError:
1159 pass
1160 break
1161 else:
1162 if name in ("GMT", "UTC"):
1163 tz = UTC
1164 elif name in time.tzname:
1165 tz = tzlocal()
1166 return tz
1168 return GettzFunc()
1171gettz = __get_gettz()
1172del __get_gettz
1175class DeprecatedTzKeyWarning(FutureWarning):
1176 """Warning raised when :func:`gettz` is given a key outside of TZPATH.
1178 Relative keys like ``"../../etc/localtime"`` resolve to files outside of
1179 every directory on :data:`TZPATH`. Support for these is deprecated, and a
1180 warnings filter turns this warning into an error by default. To keep the
1181 old behavior for now, filter it out, e.g.::
1183 warnings.filterwarnings(
1184 "ignore", category=dateutil.tz.DeprecatedTzKeyWarning
1185 )
1187 Absolute paths to TZif files are not affected.
1188 """
1191def _escapes_tzpath(name):
1192 # A relative key stays inside every TZPATH entry unless, once normalized
1193 # (which also turns os.altsep into os.sep), it starts by going up a
1194 # directory.
1195 return os.path.normpath(name).split(os.sep, 1)[0] == os.pardir
1198def _warn_tzpath_escape(name):
1199 # As in dateutil.zoneinfo.rebuild, the filter is appended at call time,
1200 # so any filter the user has installed for this category wins.
1201 warnings.filterwarnings(
1202 "error", category=DeprecatedTzKeyWarning, append=True
1203 )
1204 warnings.warn(
1205 "gettz(%r) refers to a file outside of TZPATH; support for keys "
1206 "like this is deprecated and will be removed in a future version. "
1207 "Use an absolute path instead." % (name,),
1208 DeprecatedTzKeyWarning,
1209 stacklevel=4,
1210 )
1213def available_iana_timezones():
1214 """Returns a set containing all available IANA time zones keys.
1216 These are suitable for use with :func:`gettz`. This is essentially a direct
1217 port of :py:func:`zoneinfo.available_timezones`.
1219 The search includes time zones found on :data:`TZPATH` as well as any
1220 time zones provided by the `tzdata <https://pypi.org/project/tzdata/>`_
1221 package if it is installed.
1223 :return:
1224 Returns a mutable set containing strings of time zones. Each call will
1225 return a fresh set.
1227 .. caution::
1229 This may attempt to open a large number of files, since the best way to
1230 determine if a given file on the time zone search path is to open it
1231 and check for the "magic string" at the beginning.
1233 .. note::
1235 These values are not designed to be exposed to end-users; for
1236 user-facing elements, applications should use something like CLDR (the
1237 Unicode Common Locale Data Repository) to get more user-friendly
1238 strings.
1239 """
1240 # Start with loading from the tzdata package if it exists: this has a
1241 # pre-assembled list of zones that only requires opening one file.
1242 try:
1243 valid_zones = set(_tzdata_impl._load_tzdata_keys())
1244 except ImportError:
1245 valid_zones = set()
1247 def valid_key(fpath):
1248 try:
1249 with open(fpath, "rb") as f:
1250 return f.read(4) == b"TZif"
1251 except Exception: # pragma: nocover
1252 return False
1254 for tz_root in _tzpath.TZPATH:
1255 if not os.path.exists(tz_root):
1256 continue
1258 for root, dirnames, files in os.walk(tz_root):
1259 if root == tz_root:
1260 # right/ and posix/ are special directories and shouldn't be
1261 # included in the output of available zones
1262 if "right" in dirnames:
1263 dirnames.remove("right")
1264 if "posix" in dirnames:
1265 dirnames.remove("posix")
1267 for file in files:
1268 fpath = os.path.join(root, file)
1270 key = os.path.relpath(fpath, start=tz_root)
1271 if os.sep != "/": # pragma: nocover
1272 key = key.replace(os.sep, "/")
1274 if not key or key in valid_zones:
1275 continue
1277 if valid_key(fpath):
1278 valid_zones.add(key)
1280 if "posixrules" in valid_zones:
1281 # posixrules is a special symlink-only time zone where it exists, it
1282 # should not be included in the output
1283 valid_zones.remove("posixrules")
1284 if "localtime" in valid_zones:
1285 # localtime is a special symlink-only time zone where it exists, it
1286 # should not be included in the output
1287 valid_zones.remove("localtime")
1289 return valid_zones
1292def datetime_exists(dt, tz=None):
1293 """
1294 Given a datetime and a time zone, determine whether or not a given datetime
1295 would fall in a gap.
1297 :param dt:
1298 A :class:`datetime.datetime` (whose time zone will be ignored if ``tz``
1299 is provided.)
1301 :param tz:
1302 A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If
1303 ``None`` or not provided, the datetime's own time zone will be used.
1305 :return:
1306 Returns a boolean value whether or not the "wall time" exists in
1307 ``tz``.
1309 .. versionadded:: 2.7.0
1310 """
1311 if tz is None:
1312 if dt.tzinfo is None:
1313 raise ValueError('Datetime is naive and no time zone provided.')
1314 tz = dt.tzinfo
1316 dt = dt.replace(tzinfo=None)
1318 # This is essentially a test of whether or not the datetime can survive
1319 # a round trip to UTC.
1320 dt_rt = dt.replace(tzinfo=tz).astimezone(UTC).astimezone(tz)
1321 dt_rt = dt_rt.replace(tzinfo=None)
1323 return dt == dt_rt
1326def datetime_ambiguous(dt, tz=None):
1327 """
1328 Given a datetime and a time zone, determine whether or not a given datetime
1329 is ambiguous (i.e if there are two times differentiated only by their DST
1330 status).
1332 :param dt:
1333 A :class:`datetime.datetime` (whose time zone will be ignored if ``tz``
1334 is provided.)
1336 :param tz:
1337 A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If
1338 ``None`` or not provided, the datetime's own time zone will be used.
1340 :return:
1341 Returns a boolean value whether or not the "wall time" is ambiguous in
1342 ``tz``.
1344 .. versionadded:: 2.6.0
1345 """
1346 if tz is None:
1347 if dt.tzinfo is None:
1348 raise ValueError('Datetime is naive and no time zone provided.')
1350 tz = dt.tzinfo
1352 # If a time zone defines its own "is_ambiguous" function, we'll use that.
1353 is_ambiguous_fn = getattr(tz, 'is_ambiguous', None)
1354 if is_ambiguous_fn is not None:
1355 try:
1356 return tz.is_ambiguous(dt)
1357 except Exception:
1358 pass
1360 # If it doesn't come out and tell us it's ambiguous, we'll just check if
1361 # the fold attribute has any effect on this particular date and time.
1362 dt = dt.replace(tzinfo=tz)
1363 wall_0 = enfold(dt, fold=0)
1364 wall_1 = enfold(dt, fold=1)
1366 same_offset = wall_0.utcoffset() == wall_1.utcoffset()
1367 same_dst = wall_0.dst() == wall_1.dst()
1369 return not (same_offset and same_dst)
1372def resolve_imaginary(dt):
1373 """
1374 Given a datetime that may be imaginary, return an existing datetime.
1376 This function assumes that an imaginary datetime represents what the
1377 wall time would be in a zone had the offset transition not occurred, so
1378 it will always fall forward by the transition's change in offset.
1380 .. doctest::
1382 >>> from dateutil import tz
1383 >>> from datetime import datetime
1384 >>> NYC = tz.gettz('America/New_York')
1385 >>> print(tz.resolve_imaginary(datetime(2017, 3, 12, 2, 30, tzinfo=NYC)))
1386 2017-03-12 03:30:00-04:00
1388 >>> KIR = tz.gettz('Pacific/Kiritimati')
1389 >>> print(tz.resolve_imaginary(datetime(1995, 1, 1, 12, 30, tzinfo=KIR)))
1390 1995-01-02 12:30:00+14:00
1392 As a note, :func:`datetime.astimezone` is guaranteed to produce a valid,
1393 existing datetime, so a round-trip to and from UTC is sufficient to get
1394 an extant datetime, however, this generally "falls back" to an earlier time
1395 rather than falling forward to the STD side (though no guarantees are made
1396 about this behavior).
1398 :param dt:
1399 A :class:`datetime.datetime` which may or may not exist.
1401 :return:
1402 Returns an existing :class:`datetime.datetime`. If ``dt`` was not
1403 imaginary, the datetime returned is guaranteed to be the same object
1404 passed to the function.
1406 .. versionadded:: 2.7.0
1407 """
1408 if dt.tzinfo is not None and not datetime_exists(dt):
1410 curr_offset = (dt + datetime.timedelta(hours=24)).utcoffset()
1411 old_offset = (dt - datetime.timedelta(hours=24)).utcoffset()
1413 dt += curr_offset - old_offset
1415 return dt
1418def _datetime_to_timestamp(dt):
1419 """
1420 Convert a :class:`datetime.datetime` object to an epoch timestamp in
1421 seconds since January 1, 1970, ignoring the time zone.
1422 """
1423 return (dt.replace(tzinfo=None) - EPOCH).total_seconds()
1426if sys.version_info >= (3, 6):
1427 def _get_supported_offset(second_offset):
1428 return second_offset
1429else:
1430 def _get_supported_offset(second_offset):
1431 # For python pre-3.6, round to full-minutes if that's not the case.
1432 # Python's datetime doesn't accept sub-minute timezones. Check
1433 # http://python.org/sf/1447945 or https://bugs.python.org/issue5288
1434 # for some information.
1435 old_offset = second_offset
1436 calculated_offset = 60 * ((second_offset + 30) // 60)
1437 return calculated_offset
1440if sys.version_info < (3, 8):
1442 def _isfile(path):
1443 # bpo-33721: In Python 3.8 non-UTF8 paths return False rather than
1444 # raising an error. See https://bugs.python.org/issue33721
1445 try:
1446 return os.path.isfile(path)
1447 except ValueError:
1448 return False
1450else:
1451 _isfile = os.path.isfile
1454try:
1455 # Python 3.7 feature
1456 from contextlib import nullcontext as _nullcontext
1457except ImportError:
1458 class _nullcontext(object):
1459 """
1460 Class for wrapping contexts so that they are passed through in a
1461 with statement.
1462 """
1463 def __init__(self, context):
1464 self.context = context
1466 def __enter__(self):
1467 return self.context
1469 def __exit__(*args, **kwargs):
1470 pass
1472# vim:ts=4:sw=4:et