~jbicha/friends/build-depend-on-dev-not-gir

« back to all changes in this revision

Viewing changes to friends/utils/time.py

  • Committer: Ken VanDine
  • Date: 2012-10-13 01:27:15 UTC
  • Revision ID: ken.vandine@canonical.com-20121013012715-gxfoi1oo30wdm8dv
initial import

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# friends-service -- send & receive messages from any social network
 
2
# Copyright (C) 2012  Canonical Ltd
 
3
#
 
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.
 
7
#
 
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.
 
12
#
 
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/>.
 
15
 
 
16
"""Time utilities."""
 
17
 
 
18
__all__ = [
 
19
    'parsetime',
 
20
    'iso8601utc',
 
21
    ]
 
22
 
 
23
 
 
24
import re
 
25
import time
 
26
import locale
 
27
 
 
28
from calendar import timegm
 
29
from contextlib import contextmanager
 
30
from datetime import datetime, timedelta
 
31
 
 
32
 
 
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'
 
37
 
 
38
 
 
39
@contextmanager
 
40
def _c_locale():
 
41
    locale.setlocale(locale.LC_TIME, 'C')
 
42
    try:
 
43
        yield
 
44
    finally:
 
45
        locale.setlocale(locale.LC_TIME, '')
 
46
 
 
47
 
 
48
def _from_iso8601(t):
 
49
    return datetime.strptime(t, ISO8601_FORMAT)
 
50
 
 
51
 
 
52
def _from_iso8601alt(t):
 
53
    return datetime.strptime(t, ISO8601_FORMAT.replace('T', ' '))
 
54
 
 
55
 
 
56
def _from_twitter(t):
 
57
    return datetime.strptime(t, TWITTER_FORMAT)
 
58
 
 
59
 
 
60
def _from_identica(t):
 
61
    return datetime.strptime(t, IDENTICA_FORMAT)
 
62
 
 
63
 
 
64
def _fromutctimestamp(t):
 
65
    return datetime.utcfromtimestamp(float(t))
 
66
 
 
67
 
 
68
PARSERS = (_from_iso8601, _from_iso8601alt, _from_twitter,
 
69
           _from_identica, _fromutctimestamp)
 
70
 
 
71
 
 
72
def parsetime(t):
 
73
    """Parse an ISO 8601 datetime string and return seconds since epoch.
 
74
 
 
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.
 
79
    """
 
80
    with _c_locale():
 
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.
 
87
        tz_offset = None
 
88
        def capture_tz(match_object):
 
89
            nonlocal tz_offset
 
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.
 
95
                #
 
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.
 
98
                #
 
99
                #   13:00 -0400 is 17:00 +0000
 
100
                # or
 
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.
 
108
            return ''
 
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:
 
117
            try:
 
118
                parsed_dt = parser(naive_t)
 
119
            except ValueError:
 
120
                pass
 
121
            else:
 
122
                break
 
123
        else:
 
124
            # Nothing matched.
 
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))
 
132
 
 
133
 
 
134
def iso8601utc(timestamp, timezone_offset=0, sep='T'):
 
135
    """Convert from a Unix epoch timestamp to an ISO 8601 date time string.
 
136
 
 
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
 
143
        of the result.
 
144
    :type sep: string of length 1.
 
145
    :return: ISO 8601 datetime string.
 
146
    :rtype: string
 
147
    """
 
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)