1# -*- coding: utf-8 -*- 2""" 3This module offers timezone implementations subclassing the abstract 4:py:`datetime.tzinfo` type. There are classes to handle tzfile format files 5(usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`, etc), TZ 6environment string (in all known formats), given ranges (with help from 7relative deltas), local machine timezone, fixed offset timezone, and UTC 8timezone. 9""" 10import datetime 11import struct 12import time 13import sys 14import os 15import bisect 16 17import six 18from six import string_types 19from six.moves import _thread 20from ._common import tzname_in_python2, _tzinfo 21from ._common import tzrangebase, enfold 22from ._common import _validate_fromutc_inputs 23 24from ._factories import _TzSingleton, _TzOffsetFactory 25from ._factories import _TzStrFactory 26try: 27 from .win import tzwin, tzwinlocal 28except ImportError: 29 tzwin = tzwinlocal = None 30 31ZERO = datetime.timedelta(0) 32EPOCH = datetime.datetime.utcfromtimestamp(0) 33EPOCHORDINAL = EPOCH.toordinal() 34 35 36@six.add_metaclass(_TzSingleton) 37class tzutc(datetime.tzinfo): 38 """ 39 This is a tzinfo object that represents the UTC time zone. 40 41 **Examples:** 42 43 .. doctest:: 44 45 >>> from datetime import * 46 >>> from dateutil.tz import * 47 48 >>> datetime.now() 49 datetime.datetime(2003, 9, 27, 9, 40, 1, 521290) 50 51 >>> datetime.now(tzutc()) 52 datetime.datetime(2003, 9, 27, 12, 40, 12, 156379, tzinfo=tzutc()) 53 54 >>> datetime.now(tzutc()).tzname() 55 'UTC' 56 57 .. versionchanged:: 2.7.0 58 ``tzutc()`` is now a singleton, so the result of ``tzutc()`` will 59 always return the same object. 60 61 .. doctest:: 62 63 >>> from dateutil.tz import tzutc, UTC 64 >>> tzutc() is tzutc() 65 True 66 >>> tzutc() is UTC 67 True 68 """ 69 def utcoffset(self, dt): 70 return ZERO 71 72 def dst(self, dt): 73 return ZERO 74 75 @tzname_in_python2 76 def tzname(self, dt): 77 return "UTC" 78 79 def is_ambiguous(self, dt): 80 """ 81 Whether or not the "wall time" of a given datetime is ambiguous in this 82 zone. 83 84 :param dt: 85 A :py:class:`datetime.datetime`, naive or time zone aware. 86 87 88 :return: 89 Returns ``True`` if ambiguous, ``False`` otherwise. 90 91 .. versionadded:: 2.6.0 92 """ 93 return False 94 95 @_validate_fromutc_inputs 96 def fromutc(self, dt): 97 """ 98 Fast track version of fromutc() returns the original ``dt`` object for 99 any valid :py:class:`datetime.datetime` object. 100 """ 101 return dt 102 103 def __eq__(self, other): 104 if not isinstance(other, (tzutc, tzoffset)): 105 return NotImplemented 106 107 return (isinstance(other, tzutc) or 108 (isinstance(other, tzoffset) and other._offset == ZERO)) 109 110 __hash__ = None 111 112 def __ne__(self, other): 113 return not (self == other) 114 115 def __repr__(self): 116 return "%s()" % self.__class__.__name__ 117 118 __reduce__ = object.__reduce__ 119 120 121@six.add_metaclass(_TzOffsetFactory) 122class tzoffset(datetime.tzinfo): 123 """ 124 A simple class for representing a fixed offset from UTC. 125 126 :param name: 127 The timezone name, to be returned when ``tzname()`` is called. 128 :param offset: 129 The time zone offset in seconds, or (since version 2.6.0, represented 130 as a :py:class:`datetime.timedelta` object). 131 """ 132 def __init__(self, name, offset): 133 self._name = name 134 135 try: 136 # Allow a timedelta 137 offset = offset.total_seconds() 138 except (TypeError, AttributeError): 139 pass 140 self._offset = datetime.timedelta(seconds=offset) 141 142 def utcoffset(self, dt): 143 return self._offset 144 145 def dst(self, dt): 146 return ZERO 147 148 @tzname_in_python2 149 def tzname(self, dt): 150 return self._name 151 152 @_validate_fromutc_inputs 153 def fromutc(self, dt): 154 return dt + self._offset 155 156 def is_ambiguous(self, dt): 157 """ 158 Whether or not the "wall time" of a given datetime is ambiguous in this 159 zone. 160 161 :param dt: 162 A :py:class:`datetime.datetime`, naive or time zone aware. 163 :return: 164 Returns ``True`` if ambiguous, ``False`` otherwise. 165 166 .. versionadded:: 2.6.0 167 """ 168 return False 169 170 def __eq__(self, other): 171 if not isinstance(other, tzoffset): 172 return NotImplemented 173 174 return self._offset == other._offset 175 176 __hash__ = None 177 178 def __ne__(self, other): 179 return not (self == other) 180 181 def __repr__(self): 182 return "%s(%s, %s)" % (self.__class__.__name__, 183 repr(self._name), 184 int(self._offset.total_seconds())) 185 186 __reduce__ = object.__reduce__ 187 188 189class tzlocal(_tzinfo): 190 """ 191 A :class:`tzinfo` subclass built around the ``time`` timezone functions. 192 """ 193 def __init__(self): 194 super(tzlocal, self).__init__() 195 196 self._std_offset = datetime.timedelta(seconds=-time.timezone) 197 if time.daylight: 198 self._dst_offset = datetime.timedelta(seconds=-time.altzone) 199 else: 200 self._dst_offset = self._std_offset 201 202 self._dst_saved = self._dst_offset - self._std_offset 203 self._hasdst = bool(self._dst_saved) 204 self._tznames = tuple(time.tzname) 205 206 def utcoffset(self, dt): 207 if dt is None and self._hasdst: 208 return None 209 210 if self._isdst(dt): 211 return self._dst_offset 212 else: 213 return self._std_offset 214 215 def dst(self, dt): 216 if dt is None and self._hasdst: 217 return None 218 219 if self._isdst(dt): 220 return self._dst_offset - self._std_offset 221 else: 222 return ZERO 223 224 @tzname_in_python2 225 def tzname(self, dt): 226 return self._tznames[self._isdst(dt)] 227 228 def is_ambiguous(self, dt): 229 """ 230 Whether or not the "wall time" of a given datetime is ambiguous in this 231 zone. 232 233 :param dt: 234 A :py:class:`datetime.datetime`, naive or time zone aware. 235 236 237 :return: 238 Returns ``True`` if ambiguous, ``False`` otherwise. 239 240 .. versionadded:: 2.6.0 241 """ 242 naive_dst = self._naive_is_dst(dt) 243 return (not naive_dst and 244 (naive_dst != self._naive_is_dst(dt - self._dst_saved))) 245 246 def _naive_is_dst(self, dt): 247 timestamp = _datetime_to_timestamp(dt) 248 return time.localtime(timestamp + time.timezone).tm_isdst 249 250 def _isdst(self, dt, fold_naive=True): 251 # We can't use mktime here. It is unstable when deciding if 252 # the hour near to a change is DST or not. 253 # 254 # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour, 255 # dt.minute, dt.second, dt.weekday(), 0, -1)) 256 # return time.localtime(timestamp).tm_isdst 257 # 258 # The code above yields the following result: 259 # 260 # >>> import tz, datetime 261 # >>> t = tz.tzlocal() 262 # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() 263 # 'BRDT' 264 # >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname() 265 # 'BRST' 266 # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() 267 # 'BRST' 268 # >>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname() 269 # 'BRDT' 270 # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() 271 # 'BRDT' 272 # 273 # Here is a more stable implementation: 274 # 275 if not self._hasdst: 276 return False 277 278 # Check for ambiguous times: 279 dstval = self._naive_is_dst(dt) 280 fold = getattr(dt, 'fold', None) 281 282 if self.is_ambiguous(dt): 283 if fold is not None: 284 return not self._fold(dt) 285 else: 286 return True 287 288 return dstval 289 290 def __eq__(self, other): 291 if isinstance(other, tzlocal): 292 return (self._std_offset == other._std_offset and 293 self._dst_offset == other._dst_offset) 294 elif isinstance(other, tzutc): 295 return (not self._hasdst and 296 self._tznames[0] in {'UTC', 'GMT'} and 297 self._std_offset == ZERO) 298 elif isinstance(other, tzoffset): 299 return (not self._hasdst and 300 self._tznames[0] == other._name and 301 self._std_offset == other._offset) 302 else: 303 return NotImplemented 304 305 __hash__ = None 306 307 def __ne__(self, other): 308 return not (self == other) 309 310 def __repr__(self): 311 return "%s()" % self.__class__.__name__ 312 313 __reduce__ = object.__reduce__ 314 315 316class _ttinfo(object): 317 __slots__ = ["offset", "delta", "isdst", "abbr", 318 "isstd", "isgmt", "dstoffset"] 319 320 def __init__(self): 321 for attr in self.__slots__: 322 setattr(self, attr, None) 323 324 def __repr__(self): 325 l = [] 326 for attr in self.__slots__: 327 value = getattr(self, attr) 328 if value is not None: 329 l.append("%s=%s" % (attr, repr(value))) 330 return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) 331 332 def __eq__(self, other): 333 if not isinstance(other, _ttinfo): 334 return NotImplemented 335 336 return (self.offset == other.offset and 337 self.delta == other.delta and 338 self.isdst == other.isdst and 339 self.abbr == other.abbr and 340 self.isstd == other.isstd and 341 self.isgmt == other.isgmt and 342 self.dstoffset == other.dstoffset) 343 344 __hash__ = None 345 346 def __ne__(self, other): 347 return not (self == other) 348 349 def __getstate__(self): 350 state = {} 351 for name in self.__slots__: 352 state[name] = getattr(self, name, None) 353 return state 354 355 def __setstate__(self, state): 356 for name in self.__slots__: 357 if name in state: 358 setattr(self, name, state[name]) 359 360 361class _tzfile(object): 362 """ 363 Lightweight class for holding the relevant transition and time zone 364 information read from binary tzfiles. 365 """ 366 attrs = ['trans_list', 'trans_list_utc', 'trans_idx', 'ttinfo_list', 367 'ttinfo_std', 'ttinfo_dst', 'ttinfo_before', 'ttinfo_first'] 368 369 def __init__(self, **kwargs): 370 for attr in self.attrs: 371 setattr(self, attr, kwargs.get(attr, None)) 372 373 374class tzfile(_tzinfo): 375 """ 376 This is a ``tzinfo`` subclass thant allows one to use the ``tzfile(5)`` 377 format timezone files to extract current and historical zone information. 378 379 :param fileobj: 380 This can be an opened file stream or a file name that the time zone 381 information can be read from. 382 383 :param filename: 384 This is an optional parameter specifying the source of the time zone 385 information in the event that ``fileobj`` is a file object. If omitted 386 and ``fileobj`` is a file stream, this parameter will be set either to 387 ``fileobj``'s ``name`` attribute or to ``repr(fileobj)``. 388 389 See `Sources for Time Zone and Daylight Saving Time Data 390 <https://data.iana.org/time-zones/tz-link.html>`_ for more information. Time 391 zone files can be compiled from the `IANA Time Zone database files 392 <https://www.iana.org/time-zones>`_ with the `zic time zone compiler 393 <https://www.freebsd.org/cgi/man.cgi?query=zic&sektion=8>`_ 394 """ 395 396 def __init__(self, fileobj, filename=None): 397 super(tzfile, self).__init__() 398 399 file_opened_here = False 400 if isinstance(fileobj, string_types): 401 self._filename = fileobj 402 fileobj = open(fileobj, 'rb') 403 file_opened_here = True 404 elif filename is not None: 405 self._filename = filename 406 elif hasattr(fileobj, "name"): 407 self._filename = fileobj.name 408 else: 409 self._filename = repr(fileobj) 410 411 if fileobj is not None: 412 if not file_opened_here: 413 fileobj = _ContextWrapper(fileobj) 414 415 with fileobj as file_stream: 416 tzobj = self._read_tzfile(file_stream) 417 418 self._set_tzdata(tzobj) 419 420 def _set_tzdata(self, tzobj): 421 """ Set the time zone data of this object from a _tzfile object """ 422 # Copy the relevant attributes over as private attributes 423 for attr in _tzfile.attrs: 424 setattr(self, '_' + attr, getattr(tzobj, attr)) 425 426 def _read_tzfile(self, fileobj): 427 out = _tzfile() 428 429 # From tzfile(5): 430 # 431 # The time zone information files used by tzset(3) 432 # begin with the magic characters "TZif" to identify 433 # them as time zone information files, followed by 434 # sixteen bytes reserved for future use, followed by 435 # six four-byte values of type long, written in a 436 # ``standard'' byte order (the high-order byte 437 # of the value is written first). 438 if fileobj.read(4).decode() != "TZif": 439 raise ValueError("magic not found") 440 441 fileobj.read(16) 442 443 ( 444 # The number of UTC/local indicators stored in the file. 445 ttisgmtcnt, 446 447 # The number of standard/wall indicators stored in the file. 448 ttisstdcnt, 449 450 # The number of leap seconds for which data is 451 # stored in the file. 452 leapcnt, 453 454 # The number of "transition times" for which data 455 # is stored in the file. 456 timecnt, 457 458 # The number of "local time types" for which data 459 # is stored in the file (must not be zero). 460 typecnt, 461 462 # The number of characters of "time zone 463 # abbreviation strings" stored in the file. 464 charcnt, 465 466 ) = struct.unpack(">6l", fileobj.read(24)) 467 468 # The above header is followed by tzh_timecnt four-byte 469 # values of type long, sorted in ascending order. 470 # These values are written in ``standard'' byte order. 471 # Each is used as a transition time (as returned by 472 # time(2)) at which the rules for computing local time 473 # change. 474 475 if timecnt: 476 out.trans_list_utc = list(struct.unpack(">%dl" % timecnt, 477 fileobj.read(timecnt*4))) 478 else: 479 out.trans_list_utc = [] 480 481 # Next come tzh_timecnt one-byte values of type unsigned 482 # char; each one tells which of the different types of 483 # ``local time'' types described in the file is associated 484 # with the same-indexed transition time. These values 485 # serve as indices into an array of ttinfo structures that 486 # appears next in the file. 487 488 if timecnt: 489 out.trans_idx = struct.unpack(">%dB" % timecnt, 490 fileobj.read(timecnt)) 491 else: 492 out.trans_idx = [] 493 494 # Each ttinfo structure is written as a four-byte value 495 # for tt_gmtoff of type long, in a standard byte 496 # order, followed by a one-byte value for tt_isdst 497 # and a one-byte value for tt_abbrind. In each 498 # structure, tt_gmtoff gives the number of 499 # seconds to be added to UTC, tt_isdst tells whether 500 # tm_isdst should be set by localtime(3), and 501 # tt_abbrind serves as an index into the array of 502 # time zone abbreviation characters that follow the 503 # ttinfo structure(s) in the file. 504 505 ttinfo = [] 506 507 for i in range(typecnt): 508 ttinfo.append(struct.unpack(">lbb", fileobj.read(6))) 509 510 abbr = fileobj.read(charcnt).decode() 511 512 # Then there are tzh_leapcnt pairs of four-byte 513 # values, written in standard byte order; the 514 # first value of each pair gives the time (as 515 # returned by time(2)) at which a leap second 516 # occurs; the second gives the total number of 517 # leap seconds to be applied after the given time. 518 # The pairs of values are sorted in ascending order 519 # by time. 520 521 # Not used, for now (but seek for correct file position) 522 if leapcnt: 523 fileobj.seek(leapcnt * 8, os.SEEK_CUR) 524 525 # Then there are tzh_ttisstdcnt standard/wall 526 # indicators, each stored as a one-byte value; 527 # they tell whether the transition times associated 528 # with local time types were specified as standard 529 # time or wall clock time, and are used when 530 # a time zone file is used in handling POSIX-style 531 # time zone environment variables. 532 533 if ttisstdcnt: 534 isstd = struct.unpack(">%db" % ttisstdcnt, 535 fileobj.read(ttisstdcnt)) 536 537 # Finally, there are tzh_ttisgmtcnt UTC/local 538 # indicators, each stored as a one-byte value; 539 # they tell whether the transition times associated 540 # with local time types were specified as UTC or 541 # local time, and are used when a time zone file 542 # is used in handling POSIX-style time zone envi- 543 # ronment variables. 544 545 if ttisgmtcnt: 546 isgmt = struct.unpack(">%db" % ttisgmtcnt, 547 fileobj.read(ttisgmtcnt)) 548 549 # Build ttinfo list 550 out.ttinfo_list = [] 551 for i in range(typecnt): 552 gmtoff, isdst, abbrind = ttinfo[i] 553 # Round to full-minutes if that's not the case. Python's 554 # datetime doesn't accept sub-minute timezones. Check 555 # http://python.org/sf/1447945 for some information. 556 gmtoff = 60 * ((gmtoff + 30) // 60) 557 tti = _ttinfo() 558 tti.offset = gmtoff 559 tti.dstoffset = datetime.timedelta(0) 560 tti.delta = datetime.timedelta(seconds=gmtoff) 561 tti.isdst = isdst 562 tti.abbr = abbr[abbrind:abbr.find('\x00', abbrind)] 563 tti.isstd = (ttisstdcnt > i and isstd[i] != 0) 564 tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0) 565 out.ttinfo_list.append(tti) 566 567 # Replace ttinfo indexes for ttinfo objects. 568 out.trans_idx = [out.ttinfo_list[idx] for idx in out.trans_idx] 569 570 # Set standard, dst, and before ttinfos. before will be 571 # used when a given time is before any transitions, 572 # and will be set to the first non-dst ttinfo, or to 573 # the first dst, if all of them are dst. 574 out.ttinfo_std = None 575 out.ttinfo_dst = None 576 out.ttinfo_before = None 577 if out.ttinfo_list: 578 if not out.trans_list_utc: 579 out.ttinfo_std = out.ttinfo_first = out.ttinfo_list[0] 580 else: 581 for i in range(timecnt-1, -1, -1): 582 tti = out.trans_idx[i] 583 if not out.ttinfo_std and not tti.isdst: 584 out.ttinfo_std = tti 585 elif not out.ttinfo_dst and tti.isdst: 586 out.ttinfo_dst = tti 587 588 if out.ttinfo_std and out.ttinfo_dst: 589 break 590 else: 591 if out.ttinfo_dst and not out.ttinfo_std: 592 out.ttinfo_std = out.ttinfo_dst 593 594 for tti in out.ttinfo_list: 595 if not tti.isdst: 596 out.ttinfo_before = tti 597 break 598 else: 599 out.ttinfo_before = out.ttinfo_list[0] 600 601 # Now fix transition times to become relative to wall time. 602 # 603 # I'm not sure about this. In my tests, the tz source file 604 # is setup to wall time, and in the binary file isstd and 605 # isgmt are off, so it should be in wall time. OTOH, it's 606 # always in gmt time. Let me know if you have comments 607 # about this. 608 laststdoffset = None 609 out.trans_list = [] 610 for i, tti in enumerate(out.trans_idx): 611 if not tti.isdst: 612 offset = tti.offset 613 laststdoffset = offset 614 else: 615 if laststdoffset is not None: 616 # Store the DST offset as well and update it in the list 617 tti.dstoffset = tti.offset - laststdoffset 618 out.trans_idx[i] = tti 619 620 offset = laststdoffset or 0 621 622 out.trans_list.append(out.trans_list_utc[i] + offset) 623 624 # In case we missed any DST offsets on the way in for some reason, make 625 # a second pass over the list, looking for the /next/ DST offset. 626 laststdoffset = None 627 for i in reversed(range(len(out.trans_idx))): 628 tti = out.trans_idx[i] 629 if tti.isdst: 630 if not (tti.dstoffset or laststdoffset is None): 631 tti.dstoffset = tti.offset - laststdoffset 632 else: 633 laststdoffset = tti.offset 634 635 if not isinstance(tti.dstoffset, datetime.timedelta): 636 tti.dstoffset = datetime.timedelta(seconds=tti.dstoffset) 637 638 out.trans_idx[i] = tti 639 640 out.trans_idx = tuple(out.trans_idx) 641 out.trans_list = tuple(out.trans_list) 642 out.trans_list_utc = tuple(out.trans_list_utc) 643 644 return out 645 646 def _find_last_transition(self, dt, in_utc=False): 647 # If there's no list, there are no transitions to find 648 if not self._trans_list: 649 return None 650 651 timestamp = _datetime_to_timestamp(dt) 652 653 # Find where the timestamp fits in the transition list - if the 654 # timestamp is a transition time, it's part of the "after" period. 655 trans_list = self._trans_list_utc if in_utc else self._trans_list 656 idx = bisect.bisect_right(trans_list, timestamp) 657 658 # We want to know when the previous transition was, so subtract off 1 659 return idx - 1 660 661 def _get_ttinfo(self, idx): 662 # For no list or after the last transition, default to _ttinfo_std 663 if idx is None or (idx + 1) >= len(self._trans_list): 664 return self._ttinfo_std 665 666 # If there is a list and the time is before it, return _ttinfo_before 667 if idx < 0: 668 return self._ttinfo_before 669 670 return self._trans_idx[idx] 671 672 def _find_ttinfo(self, dt): 673 idx = self._resolve_ambiguous_time(dt) 674 675 return self._get_ttinfo(idx) 676 677 def fromutc(self, dt): 678 """ 679 The ``tzfile`` implementation of :py:func:`datetime.tzinfo.fromutc`. 680 681 :param dt: 682 A :py:class:`datetime.datetime` object. 683 684 :raises TypeError: 685 Raised if ``dt`` is not a :py:class:`datetime.datetime` object. 686 687 :raises ValueError: 688 Raised if this is called with a ``dt`` which does not have this 689 ``tzinfo`` attached. 690 691 :return: 692 Returns a :py:class:`datetime.datetime` object representing the 693 wall time in ``self``'s time zone. 694 """ 695 # These isinstance checks are in datetime.tzinfo, so we'll preserve 696 # them, even if we don't care about duck typing. 697 if not isinstance(dt, datetime.datetime): 698 raise TypeError("fromutc() requires a datetime argument") 699 700 if dt.tzinfo is not self: 701 raise ValueError("dt.tzinfo is not self") 702 703 # First treat UTC as wall time and get the transition we're in. 704 idx = self._find_last_transition(dt, in_utc=True) 705 tti = self._get_ttinfo(idx) 706 707 dt_out = dt + datetime.timedelta(seconds=tti.offset) 708 709 fold = self.is_ambiguous(dt_out, idx=idx) 710 711 return enfold(dt_out, fold=int(fold)) 712 713 def is_ambiguous(self, dt, idx=None): 714 """ 715 Whether or not the "wall time" of a given datetime is ambiguous in this 716 zone. 717 718 :param dt: 719 A :py:class:`datetime.datetime`, naive or time zone aware. 720 721 722 :return: 723 Returns ``True`` if ambiguous, ``False`` otherwise. 724 725 .. versionadded:: 2.6.0 726 """ 727 if idx is None: 728 idx = self._find_last_transition(dt) 729 730 # Calculate the difference in offsets from current to previous 731 timestamp = _datetime_to_timestamp(dt) 732 tti = self._get_ttinfo(idx) 733 734 if idx is None or idx <= 0: 735 return False 736 737 od = self._get_ttinfo(idx - 1).offset - tti.offset 738 tt = self._trans_list[idx] # Transition time 739 740 return timestamp < tt + od 741 742 def _resolve_ambiguous_time(self, dt): 743 idx = self._find_last_transition(dt) 744 745 # If we have no transitions, return the index 746 _fold = self._fold(dt) 747 if idx is None or idx == 0: 748 return idx 749 750 # If it's ambiguous and we're in a fold, shift to a different index. 751 idx_offset = int(not _fold and self.is_ambiguous(dt, idx)) 752 753 return idx - idx_offset 754 755 def utcoffset(self, dt): 756 if dt is None: 757 return None 758 759 if not self._ttinfo_std: 760 return ZERO 761 762 return self._find_ttinfo(dt).delta 763 764 def dst(self, dt): 765 if dt is None: 766 return None 767 768 if not self._ttinfo_dst: 769 return ZERO 770 771 tti = self._find_ttinfo(dt) 772 773 if not tti.isdst: 774 return ZERO 775 776 # The documentation says that utcoffset()-dst() must 777 # be constant for every dt. 778 return tti.dstoffset 779 780 @tzname_in_python2 781 def tzname(self, dt): 782 if not self._ttinfo_std or dt is None: 783 return None 784 return self._find_ttinfo(dt).abbr 785 786 def __eq__(self, other): 787 if not isinstance(other, tzfile): 788 return NotImplemented 789 return (self._trans_list == other._trans_list and 790 self._trans_idx == other._trans_idx and 791 self._ttinfo_list == other._ttinfo_list) 792 793 __hash__ = None 794 795 def __ne__(self, other): 796 return not (self == other) 797 798 def __repr__(self): 799 return "%s(%s)" % (self.__class__.__name__, repr(self._filename)) 800 801 def __reduce__(self): 802 return self.__reduce_ex__(None) 803 804 def __reduce_ex__(self, protocol): 805 return (self.__class__, (None, self._filename), self.__dict__) 806 807 808class tzrange(tzrangebase): 809 """ 810 The ``tzrange`` object is a time zone specified by a set of offsets and 811 abbreviations, equivalent to the way the ``TZ`` variable can be specified 812 in POSIX-like systems, but using Python delta objects to specify DST 813 start, end and offsets. 814 815 :param stdabbr: 816 The abbreviation for standard time (e.g. ``'EST'``). 817 818 :param stdoffset: 819 An integer or :class:`datetime.timedelta` object or equivalent 820 specifying the base offset from UTC. 821 822 If unspecified, +00:00 is used. 823 824 :param dstabbr: 825 The abbreviation for DST / "Summer" time (e.g. ``'EDT'``). 826 827 If specified, with no other DST information, DST is assumed to occur 828 and the default behavior or ``dstoffset``, ``start`` and ``end`` is 829 used. If unspecified and no other DST information is specified, it 830 is assumed that this zone has no DST. 831 832 If this is unspecified and other DST information is *is* specified, 833 DST occurs in the zone but the time zone abbreviation is left 834 unchanged. 835 836 :param dstoffset: 837 A an integer or :class:`datetime.timedelta` object or equivalent 838 specifying the UTC offset during DST. If unspecified and any other DST 839 information is specified, it is assumed to be the STD offset +1 hour. 840 841 :param start: 842 A :class:`relativedelta.relativedelta` object or equivalent specifying 843 the time and time of year that daylight savings time starts. To specify, 844 for example, that DST starts at 2AM on the 2nd Sunday in March, pass: 845 846 ``relativedelta(hours=2, month=3, day=1, weekday=SU(+2))`` 847 848 If unspecified and any other DST information is specified, the default 849 value is 2 AM on the first Sunday in April. 850 851 :param end: 852 A :class:`relativedelta.relativedelta` object or equivalent representing 853 the time and time of year that daylight savings time ends, with the 854 same specification method as in ``start``. One note is that this should 855 point to the first time in the *standard* zone, so if a transition 856 occurs at 2AM in the DST zone and the clocks are set back 1 hour to 1AM, 857 set the `hours` parameter to +1. 858 859 860 **Examples:** 861 862 .. testsetup:: tzrange 863 864 from dateutil.tz import tzrange, tzstr 865 866 .. doctest:: tzrange 867 868 >>> tzstr('EST5EDT') == tzrange("EST", -18000, "EDT") 869 True 870 871 >>> from dateutil.relativedelta import * 872 >>> range1 = tzrange("EST", -18000, "EDT") 873 >>> range2 = tzrange("EST", -18000, "EDT", -14400, 874 ... relativedelta(hours=+2, month=4, day=1, 875 ... weekday=SU(+1)), 876 ... relativedelta(hours=+1, month=10, day=31, 877 ... weekday=SU(-1))) 878 >>> tzstr('EST5EDT') == range1 == range2 879 True 880 881 """ 882 def __init__(self, stdabbr, stdoffset=None, 883 dstabbr=None, dstoffset=None, 884 start=None, end=None): 885 886 global relativedelta 887 from dateutil import relativedelta 888 889 self._std_abbr = stdabbr 890 self._dst_abbr = dstabbr 891 892 try: 893 stdoffset = stdoffset.total_seconds() 894 except (TypeError, AttributeError): 895 pass 896 897 try: 898 dstoffset = dstoffset.total_seconds() 899 except (TypeError, AttributeError): 900 pass 901 902 if stdoffset is not None: 903 self._std_offset = datetime.timedelta(seconds=stdoffset) 904 else: 905 self._std_offset = ZERO 906 907 if dstoffset is not None: 908 self._dst_offset = datetime.timedelta(seconds=dstoffset) 909 elif dstabbr and stdoffset is not None: 910 self._dst_offset = self._std_offset + datetime.timedelta(hours=+1) 911 else: 912 self._dst_offset = ZERO 913 914 if dstabbr and start is None: 915 self._start_delta = relativedelta.relativedelta( 916 hours=+2, month=4, day=1, weekday=relativedelta.SU(+1)) 917 else: 918 self._start_delta = start 919 920 if dstabbr and end is None: 921 self._end_delta = relativedelta.relativedelta( 922 hours=+1, month=10, day=31, weekday=relativedelta.SU(-1)) 923 else: 924 self._end_delta = end 925 926 self._dst_base_offset_ = self._dst_offset - self._std_offset 927 self.hasdst = bool(self._start_delta) 928 929 def transitions(self, year): 930 """ 931 For a given year, get the DST on and off transition times, expressed 932 always on the standard time side. For zones with no transitions, this 933 function returns ``None``. 934 935 :param year: 936 The year whose transitions you would like to query. 937 938 :return: 939 Returns a :class:`tuple` of :class:`datetime.datetime` objects, 940 ``(dston, dstoff)`` for zones with an annual DST transition, or 941 ``None`` for fixed offset zones. 942 """ 943 if not self.hasdst: 944 return None 945 946 base_year = datetime.datetime(year, 1, 1) 947 948 start = base_year + self._start_delta 949 end = base_year + self._end_delta 950 951 return (start, end) 952 953 def __eq__(self, other): 954 if not isinstance(other, tzrange): 955 return NotImplemented 956 957 return (self._std_abbr == other._std_abbr and 958 self._dst_abbr == other._dst_abbr and 959 self._std_offset == other._std_offset and 960 self._dst_offset == other._dst_offset and 961 self._start_delta == other._start_delta and 962 self._end_delta == other._end_delta) 963 964 @property 965 def _dst_base_offset(self): 966 return self._dst_base_offset_ 967 968 969@six.add_metaclass(_TzStrFactory) 970class tzstr(tzrange): 971 """ 972 ``tzstr`` objects are time zone objects specified by a time-zone string as 973 it would be passed to a ``TZ`` variable on POSIX-style systems (see 974 the `GNU C Library: TZ Variable`_ for more details). 975 976 There is one notable exception, which is that POSIX-style time zones use an 977 inverted offset format, so normally ``GMT+3`` would be parsed as an offset 978 3 hours *behind* GMT. The ``tzstr`` time zone object will parse this as an 979 offset 3 hours *ahead* of GMT. If you would like to maintain the POSIX 980 behavior, pass a ``True`` value to ``posix_offset``. 981 982 The :class:`tzrange` object provides the same functionality, but is 983 specified using :class:`relativedelta.relativedelta` objects. rather than 984 strings. 985 986 :param s: 987 A time zone string in ``TZ`` variable format. This can be a 988 :class:`bytes` (2.x: :class:`str`), :class:`str` (2.x: :class:`unicode`) 989 or a stream emitting unicode characters (e.g. :class:`StringIO`). 990 991 :param posix_offset: 992 Optional. If set to ``True``, interpret strings such as ``GMT+3`` or 993 ``UTC+3`` as being 3 hours *behind* UTC rather than ahead, per the 994 POSIX standard. 995 996 .. caution:: 997 998 Prior to version 2.7.0, this function also supported time zones 999 in the format: 1000 1001 * ``EST5EDT,4,0,6,7200,10,0,26,7200,3600`` 1002 * ``EST5EDT,4,1,0,7200,10,-1,0,7200,3600`` 1003 1004 This format is non-standard and has been deprecated; this function 1005 will raise a :class:`DeprecatedTZFormatWarning` until 1006 support is removed in a future version. 1007 1008 .. _`GNU C Library: TZ Variable`: 1009 https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html 1010 """ 1011 def __init__(self, s, posix_offset=False): 1012 global parser 1013 from dateutil.parser import _parser as parser 1014 1015 self._s = s 1016 1017 res = parser._parsetz(s) 1018 if res is None or res.any_unused_tokens: 1019 raise ValueError("unknown string format") 1020 1021 # Here we break the compatibility with the TZ variable handling. 1022 # GMT-3 actually *means* the timezone -3. 1023 if res.stdabbr in ("GMT", "UTC") and not posix_offset: 1024 res.stdoffset *= -1 1025 1026 # We must initialize it first, since _delta() needs 1027 # _std_offset and _dst_offset set. Use False in start/end 1028 # to avoid building it two times. 1029 tzrange.__init__(self, res.stdabbr, res.stdoffset, 1030 res.dstabbr, res.dstoffset, 1031 start=False, end=False) 1032 1033 if not res.dstabbr: 1034 self._start_delta = None 1035 self._end_delta = None 1036 else: 1037 self._start_delta = self._delta(res.start) 1038 if self._start_delta: 1039 self._end_delta = self._delta(res.end, isend=1) 1040 1041 self.hasdst = bool(self._start_delta) 1042 1043 def _delta(self, x, isend=0): 1044 from dateutil import relativedelta 1045 kwargs = {} 1046 if x.month is not None: 1047 kwargs["month"] = x.month 1048 if x.weekday is not None: 1049 kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week) 1050 if x.week > 0: 1051 kwargs["day"] = 1 1052 else: 1053 kwargs["day"] = 31 1054 elif x.day: 1055 kwargs["day"] = x.day 1056 elif x.yday is not None: 1057 kwargs["yearday"] = x.yday 1058 elif x.jyday is not None: 1059 kwargs["nlyearday"] = x.jyday 1060 if not kwargs: 1061 # Default is to start on first sunday of april, and end 1062 # on last sunday of october. 1063 if not isend: 1064 kwargs["month"] = 4 1065 kwargs["day"] = 1 1066 kwargs["weekday"] = relativedelta.SU(+1) 1067 else: 1068 kwargs["month"] = 10 1069 kwargs["day"] = 31 1070 kwargs["weekday"] = relativedelta.SU(-1) 1071 if x.time is not None: 1072 kwargs["seconds"] = x.time 1073 else: 1074 # Default is 2AM. 1075 kwargs["seconds"] = 7200 1076 if isend: 1077 # Convert to standard time, to follow the documented way 1078 # of working with the extra hour. See the documentation 1079 # of the tzinfo class. 1080 delta = self._dst_offset - self._std_offset 1081 kwargs["seconds"] -= delta.seconds + delta.days * 86400 1082 return relativedelta.relativedelta(**kwargs) 1083 1084 def __repr__(self): 1085 return "%s(%s)" % (self.__class__.__name__, repr(self._s)) 1086 1087 1088class _tzicalvtzcomp(object): 1089 def __init__(self, tzoffsetfrom, tzoffsetto, isdst, 1090 tzname=None, rrule=None): 1091 self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom) 1092 self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto) 1093 self.tzoffsetdiff = self.tzoffsetto - self.tzoffsetfrom 1094 self.isdst = isdst 1095 self.tzname = tzname 1096 self.rrule = rrule 1097 1098 1099class _tzicalvtz(_tzinfo): 1100 def __init__(self, tzid, comps=[]): 1101 super(_tzicalvtz, self).__init__() 1102 1103 self._tzid = tzid 1104 self._comps = comps 1105 self._cachedate = [] 1106 self._cachecomp = [] 1107 self._cache_lock = _thread.allocate_lock() 1108 1109 def _find_comp(self, dt): 1110 if len(self._comps) == 1: 1111 return self._comps[0] 1112 1113 dt = dt.replace(tzinfo=None) 1114 1115 try: 1116 with self._cache_lock: 1117 return self._cachecomp[self._cachedate.index( 1118 (dt, self._fold(dt)))] 1119 except ValueError: 1120 pass 1121 1122 lastcompdt = None 1123 lastcomp = None 1124 1125 for comp in self._comps: 1126 compdt = self._find_compdt(comp, dt) 1127 1128 if compdt and (not lastcompdt or lastcompdt < compdt): 1129 lastcompdt = compdt 1130 lastcomp = comp 1131 1132 if not lastcomp: 1133 # RFC says nothing about what to do when a given 1134 # time is before the first onset date. We'll look for the 1135 # first standard component, or the first component, if 1136 # none is found. 1137 for comp in self._comps: 1138 if not comp.isdst: 1139 lastcomp = comp 1140 break 1141 else: 1142 lastcomp = comp[0] 1143 1144 with self._cache_lock: 1145 self._cachedate.insert(0, (dt, self._fold(dt))) 1146 self._cachecomp.insert(0, lastcomp) 1147 1148 if len(self._cachedate) > 10: 1149 self._cachedate.pop() 1150 self._cachecomp.pop() 1151 1152 return lastcomp 1153 1154 def _find_compdt(self, comp, dt): 1155 if comp.tzoffsetdiff < ZERO and self._fold(dt): 1156 dt -= comp.tzoffsetdiff 1157 1158 compdt = comp.rrule.before(dt, inc=True) 1159 1160 return compdt 1161 1162 def utcoffset(self, dt): 1163 if dt is None: 1164 return None 1165 1166 return self._find_comp(dt).tzoffsetto 1167 1168 def dst(self, dt): 1169 comp = self._find_comp(dt) 1170 if comp.isdst: 1171 return comp.tzoffsetdiff 1172 else: 1173 return ZERO 1174 1175 @tzname_in_python2 1176 def tzname(self, dt): 1177 return self._find_comp(dt).tzname 1178 1179 def __repr__(self): 1180 return "<tzicalvtz %s>" % repr(self._tzid) 1181 1182 __reduce__ = object.__reduce__ 1183 1184 1185class tzical(object): 1186 """ 1187 This object is designed to parse an iCalendar-style ``VTIMEZONE`` structure 1188 as set out in `RFC 5545`_ Section 4.6.5 into one or more `tzinfo` objects. 1189 1190 :param `fileobj`: 1191 A file or stream in iCalendar format, which should be UTF-8 encoded 1192 with CRLF endings. 1193 1194 .. _`RFC 5545`: https://tools.ietf.org/html/rfc5545 1195 """ 1196 def __init__(self, fileobj): 1197 global rrule 1198 from dateutil import rrule 1199 1200 if isinstance(fileobj, string_types): 1201 self._s = fileobj 1202 # ical should be encoded in UTF-8 with CRLF 1203 fileobj = open(fileobj, 'r') 1204 else: 1205 self._s = getattr(fileobj, 'name', repr(fileobj)) 1206 fileobj = _ContextWrapper(fileobj) 1207 1208 self._vtz = {} 1209 1210 with fileobj as fobj: 1211 self._parse_rfc(fobj.read()) 1212 1213 def keys(self): 1214 """ 1215 Retrieves the available time zones as a list. 1216 """ 1217 return list(self._vtz.keys()) 1218 1219 def get(self, tzid=None): 1220 """ 1221 Retrieve a :py:class:`datetime.tzinfo` object by its ``tzid``. 1222 1223 :param tzid: 1224 If there is exactly one time zone available, omitting ``tzid`` 1225 or passing :py:const:`None` value returns it. Otherwise a valid 1226 key (which can be retrieved from :func:`keys`) is required. 1227 1228 :raises ValueError: 1229 Raised if ``tzid`` is not specified but there are either more 1230 or fewer than 1 zone defined. 1231 1232 :returns: 1233 Returns either a :py:class:`datetime.tzinfo` object representing 1234 the relevant time zone or :py:const:`None` if the ``tzid`` was 1235 not found. 1236 """ 1237 if tzid is None: 1238 if len(self._vtz) == 0: 1239 raise ValueError("no timezones defined") 1240 elif len(self._vtz) > 1: 1241 raise ValueError("more than one timezone available") 1242 tzid = next(iter(self._vtz)) 1243 1244 return self._vtz.get(tzid) 1245 1246 def _parse_offset(self, s): 1247 s = s.strip() 1248 if not s: 1249 raise ValueError("empty offset") 1250 if s[0] in ('+', '-'): 1251 signal = (-1, +1)[s[0] == '+'] 1252 s = s[1:] 1253 else: 1254 signal = +1 1255 if len(s) == 4: 1256 return (int(s[:2]) * 3600 + int(s[2:]) * 60) * signal 1257 elif len(s) == 6: 1258 return (int(s[:2]) * 3600 + int(s[2:4]) * 60 + int(s[4:])) * signal 1259 else: 1260 raise ValueError("invalid offset: " + s) 1261 1262 def _parse_rfc(self, s): 1263 lines = s.splitlines() 1264 if not lines: 1265 raise ValueError("empty string") 1266 1267 # Unfold 1268 i = 0 1269 while i < len(lines): 1270 line = lines[i].rstrip() 1271 if not line: 1272 del lines[i] 1273 elif i > 0 and line[0] == " ": 1274 lines[i-1] += line[1:] 1275 del lines[i] 1276 else: 1277 i += 1 1278 1279 tzid = None 1280 comps = [] 1281 invtz = False 1282 comptype = None 1283 for line in lines: 1284 if not line: 1285 continue 1286 name, value = line.split(':', 1) 1287 parms = name.split(';') 1288 if not parms: 1289 raise ValueError("empty property name") 1290 name = parms[0].upper() 1291 parms = parms[1:] 1292 if invtz: 1293 if name == "BEGIN": 1294 if value in ("STANDARD", "DAYLIGHT"): 1295 # Process component 1296 pass 1297 else: 1298 raise ValueError("unknown component: "+value) 1299 comptype = value 1300 founddtstart = False 1301 tzoffsetfrom = None 1302 tzoffsetto = None 1303 rrulelines = [] 1304 tzname = None 1305 elif name == "END": 1306 if value == "VTIMEZONE": 1307 if comptype: 1308 raise ValueError("component not closed: "+comptype) 1309 if not tzid: 1310 raise ValueError("mandatory TZID not found") 1311 if not comps: 1312 raise ValueError( 1313 "at least one component is needed") 1314 # Process vtimezone 1315 self._vtz[tzid] = _tzicalvtz(tzid, comps) 1316 invtz = False 1317 elif value == comptype: 1318 if not founddtstart: 1319 raise ValueError("mandatory DTSTART not found") 1320 if tzoffsetfrom is None: 1321 raise ValueError( 1322 "mandatory TZOFFSETFROM not found") 1323 if tzoffsetto is None: 1324 raise ValueError( 1325 "mandatory TZOFFSETFROM not found") 1326 # Process component 1327 rr = None 1328 if rrulelines: 1329 rr = rrule.rrulestr("\n".join(rrulelines), 1330 compatible=True, 1331 ignoretz=True, 1332 cache=True) 1333 comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto, 1334 (comptype == "DAYLIGHT"), 1335 tzname, rr) 1336 comps.append(comp) 1337 comptype = None 1338 else: 1339 raise ValueError("invalid component end: "+value) 1340 elif comptype: 1341 if name == "DTSTART": 1342 # DTSTART in VTIMEZONE takes a subset of valid RRULE 1343 # values under RFC 5545. 1344 for parm in parms: 1345 if parm != 'VALUE=DATE-TIME': 1346 msg = ('Unsupported DTSTART param in ' + 1347 'VTIMEZONE: ' + parm) 1348 raise ValueError(msg) 1349 rrulelines.append(line) 1350 founddtstart = True 1351 elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"): 1352 rrulelines.append(line) 1353 elif name == "TZOFFSETFROM": 1354 if parms: 1355 raise ValueError( 1356 "unsupported %s parm: %s " % (name, parms[0])) 1357 tzoffsetfrom = self._parse_offset(value) 1358 elif name == "TZOFFSETTO": 1359 if parms: 1360 raise ValueError( 1361 "unsupported TZOFFSETTO parm: "+parms[0]) 1362 tzoffsetto = self._parse_offset(value) 1363 elif name == "TZNAME": 1364 if parms: 1365 raise ValueError( 1366 "unsupported TZNAME parm: "+parms[0]) 1367 tzname = value 1368 elif name == "COMMENT": 1369 pass 1370 else: 1371 raise ValueError("unsupported property: "+name) 1372 else: 1373 if name == "TZID": 1374 if parms: 1375 raise ValueError( 1376 "unsupported TZID parm: "+parms[0]) 1377 tzid = value 1378 elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"): 1379 pass 1380 else: 1381 raise ValueError("unsupported property: "+name) 1382 elif name == "BEGIN" and value == "VTIMEZONE": 1383 tzid = None 1384 comps = [] 1385 invtz = True 1386 1387 def __repr__(self): 1388 return "%s(%s)" % (self.__class__.__name__, repr(self._s)) 1389 1390 1391if sys.platform != "win32": 1392 TZFILES = ["/etc/localtime", "localtime"] 1393 TZPATHS = ["/usr/share/zoneinfo", 1394 "/usr/lib/zoneinfo", 1395 "/usr/share/lib/zoneinfo", 1396 "/etc/zoneinfo"] 1397else: 1398 TZFILES = [] 1399 TZPATHS = [] 1400 1401def __get_gettz(): 1402 tzlocal_classes = (tzlocal,) 1403 if tzwinlocal is not None: 1404 tzlocal_classes += (tzwinlocal,) 1405 1406 class GettzFunc(object): 1407 def __init__(self): 1408 1409 self.__instances = {} 1410 self._cache_lock = _thread.allocate_lock() 1411 1412 def __call__(self, name=None): 1413 with self._cache_lock: 1414 rv = self.__instances.get(name, None) 1415 1416 if rv is None: 1417 rv = self.nocache(name=name) 1418 if not (name is None or isinstance(rv, tzlocal_classes)): 1419 # tzlocal is slightly more complicated than the other 1420 # time zone providers because it depends on environment 1421 # at construction time, so don't cache that. 1422 self.__instances[name] = rv 1423 1424 return rv 1425 1426 def cache_clear(self): 1427 with self._cache_lock: 1428 self.__instances = {} 1429 1430 @staticmethod 1431 def nocache(name=None): 1432 """A non-cached version of gettz""" 1433 tz = None 1434 if not name: 1435 try: 1436 name = os.environ["TZ"] 1437 except KeyError: 1438 pass 1439 if name is None or name == ":": 1440 for filepath in TZFILES: 1441 if not os.path.isabs(filepath): 1442 filename = filepath 1443 for path in TZPATHS: 1444 filepath = os.path.join(path, filename) 1445 if os.path.isfile(filepath): 1446 break 1447 else: 1448 continue 1449 if os.path.isfile(filepath): 1450 try: 1451 tz = tzfile(filepath) 1452 break 1453 except (IOError, OSError, ValueError): 1454 pass 1455 else: 1456 tz = tzlocal() 1457 else: 1458 if name.startswith(":"): 1459 name = name[1:] 1460 if os.path.isabs(name): 1461 if os.path.isfile(name): 1462 tz = tzfile(name) 1463 else: 1464 tz = None 1465 else: 1466 for path in TZPATHS: 1467 filepath = os.path.join(path, name) 1468 if not os.path.isfile(filepath): 1469 filepath = filepath.replace(' ', '_') 1470 if not os.path.isfile(filepath): 1471 continue 1472 try: 1473 tz = tzfile(filepath) 1474 break 1475 except (IOError, OSError, ValueError): 1476 pass 1477 else: 1478 tz = None 1479 if tzwin is not None: 1480 try: 1481 tz = tzwin(name) 1482 except WindowsError: 1483 tz = None 1484 1485 if not tz: 1486 from dateutil.zoneinfo import get_zonefile_instance 1487 tz = get_zonefile_instance().get(name) 1488 1489 if not tz: 1490 for c in name: 1491 # name must have at least one offset to be a tzstr 1492 if c in "0123456789": 1493 try: 1494 tz = tzstr(name) 1495 except ValueError: 1496 pass 1497 break 1498 else: 1499 if name in ("GMT", "UTC"): 1500 tz = tzutc() 1501 elif name in time.tzname: 1502 tz = tzlocal() 1503 return tz 1504 1505 return GettzFunc() 1506 1507gettz = __get_gettz() 1508del __get_gettz 1509 1510def datetime_exists(dt, tz=None): 1511 """ 1512 Given a datetime and a time zone, determine whether or not a given datetime 1513 would fall in a gap. 1514 1515 :param dt: 1516 A :class:`datetime.datetime` (whose time zone will be ignored if ``tz`` 1517 is provided.) 1518 1519 :param tz: 1520 A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If 1521 ``None`` or not provided, the datetime's own time zone will be used. 1522 1523 :return: 1524 Returns a boolean value whether or not the "wall time" exists in ``tz``. 1525 1526 ..versionadded:: 2.7.0 1527 """ 1528 if tz is None: 1529 if dt.tzinfo is None: 1530 raise ValueError('Datetime is naive and no time zone provided.') 1531 tz = dt.tzinfo 1532 1533 dt = dt.replace(tzinfo=None) 1534 1535 # This is essentially a test of whether or not the datetime can survive 1536 # a round trip to UTC. 1537 dt_rt = dt.replace(tzinfo=tz).astimezone(tzutc()).astimezone(tz) 1538 dt_rt = dt_rt.replace(tzinfo=None) 1539 1540 return dt == dt_rt 1541 1542 1543def datetime_ambiguous(dt, tz=None): 1544 """ 1545 Given a datetime and a time zone, determine whether or not a given datetime 1546 is ambiguous (i.e if there are two times differentiated only by their DST 1547 status). 1548 1549 :param dt: 1550 A :class:`datetime.datetime` (whose time zone will be ignored if ``tz`` 1551 is provided.) 1552 1553 :param tz: 1554 A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If 1555 ``None`` or not provided, the datetime's own time zone will be used. 1556 1557 :return: 1558 Returns a boolean value whether or not the "wall time" is ambiguous in 1559 ``tz``. 1560 1561 .. versionadded:: 2.6.0 1562 """ 1563 if tz is None: 1564 if dt.tzinfo is None: 1565 raise ValueError('Datetime is naive and no time zone provided.') 1566 1567 tz = dt.tzinfo 1568 1569 # If a time zone defines its own "is_ambiguous" function, we'll use that. 1570 is_ambiguous_fn = getattr(tz, 'is_ambiguous', None) 1571 if is_ambiguous_fn is not None: 1572 try: 1573 return tz.is_ambiguous(dt) 1574 except: 1575 pass 1576 1577 # If it doesn't come out and tell us it's ambiguous, we'll just check if 1578 # the fold attribute has any effect on this particular date and time. 1579 dt = dt.replace(tzinfo=tz) 1580 wall_0 = enfold(dt, fold=0) 1581 wall_1 = enfold(dt, fold=1) 1582 1583 same_offset = wall_0.utcoffset() == wall_1.utcoffset() 1584 same_dst = wall_0.dst() == wall_1.dst() 1585 1586 return not (same_offset and same_dst) 1587 1588 1589def resolve_imaginary(dt): 1590 """ 1591 Given a datetime that may be imaginary, return an existing datetime. 1592 1593 This function assumes that an imaginary datetime represents what the 1594 wall time would be in a zone had the offset transition not occurred, so 1595 it will always fall forward by the transition's change in offset. 1596 1597 ..doctest:: 1598 >>> from dateutil import tz 1599 >>> from datetime import datetime 1600 >>> NYC = tz.gettz('America/New_York') 1601 >>> print(tz.resolve_imaginary(datetime(2017, 3, 12, 2, 30, tzinfo=NYC))) 1602 2017-03-12 03:30:00-04:00 1603 1604 >>> KIR = tz.gettz('Pacific/Kiritimati') 1605 >>> print(tz.resolve_imaginary(datetime(1995, 1, 1, 12, 30, tzinfo=KIR))) 1606 1995-01-02 12:30:00+14:00 1607 1608 As a note, :func:`datetime.astimezone` is guaranteed to produce a valid, 1609 existing datetime, so a round-trip to and from UTC is sufficient to get 1610 an extant datetime, however, this generally "falls back" to an earlier time 1611 rather than falling forward to the STD side (though no guarantees are made 1612 about this behavior). 1613 1614 :param dt: 1615 A :class:`datetime.datetime` which may or may not exist. 1616 1617 :return: 1618 Returns an existing :class:`datetime.datetime`. If ``dt`` was not 1619 imaginary, the datetime returned is guaranteed to be the same object 1620 passed to the function. 1621 1622 ..versionadded:: 2.7.0 1623 """ 1624 if dt.tzinfo is not None and not datetime_exists(dt): 1625 1626 curr_offset = (dt + datetime.timedelta(hours=24)).utcoffset() 1627 old_offset = (dt - datetime.timedelta(hours=24)).utcoffset() 1628 1629 dt += curr_offset - old_offset 1630 1631 return dt 1632 1633 1634def _datetime_to_timestamp(dt): 1635 """ 1636 Convert a :class:`datetime.datetime` object to an epoch timestamp in seconds 1637 since January 1, 1970, ignoring the time zone. 1638 """ 1639 return (dt.replace(tzinfo=None) - EPOCH).total_seconds() 1640 1641 1642class _ContextWrapper(object): 1643 """ 1644 Class for wrapping contexts so that they are passed through in a 1645 with statement. 1646 """ 1647 def __init__(self, context): 1648 self.context = context 1649 1650 def __enter__(self): 1651 return self.context 1652 1653 def __exit__(*args, **kwargs): 1654 pass 1655 1656# vim:ts=4:sw=4:et 1657