Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/dateutil/tz/tz.py: 38%

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

591 statements  

1""" 

2This module offers timezone implementations subclassing the abstract 

3:py:class:`datetime.tzinfo` type. There are classes to handle tzfile format 

4files (usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`, 

5etc), TZ environment string (in all known formats), given ranges (with help 

6from relative deltas), local machine timezone, fixed offset timezone, and UTC 

7timezone. 

8""" 

9 

10import _thread 

11import contextlib 

12import datetime 

13import os 

14import time 

15import warnings 

16import weakref 

17from collections import OrderedDict 

18 

19from .. import _tzdata_impl 

20from . import _tzpath 

21from ._common import _tzinfo, _validate_fromutc_inputs, enfold, tzrangebase 

22from ._factories import _TzOffsetFactory, _TzSingleton, _TzStrFactory 

23from ._tzfile import tzfile 

24 

25try: 

26 from .win import tzwin, tzwinlocal 

27except ImportError: 

28 tzwin = tzwinlocal = None 

29 

30ZERO = datetime.timedelta(0) 

31EPOCH = datetime.datetime(1970, 1, 1, 0, 0) 

32 

33 

34class tzutc(datetime.tzinfo, metaclass=_TzSingleton): 

35 """ 

36 This is a tzinfo object that represents the UTC time zone. 

37 

38 **Examples:** 

39 

40 .. doctest:: 

41 

42 >>> from datetime import * 

43 >>> from dateutil.tz import * 

44 

45 >>> datetime.now() 

46 datetime.datetime(2003, 9, 27, 9, 40, 1, 521290) 

47 

48 >>> datetime.now(tzutc()) 

49 datetime.datetime(2003, 9, 27, 12, 40, 12, 156379, tzinfo=tzutc()) 

50 

51 >>> datetime.now(tzutc()).tzname() 

52 'UTC' 

53 

54 .. versionchanged:: 2.7.0 

55 ``tzutc()`` is now a singleton, so the result of ``tzutc()`` will 

56 always return the same object. 

57 

58 .. doctest:: 

59 

60 >>> from dateutil.tz import tzutc, UTC 

61 >>> tzutc() is tzutc() 

62 True 

63 >>> tzutc() is UTC 

64 True 

65 """ 

66 def utcoffset(self, dt): 

67 return ZERO 

68 

69 def dst(self, dt): 

70 return ZERO 

71 

72 def tzname(self, dt): 

73 return "UTC" 

74 

75 def is_ambiguous(self, dt): 

76 """ 

77 Whether or not the "wall time" of a given datetime is ambiguous in this 

78 zone. 

79 

80 :param dt: 

81 A :py:class:`datetime.datetime`, naive or time zone aware. 

82 

83 

84 :return: 

85 Returns ``True`` if ambiguous, ``False`` otherwise. 

86 

87 .. versionadded:: 2.6.0 

88 """ 

89 return False 

90 

91 @_validate_fromutc_inputs 

92 def fromutc(self, dt): 

93 """ 

94 Fast track version of fromutc() returns the original ``dt`` object for 

95 any valid :py:class:`datetime.datetime` object. 

96 """ 

97 return dt 

98 

99 def __eq__(self, other): 

100 if not isinstance(other, (tzutc, tzoffset)): 

101 return NotImplemented 

102 

103 return (isinstance(other, tzutc) or 

104 (isinstance(other, tzoffset) and other._offset == ZERO)) 

105 

106 __hash__ = None 

107 

108 def __repr__(self): 

109 return "%s()" % self.__class__.__name__ 

110 

111 __reduce__ = object.__reduce__ 

112 

113 

114#: Convenience constant providing a :class:`tzutc()` instance 

115#: 

116#: .. versionadded:: 2.7.0 

117UTC = tzutc() 

118 

119 

120class tzoffset(datetime.tzinfo, metaclass=_TzOffsetFactory): 

121 """ 

122 A simple class for representing a fixed offset from UTC. 

123 

124 :param name: 

125 The timezone name, to be returned when ``tzname()`` is called. 

126 :param offset: 

127 The time zone offset in seconds, or (since version 2.6.0, represented 

128 as a :py:class:`datetime.timedelta` object). 

129 """ 

130 def __init__(self, name, offset): 

131 self._name = name 

132 

133 try: 

134 # Allow a timedelta 

135 offset = offset.total_seconds() 

136 except (TypeError, AttributeError): 

137 pass 

138 

139 self._offset = datetime.timedelta(seconds=offset) 

140 

141 def utcoffset(self, dt): 

142 return self._offset 

143 

144 def dst(self, dt): 

145 return ZERO 

146 

147 def tzname(self, dt): 

148 return self._name 

149 

150 @_validate_fromutc_inputs 

151 def fromutc(self, dt): 

152 return dt + self._offset 

153 

154 def is_ambiguous(self, dt): 

155 """ 

156 Whether or not the "wall time" of a given datetime is ambiguous in this 

157 zone. 

158 

159 :param dt: 

160 A :py:class:`datetime.datetime`, naive or time zone aware. 

161 :return: 

162 Returns ``True`` if ambiguous, ``False`` otherwise. 

163 

164 .. versionadded:: 2.6.0 

165 """ 

166 return False 

167 

168 def __eq__(self, other): 

169 if not isinstance(other, tzoffset): 

170 return NotImplemented 

171 

172 return self._offset == other._offset 

173 

174 __hash__ = None 

175 

176 def __repr__(self): 

177 return "%s(%s, %s)" % (self.__class__.__name__, 

178 repr(self._name), 

179 int(self._offset.total_seconds())) 

180 

181 __reduce__ = object.__reduce__ 

182 

183 

184class tzlocal(_tzinfo): 

185 """ 

186 A :class:`tzinfo` subclass built around the ``time`` timezone functions. 

187 """ 

188 def __init__(self): 

189 super().__init__() 

190 

191 self._std_offset = datetime.timedelta(seconds=-time.timezone) 

192 if time.daylight: 

193 self._dst_offset = datetime.timedelta(seconds=-time.altzone) 

194 else: 

