1
# Copyright (c) 2008, Red Innovation Ltd., Finland
4
# Redistribution and use in source and binary forms, with or without
5
# modification, are permitted provided that the following conditions are met:
6
# * Redistributions of source code must retain the above copyright
7
# notice, this list of conditions and the following disclaimer.
8
# * Redistributions in binary form must reproduce the above copyright
9
# notice, this list of conditions and the following disclaimer in the
10
# documentation and/or other materials provided with the distribution.
11
# * Neither the name of Red Innovation nor the names of its contributors
12
# may be used to endorse or promote products derived from this software
13
# without specific prior written permission.
15
# THIS SOFTWARE IS PROVIDED BY RED INNOVATION ``AS IS'' AND ANY
16
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
# DISCLAIMED. IN NO EVENT SHALL RED INNOVATION BE LIABLE FOR ANY
19
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
This module provides monkey-patched Python datetime class
28
that fully supports different time zones and conversions
31
See the source for licensing terms.
34
__author__ = 'Antti Haapala <antti@redinnovation.com>'
35
__date__ = '24 Jun 2008'
36
__version__ = '$Revision$'
37
__copyright__ = '2008 Red Innovation Ltd.'
38
__license__ = '3-clause BSD'
40
from datetime import datetime as _datetime, tzinfo as _tzinfo
41
from pytz import timezone as _timezone
47
from pytz import utc, UTC, HOUR, ZERO
53
def set_default_timezone(new_tz):
54
"""Sets the default time zone used by the objects
55
contained in this module. new_tz may be either
56
a pytz-compatible tzinfo (requires normalize
57
and localize methods), or a time zone name known
62
if type(new_tz) is str or type(new_tz) is unicode:
63
new_tz = _pytz.timezone(new_tz)
67
class FixedOffset(_tzinfo):
68
"""Fixed offset in minutes east from UTC. Based on
69
the python tutorial and pytz test code."""
71
def __init__(self, offset, name):
72
"""Constructor. Create a new tzinfo object
73
with given offset in minutes and name."""
74
self.__offset = timedelta(minutes = offset)
77
def utcoffset(self, dt):
86
def localize(self, dt, is_dst=False):
87
"""Convert naive time to local time. Copied
88
from pytz tzinfo classes"""
90
if dt.tzinfo is not None:
91
raise ValueError, 'Not naive datetime (tzinfo is already set)'
93
return dt.replace(tzinfo=self)
95
def normalize(self, dt, is_dst=False):
96
"""Correct the timezone information on the
97
given datetime. Copied from pytz tzinfo classes."""
100
raise ValueError, 'Naive time - no tzinfo set'
102
return dt.replace(tzinfo=self)
108
return '<%s>' % self.__name
111
_fixed_offset_tzs = { }
113
def _get_fixed_offset_tz(offsetmins):
114
"""For internal use only: Returns a tzinfo with
115
the given fixed offset. This creates only one instance
116
for each offset; the zones are kept in a dictionary"""
121
if not _fixed_offset_tzs.has_key(offsetmins):
129
name = "UTC%s%02d:%02d" % (sign, int(absoff / 60), absoff % 60)
130
inst = FixedOffset(offsetmins, name)
131
_fixed_offset_tzs[offsetmins] = inst
133
return _fixed_offset_tzs[offsetmins]
136
_iso8601_parser = _re.compile("""
138
(?P<year> [0-9]{4})(?P<ymdsep>-?)
139
(?P<month>[0-9]{2})(?P=ymdsep)
142
(?: # time part... optional... at least hour must be specified
146
# minutes, separated with :, or none, from hours
150
# same for seconds, separated with :, or none, from hours
157
(?: [,.] (?P<frac>[0-9]{1,10}))?
159
# timezone, Z, +-hh or +-hh:?mm. MUST BE, but complain if not there.
163
(?P<tzh>[+-][0-9]{2})
164
(?: :? # optional separator
172
def _parse_iso(timestamp):
173
"""Internal function for parsing a timestamp in
176
timestamp = timestamp.strip()
178
m = _iso8601_parser.match(timestamp)
180
raise ValueError("Not a proper ISO 8601 timestamp!")
182
year = int(m.group('year'))
183
month = int(m.group('month'))
184
day = int(m.group('day'))
186
h, min, s, us = None, None, None, 0
188
if m.group('tzempty') == None and m.group('tzh') == None:
189
raise ValueError("Not a proper ISO 8601 timestamp: " +
190
"missing timezone (Z or +hh[:mm])!")
193
frac = m.group('frac')
195
frac = long(frac) / 10.0 ** power
198
h = int(m.group('hour'))
200
if m.group('minute'):
201
min = int(m.group('minute'))
203
if m.group('second'):
204
s = int(m.group('second'))
207
# ok, fractions of hour?
209
frac, min = _math.modf(frac * 60.0)
212
# fractions of second?
214
frac, s = _math.modf(frac * 60.0)
217
# and extract microseconds...
218
us = int(frac * 1000000)
220
if m.group('tzempty') == 'Z':
223
# timezone: hour diff with sign
224
offsetmins = int(m.group('tzh')) * 60
227
# add optional minutes
230
offsetmins += tzm if offsetmins > 0 else -tzm
232
tz = _get_fixed_offset_tz(offsetmins)
233
return datetime(year, month, day, h, min, s, us, tz)
236
class datetime(_datetime):
237
"""Time zone aware subclass of Python datetime.datetime"""
239
__name__ = 'fixed_datetime.datetime'
241
def __new__(cls, year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, is_dst=False):
242
"""Creates a localized timestamp with the given parameters.
243
If tzinfo is omitted, the default time zone will be used."""
248
dt = _datetime(year, month, day, hour, minute, second, microsecond)
249
dt = tzinfo.localize(dt, is_dst=is_dst)
250
return _datetime.__new__(
251
cls, dt.year, dt.month, dt.day,
252
dt.hour, dt.minute, dt.second,
253
dt.microsecond, dt.tzinfo)
255
def __radd__(self, addend):
256
"""Autonormalized addition of datetimes and timedeltas."""
258
added = _datetime.__radd__(self, addend)
259
added = self.tzinfo.normalize(added)
260
return datetime.__from_datetime_with_tz(added)
262
def __add__(self, addend):
263
"""Autonormalized addition of datetimes and timedeltas."""
265
added = _datetime.__add__(self, addend)
266
added = self.tzinfo.normalize(added)
267
return datetime.__from_datetime_with_tz(added)
269
def utctimetuple(self):
270
"""Return UTC time tuple, compatible with time.gmtime().
272
Notice: the original datetime documentation is misleading:
273
Calling utctimetuple() on a timezone-aware datetime will
274
return the tuple in UTC, not in local time."""
275
return _datetime.utctimetuple(self)
277
def astimezone(self, tzinfo):
278
"""Convert to local time in new timezone tz.
280
The result is normalized across DST boundaries."""
282
dt = _datetime.astimezone(self, tzinfo)
283
dt = tzinfo.normalize(dt)
284
return datetime.__from_datetime_with_tz(dt)
287
def __from_datetime_with_tz(dt):
288
"""Internal: create a datetime instance from
289
a timezone-aware instance of the builtin datetime type."""
291
if dt.tzinfo == None:
292
raise ValueError("The given datetime.datetime is not timezone-aware!")
294
return datetime(dt.year, dt.month, dt.day,
295
dt.hour, dt.minute, dt.second, dt.microsecond,
299
def fromtimestamp(timestamp, tz=None):
300
"""Tz's local time from POSIX timestamp."""
302
bd = _time.gmtime(long(timestamp))
305
if isinstance(timestamp, float):
310
args += [ int(us), _utc ]
312
_tmp = datetime(*args)
317
rv = _tmp.astimezone(tz)
318
return datetime.__from_datetime_with_tz(rv)
322
"""New datetime with tz's local day and time
323
If tz is not specified, use the default timezone"""
325
return datetime.fromtimestamp(long(_time.time()), tz)
329
"""New datetime with tz's local day and time
330
If tz is not specified, use the default timezone"""
332
return datetime.fromtimestamp(_time.time(), tz)
336
"""Return a new datetime representing UTC day and time."""
338
return datetime.now(tz=_utc)
341
def utcfromtimestamp():
342
"""Return a new UTC datetime from a POSIX timestamp (like time.time())."""
344
return datetime.utcfromtimestamp(tz=_utc)
347
def parseisoformat(timestamp):
348
"""Parses the given ISO 8601 compatible timestamp string
349
and converts it to fixed_datetime.datetime. The timestamp
350
must conform to following formats:
352
- the format is DATE SEP TIME TIMEZONE without
353
any intervening spaces.
355
- the date must be in format YYYY-MM-DD
357
- the time may be either
361
FFFF is the fractional part. Decimal point can be
364
- the time zone must be either Z, -HH:MM or +HH:MM
366
- the date and time must be separated either by
367
whitespace or single T letter
369
- the separators - and : may all be omitted, or
372
Examples (Unix Epoch):
376
1969-12-31 19,5-04:30
380
return _parse_iso(timestamp)
382
def isoformat(self, sep='T', short=False):
383
"""Returns the date represented by this instance
384
in ISO 8601 conforming format. The separator
385
is used to separate the date and time, and defaults
386
to 'T'. This method supports both long and short
387
formats. The long format is
389
YYYY-MM-DD HH:MM:SS[.FFFFFF]=HH:MM
393
YYYYMMDDTHHMMSS[.FFFFFF]=HHMM
395
The fractional part is separated with decimal point and
396
is omitted if microseconds stored in this datetime are
401
return _datetime.isoformat(self, sep)
403
args = [ self.year, self.month, self.day, self.hour, self.minute, self.second ]
405
fmt = "%04d%02d%02dT%02d%02d%02d"
408
if self.microsecond != 0:
410
args += [ self.microsecond ]
412
offset = self.tzinfo.utcoffset(self)
413
tzseconds = offset.seconds + offset.days * 24 * 60 * 60
414
sign = '+' if tzseconds >= 0 else '-'
415
tzseconds = abs(tzseconds)
416
tzout = tz % (sign, int(tzseconds / 3600), int((tzseconds / 60) % 60))
418
dtout = fmt % tuple(args)
421
from datetime import date, timedelta, time, tzinfo, MAXYEAR, MINYEAR