mirror of
https://github.com/Mikaela/Limnoria.git
synced 2024-11-25 03:59:31 +01:00
884 lines
30 KiB
Python
884 lines
30 KiB
Python
|
"""
|
||
|
Copyright (c) 2003 Gustavo Niemeyer <niemeyer@conectiva.com>
|
||
|
|
||
|
This module offers extensions to the standard python 2.3+
|
||
|
datetime module.
|
||
|
"""
|
||
|
__author__ = "Gustavo Niemeyer <niemeyer@conectiva.com>"
|
||
|
__license__ = "PSF License"
|
||
|
|
||
|
import datetime
|
||
|
import struct
|
||
|
import time
|
||
|
|
||
|
relativedelta = None
|
||
|
parser = None
|
||
|
rrule = None
|
||
|
|
||
|
__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile",
|
||
|
"tzrange", "tzstr", "tzical", "gettz"]
|
||
|
|
||
|
ZERO = datetime.timedelta(0)
|
||
|
EPOCHORDINAL = datetime.datetime.utcfromtimestamp(0).toordinal()
|
||
|
|
||
|
class tzutc(datetime.tzinfo):
|
||
|
|
||
|
def utcoffset(self, dt):
|
||
|
return ZERO
|
||
|
|
||
|
def dst(self, dt):
|
||
|
return ZERO
|
||
|
|
||
|
def tzname(self, dt):
|
||
|
return "UTC"
|
||
|
|
||
|
def __eq__(self, other):
|
||
|
return (isinstance(other, tzutc) or
|
||
|
(isinstance(other, tzoffset) and other._offset == ZERO))
|
||
|
|
||
|
def __ne__(self, other):
|
||
|
return not self.__eq__(other)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "%s()" % self.__class__.__name__
|
||
|
|
||
|
class tzoffset(datetime.tzinfo):
|
||
|
|
||
|
def __init__(self, name, offset):
|
||
|
self._name = name
|
||
|
self._offset = datetime.timedelta(seconds=offset)
|
||
|
|
||
|
def utcoffset(self, dt):
|
||
|
return self._offset
|
||
|
|
||
|
def dst(self, dt):
|
||
|
return ZERO
|
||
|
|
||
|
def tzname(self, dt):
|
||
|
return self._name
|
||
|
|
||
|
def __eq__(self, other):
|
||
|
return (isinstance(other, tzoffset) and
|
||
|
self._offset == other._offset)
|
||
|
|
||
|
def __ne__(self, other):
|
||
|
return not self.__eq__(other)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "%s(%s, %s)" % (self.__class__.__name__,
|
||
|
`self._name`,
|
||
|
self._offset.days*86400+self._offset.seconds)
|
||
|
|
||
|
class tzlocal(datetime.tzinfo):
|
||
|
|
||
|
_std_offset = datetime.timedelta(seconds=-time.timezone)
|
||
|
if time.daylight:
|
||
|
_dst_offset = datetime.timedelta(seconds=-time.altzone)
|
||
|
else:
|
||
|
_dst_offset = _std_offset
|
||
|
|
||
|
def utcoffset(self, dt):
|
||
|
if self._isdst(dt):
|
||
|
return self._dst_offset
|
||
|
else:
|
||
|
return self._std_offset
|
||
|
|
||
|
def dst(self, dt):
|
||
|
if self._isdst(dt):
|
||
|
return self._dst_offset-self._std_offset
|
||
|
else:
|
||
|
return ZERO
|
||
|
|
||
|
def tzname(self, dt):
|
||
|
return time.tzname[self._isdst(dt)]
|
||
|
|
||
|
def _isdst(self, dt):
|
||
|
# We can't use mktime here. It is unstable when deciding if
|
||
|
# the hour near to a change is DST or not.
|
||
|
#
|
||
|
# timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour,
|
||
|
# dt.minute, dt.second, dt.weekday(), 0, -1))
|
||
|
# return time.localtime(timestamp).tm_isdst
|
||
|
#
|
||
|
# The code above yields the following result:
|
||
|
#
|
||
|
#>>> import tz, datetime
|
||
|
#>>> t = tz.tzlocal()
|
||
|
#>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
|
||
|
#'BRDT'
|
||
|
#>>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname()
|
||
|
#'BRST'
|
||
|
#>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
|
||
|
#'BRST'
|
||
|
#>>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname()
|
||
|
#'BRDT'
|
||
|
#>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
|
||
|
#'BRDT'
|
||
|
#
|
||
|
# Here is a more stable implementation:
|
||
|
#
|
||
|
timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400
|
||
|
+ dt.hour * 3600
|
||
|
+ dt.minute * 60
|
||
|
+ dt.second)
|
||
|
return time.localtime(timestamp+time.timezone).tm_isdst
|
||
|
|
||
|
def __eq__(self, other):
|
||
|
if not isinstance(other, tzlocal):
|
||
|
return False
|
||
|
return (self._std_offset == other._std_offset and
|
||
|
self._dst_offset == other._dst_offset)
|
||
|
return True
|
||
|
|
||
|
def __ne__(self, other):
|
||
|
return not self.__eq__(other)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "%s()" % self.__class__.__name__
|
||
|
|
||
|
class _ttinfo(object):
|
||
|
__slots__ = ["offset", "delta", "isdst", "abbr", "isstd", "isgmt"]
|
||
|
|
||
|
def __init__(self):
|
||
|
for attr in self.__slots__:
|
||
|
setattr(self, attr, None)
|
||
|
|
||
|
def __repr__(self):
|
||
|
l = []
|
||
|
for attr in self.__slots__:
|
||
|
value = getattr(self, attr)
|
||
|
if value is not None:
|
||
|
l.append("%s=%s" % (attr, `value`))
|
||
|
return "%s(%s)" % (self.__class__.__name__, ", ".join(l))
|
||
|
|
||
|
def __eq__(self, other):
|
||
|
if not isinstance(other, _ttinfo):
|
||
|
return False
|
||
|
return (self.offset == other.offset and
|
||
|
self.delta == other.delta and
|
||
|
self.isdst == other.isdst and
|
||
|
self.abbr == other.abbr and
|
||
|
self.isstd == other.isstd and
|
||
|
self.isgmt == other.isgmt)
|
||
|
|
||
|
def __ne__(self, other):
|
||
|
return not self.__eq__(other)
|
||
|
|
||
|
class tzfile(datetime.tzinfo):
|
||
|
|
||
|
# http://www.twinsun.com/tz/tz-link.htm
|
||
|
# ftp://elsie.nci.nih.gov/pub/tz*.tar.gz
|
||
|
|
||
|
def __init__(self, fileobj):
|
||
|
if isinstance(fileobj, basestring):
|
||
|
self._s = fileobj
|
||
|
fileobj = open(fileobj)
|
||
|
elif hasattr(fileobj, "name"):
|
||
|
self._s = fileobj.name
|
||
|
else:
|
||
|
self._s = `fileobj`
|
||
|
|
||
|
# From tzfile(5):
|
||
|
#
|
||
|
# The time zone information files used by tzset(3)
|
||
|
# begin with the magic characters "TZif" to identify
|
||
|
# them as time zone information files, followed by
|
||
|
# sixteen bytes reserved for future use, followed by
|
||
|
# six four-byte values of type long, written in a
|
||
|
# ``standard'' byte order (the high-order byte
|
||
|
# of the value is written first).
|
||
|
|
||
|
if fileobj.read(4) != "TZif":
|
||
|
raise ValueError, "magic not found"
|
||
|
|
||
|
fileobj.read(16)
|
||
|
|
||
|
(
|
||
|
# The number of UTC/local indicators stored in the file.
|
||
|
ttisgmtcnt,
|
||
|
|
||
|
# The number of standard/wall indicators stored in the file.
|
||
|
ttisstdcnt,
|
||
|
|
||
|
# The number of leap seconds for which data is
|
||
|
# stored in the file.
|
||
|
leapcnt,
|
||
|
|
||
|
# The number of "transition times" for which data
|
||
|
# is stored in the file.
|
||
|
timecnt,
|
||
|
|
||
|
# The number of "local time types" for which data
|
||
|
# is stored in the file (must not be zero).
|
||
|
typecnt,
|
||
|
|
||
|
# The number of characters of "time zone
|
||
|
# abbreviation strings" stored in the file.
|
||
|
charcnt,
|
||
|
|
||
|
) = struct.unpack(">6l", fileobj.read(24))
|
||
|
|
||
|
# The above header is followed by tzh_timecnt four-byte
|
||
|
# values of type long, sorted in ascending order.
|
||
|
# These values are written in ``standard'' byte order.
|
||
|
# Each is used as a transition time (as returned by
|
||
|
# time(2)) at which the rules for computing local time
|
||
|
# change.
|
||
|
|
||
|
if timecnt:
|
||
|
self._trans_list = struct.unpack(">%dl" % timecnt,
|
||
|
fileobj.read(timecnt*4))
|
||
|
else:
|
||
|
self._trans_list = []
|
||
|
|
||
|
# Next come tzh_timecnt one-byte values of type unsigned
|
||
|
# char; each one tells which of the different types of
|
||
|
# ``local time'' types described in the file is associated
|
||
|
# with the same-indexed transition time. These values
|
||
|
# serve as indices into an array of ttinfo structures that
|
||
|
# appears next in the file.
|
||
|
|
||
|
if timecnt:
|
||
|
self._trans_idx = struct.unpack(">%dB" % timecnt,
|
||
|
fileobj.read(timecnt))
|
||
|
else:
|
||
|
self._trans_idx = []
|
||
|
|
||
|
# Each ttinfo structure is written as a four-byte value
|
||
|
# for tt_gmtoff of type long, in a standard byte
|
||
|
# order, followed by a one-byte value for tt_isdst
|
||
|
# and a one-byte value for tt_abbrind. In each
|
||
|
# structure, tt_gmtoff gives the number of
|
||
|
# seconds to be added to UTC, tt_isdst tells whether
|
||
|
# tm_isdst should be set by localtime(3), and
|
||
|
# tt_abbrind serves as an index into the array of
|
||
|
# time zone abbreviation characters that follow the
|
||
|
# ttinfo structure(s) in the file.
|
||
|
|
||
|
ttinfo = []
|
||
|
|
||
|
for i in range(typecnt):
|
||
|
ttinfo.append(struct.unpack(">lbb", fileobj.read(6)))
|
||
|
|
||
|
abbr = fileobj.read(charcnt)
|
||
|
|
||
|
# Then there are tzh_leapcnt pairs of four-byte
|
||
|
# values, written in standard byte order; the
|
||
|
# first value of each pair gives the time (as
|
||
|
# returned by time(2)) at which a leap second
|
||
|
# occurs; the second gives the total number of
|
||
|
# leap seconds to be applied after the given time.
|
||
|
# The pairs of values are sorted in ascending order
|
||
|
# by time.
|
||
|
|
||
|
# Not used, for now
|
||
|
if leapcnt:
|
||
|
leap = struct.unpack(">%dl" % leapcnt*2,
|
||
|
fileobj.read(leapcnt*8))
|
||
|
|
||
|
# Then there are tzh_ttisstdcnt standard/wall
|
||
|
# indicators, each stored as a one-byte value;
|
||
|
# they tell whether the transition times associated
|
||
|
# with local time types were specified as standard
|
||
|
# time or wall clock time, and are used when
|
||
|
# a time zone file is used in handling POSIX-style
|
||
|
# time zone environment variables.
|
||
|
|
||
|
if ttisstdcnt:
|
||
|
isstd = struct.unpack(">%db" % ttisstdcnt,
|
||
|
fileobj.read(ttisstdcnt))
|
||
|
|
||
|
# Finally, there are tzh_ttisgmtcnt UTC/local
|
||
|
# indicators, each stored as a one-byte value;
|
||
|
# they tell whether the transition times associated
|
||
|
# with local time types were specified as UTC or
|
||
|
# local time, and are used when a time zone file
|
||
|
# is used in handling POSIX-style time zone envi-
|
||
|
# ronment variables.
|
||
|
|
||
|
if ttisgmtcnt:
|
||
|
isgmt = struct.unpack(">%db" % ttisgmtcnt,
|
||
|
fileobj.read(ttisgmtcnt))
|
||
|
|
||
|
# ** Everything has been read **
|
||
|
|
||
|
# Build ttinfo list
|
||
|
self._ttinfo_list = []
|
||
|
for i in range(typecnt):
|
||
|
tti = _ttinfo()
|
||
|
tti.offset = ttinfo[i][0]
|
||
|
tti.delta = datetime.timedelta(seconds=ttinfo[i][0])
|
||
|
tti.isdst = ttinfo[i][1]
|
||
|
tti.abbr = abbr[ttinfo[i][2]:abbr.find('\x00', ttinfo[i][2])]
|
||
|
tti.isstd = (ttisstdcnt > i and isstd[i] != 0)
|
||
|
tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0)
|
||
|
self._ttinfo_list.append(tti)
|
||
|
|
||
|
# Replace ttinfo indexes for ttinfo objects.
|
||
|
trans_idx = []
|
||
|
for idx in self._trans_idx:
|
||
|
trans_idx.append(self._ttinfo_list[idx])
|
||
|
self._trans_idx = tuple(trans_idx)
|
||
|
|
||
|
# Set standard, dst, and before ttinfos. before will be
|
||
|
# used when a given time is before any transitions,
|
||
|
# and will be set to the first non-dst ttinfo, or to
|
||
|
# the first dst, if all of them are dst.
|
||
|
self._ttinfo_std = None
|
||
|
self._ttinfo_dst = None
|
||
|
self._ttinfo_before = None
|
||
|
if self._ttinfo_list:
|
||
|
if not self._trans_list:
|
||
|
self._ttinfo_std = self._ttinfo_first = self._ttinfo_list[0]
|
||
|
else:
|
||
|
for i in range(timecnt-1,-1,-1):
|
||
|
tti = self._trans_idx[i]
|
||
|
if not self._ttinfo_std and not tti.isdst:
|
||
|
self._ttinfo_std = tti
|
||
|
elif not self._ttinfo_dst and tti.isdst:
|
||
|
self._ttinfo_dst = tti
|
||
|
if self._ttinfo_std and self._ttinfo_dst:
|
||
|
break
|
||
|
else:
|
||
|
if self._ttinfo_dst and not self._ttinfo_std:
|
||
|
self._ttinfo_std = self._ttinfo_dst
|
||
|
|
||
|
for tti in self._ttinfo_list:
|
||
|
if not tti.isdst:
|
||
|
self._ttinfo_before = tti
|
||
|
break
|
||
|
else:
|
||
|
self._ttinfo_before = self._ttinfo_list[0]
|
||
|
|
||
|
# Now fix transition times to become relative to wall time.
|
||
|
#
|
||
|
# I'm not sure about this. In my tests, the tz source file
|
||
|
# is setup to wall time, and in the binary file isstd and
|
||
|
# isgmt are off, so it should be in wall time. OTOH, it's
|
||
|
# always in gmt time. Let me know if you have comments
|
||
|
# about this.
|
||
|
laststdoffset = 0
|
||
|
self._trans_list = list(self._trans_list)
|
||
|
for i in range(len(self._trans_list)):
|
||
|
tti = self._trans_idx[i]
|
||
|
if not tti.isdst:
|
||
|
# This is std time.
|
||
|
self._trans_list[i] += tti.offset
|
||
|
laststdoffset = tti.offset
|
||
|
else:
|
||
|
# This is dst time. Convert to std.
|
||
|
self._trans_list[i] += laststdoffset
|
||
|
self._trans_list = tuple(self._trans_list)
|
||
|
|
||
|
def _find_ttinfo(self, dt, laststd=0):
|
||
|
timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400
|
||
|
+ dt.hour * 3600
|
||
|
+ dt.minute * 60
|
||
|
+ dt.second)
|
||
|
idx = 0
|
||
|
for trans in self._trans_list:
|
||
|
if timestamp < trans:
|
||
|
break
|
||
|
idx += 1
|
||
|
else:
|
||
|
return self._ttinfo_std
|
||
|
if idx == 0:
|
||
|
return self._ttinfo_before
|
||
|
if laststd:
|
||
|
while idx > 0:
|
||
|
tti = self._trans_idx[idx-1]
|
||
|
if not tti.isdst:
|
||
|
return tti
|
||
|
idx -= 1
|
||
|
else:
|
||
|
return self._ttinfo_std
|
||
|
else:
|
||
|
return self._trans_idx[idx-1]
|
||
|
|
||
|
def utcoffset(self, dt):
|
||
|
if not self._ttinfo_std:
|
||
|
return ZERO
|
||
|
return self._find_ttinfo(dt).delta
|
||
|
|
||
|
def dst(self, dt):
|
||
|
if not self._ttinfo_dst:
|
||
|
return ZERO
|
||
|
tti = self._find_ttinfo(dt)
|
||
|
if not tti.isdst:
|
||
|
return ZERO
|
||
|
|
||
|
# The documentation says that utcoffset()-dst() must
|
||
|
# be constant for every dt.
|
||
|
return self._find_ttinfo(dt, laststd=1).delta-tti.delta
|
||
|
|
||
|
# An alternative for that would be:
|
||
|
#
|
||
|
# return self._ttinfo_dst.offset-self._ttinfo_std.offset
|
||
|
#
|
||
|
# However, this class stores historical changes in the
|
||
|
# dst offset, so I belive that this wouldn't be the right
|
||
|
# way to implement this.
|
||
|
|
||
|
def tzname(self, dt):
|
||
|
if not self._ttinfo_std:
|
||
|
return None
|
||
|
return self._find_ttinfo(dt).abbr
|
||
|
|
||
|
def __eq__(self, other):
|
||
|
if not isinstance(other, tzfile):
|
||
|
return False
|
||
|
return (self._trans_list == other._trans_list and
|
||
|
self._trans_idx == other._trans_idx and
|
||
|
self._ttinfo_list == other._ttinfo_list)
|
||
|
|
||
|
def __ne__(self, other):
|
||
|
return not self.__eq__(other)
|
||
|
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "%s(%s)" % (self.__class__.__name__, `self._s`)
|
||
|
|
||
|
class tzrange(datetime.tzinfo):
|
||
|
|
||
|
def __init__(self, stdabbr, stdoffset=None,
|
||
|
dstabbr=None, dstoffset=None,
|
||
|
start=None, end=None):
|
||
|
global relativedelta
|
||
|
if not relativedelta:
|
||
|
from dateutil import relativedelta
|
||
|
self._std_abbr = stdabbr
|
||
|
self._dst_abbr = dstabbr
|
||
|
if stdoffset is not None:
|
||
|
self._std_offset = datetime.timedelta(seconds=stdoffset)
|
||
|
else:
|
||
|
self._std_offset = ZERO
|
||
|
if dstoffset is not None:
|
||
|
self._dst_offset = datetime.timedelta(seconds=dstoffset)
|
||
|
elif dstabbr and stdoffset is not None:
|
||
|
self._dst_offset = self._std_offset+datetime.timedelta(hours=+1)
|
||
|
else:
|
||
|
self._dst_offset = ZERO
|
||
|
if start is None:
|
||
|
self._start_delta = relativedelta.relativedelta(
|
||
|
hours=+2, month=4, day=1, weekday=relativedelta.SU(+1))
|
||
|
else:
|
||
|
self._start_delta = start
|
||
|
if end is None:
|
||
|
self._end_delta = relativedelta.relativedelta(
|
||
|
hours=+1, month=10, day=31, weekday=relativedelta.SU(-1))
|
||
|
else:
|
||
|
self._end_delta = end
|
||
|
|
||
|
def utcoffset(self, dt):
|
||
|
if self._isdst(dt):
|
||
|
return self._dst_offset
|
||
|
else:
|
||
|
return self._std_offset
|
||
|
|
||
|
def dst(self, dt):
|
||
|
if self._isdst(dt):
|
||
|
return self._dst_offset-self._std_offset
|
||
|
else:
|
||
|
return ZERO
|
||
|
|
||
|
def tzname(self, dt):
|
||
|
if self._isdst(dt):
|
||
|
return self._dst_abbr
|
||
|
else:
|
||
|
return self._std_abbr
|
||
|
|
||
|
def _isdst(self, dt):
|
||
|
if not self._start_delta:
|
||
|
return False
|
||
|
year = datetime.date(dt.year,1,1)
|
||
|
start = year+self._start_delta
|
||
|
end = year+self._end_delta
|
||
|
dt = dt.replace(tzinfo=None)
|
||
|
if start < end:
|
||
|
return dt >= start and dt < end
|
||
|
else:
|
||
|
return dt >= start or dt < end
|
||
|
|
||
|
def __eq__(self, other):
|
||
|
if not isinstance(other, tzrange):
|
||
|
return False
|
||
|
return (self._std_abbr == other._std_abbr and
|
||
|
self._dst_abbr == other._dst_abbr and
|
||
|
self._std_offset == other._std_offset and
|
||
|
self._dst_offset == other._dst_offset and
|
||
|
self._start_delta == other._start_delta and
|
||
|
self._end_delta == other._end_delta)
|
||
|
|
||
|
def __ne__(self, other):
|
||
|
return not self.__eq__(other)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "%s(...)" % self.__class__.__name__
|
||
|
|
||
|
|
||
|
class tzstr(tzrange):
|
||
|
|
||
|
def __init__(self, s):
|
||
|
global parser
|
||
|
if not parser:
|
||
|
from dateutil import parser
|
||
|
self._s = s
|
||
|
|
||
|
res = parser._parsetz(s)
|
||
|
if res is None:
|
||
|
raise ValueError, "unknown string format"
|
||
|
|
||
|
# We must initialize it first, since _delta() needs
|
||
|
# _std_offset and _dst_offset set. Use False in start/end
|
||
|
# to avoid building it two times.
|
||
|
tzrange.__init__(self, res.stdabbr, res.stdoffset,
|
||
|
res.dstabbr, res.dstoffset,
|
||
|
start=False, end=False)
|
||
|
|
||
|
self._start_delta = self._delta(res.start)
|
||
|
if self._start_delta:
|
||
|
self._end_delta = self._delta(res.end, isend=1)
|
||
|
|
||
|
def _delta(self, x, isend=0):
|
||
|
kwargs = {}
|
||
|
if x.month is not None:
|
||
|
kwargs["month"] = x.month
|
||
|
if x.weekday is not None:
|
||
|
kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week)
|
||
|
if x.week > 0:
|
||
|
kwargs["day"] = 1
|
||
|
else:
|
||
|
kwargs["day"] = 31
|
||
|
elif x.day:
|
||
|
kwargs["day"] = x.day
|
||
|
elif x.yday is not None:
|
||
|
kwargs["yearday"] = x.yday
|
||
|
elif x.jyday is not None:
|
||
|
kwargs["nlyearday"] = x.jyday
|
||
|
if not kwargs:
|
||
|
# Default is to start on first sunday of april, and end
|
||
|
# on last sunday of october.
|
||
|
if not isend:
|
||
|
kwargs["month"] = 4
|
||
|
kwargs["day"] = 1
|
||
|
kwargs["weekday"] = relativedelta.SU(+1)
|
||
|
else:
|
||
|
kwargs["month"] = 10
|
||
|
kwargs["day"] = 31
|
||
|
kwargs["weekday"] = relativedelta.SU(-1)
|
||
|
if x.time is not None:
|
||
|
kwargs["seconds"] = x.time
|
||
|
else:
|
||
|
# Default is 2AM.
|
||
|
kwargs["seconds"] = 7200
|
||
|
if isend:
|
||
|
# Convert to standard time, to follow the documented way
|
||
|
# of working with the extra hour. See the documentation
|
||
|
# of the tzinfo class.
|
||
|
delta = self._dst_offset-self._std_offset
|
||
|
kwargs["seconds"] -= delta.seconds+delta.days*86400
|
||
|
return relativedelta.relativedelta(**kwargs)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "%s(%s)" % (self.__class__.__name__, `self._s`)
|
||
|
|
||
|
class _tzicalvtzcomp:
|
||
|
def __init__(self, tzoffsetfrom, tzoffsetto, isdst,
|
||
|
tzname=None, rrule=None):
|
||
|
self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom)
|
||
|
self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto)
|
||
|
self.tzoffsetdiff = self.tzoffsetto-self.tzoffsetfrom
|
||
|
self.isdst = isdst
|
||
|
self.tzname = tzname
|
||
|
self.rrule = rrule
|
||
|
|
||
|
class _tzicalvtz(datetime.tzinfo):
|
||
|
def __init__(self, tzid, comps=[]):
|
||
|
self._tzid = tzid
|
||
|
self._comps = comps
|
||
|
self._cachedate = []
|
||
|
self._cachecomp = []
|
||
|
|
||
|
def _find_comp(self, dt):
|
||
|
if len(self._comps) == 1:
|
||
|
return self._comps[0]
|
||
|
dt = dt.replace(tzinfo=None)
|
||
|
try:
|
||
|
return self._cachecomp[self._cachedate.index(dt)]
|
||
|
except ValueError:
|
||
|
pass
|
||
|
lastcomp = None
|
||
|
lastcompdt = None
|
||
|
for comp in self._comps:
|
||
|
if not comp.isdst:
|
||
|
# Handle the extra hour in DST -> STD
|
||
|
compdt = comp.rrule.before(dt-comp.tzoffsetdiff, inc=True)
|
||
|
else:
|
||
|
compdt = comp.rrule.before(dt, inc=True)
|
||
|
if compdt and (not lastcompdt or lastcompdt < compdt):
|
||
|
lastcompdt = compdt
|
||
|
lastcomp = comp
|
||
|
if not lastcomp:
|
||
|
# RFC says nothing about what to do when a given
|
||
|
# time is before the first onset date. We'll look for the
|
||
|
# first standard component, or the first component, if
|
||
|
# none is found.
|
||
|
for comp in self._comps:
|
||
|
if not comp.isdst:
|
||
|
lastcomp = comp
|
||
|
break
|
||
|
else:
|
||
|
lastcomp = comp[0]
|
||
|
self._cachedate.insert(0, dt)
|
||
|
self._cachecomp.insert(0, lastcomp)
|
||
|
if len(self._cachedate) > 10:
|
||
|
self._cachedate.pop()
|
||
|
self._cachecomp.pop()
|
||
|
return lastcomp
|
||
|
|
||
|
def utcoffset(self, dt):
|
||
|
return self._find_comp(dt).tzoffsetto
|
||
|
|
||
|
def dst(self, dt):
|
||
|
comp = self._find_comp(dt)
|
||
|
if comp.isdst:
|
||
|
return comp.tzoffsetdiff
|
||
|
else:
|
||
|
return ZERO
|
||
|
|
||
|
def tzname(self, dt):
|
||
|
return self._find_comp(dt).tzname
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "<tzicalvtz %s>" % `self._tzid`
|
||
|
|
||
|
class tzical:
|
||
|
def __init__(self, fileobj):
|
||
|
global rrule
|
||
|
if not rrule:
|
||
|
from dateutil import rrule
|
||
|
|
||
|
if isinstance(fileobj, basestring):
|
||
|
self._s = fileobj
|
||
|
fileobj = open(fileobj)
|
||
|
elif hasattr(fileobj, "name"):
|
||
|
self._s = fileobj.name
|
||
|
else:
|
||
|
self._s = `fileobj`
|
||
|
|
||
|
self._vtz = {}
|
||
|
|
||
|
self._parse_rfc(fileobj.read())
|
||
|
|
||
|
def keys(self):
|
||
|
return self._vtz.keys()
|
||
|
|
||
|
def get(self, tzid=None):
|
||
|
if tzid is None:
|
||
|
keys = self._vtz.keys()
|
||
|
if len(keys) == 0:
|
||
|
raise "no timezones defined"
|
||
|
elif len(keys) > 1:
|
||
|
raise "more than one timezone available"
|
||
|
tzid = keys[0]
|
||
|
return self._vtz.get(tzid)
|
||
|
|
||
|
def _parse_offset(self, s):
|
||
|
s = s.strip()
|
||
|
if not s:
|
||
|
raise ValueError, "empty offset"
|
||
|
if s[0] in ('+', '-'):
|
||
|
signal = (-1,+1)[s[0]=='+']
|
||
|
s = s[1:]
|
||
|
else:
|
||
|
signal = +1
|
||
|
if len(s) == 4:
|
||
|
return (int(s[:2])*3600+int(s[2:])*60)*signal
|
||
|
elif len(s) == 6:
|
||
|
return (int(s[:2])*3600+int(s[2:4])*60+int(s[4:]))*signal
|
||
|
else:
|
||
|
raise ValueError, "invalid offset: "+s
|
||
|
|
||
|
def _parse_rfc(self, s):
|
||
|
lines = s.splitlines()
|
||
|
if not lines:
|
||
|
raise ValueError, "empty string"
|
||
|
|
||
|
# Unfold
|
||
|
i = 0
|
||
|
while i < len(lines):
|
||
|
line = lines[i].rstrip()
|
||
|
if not line:
|
||
|
del lines[i]
|
||
|
elif i > 0 and line[0] == " ":
|
||
|
lines[i-1] += line[1:]
|
||
|
del lines[i]
|
||
|
else:
|
||
|
i += 1
|
||
|
|
||
|
invtz = False
|
||
|
comptype = None
|
||
|
for line in lines:
|
||
|
if not line:
|
||
|
continue
|
||
|
name, value = line.split(':', 1)
|
||
|
parms = name.split(';')
|
||
|
if not parms:
|
||
|
raise ValueError, "empty property name"
|
||
|
name = parms[0].upper()
|
||
|
parms = parms[1:]
|
||
|
if invtz:
|
||
|
if name == "BEGIN":
|
||
|
if value in ("STANDARD", "DAYLIGHT"):
|
||
|
# Process component
|
||
|
pass
|
||
|
else:
|
||
|
raise ValueError, "unknown component: "+value
|
||
|
comptype = value
|
||
|
founddtstart = False
|
||
|
tzoffsetfrom = None
|
||
|
tzoffsetto = None
|
||
|
rrulelines = []
|
||
|
tzname = None
|
||
|
elif name == "END":
|
||
|
if value == "VTIMEZONE":
|
||
|
if comptype:
|
||
|
raise ValueError, \
|
||
|
"component not closed: "+comptype
|
||
|
if not tzid:
|
||
|
raise ValueError, \
|
||
|
"mandatory TZID not found"
|
||
|
if not comps:
|
||
|
raise ValueError, \
|
||
|
"at least one component is needed"
|
||
|
# Process vtimezone
|
||
|
self._vtz[tzid] = _tzicalvtz(tzid, comps)
|
||
|
invtz = False
|
||
|
elif value == comptype:
|
||
|
if not founddtstart:
|
||
|
raise ValueError, \
|
||
|
"mandatory DTSTART not found"
|
||
|
if not tzoffsetfrom:
|
||
|
raise ValueError, \
|
||
|
"mandatory TZOFFSETFROM not found"
|
||
|
if not tzoffsetto:
|
||
|
raise ValueError, \
|
||
|
"mandatory TZOFFSETFROM not found"
|
||
|
# Process component
|
||
|
rr = None
|
||
|
if rrulelines:
|
||
|
rr = rrule.rrulestr("\n".join(rrulelines),
|
||
|
compatible=True,
|
||
|
ignoretz=True,
|
||
|
cache=True)
|
||
|
comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto,
|
||
|
(comptype == "DAYLIGHT"),
|
||
|
tzname, rr)
|
||
|
comps.append(comp)
|
||
|
comptype = None
|
||
|
else:
|
||
|
raise ValueError, \
|
||
|
"invalid component end: "+value
|
||
|
elif comptype:
|
||
|
if name == "DTSTART":
|
||
|
rrulelines.append(line)
|
||
|
founddtstart = True
|
||
|
elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"):
|
||
|
rrulelines.append(line)
|
||
|
elif name == "TZOFFSETFROM":
|
||
|
if parms:
|
||
|
raise ValueError, \
|
||
|
"unsupported %s parm: %s "%(name, parms[0])
|
||
|
tzoffsetfrom = self._parse_offset(value)
|
||
|
elif name == "TZOFFSETTO":
|
||
|
if parms:
|
||
|
raise ValueError, \
|
||
|
"unsupported TZOFFSETTO parm: "+parms[0]
|
||
|
tzoffsetto = self._parse_offset(value)
|
||
|
elif name == "TZNAME":
|
||
|
if parms:
|
||
|
raise ValueError, \
|
||
|
"unsupported TZNAME parm: "+parms[0]
|
||
|
tzname = value
|
||
|
elif name == "COMMENT":
|
||
|
pass
|
||
|
else:
|
||
|
raise ValueError, "unsupported property: "+name
|
||
|
else:
|
||
|
if name == "TZID":
|
||
|
if parms:
|
||
|
raise ValueError, \
|
||
|
"unsupported TZID parm: "+parms[0]
|
||
|
tzid = value
|
||
|
elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"):
|
||
|
pass
|
||
|
else:
|
||
|
raise ValueError, "unsupported property: "+name
|
||
|
elif name == "BEGIN" and value == "VTIMEZONE":
|
||
|
tzid = None
|
||
|
comps = []
|
||
|
invtz = True
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "%s(%s)" % (self.__class__.__name__, `self._s`)
|
||
|
|
||
|
TZFILES = ["/etc/localtime", "localtime"]
|
||
|
TZPATHS = ["/usr/share/zoneinfo", "/usr/lib/zoneinfo", "/etc/zoneinfo"]
|
||
|
|
||
|
import os
|
||
|
|
||
|
def gettz(name=None):
|
||
|
tz = None
|
||
|
if not name:
|
||
|
try:
|
||
|
name = os.environ["TZ"]
|
||
|
except KeyError:
|
||
|
pass
|
||
|
if name is None:
|
||
|
for filepath in TZFILES:
|
||
|
if not os.path.isabs(filepath):
|
||
|
filename = filepath
|
||
|
for path in TZPATHS:
|
||
|
filepath = os.path.join(path, filename)
|
||
|
if os.path.isfile(filepath):
|
||
|
break
|
||
|
else:
|
||
|
continue
|
||
|
if os.path.isfile(filepath):
|
||
|
try:
|
||
|
tz = tzfile(filepath)
|
||
|
break
|
||
|
except (IOError, OSError, ValueError):
|
||
|
pass
|
||
|
else:
|
||
|
if name and name[0] == ":":
|
||
|
name = name[:-1]
|
||
|
for path in TZPATHS:
|
||
|
filepath = os.path.join(path, name)
|
||
|
if not os.path.isfile(filepath):
|
||
|
filepath = filepath.replace(' ','_')
|
||
|
if not os.path.isfile(filepath):
|
||
|
continue
|
||
|
try:
|
||
|
tz = tzfile(filepath)
|
||
|
break
|
||
|
except (IOError, OSError, ValueError):
|
||
|
pass
|
||
|
else:
|
||
|
for c in name:
|
||
|
# name must have at least one offset to be a tzstr
|
||
|
if c in "0123456789":
|
||
|
try:
|
||
|
tz = tzstr(name)
|
||
|
except ValueError:
|
||
|
pass
|
||
|
break
|
||
|
else:
|
||
|
if name in ("GMT", "UTC"):
|
||
|
tz = tzutc()
|
||
|
elif name in time.tzname:
|
||
|
tz = tzlocal()
|
||
|
return tz
|
||
|
|
||
|
# vim:ts=4:sw=4:et
|