195 self._dst_offset = self._std_offset 

196 

197 self._dst_saved = self._dst_offset - self._std_offset 

198 self._hasdst = bool(self._dst_saved) 

199 self._tznames = tuple(time.tzname) 

200 

201 def utcoffset(self, dt): 

202 if dt is None and self._hasdst: 

203 return None 

204 

205 if self._isdst(dt): 

206 return self._dst_offset 

207 else: 

208 return self._std_offset 

209 

210 def dst(self, dt): 

211 if dt is None and self._hasdst: 

212 return None 

213 

214 if self._isdst(dt): 

215 return self._dst_offset - self._std_offset 

216 else: 

217 return ZERO 

218 

219 def tzname(self, dt): 

220 return self._tznames[self._isdst(dt)] 

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 naive_dst = self._naive_is_dst(dt) 

237 return (not naive_dst and 

238 (naive_dst != self._naive_is_dst(dt - self._dst_saved))) 

239 

240 def _naive_is_dst(self, dt): 

241 timestamp = _datetime_to_timestamp(dt) 

242 return time.localtime(timestamp + time.timezone).tm_isdst 

243 

244 def _isdst(self, dt, fold_naive=True): 

245 # We can't use mktime here. It is unstable when deciding if 

246 # the hour near to a change is DST or not. 

247 # 

248 # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour, 

249 # dt.minute, dt.second, dt.weekday(), 0, -1)) 

250 # return time.localtime(timestamp).tm_isdst 

251 # 

252 # The code above yields the following result: 

253 # 

254 # >>> import tz, datetime 

255 # >>> t = tz.tzlocal() 

256 # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() 

257 # 'BRDT' 

258 # >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname() 

259 # 'BRST' 

260 # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() 

261 # 'BRST' 

262 # >>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname() 

263 # 'BRDT' 

264 # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() 

265 # 'BRDT' 

266 # 

267 # Here is a more stable implementation: 

268 # 

269 if not self._hasdst: 

270 return False 

271 

272 # Check for ambiguous times: 

273 dstval = self._naive_is_dst(dt) 

274 

275 if self.is_ambiguous(dt): 

276 return not dt.fold 

277 

278 return dstval 

279 

280 def __eq__(self, other): 

281 if isinstance(other, tzlocal): 

282 return (self._std_offset == other._std_offset and 

283 self._dst_offset == other._dst_offset) 

284 elif isinstance(other, tzutc): 

285 return (not self._hasdst and 

286 self._tznames[0] in {'UTC', 'GMT'} and 

287 self._std_offset == ZERO) 

288 elif isinstance(other, tzoffset): 

289 return (not self._hasdst and 

290 self._tznames[0] == other._name and 

291 self._std_offset == other._offset) 

292 else: 

293 return NotImplemented 

294 

295 __hash__ = None 

296 

297 def __repr__(self): 

298 return "%s()" % self.__class__.__name__ 

299 

300 __reduce__ = object.__reduce__ 

301 

302 

303class tzrange(tzrangebase): 

304 """ 

305 The ``tzrange`` object is a time zone specified by a set of offsets and 

306 abbreviations, equivalent to the way the ``TZ`` variable can be specified 

307 in POSIX-like systems, but using Python delta objects to specify DST 

308 start, end and offsets. 

309 

310 :param stdabbr: 

311 The abbreviation for standard time (e.g. ``'EST'``). 

312 

313 :param stdoffset: 

314 An integer or :class:`datetime.timedelta` object or equivalent 

315 specifying the base offset from UTC. 

316 

317 If unspecified, +00:00 is used. 

318 

319 :param dstabbr: 

320 The abbreviation for DST / "Summer" time (e.g. ``'EDT'``). 

321 

322 If specified, with no other DST information, DST is assumed to occur 

323 and the default behavior or ``dstoffset``, ``start`` and ``end`` is 

324 used. If unspecified and no other DST information is specified, it 

325 is assumed that this zone has no DST. 

326 

327 If this is unspecified and other DST information is *is* specified, 

328 DST occurs in the zone but the time zone abbreviation is left 

329 unchanged. 

330 

331 :param dstoffset: 

332 A an integer or :class:`datetime.timedelta` object or equivalent 

333 specifying the UTC offset during DST. If unspecified and any other DST 

334 information is specified, it is assumed to be the STD offset +1 hour. 

335 

336 :param start: 

337 A :class:`relativedelta.relativedelta` object or equivalent specifying 

338 the time and time of year that daylight savings time starts. To 

339 specify, for example, that DST starts at 2AM on the 2nd Sunday in 

340 March, pass: 

341 

342 ``relativedelta(hours=2, month=3, day=1, weekday=SU(+2))`` 

343 

344 If unspecified and any other DST information is specified, the default 

345 value is 2 AM on the first Sunday in April. 

346 

347 :param end: 

348 A :class:`relativedelta.relativedelta` object or equivalent 

349 representing the time and time of year that daylight savings time 

350 ends, with the same specification method as in ``start``. One note is 

351 that this should point to the first time in the *standard* zone, so if 

352 a transition occurs at 2AM in the DST zone and the clocks are set back 

353 1 hour to 1AM, set the ``hours`` parameter to +1. 

354 

355 

356 **Examples:** 

357 

358 .. testsetup:: tzrange 

359 

360 from dateutil.tz import tzrange, tzstr 

361 

362 .. doctest:: tzrange 

363 

364 >>> tzstr('EST5EDT') == tzrange("EST", -18000, "EDT") 

365 True 

366 

367 >>> from dateutil.relativedelta import * 

368 >>> range1 = tzrange("EST", -18000, "EDT") 

369 >>> range2 = tzrange("EST", -18000, "EDT", -14400, 

370 ... relativedelta(hours=+2, month=4, day=1, 

371 ... weekday=SU(+1)), 

372 ... relativedelta(hours=+1, month=10, day=31, 

373 ... weekday=SU(-1))) 

374 >>> tzstr('EST5EDT') == range1 == range2 

375 True 

376 

377 """ 

