2
Copyright (c) 2003 Gustavo Niemeyer <niemeyer@conectiva.com>
4
This module offers extensions to the standard python 2.3+
7
__author__ = "Gustavo Niemeyer <niemeyer@conectiva.com>"
8
__license__ = "PSF License"
18
__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile",
19
"tzrange", "tzstr", "tzical", "gettz"]
21
ZERO = datetime.timedelta(0)
22
EPOCHORDINAL = datetime.datetime.utcfromtimestamp(0).toordinal()
24
class tzutc(datetime.tzinfo):
26
def utcoffset(self, dt):
35
def __eq__(self, other):
36
return (isinstance(other, tzutc) or
37
(isinstance(other, tzoffset) and other._offset == ZERO))
39
def __ne__(self, other):
40
return not self.__eq__(other)
43
return "%s()" % self.__class__.__name__
45
class tzoffset(datetime.tzinfo):
47
def __init__(self, name, offset):
49
self._offset = datetime.timedelta(seconds=offset)
51
def utcoffset(self, dt):
60
def __eq__(self, other):
61
return (isinstance(other, tzoffset) and
62
self._offset == other._offset)
64
def __ne__(self, other):
65
return not self.__eq__(other)
68
return "%s(%s, %s)" % (self.__class__.__name__,
70
self._offset.days*86400+self._offset.seconds)
72
class tzlocal(datetime.tzinfo):
74
_std_offset = datetime.timedelta(seconds=-time.timezone)
76
_dst_offset = datetime.timedelta(seconds=-time.altzone)
78
_dst_offset = _std_offset
80
def utcoffset(self, dt):
82
return self._dst_offset
84
return self._std_offset
88
return self._dst_offset-self._std_offset
93
return time.tzname[self._isdst(dt)]
96
# We can't use mktime here. It is unstable when deciding if
97
# the hour near to a change is DST or not.
99
# timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour,
100
# dt.minute, dt.second, dt.weekday(), 0, -1))
101
# return time.localtime(timestamp).tm_isdst
103
# The code above yields the following result:
105
#>>> import tz, datetime
106
#>>> t = tz.tzlocal()
107
#>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
109
#>>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname()
111
#>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
113
#>>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname()
115
#>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
118
# Here is a more stable implementation:
120
timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400
124
return time.localtime(timestamp+time.timezone).tm_isdst
126
def __eq__(self, other):
127
if not isinstance(other, tzlocal):
129
return (self._std_offset == other._std_offset and
130
self._dst_offset == other._dst_offset)
133
def __ne__(self, other):
134
return not self.__eq__(other)
137
return "%s()" % self.__class__.__name__
139
class _ttinfo(object):
140
__slots__ = ["offset", "delta", "isdst", "abbr", "isstd", "isgmt"]
143
for attr in self.__slots__:
144
setattr(self, attr, None)
148
for attr in self.__slots__:
149
value = getattr(self, attr)
150
if value is not None:
151
l.append("%s=%s" % (attr, `value`))
152
return "%s(%s)" % (self.__class__.__name__, ", ".join(l))
154
def __eq__(self, other):
155
if not isinstance(other, _ttinfo):
157
return (self.offset == other.offset and
158
self.delta == other.delta and
159
self.isdst == other.isdst and
160
self.abbr == other.abbr and
161
self.isstd == other.isstd and
162
self.isgmt == other.isgmt)
164
def __ne__(self, other):
165
return not self.__eq__(other)
167
class tzfile(datetime.tzinfo):
169
# http://www.twinsun.com/tz/tz-link.htm
170
# ftp://elsie.nci.nih.gov/pub/tz*.tar.gz
172
def __init__(self, fileobj):
173
if isinstance(fileobj, basestring):
175
fileobj = open(fileobj)
176
elif hasattr(fileobj, "name"):
177
self._s = fileobj.name
183
# The time zone information files used by tzset(3)
184
# begin with the magic characters "TZif" to identify
185
# them as time zone information files, followed by
186
# sixteen bytes reserved for future use, followed by
187
# six four-byte values of type long, written in a
188
# ``standard'' byte order (the high-order byte
189
# of the value is written first).
191
if fileobj.read(4) != "TZif":
192
raise ValueError, "magic not found"
197
# The number of UTC/local indicators stored in the file.
200
# The number of standard/wall indicators stored in the file.
203
# The number of leap seconds for which data is
204
# stored in the file.
207
# The number of "transition times" for which data
208
# is stored in the file.
211
# The number of "local time types" for which data
212
# is stored in the file (must not be zero).
215
# The number of characters of "time zone
216
# abbreviation strings" stored in the file.
219
) = struct.unpack(">6l", fileobj.read(24))
221
# The above header is followed by tzh_timecnt four-byte
222
# values of type long, sorted in ascending order.
223
# These values are written in ``standard'' byte order.
224
# Each is used as a transition time (as returned by
225
# time(2)) at which the rules for computing local time
229
self._trans_list = struct.unpack(">%dl" % timecnt,
230
fileobj.read(timecnt*4))
232
self._trans_list = []
234
# Next come tzh_timecnt one-byte values of type unsigned
235
# char; each one tells which of the different types of
236
# ``local time'' types described in the file is associated
237
# with the same-indexed transition time. These values
238
# serve as indices into an array of ttinfo structures that
239
# appears next in the file.
242
self._trans_idx = struct.unpack(">%dB" % timecnt,
243
fileobj.read(timecnt))
247
# Each ttinfo structure is written as a four-byte value
248
# for tt_gmtoff of type long, in a standard byte
249
# order, followed by a one-byte value for tt_isdst
250
# and a one-byte value for tt_abbrind. In each
251
# structure, tt_gmtoff gives the number of
252
# seconds to be added to UTC, tt_isdst tells whether
253
# tm_isdst should be set by localtime(3), and
254
# tt_abbrind serves as an index into the array of
255
# time zone abbreviation characters that follow the
256
# ttinfo structure(s) in the file.
260
for i in range(typecnt):
261
ttinfo.append(struct.unpack(">lbb", fileobj.read(6)))
263
abbr = fileobj.read(charcnt)
265
# Then there are tzh_leapcnt pairs of four-byte
266
# values, written in standard byte order; the
267
# first value of each pair gives the time (as
268
# returned by time(2)) at which a leap second
269
# occurs; the second gives the total number of
270
# leap seconds to be applied after the given time.
271
# The pairs of values are sorted in ascending order
276
leap = struct.unpack(">%dl" % leapcnt*2,
277
fileobj.read(leapcnt*8))
279
# Then there are tzh_ttisstdcnt standard/wall
280
# indicators, each stored as a one-byte value;
281
# they tell whether the transition times associated
282
# with local time types were specified as standard
283
# time or wall clock time, and are used when
284
# a time zone file is used in handling POSIX-style
285
# time zone environment variables.
288
isstd = struct.unpack(">%db" % ttisstdcnt,
289
fileobj.read(ttisstdcnt))
291
# Finally, there are tzh_ttisgmtcnt UTC/local
292
# indicators, each stored as a one-byte value;
293
# they tell whether the transition times associated
294
# with local time types were specified as UTC or
295
# local time, and are used when a time zone file
296
# is used in handling POSIX-style time zone envi-
300
isgmt = struct.unpack(">%db" % ttisgmtcnt,
301
fileobj.read(ttisgmtcnt))
303
# ** Everything has been read **
306
self._ttinfo_list = []
307
for i in range(typecnt):
309
tti.offset = ttinfo[i][0]
310
tti.delta = datetime.timedelta(seconds=ttinfo[i][0])
311
tti.isdst = ttinfo[i][1]
312
tti.abbr = abbr[ttinfo[i][2]:abbr.find('\x00', ttinfo[i][2])]
313
tti.isstd = (ttisstdcnt > i and isstd[i] != 0)
314
tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0)
315
self._ttinfo_list.append(tti)
317
# Replace ttinfo indexes for ttinfo objects.
319
for idx in self._trans_idx:
320
trans_idx.append(self._ttinfo_list[idx])
321
self._trans_idx = tuple(trans_idx)
323
# Set standard, dst, and before ttinfos. before will be
324
# used when a given time is before any transitions,
325
# and will be set to the first non-dst ttinfo, or to
326
# the first dst, if all of them are dst.
327
self._ttinfo_std = None
328
self._ttinfo_dst = None
329
self._ttinfo_before = None
330
if self._ttinfo_list:
331
if not self._trans_list:
332
self._ttinfo_std = self._ttinfo_first = self._ttinfo_list[0]
334
for i in range(timecnt-1,-1,-1):
335
tti = self._trans_idx[i]
336
if not self._ttinfo_std and not tti.isdst:
337
self._ttinfo_std = tti
338
elif not self._ttinfo_dst and tti.isdst:
339
self._ttinfo_dst = tti
340
if self._ttinfo_std and self._ttinfo_dst:
343
if self._ttinfo_dst and not self._ttinfo_std:
344
self._ttinfo_std = self._ttinfo_dst
346
for tti in self._ttinfo_list:
348
self._ttinfo_before = tti
351
self._ttinfo_before = self._ttinfo_list[0]
353
# Now fix transition times to become relative to wall time.
355
# I'm not sure about this. In my tests, the tz source file
356
# is setup to wall time, and in the binary file isstd and
357
# isgmt are off, so it should be in wall time. OTOH, it's
358
# always in gmt time. Let me know if you have comments
361
self._trans_list = list(self._trans_list)
362
for i in range(len(self._trans_list)):
363
tti = self._trans_idx[i]
366
self._trans_list[i] += tti.offset
367
laststdoffset = tti.offset
369
# This is dst time. Convert to std.
370
self._trans_list[i] += laststdoffset
371
self._trans_list = tuple(self._trans_list)
373
def _find_ttinfo(self, dt, laststd=0):
374
timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400
379
for trans in self._trans_list:
380
if timestamp < trans:
384
return self._ttinfo_std
386
return self._ttinfo_before
389
tti = self._trans_idx[idx-1]
394
return self._ttinfo_std
396
return self._trans_idx[idx-1]
398
def utcoffset(self, dt):
399
if not self._ttinfo_std:
401
return self._find_ttinfo(dt).delta
404
if not self._ttinfo_dst:
406
tti = self._find_ttinfo(dt)
410
# The documentation says that utcoffset()-dst() must
411
# be constant for every dt.
412
return self._find_ttinfo(dt, laststd=1).delta-tti.delta
414
# An alternative for that would be:
416
# return self._ttinfo_dst.offset-self._ttinfo_std.offset
418
# However, this class stores historical changes in the
419
# dst offset, so I belive that this wouldn't be the right
420
# way to implement this.
422
def tzname(self, dt):
423
if not self._ttinfo_std:
425
return self._find_ttinfo(dt).abbr
427
def __eq__(self, other):
428
if not isinstance(other, tzfile):
430
return (self._trans_list == other._trans_list and
431
self._trans_idx == other._trans_idx and
432
self._ttinfo_list == other._ttinfo_list)
434
def __ne__(self, other):
435
return not self.__eq__(other)
439
return "%s(%s)" % (self.__class__.__name__, `self._s`)
441
class tzrange(datetime.tzinfo):
443
def __init__(self, stdabbr, stdoffset=None,
444
dstabbr=None, dstoffset=None,
445
start=None, end=None):
447
if not relativedelta:
448
from dateutil import relativedelta
449
self._std_abbr = stdabbr
450
self._dst_abbr = dstabbr
451
if stdoffset is not None:
452
self._std_offset = datetime.timedelta(seconds=stdoffset)
454
self._std_offset = ZERO
455
if dstoffset is not None:
456
self._dst_offset = datetime.timedelta(seconds=dstoffset)
457
elif dstabbr and stdoffset is not None:
458
self._dst_offset = self._std_offset+datetime.timedelta(hours=+1)
460
self._dst_offset = ZERO
462
self._start_delta = relativedelta.relativedelta(
463
hours=+2, month=4, day=1, weekday=relativedelta.SU(+1))
465
self._start_delta = start
467
self._end_delta = relativedelta.relativedelta(
468
hours=+1, month=10, day=31, weekday=relativedelta.SU(-1))
470
self._end_delta = end
472
def utcoffset(self, dt):
474
return self._dst_offset
476
return self._std_offset
480
return self._dst_offset-self._std_offset
484
def tzname(self, dt):
486
return self._dst_abbr
488
return self._std_abbr
490
def _isdst(self, dt):
491
if not self._start_delta:
493
year = datetime.date(dt.year,1,1)
494
start = year+self._start_delta
495
end = year+self._end_delta
496
dt = dt.replace(tzinfo=None)
498
return dt >= start and dt < end
500
return dt >= start or dt < end
502
def __eq__(self, other):
503
if not isinstance(other, tzrange):
505
return (self._std_abbr == other._std_abbr and
506
self._dst_abbr == other._dst_abbr and
507
self._std_offset == other._std_offset and
508
self._dst_offset == other._dst_offset and
509
self._start_delta == other._start_delta and
510
self._end_delta == other._end_delta)
512
def __ne__(self, other):
513
return not self.__eq__(other)
516
return "%s(...)" % self.__class__.__name__
519
class tzstr(tzrange):
521
def __init__(self, s):
524
from dateutil import parser
527
res = parser._parsetz(s)
529
raise ValueError, "unknown string format"
531
# We must initialize it first, since _delta() needs
532
# _std_offset and _dst_offset set. Use False in start/end
533
# to avoid building it two times.
534
tzrange.__init__(self, res.stdabbr, res.stdoffset,
535
res.dstabbr, res.dstoffset,
536
start=False, end=False)
538
self._start_delta = self._delta(res.start)
539
if self._start_delta:
540
self._end_delta = self._delta(res.end, isend=1)
542
def _delta(self, x, isend=0):
544
if x.month is not None:
545
kwargs["month"] = x.month
546
if x.weekday is not None:
547
kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week)
553
kwargs["day"] = x.day
554
elif x.yday is not None:
555
kwargs["yearday"] = x.yday
556
elif x.jyday is not None:
557
kwargs["nlyearday"] = x.jyday
559
# Default is to start on first sunday of april, and end
560
# on last sunday of october.
564
kwargs["weekday"] = relativedelta.SU(+1)
568
kwargs["weekday"] = relativedelta.SU(-1)
569
if x.time is not None:
570
kwargs["seconds"] = x.time
573
kwargs["seconds"] = 7200
575
# Convert to standard time, to follow the documented way
576
# of working with the extra hour. See the documentation
577
# of the tzinfo class.
578
delta = self._dst_offset-self._std_offset
579
kwargs["seconds"] -= delta.seconds+delta.days*86400
580
return relativedelta.relativedelta(**kwargs)
583
return "%s(%s)" % (self.__class__.__name__, `self._s`)
585
class _tzicalvtzcomp:
586
def __init__(self, tzoffsetfrom, tzoffsetto, isdst,
587
tzname=None, rrule=None):
588
self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom)
589
self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto)
590
self.tzoffsetdiff = self.tzoffsetto-self.tzoffsetfrom
595
class _tzicalvtz(datetime.tzinfo):
596
def __init__(self, tzid, comps=[]):
602
def _find_comp(self, dt):
603
if len(self._comps) == 1:
604
return self._comps[0]
605
dt = dt.replace(tzinfo=None)
607
return self._cachecomp[self._cachedate.index(dt)]
612
for comp in self._comps:
614
# Handle the extra hour in DST -> STD
615
compdt = comp.rrule.before(dt-comp.tzoffsetdiff, inc=True)
617
compdt = comp.rrule.before(dt, inc=True)
618
if compdt and (not lastcompdt or lastcompdt < compdt):
622
# RFC says nothing about what to do when a given
623
# time is before the first onset date. We'll look for the
624
# first standard component, or the first component, if
626
for comp in self._comps:
632
self._cachedate.insert(0, dt)
633
self._cachecomp.insert(0, lastcomp)
634
if len(self._cachedate) > 10:
635
self._cachedate.pop()
636
self._cachecomp.pop()
639
def utcoffset(self, dt):
640
return self._find_comp(dt).tzoffsetto
643
comp = self._find_comp(dt)
645
return comp.tzoffsetdiff
649
def tzname(self, dt):
650
return self._find_comp(dt).tzname
653
return "<tzicalvtz %s>" % `self._tzid`
656
def __init__(self, fileobj):
659
from dateutil import rrule
661
if isinstance(fileobj, basestring):
663
fileobj = open(fileobj)
664
elif hasattr(fileobj, "name"):
665
self._s = fileobj.name
671
self._parse_rfc(fileobj.read())
674
return self._vtz.keys()
676
def get(self, tzid=None):
678
keys = self._vtz.keys()
680
raise "no timezones defined"
682
raise "more than one timezone available"
684
return self._vtz.get(tzid)
686
def _parse_offset(self, s):
689
raise ValueError, "empty offset"
690
if s[0] in ('+', '-'):
691
signal = (-1,+1)[s[0]=='+']
696
return (int(s[:2])*3600+int(s[2:])*60)*signal
698
return (int(s[:2])*3600+int(s[2:4])*60+int(s[4:]))*signal
700
raise ValueError, "invalid offset: "+s
702
def _parse_rfc(self, s):
703
lines = s.splitlines()
705
raise ValueError, "empty string"
709
while i < len(lines):
710
line = lines[i].rstrip()
713
elif i > 0 and line[0] == " ":
714
lines[i-1] += line[1:]
724
name, value = line.split(':', 1)
725
parms = name.split(';')
727
raise ValueError, "empty property name"
728
name = parms[0].upper()
732
if value in ("STANDARD", "DAYLIGHT"):
736
raise ValueError, "unknown component: "+value
744
if value == "VTIMEZONE":
747
"component not closed: "+comptype
750
"mandatory TZID not found"
753
"at least one component is needed"
755
self._vtz[tzid] = _tzicalvtz(tzid, comps)
757
elif value == comptype:
760
"mandatory DTSTART not found"
763
"mandatory TZOFFSETFROM not found"
766
"mandatory TZOFFSETFROM not found"
770
rr = rrule.rrulestr("\n".join(rrulelines),
774
comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto,
775
(comptype == "DAYLIGHT"),
781
"invalid component end: "+value
783
if name == "DTSTART":
784
rrulelines.append(line)
786
elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"):
787
rrulelines.append(line)
788
elif name == "TZOFFSETFROM":
791
"unsupported %s parm: %s "%(name, parms[0])
792
tzoffsetfrom = self._parse_offset(value)
793
elif name == "TZOFFSETTO":
796
"unsupported TZOFFSETTO parm: "+parms[0]
797
tzoffsetto = self._parse_offset(value)
798
elif name == "TZNAME":
801
"unsupported TZNAME parm: "+parms[0]
803
elif name == "COMMENT":
806
raise ValueError, "unsupported property: "+name
811
"unsupported TZID parm: "+parms[0]
813
elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"):
816
raise ValueError, "unsupported property: "+name
817
elif name == "BEGIN" and value == "VTIMEZONE":
823
return "%s(%s)" % (self.__class__.__name__, `self._s`)
825
TZFILES = ["/etc/localtime", "localtime"]
826
TZPATHS = ["/usr/share/zoneinfo", "/usr/lib/zoneinfo", "/etc/zoneinfo"]
830
def gettz(name=None):
834
name = os.environ["TZ"]
838
for filepath in TZFILES:
839
if not os.path.isabs(filepath):
842
filepath = os.path.join(path, filename)
843
if os.path.isfile(filepath):
847
if os.path.isfile(filepath):
849
tz = tzfile(filepath)
851
except (IOError, OSError, ValueError):
854
if name and name[0] == ":":
857
filepath = os.path.join(path, name)
858
if not os.path.isfile(filepath):
859
filepath = filepath.replace(' ','_')
860
if not os.path.isfile(filepath):
863
tz = tzfile(filepath)
865
except (IOError, OSError, ValueError):
869
# name must have at least one offset to be a tzstr
870
if c in "0123456789":
877
if name in ("GMT", "UTC"):
879
elif name in time.tzname: