1
'''Base classes and helpers for building zone specific tzinfo classes'''
3
from datetime import datetime, timedelta, tzinfo
4
from bisect import bisect_right
8
from sets import Set as set
15
def memorized_timedelta(seconds):
16
'''Create only one instance of each distinct timedelta'''
18
return _timedelta_cache[seconds]
20
delta = timedelta(seconds=seconds)
21
_timedelta_cache[seconds] = delta
24
_epoch = datetime.utcfromtimestamp(0)
25
_datetime_cache = {0: _epoch}
26
def memorized_datetime(seconds):
27
'''Create only one instance of each distinct datetime'''
29
return _datetime_cache[seconds]
31
# NB. We can't just do datetime.utcfromtimestamp(seconds) as this
32
# fails with negative values under Windows (Bug #90096)
33
dt = _epoch + timedelta(seconds=seconds)
34
_datetime_cache[seconds] = dt
38
def memorized_ttinfo(*args):
39
'''Create only one instance of each distinct tuple'''
41
return _ttinfo_cache[args]
44
memorized_timedelta(args[0]),
45
memorized_timedelta(args[1]),
48
_ttinfo_cache[args] = ttinfo
51
_notime = memorized_timedelta(0)
54
'''Convert a timedelta to seconds'''
55
return td.seconds + td.days * 24 * 60 * 60
58
class BaseTzInfo(tzinfo):
59
# Overridden in subclass
68
class StaticTzInfo(BaseTzInfo):
69
'''A timezone that has a constant offset from UTC
71
These timezones are rare, as most locations have changed their
72
offset at some point in their history
74
def fromutc(self, dt):
75
'''See datetime.tzinfo.fromutc'''
76
return (dt + self._utcoffset).replace(tzinfo=self)
78
def utcoffset(self,dt):
79
'''See datetime.tzinfo.utcoffset'''
80
return self._utcoffset
83
'''See datetime.tzinfo.dst'''
87
'''See datetime.tzinfo.tzname'''
90
def localize(self, dt, is_dst=False):
91
'''Convert naive time to local time'''
92
if dt.tzinfo is not None:
93
raise ValueError, 'Not naive datetime (tzinfo is already set)'
94
return dt.replace(tzinfo=self)
96
def normalize(self, dt, is_dst=False):
97
'''Correct the timezone information on the given datetime'''
99
raise ValueError, 'Naive time - no tzinfo set'
100
return dt.replace(tzinfo=self)
103
return '<StaticTzInfo %r>' % (self.zone,)
105
def __reduce__(self):
106
# Special pickle to zone remains a singleton and to cope with
108
return pytz._p, (self.zone,)
111
class DstTzInfo(BaseTzInfo):
112
'''A timezone that has a variable offset from UTC
114
The offset might change if daylight savings time comes into effect,
115
or at a point in history when the region decides to change their
118
# Overridden in subclass
119
_utc_transition_times = None # Sorted list of DST transition times in UTC
120
_transition_info = None # [(utcoffset, dstoffset, tzname)] corresponding
121
# to _utc_transition_times entries
126
_dst = None # DST offset
128
def __init__(self, _inf=None, _tzinfos=None):
130
self._tzinfos = _tzinfos
131
self._utcoffset, self._dst, self._tzname = _inf
134
self._tzinfos = _tzinfos
135
self._utcoffset, self._dst, self._tzname = self._transition_info[0]
136
_tzinfos[self._transition_info[0]] = self
137
for inf in self._transition_info[1:]:
138
if not _tzinfos.has_key(inf):
139
_tzinfos[inf] = self.__class__(inf, _tzinfos)
141
def fromutc(self, dt):
142
'''See datetime.tzinfo.fromutc'''
143
dt = dt.replace(tzinfo=None)
144
idx = max(0, bisect_right(self._utc_transition_times, dt) - 1)
145
inf = self._transition_info[idx]
146
return (dt + inf[0]).replace(tzinfo=self._tzinfos[inf])
148
def normalize(self, dt):
149
'''Correct the timezone information on the given datetime
151
If date arithmetic crosses DST boundaries, the tzinfo
152
is not magically adjusted. This method normalizes the
153
tzinfo to the correct one.
155
To test, first we need to do some setup
157
>>> from pytz import timezone
158
>>> utc = timezone('UTC')
159
>>> eastern = timezone('US/Eastern')
160
>>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'
162
We next create a datetime right on an end-of-DST transition point,
163
the instant when the wallclocks are wound back one hour.
165
>>> utc_dt = datetime(2002, 10, 27, 6, 0, 0, tzinfo=utc)
166
>>> loc_dt = utc_dt.astimezone(eastern)
167
>>> loc_dt.strftime(fmt)
168
'2002-10-27 01:00:00 EST (-0500)'
170
Now, if we subtract a few minutes from it, note that the timezone
171
information has not changed.
173
>>> before = loc_dt - timedelta(minutes=10)
174
>>> before.strftime(fmt)
175
'2002-10-27 00:50:00 EST (-0500)'
177
But we can fix that by calling the normalize method
179
>>> before = eastern.normalize(before)
180
>>> before.strftime(fmt)
181
'2002-10-27 01:50:00 EDT (-0400)'
183
if dt.tzinfo is None:
184
raise ValueError, 'Naive time - no tzinfo set'
186
# Convert dt in localtime to UTC
187
offset = dt.tzinfo._utcoffset
188
dt = dt.replace(tzinfo=None)
190
# convert it back, and return it
191
return self.fromutc(dt)
193
def localize(self, dt, is_dst=False):
194
'''Convert naive time to local time.
196
This method should be used to construct localtimes, rather
197
than passing a tzinfo argument to a datetime constructor.
199
is_dst is used to determine the correct timezone in the ambigous
200
period at the end of daylight savings time.
202
>>> from pytz import timezone
203
>>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'
204
>>> amdam = timezone('Europe/Amsterdam')
205
>>> dt = datetime(2004, 10, 31, 2, 0, 0)
206
>>> loc_dt1 = amdam.localize(dt, is_dst=True)
207
>>> loc_dt2 = amdam.localize(dt, is_dst=False)
208
>>> loc_dt1.strftime(fmt)
209
'2004-10-31 02:00:00 CEST (+0200)'
210
>>> loc_dt2.strftime(fmt)
211
'2004-10-31 02:00:00 CET (+0100)'
212
>>> str(loc_dt2 - loc_dt1)
215
Use is_dst=None to raise an AmbiguousTimeError for ambiguous
216
times at the end of daylight savings
218
>>> loc_dt1 = amdam.localize(dt, is_dst=None)
219
Traceback (most recent call last):
221
AmbiguousTimeError: 2004-10-31 02:00:00
223
is_dst defaults to False
225
>>> amdam.localize(dt) == amdam.localize(dt, False)
228
is_dst is also used to determine the correct timezone in the
229
wallclock times jumped over at the start of daylight savings time.
231
>>> pacific = timezone('US/Pacific')
232
>>> dt = datetime(2008, 3, 9, 2, 0, 0)
233
>>> ploc_dt1 = pacific.localize(dt, is_dst=True)
234
>>> ploc_dt2 = pacific.localize(dt, is_dst=False)
235
>>> ploc_dt1.strftime(fmt)
236
'2008-03-09 02:00:00 PDT (-0700)'
237
>>> ploc_dt2.strftime(fmt)
238
'2008-03-09 02:00:00 PST (-0800)'
239
>>> str(ploc_dt2 - ploc_dt1)
242
Use is_dst=None to raise a NonExistentTimeError for these skipped
245
>>> loc_dt1 = pacific.localize(dt, is_dst=None)
246
Traceback (most recent call last):
248
NonExistentTimeError: 2008-03-09 02:00:00
250
if dt.tzinfo is not None:
251
raise ValueError, 'Not naive datetime (tzinfo is already set)'
253
# Find the two best possibilities.
254
possible_loc_dt = set()
255
for delta in [timedelta(days=-1), timedelta(days=1)]:
257
idx = max(0, bisect_right(
258
self._utc_transition_times, loc_dt) - 1)
259
inf = self._transition_info[idx]
260
tzinfo = self._tzinfos[inf]
261
loc_dt = tzinfo.normalize(dt.replace(tzinfo=tzinfo))
262
if loc_dt.replace(tzinfo=None) == dt:
263
possible_loc_dt.add(loc_dt)
265
if len(possible_loc_dt) == 1:
266
return possible_loc_dt.pop()
268
# If there are no possibly correct timezones, we are attempting
269
# to convert a time that never happened - the time period jumped
270
# during the start-of-DST transition period.
271
if len(possible_loc_dt) == 0:
272
# If we refuse to guess, raise an exception.
274
raise NonExistentTimeError(dt)
276
# If we are forcing the pre-DST side of the DST transition, we
277
# obtain the correct timezone by winding the clock forward a few
280
return self.localize(
281
dt + timedelta(hours=6), is_dst=True) - timedelta(hours=6)
283
# If we are forcing the post-DST side of the DST transition, we
284
# obtain the correct timezone by winding the clock back.
286
return self.localize(
287
dt - timedelta(hours=6), is_dst=False) + timedelta(hours=6)
290
# If we get this far, we have multiple possible timezones - this
291
# is an ambiguous case occuring during the end-of-DST transition.
293
# If told to be strict, raise an exception since we have an
296
raise AmbiguousTimeError(dt)
298
# Filter out the possiblilities that don't match the requested
300
filtered_possible_loc_dt = [
301
p for p in possible_loc_dt
302
if bool(p.tzinfo._dst) == is_dst
305
# Hopefully we only have one possibility left. Return it.
306
if len(filtered_possible_loc_dt) == 1:
307
return filtered_possible_loc_dt[0]
309
if len(filtered_possible_loc_dt) == 0:
310
filtered_possible_loc_dt = list(possible_loc_dt)
312
# If we get this far, we have in a wierd timezone transition
313
# where the clocks have been wound back but is_dst is the same
314
# in both (eg. Europe/Warsaw 1915 when they switched to CET).
315
# At this point, we just have to guess unless we allow more
316
# hints to be passed in (such as the UTC offset or abbreviation),
317
# but that is just getting silly.
319
# Choose the earliest (by UTC) applicable timezone.
322
a.replace(tzinfo=None) - a.tzinfo._utcoffset,
323
b.replace(tzinfo=None) - b.tzinfo._utcoffset,
325
filtered_possible_loc_dt.sort(mycmp)
326
return filtered_possible_loc_dt[0]
328
def utcoffset(self, dt):
329
'''See datetime.tzinfo.utcoffset'''
330
return self._utcoffset
333
'''See datetime.tzinfo.dst'''
336
def tzname(self, dt):
337
'''See datetime.tzinfo.tzname'''
345
if self._utcoffset > _notime:
346
return '<DstTzInfo %r %s+%s %s>' % (
347
self.zone, self._tzname, self._utcoffset, dst
350
return '<DstTzInfo %r %s%s %s>' % (
351
self.zone, self._tzname, self._utcoffset, dst
354
def __reduce__(self):
355
# Special pickle to zone remains a singleton and to cope with
359
_to_seconds(self._utcoffset),
360
_to_seconds(self._dst),
365
class InvalidTimeError(Exception):
366
'''Base class for invalid time exceptions.'''
369
class AmbiguousTimeError(InvalidTimeError):
370
'''Exception raised when attempting to create an ambiguous wallclock time.
372
At the end of a DST transition period, a particular wallclock time will
373
occur twice (once before the clocks are set back, once after). Both
374
possibilities may be correct, unless further information is supplied.
376
See DstTzInfo.normalize() for more info
380
class NonExistentTimeError(InvalidTimeError):
381
'''Exception raised when attempting to create a wallclock time that
384
At the start of a DST transition period, the wallclock time jumps forward.
385
The instants jumped over never occur.
389
def unpickler(zone, utcoffset=None, dstoffset=None, tzname=None):
390
"""Factory function for unpickling pytz tzinfo instances.
392
This is shared for both StaticTzInfo and DstTzInfo instances, because
393
database changes could cause a zones implementation to switch between
394
these two base classes and we can't break pickles on a pytz version
397
# Raises a KeyError if zone no longer exists, which should never happen
398
# and would be a bug.
399
tz = pytz.timezone(zone)
401
# A StaticTzInfo - just return it
402
if utcoffset is None:
405
# This pickle was created from a DstTzInfo. We need to
406
# determine which of the list of tzinfo instances for this zone
407
# to use in order to restore the state of any datetime instances using
409
utcoffset = memorized_timedelta(utcoffset)
410
dstoffset = memorized_timedelta(dstoffset)
412
return tz._tzinfos[(utcoffset, dstoffset, tzname)]
414
# The particular state requested in this timezone no longer exists.
415
# This indicates a corrupt pickle, or the timezone database has been
416
# corrected violently enough to make this particular
417
# (utcoffset,dstoffset) no longer exist in the zone, or the
418
# abbreviation has been changed.
421
# See if we can find an entry differing only by tzname. Abbreviations
422
# get changed from the initial guess by the database maintainers to
423
# match reality when this information is discovered.
424
for localized_tz in tz._tzinfos.values():
425
if (localized_tz._utcoffset == utcoffset
426
and localized_tz._dst == dstoffset):
429
# This (utcoffset, dstoffset) information has been removed from the
430
# zone. Add it back. This might occur when the database maintainers have
431
# corrected incorrect information. datetime instances using this
432
# incorrect information will continue to do so, exactly as they were
433
# before being pickled. This is purely an overly paranoid safety net - I
434
# doubt this will ever been needed in real life.
435
inf = (utcoffset, dstoffset, tzname)
436
tz._tzinfos[inf] = tz.__class__(inf, tz._tzinfos)
437
return tz._tzinfos[inf]