378 def __init__(self, stdabbr, stdoffset=None, 

379 dstabbr=None, dstoffset=None, 

380 start=None, end=None): 

381 

382 global relativedelta 

383 from dateutil import relativedelta 

384 

385 self._std_abbr = stdabbr 

386 self._dst_abbr = dstabbr 

387 

388 try: 

389 stdoffset = stdoffset.total_seconds() 

390 except (TypeError, AttributeError): 

391 pass 

392 

393 try: 

394 dstoffset = dstoffset.total_seconds() 

395 except (TypeError, AttributeError): 

396 pass 

397 

398 if stdoffset is not None: 

399 self._std_offset = datetime.timedelta(seconds=stdoffset) 

400 else: 

401 self._std_offset = ZERO 

402 

403 if dstoffset is not None: 

404 self._dst_offset = datetime.timedelta(seconds=dstoffset) 

405 elif dstabbr and stdoffset is not None: 

406 self._dst_offset = self._std_offset + datetime.timedelta(hours=+1) 

407 else: 

408 self._dst_offset = ZERO 

409 

410 if dstabbr and start is None: 

411 self._start_delta = relativedelta.relativedelta( 

412 hours=+2, month=4, day=1, weekday=relativedelta.SU(+1)) 

413 else: 

414 self._start_delta = start 

415 

416 if dstabbr and end is None: 

417 self._end_delta = relativedelta.relativedelta( 

418 hours=+1, month=10, day=31, weekday=relativedelta.SU(-1)) 

419 else: 

420 self._end_delta = end 

421 

422 self._dst_base_offset_ = self._dst_offset - self._std_offset 

423 self.hasdst = bool(self._start_delta) 

424 

425 def transitions(self, year): 

426 """ 

427 For a given year, get the DST on and off transition times, expressed 

428 always on the standard time side. For zones with no transitions, this 

429 function returns ``None``. 

430 

431 :param year: 

432 The year whose transitions you would like to query. 

433 

434 :return: 

435 Returns a :class:`tuple` of :class:`datetime.datetime` objects, 

436 ``(dston, dstoff)`` for zones with an annual DST transition, or 

437 ``None`` for fixed offset zones. 

438 """ 

439 if not self.hasdst: 

440 return None 

441 

442 base_year = datetime.datetime(year, 1, 1) 

443 

444 start = base_year + self._start_delta 

445 end = base_year + self._end_delta 

446 

447 return (start, end) 

448 

449 def __eq__(self, other): 

450 if not isinstance(other, tzrange): 

451 return NotImplemented 

452 

453 return (self._std_abbr == other._std_abbr and 

454 self._dst_abbr == other._dst_abbr and 

455 self._std_offset == other._std_offset and 

456 self._dst_offset == other._dst_offset and 

457 self._start_delta == other._start_delta and 

458 self._end_delta == other._end_delta) 

459 

460 @property 

461 def _dst_base_offset(self): 

462 return self._dst_base_offset_ 

463 

464 

465class tzstr(tzrange, metaclass=_TzStrFactory): 

466 """ 

467 ``tzstr`` objects are time zone objects specified by a time-zone string as 

468 it would be passed to a ``TZ`` variable on POSIX-style systems (see 

469 the `GNU C Library: TZ Variable`_ for more details). 

470 

471 There is one notable exception, which is that POSIX-style time zones use an 

472 inverted offset format, so normally ``GMT+3`` would be parsed as an offset 

473 3 hours *behind* GMT. The ``tzstr`` time zone object will parse this as an 

474 offset 3 hours *ahead* of GMT. If you would like to maintain the POSIX 

475 behavior, pass a ``True`` value to ``posix_offset``. 

476 

477 The :class:`tzrange` object provides the same functionality, but is 

478 specified using :class:`relativedelta.relativedelta` objects. rather than 

479 strings. 

480 

481 :param s: 

482 A time zone string in ``TZ`` variable format. This can be a 

483 :class:`bytes`, :class:`str` or a stream emitting Unicode characters 

484 (e.g. :class:`StringIO`). 

485 

486 :param posix_offset: 

487 Optional. If set to ``True``, interpret strings such as ``GMT+3`` or 

488 ``UTC+3`` as being 3 hours *behind* UTC rather than ahead, per the 

489 POSIX standard. 

490 

491 .. caution:: 

492 

493 Prior to version 2.7.0, this function also supported time zones 

494 in the format: 

495 

496 * ``EST5EDT,4,0,6,7200,10,0,26,7200,3600`` 

497 * ``EST5EDT,4,1,0,7200,10,-1,0,7200,3600`` 

498 

499 This format is non-standard and has been deprecated; this function 

500 will raise a :class:`DeprecatedTZFormatWarning` until 

501 support is removed in a future version. 

502 

503 .. _`GNU C Library: TZ Variable`: 

504 https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html 

505 """ 

506 def __init__(self, s, posix_offset=False): 

507 global parser 

508 from dateutil.parser import _parser as parser 

509 

510 self._s = s 

511 

512 res = parser._parsetz(s) 

513 if res is None or res.any_unused_tokens: 

514 raise ValueError("unknown string format") 

515 

516 # Here we break the compatibility with the TZ variable handling. 

517 # GMT-3 actually *means* the timezone -3. 

518 if res.stdabbr in ("GMT", "UTC") and not posix_offset: 

519 res.stdoffset *= -1 

520 

521 # We must initialize it first, since _delta() needs 

522 # _std_offset and _dst_offset set. Use False in start/end 

523 # to avoid building it two times. 

524 tzrange.__init__(self, res.stdabbr, res.stdoffset, 

525 res.dstabbr, res.dstoffset, 

526 start=False, end=False) 

527 

528 if not res.dstabbr: 

529 self._start_delta = None 

530 self._end_delta = None 

531 else: 

532 self._start_delta = self._delta(res.start) 

533 if self._start_delta: 

534 self._end_delta = self._delta(res.end, isend=1) 

535 

536 self.hasdst = bool(self._start_delta) 

537 

538 def _delta(self, x, isend=0): 

539 from dateutil import relativedelta 

540 kwargs = {} 

541 if x.month is not None: 

