tz.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951
  1. """
  2. Copyright (c) 2003-2007 Gustavo Niemeyer <gustavo@niemeyer.net>
  3. This module offers extensions to the standard python 2.3+
  4. datetime module.
  5. """
  6. __author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
  7. __license__ = "PSF License"
  8. import datetime
  9. import struct
  10. import time
  11. import sys
  12. import os
  13. relativedelta = None
  14. parser = None
  15. rrule = None
  16. __all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
  17. "tzstr", "tzical", "tzwin", "tzwinlocal", "gettz"]
  18. try:
  19. from dateutil.tzwin import tzwin, tzwinlocal
  20. except (ImportError, OSError):
  21. tzwin, tzwinlocal = None, None
  22. ZERO = datetime.timedelta(0)
  23. EPOCHORDINAL = datetime.datetime.utcfromtimestamp(0).toordinal()
  24. class tzutc(datetime.tzinfo):
  25. def utcoffset(self, dt):
  26. return ZERO
  27. def dst(self, dt):
  28. return ZERO
  29. def tzname(self, dt):
  30. return "UTC"
  31. def __eq__(self, other):
  32. return (isinstance(other, tzutc) or
  33. (isinstance(other, tzoffset) and other._offset == ZERO))
  34. def __ne__(self, other):
  35. return not self.__eq__(other)
  36. def __repr__(self):
  37. return "%s()" % self.__class__.__name__
  38. __reduce__ = object.__reduce__
  39. class tzoffset(datetime.tzinfo):
  40. def __init__(self, name, offset):
  41. self._name = name
  42. self._offset = datetime.timedelta(seconds=offset)
  43. def utcoffset(self, dt):
  44. return self._offset
  45. def dst(self, dt):
  46. return ZERO
  47. def tzname(self, dt):
  48. return self._name
  49. def __eq__(self, other):
  50. return (isinstance(other, tzoffset) and
  51. self._offset == other._offset)
  52. def __ne__(self, other):
  53. return not self.__eq__(other)
  54. def __repr__(self):
  55. return "%s(%s, %s)" % (self.__class__.__name__,
  56. `self._name`,
  57. self._offset.days*86400+self._offset.seconds)
  58. __reduce__ = object.__reduce__
  59. class tzlocal(datetime.tzinfo):
  60. _std_offset = datetime.timedelta(seconds=-time.timezone)
  61. if time.daylight:
  62. _dst_offset = datetime.timedelta(seconds=-time.altzone)
  63. else:
  64. _dst_offset = _std_offset
  65. def utcoffset(self, dt):
  66. if self._isdst(dt):
  67. return self._dst_offset
  68. else:
  69. return self._std_offset
  70. def dst(self, dt):
  71. if self._isdst(dt):
  72. return self._dst_offset-self._std_offset
  73. else:
  74. return ZERO
  75. def tzname(self, dt):
  76. return time.tzname[self._isdst(dt)]
  77. def _isdst(self, dt):
  78. # We can't use mktime here. It is unstable when deciding if
  79. # the hour near to a change is DST or not.
  80. #
  81. # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour,
  82. # dt.minute, dt.second, dt.weekday(), 0, -1))
  83. # return time.localtime(timestamp).tm_isdst
  84. #
  85. # The code above yields the following result:
  86. #
  87. #>>> import tz, datetime
  88. #>>> t = tz.tzlocal()
  89. #>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
  90. #'BRDT'
  91. #>>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname()
  92. #'BRST'
  93. #>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
  94. #'BRST'
  95. #>>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname()
  96. #'BRDT'
  97. #>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
  98. #'BRDT'
  99. #
  100. # Here is a more stable implementation:
  101. #
  102. timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400
  103. + dt.hour * 3600
  104. + dt.minute * 60
  105. + dt.second)
  106. return time.localtime(timestamp+time.timezone).tm_isdst
  107. def __eq__(self, other):
  108. if not isinstance(other, tzlocal):
  109. return False
  110. return (self._std_offset == other._std_offset and
  111. self._dst_offset == other._dst_offset)
  112. return True
  113. def __ne__(self, other):
  114. return not self.__eq__(other)
  115. def __repr__(self):
  116. return "%s()" % self.__class__.__name__
  117. __reduce__ = object.__reduce__
  118. class _ttinfo(object):
  119. __slots__ = ["offset", "delta", "isdst", "abbr", "isstd", "isgmt"]
  120. def __init__(self):
  121. for attr in self.__slots__:
  122. setattr(self, attr, None)
  123. def __repr__(self):
  124. l = []
  125. for attr in self.__slots__:
  126. value = getattr(self, attr)
  127. if value is not None:
  128. l.append("%s=%s" % (attr, `value`))
  129. return "%s(%s)" % (self.__class__.__name__, ", ".join(l))
  130. def __eq__(self, other):
  131. if not isinstance(other, _ttinfo):
  132. return False
  133. return (self.offset == other.offset and
  134. self.delta == other.delta and
  135. self.isdst == other.isdst and
  136. self.abbr == other.abbr and
  137. self.isstd == other.isstd and
  138. self.isgmt == other.isgmt)
  139. def __ne__(self, other):
  140. return not self.__eq__(other)
  141. def __getstate__(self):
  142. state = {}
  143. for name in self.__slots__:
  144. state[name] = getattr(self, name, None)
  145. return state
  146. def __setstate__(self, state):
  147. for name in self.__slots__:
  148. if name in state:
  149. setattr(self, name, state[name])
  150. class tzfile(datetime.tzinfo):
  151. # http://www.twinsun.com/tz/tz-link.htm
  152. # ftp://elsie.nci.nih.gov/pub/tz*.tar.gz
  153. def __init__(self, fileobj):
  154. if isinstance(fileobj, basestring):
  155. self._filename = fileobj
  156. fileobj = open(fileobj)
  157. elif hasattr(fileobj, "name"):
  158. self._filename = fileobj.name
  159. else:
  160. self._filename = `fileobj`
  161. # From tzfile(5):
  162. #
  163. # The time zone information files used by tzset(3)
  164. # begin with the magic characters "TZif" to identify
  165. # them as time zone information files, followed by
  166. # sixteen bytes reserved for future use, followed by
  167. # six four-byte values of type long, written in a
  168. # ``standard'' byte order (the high-order byte
  169. # of the value is written first).
  170. if fileobj.read(4) != "TZif":
  171. raise ValueError, "magic not found"
  172. fileobj.read(16)
  173. (
  174. # The number of UTC/local indicators stored in the file.
  175. ttisgmtcnt,
  176. # The number of standard/wall indicators stored in the file.
  177. ttisstdcnt,
  178. # The number of leap seconds for which data is
  179. # stored in the file.
  180. leapcnt,
  181. # The number of "transition times" for which data
  182. # is stored in the file.
  183. timecnt,
  184. # The number of "local time types" for which data
  185. # is stored in the file (must not be zero).
  186. typecnt,
  187. # The number of characters of "time zone
  188. # abbreviation strings" stored in the file.
  189. charcnt,
  190. ) = struct.unpack(">6l", fileobj.read(24))
  191. # The above header is followed by tzh_timecnt four-byte
  192. # values of type long, sorted in ascending order.
  193. # These values are written in ``standard'' byte order.
  194. # Each is used as a transition time (as returned by
  195. # time(2)) at which the rules for computing local time
  196. # change.
  197. if timecnt:
  198. self._trans_list = struct.unpack(">%dl" % timecnt,
  199. fileobj.read(timecnt*4))
  200. else:
  201. self._trans_list = []
  202. # Next come tzh_timecnt one-byte values of type unsigned
  203. # char; each one tells which of the different types of
  204. # ``local time'' types described in the file is associated
  205. # with the same-indexed transition time. These values
  206. # serve as indices into an array of ttinfo structures that
  207. # appears next in the file.
  208. if timecnt:
  209. self._trans_idx = struct.unpack(">%dB" % timecnt,
  210. fileobj.read(timecnt))
  211. else:
  212. self._trans_idx = []
  213. # Each ttinfo structure is written as a four-byte value
  214. # for tt_gmtoff of type long, in a standard byte
  215. # order, followed by a one-byte value for tt_isdst
  216. # and a one-byte value for tt_abbrind. In each
  217. # structure, tt_gmtoff gives the number of
  218. # seconds to be added to UTC, tt_isdst tells whether
  219. # tm_isdst should be set by localtime(3), and
  220. # tt_abbrind serves as an index into the array of
  221. # time zone abbreviation characters that follow the
  222. # ttinfo structure(s) in the file.
  223. ttinfo = []
  224. for i in range(typecnt):
  225. ttinfo.append(struct.unpack(">lbb", fileobj.read(6)))
  226. abbr = fileobj.read(charcnt)
  227. # Then there are tzh_leapcnt pairs of four-byte
  228. # values, written in standard byte order; the
  229. # first value of each pair gives the time (as
  230. # returned by time(2)) at which a leap second
  231. # occurs; the second gives the total number of
  232. # leap seconds to be applied after the given time.
  233. # The pairs of values are sorted in ascending order
  234. # by time.
  235. # Not used, for now
  236. if leapcnt:
  237. leap = struct.unpack(">%dl" % (leapcnt*2),
  238. fileobj.read(leapcnt*8))
  239. # Then there are tzh_ttisstdcnt standard/wall
  240. # indicators, each stored as a one-byte value;
  241. # they tell whether the transition times associated
  242. # with local time types were specified as standard
  243. # time or wall clock time, and are used when
  244. # a time zone file is used in handling POSIX-style
  245. # time zone environment variables.
  246. if ttisstdcnt:
  247. isstd = struct.unpack(">%db" % ttisstdcnt,
  248. fileobj.read(ttisstdcnt))
  249. # Finally, there are tzh_ttisgmtcnt UTC/local
  250. # indicators, each stored as a one-byte value;
  251. # they tell whether the transition times associated
  252. # with local time types were specified as UTC or
  253. # local time, and are used when a time zone file
  254. # is used in handling POSIX-style time zone envi-
  255. # ronment variables.
  256. if ttisgmtcnt:
  257. isgmt = struct.unpack(">%db" % ttisgmtcnt,
  258. fileobj.read(ttisgmtcnt))
  259. # ** Everything has been read **
  260. # Build ttinfo list
  261. self._ttinfo_list = []
  262. for i in range(typecnt):
  263. gmtoff, isdst, abbrind = ttinfo[i]
  264. # Round to full-minutes if that's not the case. Python's
  265. # datetime doesn't accept sub-minute timezones. Check
  266. # http://python.org/sf/1447945 for some information.
  267. gmtoff = (gmtoff+30)//60*60
  268. tti = _ttinfo()
  269. tti.offset = gmtoff
  270. tti.delta = datetime.timedelta(seconds=gmtoff)
  271. tti.isdst = isdst
  272. tti.abbr = abbr[abbrind:abbr.find('\x00', abbrind)]
  273. tti.isstd = (ttisstdcnt > i and isstd[i] != 0)
  274. tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0)
  275. self._ttinfo_list.append(tti)
  276. # Replace ttinfo indexes for ttinfo objects.
  277. trans_idx = []
  278. for idx in self._trans_idx:
  279. trans_idx.append(self._ttinfo_list[idx])
  280. self._trans_idx = tuple(trans_idx)
  281. # Set standard, dst, and before ttinfos. before will be
  282. # used when a given time is before any transitions,
  283. # and will be set to the first non-dst ttinfo, or to
  284. # the first dst, if all of them are dst.
  285. self._ttinfo_std = None
  286. self._ttinfo_dst = None
  287. self._ttinfo_before = None
  288. if self._ttinfo_list:
  289. if not self._trans_list:
  290. self._ttinfo_std = self._ttinfo_first = self._ttinfo_list[0]
  291. else:
  292. for i in range(timecnt-1,-1,-1):
  293. tti = self._trans_idx[i]
  294. if not self._ttinfo_std and not tti.isdst:
  295. self._ttinfo_std = tti
  296. elif not self._ttinfo_dst and tti.isdst:
  297. self._ttinfo_dst = tti
  298. if self._ttinfo_std and self._ttinfo_dst:
  299. break
  300. else:
  301. if self._ttinfo_dst and not self._ttinfo_std:
  302. self._ttinfo_std = self._ttinfo_dst
  303. for tti in self._ttinfo_list:
  304. if not tti.isdst:
  305. self._ttinfo_before = tti
  306. break
  307. else:
  308. self._ttinfo_before = self._ttinfo_list[0]
  309. # Now fix transition times to become relative to wall time.
  310. #
  311. # I'm not sure about this. In my tests, the tz source file
  312. # is setup to wall time, and in the binary file isstd and
  313. # isgmt are off, so it should be in wall time. OTOH, it's
  314. # always in gmt time. Let me know if you have comments
  315. # about this.
  316. laststdoffset = 0
  317. self._trans_list = list(self._trans_list)
  318. for i in range(len(self._trans_list)):
  319. tti = self._trans_idx[i]
  320. if not tti.isdst:
  321. # This is std time.
  322. self._trans_list[i] += tti.offset
  323. laststdoffset = tti.offset
  324. else:
  325. # This is dst time. Convert to std.
  326. self._trans_list[i] += laststdoffset
  327. self._trans_list = tuple(self._trans_list)
  328. def _find_ttinfo(self, dt, laststd=0):
  329. timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400
  330. + dt.hour * 3600
  331. + dt.minute * 60
  332. + dt.second)
  333. idx = 0
  334. for trans in self._trans_list:
  335. if timestamp < trans:
  336. break
  337. idx += 1
  338. else:
  339. return self._ttinfo_std
  340. if idx == 0:
  341. return self._ttinfo_before
  342. if laststd:
  343. while idx > 0:
  344. tti = self._trans_idx[idx-1]
  345. if not tti.isdst:
  346. return tti
  347. idx -= 1
  348. else:
  349. return self._ttinfo_std
  350. else:
  351. return self._trans_idx[idx-1]
  352. def utcoffset(self, dt):
  353. if not self._ttinfo_std:
  354. return ZERO
  355. return self._find_ttinfo(dt).delta
  356. def dst(self, dt):
  357. if not self._ttinfo_dst:
  358. return ZERO
  359. tti = self._find_ttinfo(dt)
  360. if not tti.isdst:
  361. return ZERO
  362. # The documentation says that utcoffset()-dst() must
  363. # be constant for every dt.
  364. return tti.delta-self._find_ttinfo(dt, laststd=1).delta
  365. # An alternative for that would be:
  366. #
  367. # return self._ttinfo_dst.offset-self._ttinfo_std.offset
  368. #
  369. # However, this class stores historical changes in the
  370. # dst offset, so I belive that this wouldn't be the right
  371. # way to implement this.
  372. def tzname(self, dt):
  373. if not self._ttinfo_std:
  374. return None
  375. return self._find_ttinfo(dt).abbr
  376. def __eq__(self, other):
  377. if not isinstance(other, tzfile):
  378. return False
  379. return (self._trans_list == other._trans_list and
  380. self._trans_idx == other._trans_idx and
  381. self._ttinfo_list == other._ttinfo_list)
  382. def __ne__(self, other):
  383. return not self.__eq__(other)
  384. def __repr__(self):
  385. return "%s(%s)" % (self.__class__.__name__, `self._filename`)
  386. def __reduce__(self):
  387. if not os.path.isfile(self._filename):
  388. raise ValueError, "Unpickable %s class" % self.__class__.__name__
  389. return (self.__class__, (self._filename,))
  390. class tzrange(datetime.tzinfo):
  391. def __init__(self, stdabbr, stdoffset=None,
  392. dstabbr=None, dstoffset=None,
  393. start=None, end=None):
  394. global relativedelta
  395. if not relativedelta:
  396. from dateutil import relativedelta
  397. self._std_abbr = stdabbr
  398. self._dst_abbr = dstabbr
  399. if stdoffset is not None:
  400. self._std_offset = datetime.timedelta(seconds=stdoffset)
  401. else:
  402. self._std_offset = ZERO
  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. if dstabbr and start is None:
  410. self._start_delta = relativedelta.relativedelta(
  411. hours=+2, month=4, day=1, weekday=relativedelta.SU(+1))
  412. else:
  413. self._start_delta = start
  414. if dstabbr and end is None:
  415. self._end_delta = relativedelta.relativedelta(
  416. hours=+1, month=10, day=31, weekday=relativedelta.SU(-1))
  417. else:
  418. self._end_delta = end
  419. def utcoffset(self, dt):
  420. if self._isdst(dt):
  421. return self._dst_offset
  422. else:
  423. return self._std_offset
  424. def dst(self, dt):
  425. if self._isdst(dt):
  426. return self._dst_offset-self._std_offset
  427. else:
  428. return ZERO
  429. def tzname(self, dt):
  430. if self._isdst(dt):
  431. return self._dst_abbr
  432. else:
  433. return self._std_abbr
  434. def _isdst(self, dt):
  435. if not self._start_delta:
  436. return False
  437. year = datetime.datetime(dt.year,1,1)
  438. start = year+self._start_delta
  439. end = year+self._end_delta
  440. dt = dt.replace(tzinfo=None)
  441. if start < end:
  442. return dt >= start and dt < end
  443. else:
  444. return dt >= start or dt < end
  445. def __eq__(self, other):
  446. if not isinstance(other, tzrange):
  447. return False
  448. return (self._std_abbr == other._std_abbr and
  449. self._dst_abbr == other._dst_abbr and
  450. self._std_offset == other._std_offset and
  451. self._dst_offset == other._dst_offset and
  452. self._start_delta == other._start_delta and
  453. self._end_delta == other._end_delta)
  454. def __ne__(self, other):
  455. return not self.__eq__(other)
  456. def __repr__(self):
  457. return "%s(...)" % self.__class__.__name__
  458. __reduce__ = object.__reduce__
  459. class tzstr(tzrange):
  460. def __init__(self, s):
  461. global parser
  462. if not parser:
  463. from dateutil import parser
  464. self._s = s
  465. res = parser._parsetz(s)
  466. if res is None:
  467. raise ValueError, "unknown string format"
  468. # Here we break the compatibility with the TZ variable handling.
  469. # GMT-3 actually *means* the timezone -3.
  470. if res.stdabbr in ("GMT", "UTC"):
  471. res.stdoffset *= -1
  472. # We must initialize it first, since _delta() needs
  473. # _std_offset and _dst_offset set. Use False in start/end
  474. # to avoid building it two times.
  475. tzrange.__init__(self, res.stdabbr, res.stdoffset,
  476. res.dstabbr, res.dstoffset,
  477. start=False, end=False)
  478. if not res.dstabbr:
  479. self._start_delta = None
  480. self._end_delta = None
  481. else:
  482. self._start_delta = self._delta(res.start)
  483. if self._start_delta:
  484. self._end_delta = self._delta(res.end, isend=1)
  485. def _delta(self, x, isend=0):
  486. kwargs = {}
  487. if x.month is not None:
  488. kwargs["month"] = x.month
  489. if x.weekday is not None:
  490. kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week)
  491. if x.week > 0:
  492. kwargs["day"] = 1
  493. else:
  494. kwargs["day"] = 31
  495. elif x.day:
  496. kwargs["day"] = x.day
  497. elif x.yday is not None:
  498. kwargs["yearday"] = x.yday
  499. elif x.jyday is not None:
  500. kwargs["nlyearday"] = x.jyday
  501. if not kwargs:
  502. # Default is to start on first sunday of april, and end
  503. # on last sunday of october.
  504. if not isend:
  505. kwargs["month"] = 4
  506. kwargs["day"] = 1
  507. kwargs["weekday"] = relativedelta.SU(+1)
  508. else:
  509. kwargs["month"] = 10
  510. kwargs["day"] = 31
  511. kwargs["weekday"] = relativedelta.SU(-1)
  512. if x.time is not None:
  513. kwargs["seconds"] = x.time
  514. else:
  515. # Default is 2AM.
  516. kwargs["seconds"] = 7200
  517. if isend:
  518. # Convert to standard time, to follow the documented way
  519. # of working with the extra hour. See the documentation
  520. # of the tzinfo class.
  521. delta = self._dst_offset-self._std_offset
  522. kwargs["seconds"] -= delta.seconds+delta.days*86400
  523. return relativedelta.relativedelta(**kwargs)
  524. def __repr__(self):
  525. return "%s(%s)" % (self.__class__.__name__, `self._s`)
  526. class _tzicalvtzcomp:
  527. def __init__(self, tzoffsetfrom, tzoffsetto, isdst,
  528. tzname=None, rrule=None):
  529. self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom)
  530. self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto)
  531. self.tzoffsetdiff = self.tzoffsetto-self.tzoffsetfrom
  532. self.isdst = isdst
  533. self.tzname = tzname
  534. self.rrule = rrule
  535. class _tzicalvtz(datetime.tzinfo):
  536. def __init__(self, tzid, comps=[]):
  537. self._tzid = tzid
  538. self._comps = comps
  539. self._cachedate = []
  540. self._cachecomp = []
  541. def _find_comp(self, dt):
  542. if len(self._comps) == 1:
  543. return self._comps[0]
  544. dt = dt.replace(tzinfo=None)
  545. try:
  546. return self._cachecomp[self._cachedate.index(dt)]
  547. except ValueError:
  548. pass
  549. lastcomp = None
  550. lastcompdt = None
  551. for comp in self._comps:
  552. if not comp.isdst:
  553. # Handle the extra hour in DST -> STD
  554. compdt = comp.rrule.before(dt-comp.tzoffsetdiff, inc=True)
  555. else:
  556. compdt = comp.rrule.before(dt, inc=True)
  557. if compdt and (not lastcompdt or lastcompdt < compdt):
  558. lastcompdt = compdt
  559. lastcomp = comp
  560. if not lastcomp:
  561. # RFC says nothing about what to do when a given
  562. # time is before the first onset date. We'll look for the
  563. # first standard component, or the first component, if
  564. # none is found.
  565. for comp in self._comps:
  566. if not comp.isdst:
  567. lastcomp = comp
  568. break
  569. else:
  570. lastcomp = comp[0]
  571. self._cachedate.insert(0, dt)
  572. self._cachecomp.insert(0, lastcomp)
  573. if len(self._cachedate) > 10:
  574. self._cachedate.pop()
  575. self._cachecomp.pop()
  576. return lastcomp
  577. def utcoffset(self, dt):
  578. return self._find_comp(dt).tzoffsetto
  579. def dst(self, dt):
  580. comp = self._find_comp(dt)
  581. if comp.isdst:
  582. return comp.tzoffsetdiff
  583. else:
  584. return ZERO
  585. def tzname(self, dt):
  586. return self._find_comp(dt).tzname
  587. def __repr__(self):
  588. return "<tzicalvtz %s>" % `self._tzid`
  589. __reduce__ = object.__reduce__
  590. class tzical:
  591. def __init__(self, fileobj):
  592. global rrule
  593. if not rrule:
  594. from dateutil import rrule
  595. if isinstance(fileobj, basestring):
  596. self._s = fileobj
  597. fileobj = open(fileobj)
  598. elif hasattr(fileobj, "name"):
  599. self._s = fileobj.name
  600. else:
  601. self._s = `fileobj`
  602. self._vtz = {}
  603. self._parse_rfc(fileobj.read())
  604. def keys(self):
  605. return self._vtz.keys()
  606. def get(self, tzid=None):
  607. if tzid is None:
  608. keys = self._vtz.keys()
  609. if len(keys) == 0:
  610. raise ValueError, "no timezones defined"
  611. elif len(keys) > 1:
  612. raise ValueError, "more than one timezone available"
  613. tzid = keys[0]
  614. return self._vtz.get(tzid)
  615. def _parse_offset(self, s):
  616. s = s.strip()
  617. if not s:
  618. raise ValueError, "empty offset"
  619. if s[0] in ('+', '-'):
  620. signal = (-1,+1)[s[0]=='+']
  621. s = s[1:]
  622. else:
  623. signal = +1
  624. if len(s) == 4:
  625. return (int(s[:2])*3600+int(s[2:])*60)*signal
  626. elif len(s) == 6:
  627. return (int(s[:2])*3600+int(s[2:4])*60+int(s[4:]))*signal
  628. else:
  629. raise ValueError, "invalid offset: "+s
  630. def _parse_rfc(self, s):
  631. lines = s.splitlines()
  632. if not lines:
  633. raise ValueError, "empty string"
  634. # Unfold
  635. i = 0
  636. while i < len(lines):
  637. line = lines[i].rstrip()
  638. if not line:
  639. del lines[i]
  640. elif i > 0 and line[0] == " ":
  641. lines[i-1] += line[1:]
  642. del lines[i]
  643. else:
  644. i += 1
  645. tzid = None
  646. comps = []
  647. invtz = False
  648. comptype = None
  649. for line in lines:
  650. if not line:
  651. continue
  652. name, value = line.split(':', 1)
  653. parms = name.split(';')
  654. if not parms:
  655. raise ValueError, "empty property name"
  656. name = parms[0].upper()
  657. parms = parms[1:]
  658. if invtz:
  659. if name == "BEGIN":
  660. if value in ("STANDARD", "DAYLIGHT"):
  661. # Process component
  662. pass
  663. else:
  664. raise ValueError, "unknown component: "+value
  665. comptype = value
  666. founddtstart = False
  667. tzoffsetfrom = None
  668. tzoffsetto = None
  669. rrulelines = []
  670. tzname = None
  671. elif name == "END":
  672. if value == "VTIMEZONE":
  673. if comptype:
  674. raise ValueError, \
  675. "component not closed: "+comptype
  676. if not tzid:
  677. raise ValueError, \
  678. "mandatory TZID not found"
  679. if not comps:
  680. raise ValueError, \
  681. "at least one component is needed"
  682. # Process vtimezone
  683. self._vtz[tzid] = _tzicalvtz(tzid, comps)
  684. invtz = False
  685. elif value == comptype:
  686. if not founddtstart:
  687. raise ValueError, \
  688. "mandatory DTSTART not found"
  689. if tzoffsetfrom is None:
  690. raise ValueError, \
  691. "mandatory TZOFFSETFROM not found"
  692. if tzoffsetto is None:
  693. raise ValueError, \
  694. "mandatory TZOFFSETFROM not found"
  695. # Process component
  696. rr = None
  697. if rrulelines:
  698. rr = rrule.rrulestr("\n".join(rrulelines),
  699. compatible=True,
  700. ignoretz=True,
  701. cache=True)
  702. comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto,
  703. (comptype == "DAYLIGHT"),
  704. tzname, rr)
  705. comps.append(comp)
  706. comptype = None
  707. else:
  708. raise ValueError, \
  709. "invalid component end: "+value
  710. elif comptype:
  711. if name == "DTSTART":
  712. rrulelines.append(line)
  713. founddtstart = True
  714. elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"):
  715. rrulelines.append(line)
  716. elif name == "TZOFFSETFROM":
  717. if parms:
  718. raise ValueError, \
  719. "unsupported %s parm: %s "%(name, parms[0])
  720. tzoffsetfrom = self._parse_offset(value)
  721. elif name == "TZOFFSETTO":
  722. if parms:
  723. raise ValueError, \
  724. "unsupported TZOFFSETTO parm: "+parms[0]
  725. tzoffsetto = self._parse_offset(value)
  726. elif name == "TZNAME":
  727. if parms:
  728. raise ValueError, \
  729. "unsupported TZNAME parm: "+parms[0]
  730. tzname = value
  731. elif name == "COMMENT":
  732. pass
  733. else:
  734. raise ValueError, "unsupported property: "+name
  735. else:
  736. if name == "TZID":
  737. if parms:
  738. raise ValueError, \
  739. "unsupported TZID parm: "+parms[0]
  740. tzid = value
  741. elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"):
  742. pass
  743. else:
  744. raise ValueError, "unsupported property: "+name
  745. elif name == "BEGIN" and value == "VTIMEZONE":
  746. tzid = None
  747. comps = []
  748. invtz = True
  749. def __repr__(self):
  750. return "%s(%s)" % (self.__class__.__name__, `self._s`)
  751. if sys.platform != "win32":
  752. TZFILES = ["/etc/localtime", "localtime"]
  753. TZPATHS = ["/usr/share/zoneinfo", "/usr/lib/zoneinfo", "/etc/zoneinfo"]
  754. else:
  755. TZFILES = []
  756. TZPATHS = []
  757. def gettz(name=None):
  758. tz = None
  759. if not name:
  760. try:
  761. name = os.environ["TZ"]
  762. except KeyError:
  763. pass
  764. if name is None or name == ":":
  765. for filepath in TZFILES:
  766. if not os.path.isabs(filepath):
  767. filename = filepath
  768. for path in TZPATHS:
  769. filepath = os.path.join(path, filename)
  770. if os.path.isfile(filepath):
  771. break
  772. else:
  773. continue
  774. if os.path.isfile(filepath):
  775. try:
  776. tz = tzfile(filepath)
  777. break
  778. except (IOError, OSError, ValueError):
  779. pass
  780. else:
  781. tz = tzlocal()
  782. else:
  783. if name.startswith(":"):
  784. name = name[:-1]
  785. if os.path.isabs(name):
  786. if os.path.isfile(name):
  787. tz = tzfile(name)
  788. else:
  789. tz = None
  790. else:
  791. for path in TZPATHS:
  792. filepath = os.path.join(path, name)
  793. if not os.path.isfile(filepath):
  794. filepath = filepath.replace(' ','_')
  795. if not os.path.isfile(filepath):
  796. continue
  797. try:
  798. tz = tzfile(filepath)
  799. break
  800. except (IOError, OSError, ValueError):
  801. pass
  802. else:
  803. tz = None
  804. if tzwin:
  805. try:
  806. tz = tzwin(name)
  807. except OSError:
  808. pass
  809. if not tz:
  810. from dateutil.zoneinfo import gettz
  811. tz = gettz(name)
  812. if not tz:
  813. for c in name:
  814. # name must have at least one offset to be a tzstr
  815. if c in "0123456789":
  816. try:
  817. tz = tzstr(name)
  818. except ValueError:
  819. pass
  820. break
  821. else:
  822. if name in ("GMT", "UTC"):
  823. tz = tzutc()
  824. elif name in time.tzname:
  825. tz = tzlocal()
  826. return tz
  827. # vim:ts=4:sw=4:et