~lucio.torre/graphite/add-events

« back to all changes in this revision

Viewing changes to webapp/graphite/thirdparty/pytz/tzinfo.py

  • Committer: Lucio Torre
  • Date: 2011-07-25 18:59:21 UTC
  • mfrom: (299.1.145 graphite)
  • Revision ID: lucio.torre@gmail.com-20110725185921-1f7decdj8pwfpnmn
merged with trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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):
 
79
        '''See datetime.tzinfo.utcoffset'''
 
80
        return self._utcoffset
 
81
 
 
82
    def dst(self,dt):
 
83
        '''See datetime.tzinfo.dst'''
 
84
        return _notime
 
85
 
 
86
    def tzname(self,dt):
 
87
        '''See datetime.tzinfo.tzname'''
 
88
        return self._tzname
 
89
 
 
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)
 
95
 
 
96
    def normalize(self, dt, is_dst=False):
 
97
        '''Correct the timezone information on the given datetime'''
 
98
        if dt.tzinfo is None:
 
99
            raise ValueError, 'Naive time - no tzinfo set'
 
100
        return dt.replace(tzinfo=self)
 
101
 
 
102
    def __repr__(self):
 
103
        return '<StaticTzInfo %r>' % (self.zone,)
 
104
 
 
105
    def __reduce__(self):
 
106
        # Special pickle to zone remains a singleton and to cope with
 
107
        # database changes. 
 
108
        return pytz._p, (self.zone,)
 
109
 
 
110
 
 
111
class DstTzInfo(BaseTzInfo):
 
112
    '''A timezone that has a variable offset from UTC
 
113
 
 
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
 
116
    timezone definition.
 
117
    '''
 
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
 
122
    zone = None
 
123
 
 
124
    # Set in __init__
 
125
    _tzinfos = None
 
126
    _dst = None # DST offset
 
127
 
 
128
    def __init__(self, _inf=None, _tzinfos=None):
 
129
        if _inf:
 
130
            self._tzinfos = _tzinfos
 
131
            self._utcoffset, self._dst, self._tzname = _inf
 
132
        else:
 
133
            _tzinfos = {}
 
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)
 
140
 
 
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])
 
147
 
 
148
    def normalize(self, dt):
 
149
        '''Correct the timezone information on the given datetime
 
150
 
 
151
        If date arithmetic crosses DST boundaries, the tzinfo
 
152
        is not magically adjusted. This method normalizes the
 
153
        tzinfo to the correct one.
 
154
 
 
155
        To test, first we need to do some setup
 
156
 
 
157
        >>> from pytz import timezone
 
158
        >>> utc = timezone('UTC')
 
159
        >>> eastern = timezone('US/Eastern')
 
160
        >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'
 
161
 
 
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.
 
164
 
 
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)'
 
169
 
 
170
        Now, if we subtract a few minutes from it, note that the timezone
 
171
        information has not changed.
 
172
 
 
173
        >>> before = loc_dt - timedelta(minutes=10)
 
174
        >>> before.strftime(fmt)
 
175
        '2002-10-27 00:50:00 EST (-0500)'
 
176
 
 
177
        But we can fix that by calling the normalize method
 
178
 
 
179
        >>> before = eastern.normalize(before)
 
180
        >>> before.strftime(fmt)
 
181
        '2002-10-27 01:50:00 EDT (-0400)'
 
182
        '''
 
183
        if dt.tzinfo is None:
 
184
            raise ValueError, 'Naive time - no tzinfo set'
 
185
 
 
186
        # Convert dt in localtime to UTC
 
187
        offset = dt.tzinfo._utcoffset
 
188
        dt = dt.replace(tzinfo=None)
 
189
        dt = dt - offset
 
190
        # convert it back, and return it
 
191
        return self.fromutc(dt)
 
192
 
 
193
    def localize(self, dt, is_dst=False):
 
194
        '''Convert naive time to local time.
 
195
 
 
196
        This method should be used to construct localtimes, rather
 
197
        than passing a tzinfo argument to a datetime constructor.
 
198
 
 
199
        is_dst is used to determine the correct timezone in the ambigous
 
200
        period at the end of daylight savings time.
 
201
 
 
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)
 
213
        '1:00:00'
 
214
 
 
215
        Use is_dst=None to raise an AmbiguousTimeError for ambiguous
 
216
        times at the end of daylight savings
 
217
 
 
218
        >>> loc_dt1 = amdam.localize(dt, is_dst=None)
 
219
        Traceback (most recent call last):
 
220
            [...]
 
221
        AmbiguousTimeError: 2004-10-31 02:00:00
 
222
 
 
223
        is_dst defaults to False
 
224
 
 
225
        >>> amdam.localize(dt) == amdam.localize(dt, False)
 
226
        True
 
227
 
 
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.
 
230
 
 
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)
 
240
        '1:00:00'
 
241
 
 
242
        Use is_dst=None to raise a NonExistentTimeError for these skipped
 
243
        times.
 
244
 
 
245
        >>> loc_dt1 = pacific.localize(dt, is_dst=None)
 
246
        Traceback (most recent call last):
 
247
            [...]
 
248
        NonExistentTimeError: 2008-03-09 02:00:00
 
249
        '''
 
250
        if dt.tzinfo is not None:
 
251
            raise ValueError, 'Not naive datetime (tzinfo is already set)'
 