542 kwargs["month"] = x.month 

543 if x.weekday is not None: 

544 kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week) 

545 if x.week > 0: 

546 kwargs["day"] = 1 

547 else: 

548 kwargs["day"] = 31 

549 elif x.day: 

550 kwargs["day"] = x.day 

551 elif x.yday is not None: 

552 kwargs["yearday"] = x.yday 

553 elif x.jyday is not None: 

554 kwargs["nlyearday"] = x.jyday 

555 if not kwargs: 

556 # Default is to start on first sunday of april, and end 

557 # on last sunday of october. 

558 if not isend: 

559 kwargs["month"] = 4 

560 kwargs["day"] = 1 

561 kwargs["weekday"] = relativedelta.SU(+1) 

562 else: 

563 kwargs["month"] = 10 

564 kwargs["day"] = 31 

565 kwargs["weekday"] = relativedelta.SU(-1) 

566 if x.time is not None: 

567 kwargs["seconds"] = x.time 

568 else: 

569 # Default is 2AM. 

570 kwargs["seconds"] = 7200 

571 if isend: 

572 # Convert to standard time, to follow the documented way 

573 # of working with the extra hour. See the documentation 

574 # of the tzinfo class. 

575 delta = self._dst_offset - self._std_offset 

576 kwargs["seconds"] -= delta.seconds + delta.days * 86400 

577 return relativedelta.relativedelta(**kwargs) 

578 

579 def __repr__(self): 

580 return "%s(%s)" % (self.__class__.__name__, repr(self._s)) 

581 

582 

583class _tzicalvtzcomp: 

584 def __init__(self, tzoffsetfrom, tzoffsetto, isdst, 

585 tzname=None, rrule=None): 

586 self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom) 

587 self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto) 

588 self.tzoffsetdiff = self.tzoffsetto - self.tzoffsetfrom 

589 self.isdst = isdst 

590 self.tzname = tzname 

591 self.rrule = rrule 

592 

593 

594class _tzicalvtz(_tzinfo): 

595 def __init__(self, tzid, comps=[]): 

596 super().__init__() 

597 

598 self._tzid = tzid 

599 self._comps = comps 

600 self._cachedate = [] 

601 self._cachecomp = [] 

602 self._cache_lock = _thread.allocate_lock() 

603 

604 def _find_comp(self, dt): 

605 if len(self._comps) == 1: 

606 return self._comps[0] 

607 

608 dt = dt.replace(tzinfo=None) 

609 

610 try: 

611 with self._cache_lock: 

612 return self._cachecomp[self._cachedate.index((dt, dt.fold))] 

613 except ValueError: 

614 pass 

615 

616 lastcompdt = None 

617 lastcomp = None 

618 

619 for comp in self._comps: 

620 compdt = self._find_compdt(comp, dt) 

621 

622 if compdt and (not lastcompdt or lastcompdt < compdt): 

623 lastcompdt = compdt 

624 lastcomp = comp 

625 

626 if not lastcomp: 

627 # RFC says nothing about what to do when a given 

628 # time is before the first onset date. We'll look for the 

629 # first standard component, or the first component, if 

630 # none is found. 

631 for comp in self._comps: 

632 if not comp.isdst: 

633 lastcomp = comp 

634 break 

635 else: 

636 lastcomp = comp[0] 

637 

638 with self._cache_lock: 

639 self._cachedate.insert(0, (dt, dt.fold)) 

640 self._cachecomp.insert(0, lastcomp) 

641 

642 if len(self._cachedate) > 10: 

643 self._cachedate.pop() 

644 self._cachecomp.pop() 

645 

646 return lastcomp 

647 

648 def _find_compdt(self, comp, dt): 

649 if comp.tzoffsetdiff < ZERO and dt.fold: 

650 dt -= comp.tzoffsetdiff 

651 

652 compdt = comp.rrule.before(dt, inc=True) 

653 

654 return compdt 

655 

656 def utcoffset(self, dt): 

657 if dt is None: 

658 return None 

659 

660 return self._find_comp(dt).tzoffsetto 

661 

662 def dst(self, dt): 

663 comp = self._find_comp(dt) 

664 if comp.isdst: 

665 return comp.tzoffsetdiff 

666 else: 

667 return ZERO 

668 

669 def tzname(self, dt): 

670 return self._find_comp(dt).tzname 

671 

672 def __repr__(self): 

673 return "<tzicalvtz %s>" % repr(self._tzid) 

674 

675 __reduce__ = object.__reduce__ 

676 

677 

678class tzical: 

679 """ 

680 This object is designed to parse an iCalendar-style ``VTIMEZONE`` structure 

681 as set out in `RFC 5545`_ Section 4.6.5 into one or more `tzinfo` objects. 

682 

683 :param `fileobj`: 

684 A file or stream in iCalendar format, which should be UTF-8 encoded 

685 with CRLF endings. 

686 

687 .. _`RFC 5545`: https://tools.ietf.org/html/rfc5545 

688 """ 

689 def __init__(self, fileobj): 

690 global rrule 

691 from dateutil import rrule 

692 

693 if isinstance(fileobj, str): 

694 self._s = fileobj 

695 # ical should be encoded in UTF-8 with CRLF 

696 fileobj = open(fileobj, 'r') 

697 else: 

698 self._s = getattr(fileobj, 'name', repr(fileobj)) 

699 fileobj = contextlib.nullcontext(fileobj) 

700 

701 self._vtz = {} 

702 

703 with fileobj as fobj: 

704 self._parse_rfc(fobj.read()) 

705 

706 def keys(self): 

707 """ 

708 Retrieves the available time zones as a list. 

709 """ 

710 return list(self._vtz.keys()) 

711 

712 def get(self, tzid=None): 

713 """ 

714 Retrieve a :py:class:`datetime.tzinfo` object by its ``tzid``. 

715 

716 :param tzid: 

717 If there is exactly one time zone available, omitting ``tzid`` 

718 or passing :py:const:`None` value returns it. Otherwise a valid 

719 key (which can be retrieved from :func:`keys`) is required. 

720 

721 :raises ValueError: 

722 Raised if ``tzid`` is not specified but there are either more 

723 or fewer than 1 zone defined. 

724 

725 :returns: 

726 Returns either a :py:class:`datetime.tzinfo` object representing 

727 the relevant time zone or :py:const:`None` if the ``tzid`` was 

728 not found. 

729 """ 

