1
# friends-service -- send & receive messages from any social network
2
# Copyright (C) 2012 Canonical Ltd
4
# This program is free software: you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation, version 3 of the License.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program. If not, see <http://www.gnu.org/licenses/>.
28
from calendar import timegm
29
from contextlib import contextmanager
30
from datetime import datetime, timedelta
33
# Date time formats. Assume no microseconds and no timezone.
34
ISO8601_FORMAT = '%Y-%m-%dT%H:%M:%S'
35
TWITTER_FORMAT = '%a %b %d %H:%M:%S %Y'
36
IDENTICA_FORMAT = '%a, %d %b %Y %H:%M:%S'
41
locale.setlocale(locale.LC_TIME, 'C')
45
locale.setlocale(locale.LC_TIME, '')
49
return datetime.strptime(t, ISO8601_FORMAT)
52
def _from_iso8601alt(t):
53
return datetime.strptime(t, ISO8601_FORMAT.replace('T', ' '))
57
return datetime.strptime(t, TWITTER_FORMAT)
60
def _from_identica(t):
61
return datetime.strptime(t, IDENTICA_FORMAT)
64
def _fromutctimestamp(t):
65
return datetime.utcfromtimestamp(float(t))
68
PARSERS = (_from_iso8601, _from_iso8601alt, _from_twitter,
69
_from_identica, _fromutctimestamp)
73
"""Parse an ISO 8601 datetime string and return seconds since epoch.
75
This accepts either a naive (i.e. timezone-less) string or a timezone
76
aware string. The timezone must start with a + or - and must be followed
77
by exactly four digits. This string is parsed and converted to UTC. This
78
value is then converted to an integer seconds since epoch.
81
# In Python 3.2, strptime() is implemented in Python, so in order to
82
# parse the UTC timezone (e.g. +0000), you'd think we could just
83
# append %z on the format. We can't rely on it though because of the
84
# non-ISO 8601 formats that some APIs use (I'm looking at you Twitter
85
# and Facebook). We'll use a regular expression to tear out the
86
# timezone string and do the conversion ourselves.
88
def capture_tz(match_object):
90
tz_string = match_object.group('tz')
91
if tz_string is not None:
92
# It's possible that we'll see more than one substring
93
# matching the timezone pattern. It should be highly unlikely
94
# so we won't test for that here, at least not now.
96
# The tz_offset is positive, so it must be subtracted from the
97
# naive datetime in order to return it to UTC. E.g.
99
# 13:00 -0400 is 17:00 +0000
101
# 1300 - (-0400 / 100)
102
if tz_offset is not None:
103
# This is not the first time we're seeing a timezone.
104
raise ValueError('Unsupported time string: {0}'.format(t))
105
tz_offset = timedelta(hours=int(tz_string) / 100)
106
# Return the empty string so as to remove the timezone pattern
107
# from the string we're going to parse.
109
# Parse the time string, calling capture_tz() for each timezone match
110
# group we find. The callback itself will ensure we see no more
111
# than one timezone string.
112
naive_t = re.sub(r'[ ]*(?P<tz>[-+]\d{4})', capture_tz, t)
113
if tz_offset is None:
114
# No timezone string was found.
115
tz_offset = timedelta()
116
for parser in PARSERS:
118
parsed_dt = parser(naive_t)
125
raise ValueError('Unsupported time string: {0}'.format(t))
126
# We must have gotten a valid datetime. Normalize out the timezone
127
# offset and convert it to Epoch seconds. Use timegm() to give us
128
# UTC-based conversion from a struct_time to seconds-since-epoch.
129
utc_dt = parsed_dt - tz_offset
130
timetup = utc_dt.timetuple()
131
return int(timegm(timetup))
134
def iso8601utc(timestamp, timezone_offset=0, sep='T'):
135
"""Convert from a Unix epoch timestamp to an ISO 8601 date time string.
137
:param timestamp: Unix epoch timestamp in seconds.
138
:type timestamp: float
139
:param timezone_offset: Offset in hours*100 east/west of UTC. E.g. -400
140
means 4 hours west of UTC; 130 means 1.5 hours east of UTC.
141
:type timezone_offset: int
142
:param sep: ISO 8601 separator placed between the date and time portions
144
:type sep: string of length 1.
145
:return: ISO 8601 datetime string.
148
dt = datetime.utcfromtimestamp(timestamp)
149
hours_east, minutes_east = divmod(timezone_offset, 100)
150
correction = timedelta(hours=hours_east, minutes=minutes_east)
151
# Subtract the correction to move closer to UTC, since the offset is
152
# positive when east of UTC and negative when west of UTC.
153
return (dt - correction).isoformat(sep=sep)