252
 
 
253
        # Find the two best possibilities.
 
254
        possible_loc_dt = set()
 
255
        for delta in [timedelta(days=-1), timedelta(days=1)]:
 
256
            loc_dt = dt + delta
 
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)
 
264
 
 
265
        if len(possible_loc_dt) == 1:
 
266
            return possible_loc_dt.pop()
 
267
 
 
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.
 
273
            if is_dst is None:
 
274
                raise NonExistentTimeError(dt)
 
275
 
 
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
 
278
            # hours.
 
279
            elif is_dst:
 
280
                return self.localize(
 
281
                    dt + timedelta(hours=6), is_dst=True) - timedelta(hours=6)
 
282
 
 
283
            # If we are forcing the post-DST side of the DST transition, we
 
284
            # obtain the correct timezone by winding the clock back.
 
285
            else:
 
286
                return self.localize(
 
287
                    dt - timedelta(hours=6), is_dst=False) + timedelta(hours=6)
 
288
 
 
289
 
 
290
        # If we get this far, we have multiple possible timezones - this
 
291
        # is an ambiguous case occuring during the end-of-DST transition.
 
292
 
 
293
        # If told to be strict, raise an exception since we have an
 
294
        # ambiguous case
 
295
        if is_dst is None:
 
296
            raise AmbiguousTimeError(dt)
 
297
 
 
298
        # Filter out the possiblilities that don't match the requested
 
299
        # is_dst
 
300
        filtered_possible_loc_dt = [
 
301
            p for p in possible_loc_dt
 
302
                if bool(p.tzinfo._dst) == is_dst
 
303
            ]
 
304
 
 
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]
 
308
 
 
309
        if len(filtered_possible_loc_dt) == 0:
 
310
            filtered_possible_loc_dt = list(possible_loc_dt)
 
311
 
 
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.
 
318
        #
 
319
        # Choose the earliest (by UTC) applicable timezone.
 
320
        def mycmp(a,b):
 
321
            return cmp(
 
322
                    a.replace(tzinfo=None) - a.tzinfo._utcoffset,
 
323
                    b.replace(tzinfo=None) - b.tzinfo._utcoffset,
 
324
                    )
 
325
        filtered_possible_loc_dt.sort(mycmp)
 
326
        return filtered_possible_loc_dt[0]
 
327
 
 
328
    def utcoffset(self, dt):
 
329
        '''See datetime.tzinfo.utcoffset'''
 
330
        return self._utcoffset
 
331
 
 
332
    def dst(self, dt):
 
333
        '''See datetime.tzinfo.dst'''
 
334
        return self._dst
 
335
 
 
336
    def tzname(self, dt):
 
337
        '''See datetime.tzinfo.tzname'''
 
338
        return self._tzname
 
339
 
 
340
    def __repr__(self):
 
341
        if self._dst:
 
342
            dst = 'DST'
 
343
        else:
 
344
            dst = 'STD'
 
345
        if self._utcoffset > _notime:
 
346
            return '<DstTzInfo %r %s+%s %s>' % (
 
347
                    self.zone, self._tzname, self._utcoffset, dst
 
348
                )
 
349
        else:
 
350
            return '<DstTzInfo %r %s%s %s>' % (
 
351
                    self.zone, self._tzname, self._utcoffset, dst
 
352
                )
 
353
 
 
354
    def __reduce__(self):
 
355
        # Special pickle to zone remains a singleton and to cope with
 
356
        # database changes.
 
357
        return pytz._p, (
 
358
                self.zone,
 
359
                _to_seconds(self._utcoffset),
 
360
                _to_seconds(self._dst),
 
361
                self._tzname
 
362
                )
 
363
 
 
364
 
 
365
class InvalidTimeError(Exception):
 
366
    '''Base class for invalid time exceptions.'''
 
367
 
 
368
 
 
369
class AmbiguousTimeError(InvalidTimeError):
 
370
    '''Exception raised when attempting to create an ambiguous wallclock time.
 
371
 
 
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.
 
375
 
 
376
    See DstTzInfo.normalize() for more info
 
377
    '''
 
378
 
 
379
 
 
380
class NonExistentTimeError(InvalidTimeError):
 
381
    '''Exception raised when attempting to create a wallclock time that
 
382
    cannot exist.
 
383
 
 
384
    At the start of a DST transition period, the wallclock time jumps forward.
 
385
    The instants jumped over never occur.
 
386
    '''
 
387
 
 
388
 
 
389
def unpickler(zone, utcoffset=None, dstoffset=None, tzname=None):
 
390
    """Factory function for unpickling pytz tzinfo instances.
 
391
 
 
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
 
395
    upgrade.
 
396
    """
 
397
    # Raises a KeyError if zone no longer exists, which should never happen
 
398
    # and would be a bug.
 
399
    tz = pytz.timezone(zone)
 
400
 
 
401
    # A StaticTzInfo - just return it
 
402
    if utcoffset is None:
 
403
        return tz
 
404
 
 
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
 
408
    # it correctly.
 
409
    utcoffset = memorized_timedelta(utcoffset)
 
410
    dstoffset = memorized_timedelta(dstoffset)
 
411
    try:
 
412
        return tz._tzinfos[(utcoffset, dstoffset, tzname)]
 
413
    except KeyError:
 
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.
 
419
        pass
 
420
 
 
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):
 
427
            return localized_tz
 
428
 
 
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]
 
438