730 if tzid is None: 

731 if len(self._vtz) == 0: 

732 raise ValueError("no timezones defined") 

733 elif len(self._vtz) > 1: 

734 raise ValueError("more than one timezone available") 

735 tzid = next(iter(self._vtz)) 

736 

737 return self._vtz.get(tzid) 

738 

739 def _parse_offset(self, s): 

740 s = s.strip() 

741 if not s: 

742 raise ValueError("empty offset") 

743 if s[0] in ('+', '-'): 

744 signal = (-1, +1)[s[0] == '+'] 

745 s = s[1:] 

746 else: 

747 signal = +1 

748 if len(s) == 4: 

749 return (int(s[:2]) * 3600 + int(s[2:]) * 60) * signal 

750 elif len(s) == 6: 

751 return (int(s[:2]) * 3600 + int(s[2:4]) * 60 + int(s[4:])) * signal 

752 else: 

753 raise ValueError("invalid offset: " + s) 

754 

755 def _parse_rfc(self, s): 

756 lines = s.splitlines() 

757 if not lines: 

758 raise ValueError("empty string") 

759 

760 # Unfold 

761 i = 0 

762 while i < len(lines): 

763 line = lines[i].rstrip() 

764 if not line: 

765 del lines[i] 

766 elif i > 0 and line[0] == " ": 

767 lines[i-1] += line[1:] 

768 del lines[i] 

769 else: 

770 i += 1 

771 

772 tzid = None 

773 comps = [] 

774 invtz = False 

775 comptype = None 

776 for line in lines: 

777 if not line: 

778 continue 

779 name, value = line.split(':', 1) 

780 parms = name.split(';') 

781 if not parms: 

782 raise ValueError("empty property name") 

783 name = parms[0].upper() 

784 parms = parms[1:] 

785 if invtz: 

786 if name == "BEGIN": 

787 if value in ("STANDARD", "DAYLIGHT"): 

788 # Process component 

789 pass 

790 else: 

791 raise ValueError("unknown component: "+value) 

792 comptype = value 

793 founddtstart = False 

794 tzoffsetfrom = None 

795 tzoffsetto = None 

796 rrulelines = [] 

797 tzname = None 

798 elif name == "END": 

799 if value == "VTIMEZONE": 

800 if comptype: 

801 raise ValueError("component not closed: "+comptype) 

802 if not tzid: 

803 raise ValueError("mandatory TZID not found") 

804 if not comps: 

805 raise ValueError( 

806 "at least one component is needed") 

807 # Process vtimezone 

808 self._vtz[tzid] = _tzicalvtz(tzid, comps) 

809 invtz = False 

810 elif value == comptype: 

811 if not founddtstart: 

812 raise ValueError("mandatory DTSTART not found") 

813 if tzoffsetfrom is None: 

814 raise ValueError( 

815 "mandatory TZOFFSETFROM not found") 

816 if tzoffsetto is None: 

817 raise ValueError( 

818 "mandatory TZOFFSETFROM not found") 

819 # Process component 

820 rr = None 

821 if rrulelines: 

822 rr = rrule.rrulestr("\n".join(rrulelines), 

823 compatible=True, 

824 ignoretz=True, 

825 cache=True) 

826 comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto, 

827 (comptype == "DAYLIGHT"), 

828 tzname, rr) 

829 comps.append(comp) 

830 comptype = None 

831 else: 

832 raise ValueError("invalid component end: "+value) 

833 elif comptype: 

834 if name == "DTSTART": 

835 # DTSTART in VTIMEZONE takes a subset of valid RRULE 

836 # values under RFC 5545. 

837 for parm in parms: 

838 if parm != 'VALUE=DATE-TIME': 

839 msg = ('Unsupported DTSTART param in ' + 

840 'VTIMEZONE: ' + parm) 

841 raise ValueError(msg) 

842 rrulelines.append(line) 

843 founddtstart = True 

844 elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"): 

845 rrulelines.append(line) 

846 elif name == "TZOFFSETFROM": 

847 if parms: 

848 raise ValueError( 

849 "unsupported %s parm: %s " % (name, parms[0])) 

850 tzoffsetfrom = self._parse_offset(value) 

851 elif name == "TZOFFSETTO": 

852 if parms: 

853 raise ValueError( 

854 "unsupported TZOFFSETTO parm: "+parms[0]) 

855 tzoffsetto = self._parse_offset(value) 

856 elif name == "TZNAME": 

857 if parms: 

858 raise ValueError( 

859 "unsupported TZNAME parm: "+parms[0]) 

860 tzname = value 

861 elif name == "COMMENT": 

862 pass 

863 else: 

864 raise ValueError("unsupported property: "+name) 

865 else: 

866 if name == "TZID": 

867 if parms: 

868 raise ValueError( 

869 "unsupported TZID parm: "+parms[0]) 

870 tzid = value 

871 elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"): 

872 pass 

873 else: 

874 raise ValueError("unsupported property: "+name) 

875 elif name == "BEGIN" and value == "VTIMEZONE": 

876 tzid = None 

877 comps = [] 

878 invtz = True 

879 

880 def __repr__(self): 

881 return "%s(%s)" % (self.__class__.__name__, repr(self._s)) 

882 

883 

884TZFILES = ["/etc/localtime", "localtime"] 

885 

886def __get_gettz(): 

887 tzlocal_classes = (tzlocal,) 

888 if tzwinlocal is not None: 

889 tzlocal_classes += (tzwinlocal,) 

890 

891 class GettzFunc: 

