Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 536 lines (431 sloc) 18.228 kb
47a7f7a @progrium basic form validation, added end time, start of notifications, some f…
progrium authored
1 '''Base classes and helpers for building zone specific tzinfo classes'''
2
3 from datetime import datetime, timedelta, tzinfo
4 from bisect import bisect_right
5 try:
6 set
7 except NameError:
8 from sets import Set as set
9
10 import pytz
11
12 __all__ = []
13
14 _timedelta_cache = {}
15 def memorized_timedelta(seconds):
16 '''Create only one instance of each distinct timedelta'''
17 try:
18 return _timedelta_cache[seconds]
19 except KeyError:
20 delta = timedelta(seconds=seconds)
21 _timedelta_cache[seconds] = delta
22 return delta
23
24 _epoch = datetime.utcfromtimestamp(0)
25 _datetime_cache = {0: _epoch}
26 def memorized_datetime(seconds):
27 '''Create only one instance of each distinct datetime'''
28 try:
29 return _datetime_cache[seconds]
30 except KeyError:
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
35 return dt
36
37 _ttinfo_cache = {}
38 def memorized_ttinfo(*args):
39 '''Create only one instance of each distinct tuple'''
40 try:
41 return _ttinfo_cache[args]
42 except KeyError:
43 ttinfo = (
44 memorized_timedelta(args[0]),
45 memorized_timedelta(args[1]),
46 args[2]
47 )
48 _ttinfo_cache[args] = ttinfo
49 return ttinfo
50
51 _notime = memorized_timedelta(0)
52
53 def _to_seconds(td):
54 '''Convert a timedelta to seconds'''
55 return td.seconds + td.days * 24 * 60 * 60
56
57
58 class BaseTzInfo(tzinfo):
59 # Overridden in subclass
60 _utcoffset = None
61 _tzname = None
62 zone = None
63
64 def __str__(self):
65 return self.zone
66
67
68 class StaticTzInfo(BaseTzInfo):
69 '''A timezone that has a constant offset from UTC
70
71 These timezones are rare, as most locations have changed their
72 offset at some point in their history
73 '''
74 def fromutc(self, dt):
75 '''See datetime.tzinfo.fromutc'''
76 return (dt + self._utcoffset).replace(tzinfo=self)
77
78 def utcoffset(self, dt, is_dst=None):
79 '''See datetime.tzinfo.utcoffset
80
81 is_dst is ignored for StaticTzInfo, and exists only to
82 retain compatibility with DstTzInfo.
83 '''
84 return self._utcoffset
85
86 def dst(self, dt, is_dst=None):
87 '''See datetime.tzinfo.dst
88
89 is_dst is ignored for StaticTzInfo, and exists only to
90 retain compatibility with DstTzInfo.
91 '''
92 return _notime
93
94 def tzname(self, dt, is_dst=None):
95 '''See datetime.tzinfo.tzname
96
97 is_dst is ignored for StaticTzInfo, and exists only to
98 retain compatibility with DstTzInfo.
99 '''
100 return self._tzname
101
102 def localize(self, dt, is_dst=False):
103 '''Convert naive time to local time'''
104 if dt.tzinfo is not None:
105 raise ValueError, 'Not naive datetime (tzinfo is already set)'
106 return dt.replace(tzinfo=self)
107
108 def normalize(self, dt, is_dst=False):
109 '''Correct the timezone information on the given datetime'''
110 if dt.tzinfo is None:
111 raise ValueError, 'Naive time - no tzinfo set'
112 return dt.replace(tzinfo=self)
113
114 def __repr__(self):
115 return '<StaticTzInfo %r>' % (self.zone,)
116
117 def __reduce__(self):
118 # Special pickle to zone remains a singleton and to cope with
119 # database changes.
120 return pytz._p, (self.zone,)
121
122
123 class DstTzInfo(BaseTzInfo):
124 '''A timezone that has a variable offset from UTC
125
126 The offset might change if daylight savings time comes into effect,
127 or at a point in history when the region decides to change their
128 timezone definition.
129 '''
130 # Overridden in subclass
131 _utc_transition_times = None # Sorted list of DST transition times in UTC
132 _transition_info = None # [(utcoffset, dstoffset, tzname)] corresponding
133 # to _utc_transition_times entries
134 zone = None
135
136 # Set in __init__
137 _tzinfos = None
138 _dst = None # DST offset
139
140 def __init__(self, _inf=None, _tzinfos=None):
141 if _inf:
142 self._tzinfos = _tzinfos
143 self._utcoffset, self._dst, self._tzname = _inf
144 else:
145 _tzinfos = {}
146 self._tzinfos = _tzinfos
147 self._utcoffset, self._dst, self._tzname = self._transition_info[0]
148 _tzinfos[self._transition_info[0]] = self
149 for inf in self._transition_info[1:]:
150 if not _tzinfos.has_key(inf):
151 _tzinfos[inf] = self.__class__(inf, _tzinfos)
152
153 def fromutc(self, dt):
154 '''See datetime.tzinfo.fromutc'''
155 dt = dt.replace(tzinfo=None)
156 idx = max(0, bisect_right(self._utc_transition_times, dt) - 1)
157 inf = self._transition_info[idx]
158 return (dt + inf[0]).replace(tzinfo=self._tzinfos[inf])
159
160 def normalize(self, dt):
161 '''Correct the timezone information on the given datetime
162
163 If date arithmetic crosses DST boundaries, the tzinfo
164 is not magically adjusted. This method normalizes the
165 tzinfo to the correct one.
166
167 To test, first we need to do some setup
168
169 >>> from pytz import timezone
170 >>> utc = timezone('UTC')
171 >>> eastern = timezone('US/Eastern')
172 >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'
173
174 We next create a datetime right on an end-of-DST transition point,
175 the instant when the wallclocks are wound back one hour.
176
177 >>> utc_dt = datetime(2002, 10, 27, 6, 0, 0, tzinfo=utc)
178 >>> loc_dt = utc_dt.astimezone(eastern)
179 >>> loc_dt.strftime(fmt)
180 '2002-10-27 01:00:00 EST (-0500)'
181
182 Now, if we subtract a few minutes from it, note that the timezone
183 information has not changed.
184
185 >>> before = loc_dt - timedelta(minutes=10)
186 >>> before.strftime(fmt)
187 '2002-10-27 00:50:00 EST (-0500)'
188
189 But we can fix that by calling the normalize method
190
191 >>> before = eastern.normalize(before)
192 >>> before.strftime(fmt)
193 '2002-10-27 01:50:00 EDT (-0400)'
194 '''
195 if dt.tzinfo is None:
196 raise ValueError, 'Naive time - no tzinfo set'
197
198 # Convert dt in localtime to UTC
199 offset = dt.tzinfo._utcoffset
200 dt = dt.replace(tzinfo=None)
201 dt = dt - offset
202 # convert it back, and return it
203 return self.fromutc(dt)
204
205 def localize(self, dt, is_dst=False):
206 '''Convert naive time to local time.
207
208 This method should be used to construct localtimes, rather
209 than passing a tzinfo argument to a datetime constructor.
210
211 is_dst is used to determine the correct timezone in the ambigous
212 period at the end of daylight savings time.
213
214 >>> from pytz import timezone
215 >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'
216 >>> amdam = timezone('Europe/Amsterdam')
217 >>> dt = datetime(2004, 10, 31, 2, 0, 0)
218 >>> loc_dt1 = amdam.localize(dt, is_dst=True)
219 >>> loc_dt2 = amdam.localize(dt, is_dst=False)
220 >>> loc_dt1.strftime(fmt)
221 '2004-10-31 02:00:00 CEST (+0200)'
222 >>> loc_dt2.strftime(fmt)
223 '2004-10-31 02:00:00 CET (+0100)'
224 >>> str(loc_dt2 - loc_dt1)
225 '1:00:00'
226
227 Use is_dst=None to raise an AmbiguousTimeError for ambiguous
228 times at the end of daylight savings
229
230 >>> loc_dt1 = amdam.localize(dt, is_dst=None)
231 Traceback (most recent call last):
232 [...]
233 AmbiguousTimeError: 2004-10-31 02:00:00
234
235 is_dst defaults to False
236
237 >>> amdam.localize(dt) == amdam.localize(dt, False)
238 True
239
240 is_dst is also used to determine the correct timezone in the
241 wallclock times jumped over at the start of daylight savings time.
242
243 >>> pacific = timezone('US/Pacific')
244 >>> dt = datetime(2008, 3, 9, 2, 0, 0)
245 >>> ploc_dt1 = pacific.localize(dt, is_dst=True)
246 >>> ploc_dt2 = pacific.localize(dt, is_dst=False)
247 >>> ploc_dt1.strftime(fmt)
248 '2008-03-09 02:00:00 PDT (-0700)'
249 >>> ploc_dt2.strftime(fmt)
250 '2008-03-09 02:00:00 PST (-0800)'
251 >>> str(ploc_dt2 - ploc_dt1)
252 '1:00:00'
253
254 Use is_dst=None to raise a NonExistentTimeError for these skipped
255 times.
256
257 >>> loc_dt1 = pacific.localize(dt, is_dst=None)
258 Traceback (most recent call last):
259 [...]
260 NonExistentTimeError: 2008-03-09 02:00:00
261 '''
262 if dt.tzinfo is not None:
263 raise ValueError, 'Not naive datetime (tzinfo is already set)'
264
265 # Find the two best possibilities.
266 possible_loc_dt = set()
267 for delta in [timedelta(days=-1), timedelta(days=1)]:
268 loc_dt = dt + delta
269 idx = max(0, bisect_right(
270 self._utc_transition_times, loc_dt) - 1)
271 inf = self._transition_info[idx]
272 tzinfo = self._tzinfos[inf]
273 loc_dt = tzinfo.normalize(dt.replace(tzinfo=tzinfo))
274 if loc_dt.replace(tzinfo=None) == dt:
275 possible_loc_dt.add(loc_dt)
276
277 if len(possible_loc_dt) == 1:
278 return possible_loc_dt.pop()
279
280 # If there are no possibly correct timezones, we are attempting
281 # to convert a time that never happened - the time period jumped
282 # during the start-of-DST transition period.
283 if len(possible_loc_dt) == 0:
284 # If we refuse to guess, raise an exception.
285 if is_dst is None:
286 raise NonExistentTimeError(dt)
287
288 # If we are forcing the pre-DST side of the DST transition, we
289 # obtain the correct timezone by winding the clock forward a few
290 # hours.
291 elif is_dst:
292 return self.localize(
293 dt + timedelta(hours=6), is_dst=True) - timedelta(hours=6)
294
295 # If we are forcing the post-DST side of the DST transition, we
296 # obtain the correct timezone by winding the clock back.
297 else:
298 return self.localize(
299 dt - timedelta(hours=6), is_dst=False) + timedelta(hours=6)
300
301
302 # If we get this far, we have multiple possible timezones - this
303 # is an ambiguous case occuring during the end-of-DST transition.
304
305 # If told to be strict, raise an exception since we have an
306 # ambiguous case
307 if is_dst is None:
308 raise AmbiguousTimeError(dt)
309
310 # Filter out the possiblilities that don't match the requested
311 # is_dst
312 filtered_possible_loc_dt = [
313 p for p in possible_loc_dt
314 if bool(p.tzinfo._dst) == is_dst
315 ]
316
317 # Hopefully we only have one possibility left. Return it.
318 if len(filtered_possible_loc_dt) == 1:
319 return filtered_possible_loc_dt[0]
320
321 if len(filtered_possible_loc_dt) == 0:
322 filtered_possible_loc_dt = list(possible_loc_dt)
323
324 # If we get this far, we have in a wierd timezone transition
325 # where the clocks have been wound back but is_dst is the same
326 # in both (eg. Europe/Warsaw 1915 when they switched to CET).
327 # At this point, we just have to guess unless we allow more
328 # hints to be passed in (such as the UTC offset or abbreviation),
329 # but that is just getting silly.
330 #
331 # Choose the earliest (by UTC) applicable timezone.
332 def mycmp(a,b):
333 return cmp(
334 a.replace(tzinfo=None) - a.tzinfo._utcoffset,
335 b.replace(tzinfo=None) - b.tzinfo._utcoffset,
336 )
337 filtered_possible_loc_dt.sort(mycmp)
338 return filtered_possible_loc_dt[0]
339
340 def utcoffset(self, dt, is_dst=None):
341 '''See datetime.tzinfo.utcoffset
342
343 The is_dst parameter may be used to remove ambiguity during DST
344 transitions.
345
346 >>> from pytz import timezone
347 >>> tz = timezone('America/St_Johns')
348 >>> ambiguous = datetime(2009, 10, 31, 23, 30)
349
350 >>> tz.utcoffset(ambiguous, is_dst=False)
351 datetime.timedelta(-1, 73800)
352
353 >>> tz.utcoffset(ambiguous, is_dst=True)
354 datetime.timedelta(-1, 77400)
355
356 >>> tz.utcoffset(ambiguous)
357 Traceback (most recent call last):
358 [...]
359 AmbiguousTimeError: 2009-10-31 23:30:00
360 '''
361 if dt.tzinfo is not self:
362 dt = self.localize(dt, is_dst)
363 return dt.tzinfo._utcoffset
364 else:
365 return self._utcoffset
366
367 def dst(self, dt, is_dst=None):
368 '''See datetime.tzinfo.dst
369
370 The is_dst parameter may be used to remove ambiguity during DST
371 transitions.
372
373 >>> from pytz import timezone
374 >>> tz = timezone('America/St_Johns')
375
376 >>> normal = datetime(2009, 9, 1)
377
378 >>> tz.dst(normal)
379 datetime.timedelta(0, 3600)
380 >>> tz.dst(normal, is_dst=False)
381 datetime.timedelta(0, 3600)
382 >>> tz.dst(normal, is_dst=True)
383 datetime.timedelta(0, 3600)
384
385 >>> ambiguous = datetime(2009, 10, 31, 23, 30)
386
387 >>> tz.dst(ambiguous, is_dst=False)
388 datetime.timedelta(0)
389 >>> tz.dst(ambiguous, is_dst=True)
390 datetime.timedelta(0, 3600)
391 >>> tz.dst(ambiguous)
392 Traceback (most recent call last):
393 [...]
394 AmbiguousTimeError: 2009-10-31 23:30:00
395 '''
396 if dt.tzinfo is not self:
397 dt = self.localize(dt, is_dst)
398 return dt.tzinfo._dst
399 else:
400 return self._dst
401
402 def tzname(self, dt, is_dst=None):
403 '''See datetime.tzinfo.tzname
404
405 The is_dst parameter may be used to remove ambiguity during DST
406 transitions.
407
408 >>> from pytz import timezone
409 >>> tz = timezone('America/St_Johns')
410
411 >>> normal = datetime(2009, 9, 1)
412
413 >>> tz.tzname(normal)
414 'NDT'
415 >>> tz.tzname(normal, is_dst=False)
416 'NDT'
417 >>> tz.tzname(normal, is_dst=True)
418 'NDT'
419
420 >>> ambiguous = datetime(2009, 10, 31, 23, 30)
421
422 >>> tz.tzname(ambiguous, is_dst=False)
423 'NST'
424 >>> tz.tzname(ambiguous, is_dst=True)
425 'NDT'
426 >>> tz.tzname(ambiguous)
427 Traceback (most recent call last):
428 [...]
429 AmbiguousTimeError: 2009-10-31 23:30:00
430 '''
431 if dt.tzinfo is not self:
432 dt = self.localize(dt, is_dst)
433 return dt.tzinfo._tzname
434 else:
435 return self._tzname
436
437 def __repr__(self):
438 if self._dst:
439 dst = 'DST'
440 else:
441 dst = 'STD'
442 if self._utcoffset > _notime:
443 return '<DstTzInfo %r %s+%s %s>' % (
444 self.zone, self._tzname, self._utcoffset, dst
445 )
446 else:
447 return '<DstTzInfo %r %s%s %s>' % (
448 self.zone, self._tzname, self._utcoffset, dst
449 )
450
451 def __reduce__(self):
452 # Special pickle to zone remains a singleton and to cope with
453 # database changes.
454 return pytz._p, (
455 self.zone,
456 _to_seconds(self._utcoffset),
457 _to_seconds(self._dst),
458 self._tzname
459 )
460
461
462 class InvalidTimeError(Exception):
463 '''Base class for invalid time exceptions.'''
464
465
466 class AmbiguousTimeError(InvalidTimeError):
467 '''Exception raised when attempting to create an ambiguous wallclock time.
468
469 At the end of a DST transition period, a particular wallclock time will
470 occur twice (once before the clocks are set back, once after). Both
471 possibilities may be correct, unless further information is supplied.
472
473 See DstTzInfo.normalize() for more info
474 '''
475
476
477 class NonExistentTimeError(InvalidTimeError):
478 '''Exception raised when attempting to create a wallclock time that
479 cannot exist.
480
481 At the start of a DST transition period, the wallclock time jumps forward.
482 The instants jumped over never occur.
483 '''
484
485
486 def unpickler(zone, utcoffset=None, dstoffset=None, tzname=None):
487 """Factory function for unpickling pytz tzinfo instances.
488
489 This is shared for both StaticTzInfo and DstTzInfo instances, because
490 database changes could cause a zones implementation to switch between
491 these two base classes and we can't break pickles on a pytz version
492 upgrade.
493 """
494 # Raises a KeyError if zone no longer exists, which should never happen
495 # and would be a bug.
496 tz = pytz.timezone(zone)
497
498 # A StaticTzInfo - just return it
499 if utcoffset is None:
500 return tz
501
502 # This pickle was created from a DstTzInfo. We need to
503 # determine which of the list of tzinfo instances for this zone
504 # to use in order to restore the state of any datetime instances using
505 # it correctly.
506 utcoffset = memorized_timedelta(utcoffset)
507 dstoffset = memorized_timedelta(dstoffset)
508 try:
509 return tz._tzinfos[(utcoffset, dstoffset, tzname)]
510 except KeyError:
511 # The particular state requested in this timezone no longer exists.
512 # This indicates a corrupt pickle, or the timezone database has been
513 # corrected violently enough to make this particular
514 # (utcoffset,dstoffset) no longer exist in the zone, or the
515 # abbreviation has been changed.
516 pass
517
518 # See if we can find an entry differing only by tzname. Abbreviations
519 # get changed from the initial guess by the database maintainers to
520 # match reality when this information is discovered.
521 for localized_tz in tz._tzinfos.values():
522 if (localized_tz._utcoffset == utcoffset
523 and localized_tz._dst == dstoffset):
524 return localized_tz
525
526 # This (utcoffset, dstoffset) information has been removed from the
527 # zone. Add it back. This might occur when the database maintainers have
528 # corrected incorrect information. datetime instances using this
529 # incorrect information will continue to do so, exactly as they were
530 # before being pickled. This is purely an overly paranoid safety net - I
531 # doubt this will ever been needed in real life.
532 inf = (utcoffset, dstoffset, tzname)
533 tz._tzinfos[inf] = tz.__class__(inf, tz._tzinfos)
534 return tz._tzinfos[inf]
535
Something went wrong with that request. Please try again.