892 """ 

893 Retrieve a time zone object from a string representation 

894 

895 This function is intended to retrieve the :py:class:`tzinfo` subclass 

896 that best represents the time zone that would be used if a POSIX 

897 `TZ variable`_ were set to the same value. 

898 

899 If no argument or an empty string is passed to ``gettz``, local time 

900 is returned: 

901 

902 .. code-block:: python3 

903 

904 >>> gettz() 

905 tzfile('/etc/localtime') 

906 

907 This function is also the preferred way to map IANA tz database keys 

908 to :class:`tzfile` objects. It searches for these keys in the 

909 following order: 

910 

911 1. On the search path defined in :data:`TZPATH`. 

912 2. In the `tzdata <https://pypi.org/project/tzdata/>`_ Python package. 

913 3. Using the system-specific mechanisms (e.g. the Windows registry). 

914 

915 .. code-block:: python3 

916 

917 >>> gettz('Pacific/Kiritimati') 

918 tzfile('/usr/share/zoneinfo/Pacific/Kiritimati') 

919 

920 On Windows, the standard is extended to include the Windows-specific 

921 zone names provided by the operating system: 

922 

923 .. code-block:: python3 

924 

925 >>> gettz('Egypt Standard Time') 

926 tzwin('Egypt Standard Time') 

927 

928 Passing a GNU ``TZ`` style string time zone specification returns a 

929 :class:`tzstr` object: 

930 

931 .. code-block:: python3 

932 

933 >>> gettz('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3') 

934 tzstr('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3') 

935 

936 :param name: 

937 A time zone name (IANA, or, on Windows, Windows keys), location of 

938 a ``tzfile(5)`` zoneinfo file or ``TZ`` variable style time zone 

939 specifier. An empty string, no argument or ``None`` is interpreted 

940 as local time. 

941 

942 :return: 

943 Returns an instance of one of ``dateutil``'s :py:class:`tzinfo` 

944 subclasses. 

945 

946 .. versionchanged:: 2.7.0 

947 

948 After version 2.7.0, any two calls to ``gettz`` using the same 

949 input strings will return the same object: 

950 

951 .. code-block:: python3 

952 

953 >>> tz.gettz('America/Chicago') is tz.gettz('America/Chicago') 

954 True 

955 

956 In addition to improving performance, this ensures that 

957 `"same zone" semantics`_ are used for datetimes in the same zone. 

958 

959 

960 .. _`TZ variable`: 

961 https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html 

962 

963 .. _`"same zone" semantics`: 

964 https://blog.ganssle.io/articles/2018/02/aware-datetime-arithmetic.html 

965 """ 

966 def __init__(self): 

967 

968 self.__instances = weakref.WeakValueDictionary() 

969 self.__strong_cache_size = 8 

970 self.__strong_cache = OrderedDict() 

971 self._cache_lock = _thread.allocate_lock() 

972 

973 def __call__(self, name=None): 

974 with self._cache_lock: 

975 rv = self.__instances.get(name, None) 

976 

977 if rv is None: 

978 # TODO: This holds the cache lock while nocache() reads 

979 # the file, so a slow lookup (e.g. walking TZPATH on a 

980 # network file system) blocks every other gettz call, 

981 # including cache hits for unrelated keys. 

982 rv = self.nocache(name=name) 

983 if not (name is None 

984 or isinstance(rv, tzlocal_classes) 

985 or rv is None): 

986 # tzlocal is slightly more complicated than the other 

987 # time zone providers because it depends on environment 

988 # at construction time, so don't cache that. 

989 # 

990 # We also cannot store weak references to None, so we 

991 # will also not store that. 

992 self.__instances[name] = rv 

993 else: 

994 # No need for strong caching, return immediately 

995 return rv 

996 

997 self.__strong_cache[name] = self.__strong_cache.pop(name, rv) 

998 

999 if len(self.__strong_cache) > self.__strong_cache_size: 

1000 self.__strong_cache.popitem(last=False) 

1001 

1002 return rv 

1003 

1004 def set_cache_size(self, size): 

1005 with self._cache_lock: 

1006 self.__strong_cache_size = size 

1007 while len(self.__strong_cache) > size: 

1008 self.__strong_cache.popitem(last=False) 

1009 

1010 def cache_clear(self): 

1011 with self._cache_lock: 

1012 self.__instances = weakref.WeakValueDictionary() 

1013 self.__strong_cache.clear() 

1014 

1015 @staticmethod 

1016 def nocache(name=None): 

1017 """A non-cached version of gettz""" 

1018 tz = None 

1019 if not name: 

1020 try: 

1021 name = os.environ["TZ"] 

1022 except KeyError: 

1023 pass 

1024 if name is None or name in ("", ":"): 

1025 for filepath in TZFILES: 

1026 if not os.path.isabs(filepath): 

1027 filename = filepath 

1028 for path in _tzpath.TZPATH: 

1029 filepath = os.path.join(path, filename) 

1030 if os.path.isfile(filepath): 

1031 break 

1032 else: 

1033 continue 

1034 if os.path.isfile(filepath): 

1035 try: 

1036 tz = tzfile(filepath) 

1037 break 

1038 except (OSError, ValueError): 

1039 pass 

1040 else: 

1041 tz = tzlocal() 

1042 else: 

1043 try: 

1044 if name.startswith(":"): 

1045 name = name[1:] 

1046 except TypeError as e: 

1047 if isinstance(name, bytes): 

1048 new_msg = "gettz argument should be str, not bytes" 

1049 raise TypeError(new_msg) from e 

1050 else: 

1051 raise 

1052 

1053 # A NUL byte can never be part of a key or a path, and 

1054 # depending on the platform and Python version it raises 

1055 # ValueError from os.path, winreg or open(), so reject it 

1056 # up front rather than catching it in every branch. 

1057 if "\x00" in name: 

1058 return None 

1059 

1060 if os.path.isabs(name): 

1061 if os.path.isfile(name): 

1062 tz = tzfile(name) 

1063 else: 

1064 tz = None 

1065 else: 

1066 if _escapes_tzpath(name): 

1067 _warn_tzpath_escape(name) 

1068 

1069 for path in _tzpath.TZPATH: 

1070 filepath = os.path.join(path, name) 

1071 if not os.path.isfile(filepath): 

1072 filepath = filepath.replace(' ', '_') 

1073 if not os.path.isfile(filepath): 

1074 continue 

1075 try: 

1076 tz = tzfile(filepath, key=name) 

1077 break 

1078 except (OSError, ValueError): 

1079 # TODO: A file that exists but can't be read or 

1080 # isn't a valid TZif file is silently skipped, 

1081 # and the key is looked up in the next TZPATH 

1082 # entry and then in tzdata. Consider warning 

1083 # here, since this usually means the system time 

1084 # zone data is broken. 

1085 pass 

1086 else: 

1087 tz = None 

1088 # As with the search path lookup above, also try 

1089 # the name with spaces replaced by underscores. 

1090 candidates = [name] 

1091 if " " in name: 

1092 candidates.append(name.replace(" ", "_")) 

1093 

1094 for candidate in candidates: 

1095 try: 

1096 with _tzdata_impl._load_tzdata(candidate) as f: 

1097 return tzfile(f, key=name) 

1098 except ValueError: 

1099 # TZFileNotFound is a ValueError, and so is 

1100 # the error raised for files in tzdata that 

1101 # are not TZif files (e.g. "zone.tab"), 

1102 # which are skipped just as on TZPATH. 

1103 pass 

1104 

1105 if tzwin is not None: 

1106 try: 

1107 tz = tzwin(name) 

1108 except OSError: 

1109 tz = None 

1110 

1111 if not tz: 

1112 for c in name: 

1113 # name is not a tzstr unless it has at least 

1114 # one offset. For short values of "name", an 

1115 # explicit for loop seems to be the fastest way 

1116 # To determine if a string contains a digit 

1117 if c in "0123456789": 

1118 try: 

1119 tz = tzstr(name) 

1120 except ValueError: 

1121 pass 

1122 break 

1123 else: 

1124 if name in ("GMT", "UTC"): 

1125 tz = UTC 

1126 elif name in time.tzname: 

1127 tz = tzlocal() 

1128 return tz 

1129 

1130 return GettzFunc() 

1131 

1132 

1133gettz = __get_gettz() 

1134del __get_gettz 

1135 

1136 

1137class DeprecatedTzKeyWarning(FutureWarning): 

1138 """Warning raised when :func:`gettz` is given a key outside of TZPATH. 

1139 

1140 Relative keys like ``"../../etc/localtime"`` resolve to files outside of 

1141 every directory on :data:`TZPATH`. Support for these is deprecated, and a 

1142 warnings filter turns this warning into an error by default. To keep the 

1143 old behavior for now, filter it out, e.g.:: 

1144 

1145 warnings.filterwarnings( 

1146 "ignore", category=dateutil.tz.DeprecatedTzKeyWarning 

1147 ) 

1148 

1149 Absolute paths to TZif files are not affected. 

1150 """ 

1151 

1152 

1153def _escapes_tzpath(name): 

1154 # A relative key stays inside every TZPATH entry unless, once normalized 

1155 # (which also turns os.altsep into os.sep), it starts by going up a 

1156 # directory. 

1157 return os.path.normpath(name).split(os.sep, 1)[0] == os.pardir 

1158 

1159 

1160def _warn_tzpath_escape(name): 

1161 # As in dateutil.zoneinfo.rebuild, the filter is appended at call time, 

1162 # so any filter the user has installed for this category wins. 

1163 warnings.filterwarnings( 

1164 "error", category=DeprecatedTzKeyWarning, append=True 

1165 ) 

1166 warnings.warn( 

1167 "gettz(%r) refers to a file outside of TZPATH; support for keys " 

1168 "like this is deprecated and will be removed in a future version. " 

1169 "Use an absolute path instead." % (name,), 

1170 DeprecatedTzKeyWarning, 

1171 stacklevel=4, 

1172 ) 

1173 

1174 

1175def available_iana_timezones(): 

1176 """Returns a set containing all available IANA time zones keys. 

1177 

1178 These are suitable for use with :func:`gettz`. This is essentially a direct 

1179 port of :py:func:`zoneinfo.available_timezones`. 

1180 

1181 The search includes time zones found on :data:`TZPATH` as well as any 

1182 time zones provided by the `tzdata <https://pypi.org/project/tzdata/>`_ 

1183 package if it is installed. 

1184 

1185 :return: 

1186 Returns a mutable set containing strings of time zones. Each call will 

1187 return a fresh set. 

1188 

1189 .. caution:: 

1190 

1191 This may attempt to open a large number of files, since the best way to 

1192 determine if a given file on the time zone search path is to open it 

1193 and check for the "magic string" at the beginning. 

1194 

1195 .. note:: 

1196 

1197 These values are not designed to be exposed to end-users; for 

1198 user-facing elements, applications should use something like CLDR (the 

1199 Unicode Common Locale Data Repository) to get more user-friendly 

1200 strings. 

1201 """ 

1202 # Start with loading from the tzdata package if it exists: this has a 

1203 # pre-assembled list of zones that only requires opening one file. 

1204 try: 

1205 valid_zones = set(_tzdata_impl._load_tzdata_keys()) 

1206 except ImportError: 

1207 valid_zones = set() 

1208 

1209 def valid_key(fpath): 

1210 try: 

1211 with open(fpath, "rb") as f: 

1212 return f.read(4) == b"TZif" 

1213 except Exception: # pragma: nocover 

1214 return False 

1215 

1216 for tz_root in _tzpath.TZPATH: 

1217 if not os.path.exists(tz_root): 

1218 continue 

1219 

1220 for root, dirnames, files in os.walk(tz_root): 

1221 if root == tz_root: 

1222 # right/ and posix/ are special directories and shouldn't be 

1223 # included in the output of available zones 

1224 if "right" in dirnames: 

1225 dirnames.remove("right") 

1226 if "posix" in dirnames: 

1227 dirnames.remove("posix") 

1228 

1229 for file in files: 

1230 fpath = os.path.join(root, file) 

1231 

1232 key = os.path.relpath(fpath, start=tz_root) 

1233 if os.sep != "/": # pragma: nocover 

1234 key = key.replace(os.sep, "/") 

1235 

1236 if not key or key in valid_zones: 

1237 continue 

1238 

1239 if valid_key(fpath): 

1240 valid_zones.add(key) 

1241 

1242 if "posixrules" in valid_zones: 

1243 # posixrules is a special symlink-only time zone where it exists, it 

1244 # should not be included in the output 

1245 valid_zones.remove("posixrules") 

1246 if "localtime" in valid_zones: 

1247 # localtime is a special symlink-only time zone where it exists, it 

1248 # should not be included in the output 

1249 valid_zones.remove("localtime") 

1250 

1251 return valid_zones 

1252 

1253 

1254def datetime_exists(dt, tz=None): 

1255 """ 

1256 Given a datetime and a time zone, determine whether or not a given datetime 

1257 would fall in a gap. 

1258 

1259 :param dt: 

1260 A :class:`datetime.datetime` (whose time zone will be ignored if ``tz`` 

1261 is provided.) 

1262 

1263 :param tz: 

1264 A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If 

1265 ``None`` or not provided, the datetime's own time zone will be used. 

1266 

1267 :return: 

1268 Returns a boolean value whether or not the "wall time" exists in 

1269 ``tz``. 

1270 

1271 .. versionadded:: 2.7.0 

1272 """ 

1273 if tz is None: 

1274 if dt.tzinfo is None: 

1275 raise ValueError('Datetime is naive and no time zone provided.') 

1276 tz = dt.tzinfo 

1277 

1278 dt = dt.replace(tzinfo=None) 

1279 

1280 # This is essentially a test of whether or not the datetime can survive 

1281 # a round trip to UTC. 

1282 dt_rt = dt.replace(tzinfo=tz).astimezone(UTC).astimezone(tz) 

1283 dt_rt = dt_rt.replace(tzinfo=None) 

1284 

1285 return dt == dt_rt 

1286 

1287 

1288def datetime_ambiguous(dt, tz=None): 

1289 """ 

1290 Given a datetime and a time zone, determine whether or not a given datetime 

1291 is ambiguous (i.e if there are two times differentiated only by their DST 

1292 status). 

1293 

1294 :param dt: 

1295 A :class:`datetime.datetime` (whose time zone will be ignored if ``tz`` 

1296 is provided.) 

1297 

1298 :param tz: 

1299 A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If 

1300 ``None`` or not provided, the datetime's own time zone will be used. 

1301 

1302 :return: 

1303 Returns a boolean value whether or not the "wall time" is ambiguous in 

1304 ``tz``. 

1305 

1306 .. versionadded:: 2.6.0 

1307 """ 

1308 if tz is None: 

1309 if dt.tzinfo is None: 

1310 raise ValueError('Datetime is naive and no time zone provided.') 

1311 

1312 tz = dt.tzinfo 

1313 

1314 # If a time zone defines its own "is_ambiguous" function, we'll use that. 

1315 is_ambiguous_fn = getattr(tz, 'is_ambiguous', None) 

1316 if is_ambiguous_fn is not None: 

1317 try: 

1318 return tz.is_ambiguous(dt) 

1319 except Exception: 

1320 pass 

1321 

1322 # If it doesn't come out and tell us it's ambiguous, we'll just check if 

1323 # the fold attribute has any effect on this particular date and time. 

1324 dt = dt.replace(tzinfo=tz) 

1325 wall_0 = enfold(dt, fold=0) 

1326 wall_1 = enfold(dt, fold=1) 

1327 

1328 same_offset = wall_0.utcoffset() == wall_1.utcoffset() 

1329 same_dst = wall_0.dst() == wall_1.dst() 

1330 

1331 return not (same_offset and same_dst) 

1332 

1333 

1334def resolve_imaginary(dt): 

1335 """ 

1336 Given a datetime that may be imaginary, return an existing datetime. 

1337 

1338 This function assumes that an imaginary datetime represents what the 

1339 wall time would be in a zone had the offset transition not occurred, so 

1340 it will always fall forward by the transition's change in offset. 

1341 

1342 .. doctest:: 

1343 

1344 >>> from dateutil import tz 

1345 >>> from datetime import datetime 

1346 >>> NYC = tz.gettz('America/New_York') 

1347 >>> print(tz.resolve_imaginary(datetime(2017, 3, 12, 2, 30, tzinfo=NYC))) 

1348 2017-03-12 03:30:00-04:00 

1349 

1350 >>> KIR = tz.gettz('Pacific/Kiritimati') 

1351 >>> print(tz.resolve_imaginary(datetime(1995, 1, 1, 12, 30, tzinfo=KIR))) 

1352 1995-01-02 12:30:00+14:00 

1353 

1354 As a note, :func:`datetime.astimezone` is guaranteed to produce a valid, 

1355 existing datetime, so a round-trip to and from UTC is sufficient to get 

1356 an extant datetime, however, this generally "falls back" to an earlier time 

1357 rather than falling forward to the STD side (though no guarantees are made 

1358 about this behavior). 

1359 

1360 :param dt: 

1361 A :class:`datetime.datetime` which may or may not exist. 

1362 

1363 :return: 

1364 Returns an existing :class:`datetime.datetime`. If ``dt`` was not 

1365 imaginary, the datetime returned is guaranteed to be the same object 

1366 passed to the function. 

1367 

1368 .. versionadded:: 2.7.0 

1369 """ 

1370 if dt.tzinfo is not None and not datetime_exists(dt): 

1371 

1372 curr_offset = (dt + datetime.timedelta(hours=24)).utcoffset() 

1373 old_offset = (dt - datetime.timedelta(hours=24)).utcoffset() 

1374 

1375 dt += curr_offset - old_offset 

1376 

1377 return dt 

1378 

1379 

1380def _datetime_to_timestamp(dt): 

1381 """ 

1382 Convert a :class:`datetime.datetime` object to an epoch timestamp in 

1383 seconds since January 1, 1970, ignoring the time zone. 

1384 """ 

1385 return (dt.replace(tzinfo=None) - EPOCH).total_seconds() 

1386 

1387 

1388# vim:ts=4:sw=4:et