Passed
Push — master ( 91488b...788cfc )
by dgw
01:45 queued 10s
created

sopel.tools.time.validate_timezone()   A

Complexity

Conditions 4

Size

Total Lines 37
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 37
rs 9.9
c 0
b 0
f 0
cc 4
nop 1
1
# coding=utf-8
2
"""Tools for getting and displaying the time."""
3
from __future__ import unicode_literals, absolute_import, print_function, division
4
5
import datetime
6
7
import pytz
8
9
10
def validate_timezone(zone):
11
    """Return an IETF timezone from the given IETF zone or common abbreviation.
12
13
    :param string zone: in a strict or a human-friendly format
14
    :return: the valid IETF timezone properly formatted
15
    :raise ValueError: when ``zone`` is not a valid timezone
16
17
    Prior to checking timezones, two transformations are made to make the zone
18
    names more human-friendly:
19
20
    1. the string is split on ``', '``, the pieces reversed, and then joined
21
       with ``/`` (*"New York, America"* becomes *"America/New York"*)
22
    2. Remaining spaces are replaced with ``_``
23
    3. Finally, strings longer than 4 characters are made title-case,
24
       and those 4 characters and shorter are made upper-case.
25
26
    This means ``new york, america`` becomes ``America/New_York``, and ``utc``
27
    becomes ``UTC``.
28
29
    This is the expected case-insensitivity behavior in the majority of cases.
30
    For example, ``edt`` and ``america/new_york`` will both return
31
    ``America/New_York``.
32
33
    If the zone is not valid, ``ValueError`` will be raised.
34
    """
35
    if zone is None:
36
        return None
37
38
    zone = '/'.join(reversed(zone.split(', '))).replace(' ', '_')
39
    if len(zone) <= 4:
40
        zone = zone.upper()
41
    else:
42
        zone = zone.title()
43
    if zone in pytz.all_timezones:
44
        return zone
45
    else:
46
        raise ValueError("Invalid time zone.")
47
48
49
def validate_format(tformat):
50
    """Returns the format, if valid, else None"""
51
    try:
52
        time = datetime.datetime.utcnow()
53
        time.strftime(tformat)
54
    except Exception:  # TODO: Be specific
55
        raise ValueError('Invalid time format')
56
    return tformat
57
58
59
def get_nick_timezone(db, nick):
60
    """Get a nick's timezone from database.
61
62
    :param db: Bot's database handler (usually ``bot.db``)
63
    :type db: :class:`~sopel.db.SopelDB`
64
    :param nick: IRC nickname
65
    :type nick: :class:`~sopel.tools.Identifier`
66
    :return: the timezone associated with the ``nick``
67
68
    If a timezone cannot be found for ``nick``, or if it is invalid, ``None``
69
    will be returned.
70
    """
71
    try:
72
        return validate_timezone(db.get_nick_value(nick, 'timezone'))
73
    except ValueError:
74
        return None
75
76
77
def get_channel_timezone(db, channel):
78
    """Get a channel's timezone from database.
79
80
    :param db: Bot's database handler (usually ``bot.db``)
81
    :type db: :class:`~sopel.db.SopelDB`
82
    :param channel: IRC channel name
83
    :type channel: :class:`~sopel.tools.Identifier`
84
    :return: the timezone associated with the ``channel``
85
86
    If a timezone cannot be found for ``channel``, or if it is invalid,
87
    ``None`` will be returned.
88
    """
89
    try:
90
        return validate_timezone(db.get_channel_value(channel, 'timezone'))
91
    except ValueError:
92
        return None
93
94
95
def get_timezone(db=None, config=None, zone=None, nick=None, channel=None):
96
    """Find, and return, the approriate timezone
97
98
    Time zone is pulled in the following priority:
99
100
    1. ``zone``, if it is valid
101
    2. The timezone for the channel or nick ``zone`` in ``db`` if one is set
102
       and valid.
103
    3. The timezone for the nick ``nick`` in ``db``, if one is set and valid.
104
    4. The timezone for the channel ``channel`` in ``db``, if one is set and
105
       valid.
106
    5. The default timezone in ``config``, if one is set and valid.
107
108
    If ``db`` is not given, or given but not set up, steps 2 and 3 will be
109
    skipped. If ``config`` is not given, step 4 will be skipped. If no step
110
    yeilds a valid timezone, ``None`` is returned.
111
112
    Valid timezones are those present in the IANA Time Zone Database.
113
114
    .. seealso::
115
116
       The :func:`validate_timezone` function handles the validation and
117
       formatting of the timezone.
118
119
    """
120
    def _check(zone):
121
        try:
122
            return validate_timezone(zone)
123
        except ValueError:
124
            return None
125
126
    tz = None
127
128
    if zone:
129
        tz = _check(zone)
130
        if not tz:
131
            tz = _check(
132
                db.get_nick_or_channel_value(zone, 'timezone'))
133
    if not tz and nick:
134
        tz = _check(db.get_nick_value(nick, 'timezone'))
135
    if not tz and channel:
136
        tz = _check(db.get_channel_value(channel, 'timezone'))
137
    if not tz and config and config.core.default_timezone:
138
        tz = _check(config.core.default_timezone)
139
    return tz
140
141
142
def format_time(db=None, config=None, zone=None, nick=None, channel=None,
143
                time=None):
144
    """Return a formatted string of the given time in the given zone.
145
146
    ``time``, if given, should be a naive ``datetime.datetime`` object and will
147
    be treated as being in the UTC timezone. If it is not given, the current
148
    time will be used. If ``zone`` is given it must be present in the IANA Time
149
    Zone Database; ``get_timezone`` can be helpful for this. If ``zone`` is not
150
    given, UTC will be assumed.
151
152
    The format for the string is chosen in the following order:
153
154
    1. The format for the nick ``nick`` in ``db``, if one is set and valid.
155
    2. The format for the channel ``channel`` in ``db``, if one is set and
156
       valid.
157
    3. The default format in ``config``, if one is set and valid.
158
    4. ISO-8601
159
160
    If ``db`` is not given or is not set up, steps 1 and 2 are skipped. If
161
    config is not given, step 3 will be skipped.
162
    """
163
    tformat = None
164
    if db:
165
        if nick:
166
            tformat = db.get_nick_value(nick, 'time_format')
167
        if not tformat and channel:
168
            tformat = db.get_channel_value(channel, 'time_format')
169
    if not tformat and config and config.core.default_time_format:
170
        tformat = config.core.default_time_format
171
    if not tformat:
172
        tformat = '%Y-%m-%d - %T%Z'
173
174
    if not time:
175
        time = datetime.datetime.utcnow()
176
177
    if not zone:
178
        return time.strftime(tformat)
179
    else:
180
        if not time.tzinfo:
181
            utc = pytz.timezone('UTC')
182
            time = utc.localize(time)
183
        zone = pytz.timezone(zone)
184
        return time.astimezone(zone).strftime(tformat)
185
186
187
def seconds_to_human(secs):
188
    """Return a human readable string that is more readable than the built-in
189
    str(timedelta).
190
191
    Inspiration for function structure from:
192
    https://gist.github.com/Highstaker/280a09591df4a5fb1363b0bbaf858f0d
193
194
    Example outputs are::
195
196
        2 years, 1 month ago
197
        in 4 hours, 45 minutes
198
        in 8 days, 5 hours
199
        1 year ago
200
201
    """
202
    if isinstance(secs, datetime.timedelta):
203
        secs = secs.total_seconds()
204
205
    future = False
206
    if secs < 0:
207
        future = True
208
209
    secs = int(secs)
210
    secs = abs(secs)
211
212
    years = secs // 31536000
213
    months = (secs - years * 31536000) // 2635200
214
    days = (secs - years * 31536000 - months * 2635200) // 86400
215
    hours = (secs - years * 31536000 - months * 2635200 - days * 86400) // 3600
216
    minutes = (secs - years * 31536000 - months * 2635200 - days * 86400 - hours * 3600) // 60
217
    seconds = secs - years * 31536000 - months * 2635200 - days * 86400 - hours * 3600 - minutes * 60
218
219
    years_text = "year{}".format("s" if years != 1 else "")
220
    months_text = "month{}".format("s" if months != 1 else "")
221
    days_text = "day{}".format("s" if days != 1 else "")
222
    hours_text = "hour{}".format("s" if hours != 1 else "")
223
    minutes_text = "minute{}".format("s" if minutes != 1 else "")
224
    seconds_text = "second{}".format("s" if seconds != 1 else "")
225
226
    result = ", ".join(filter(lambda x: bool(x), [
227
        "{0} {1}".format(years, years_text) if years else "",
228
        "{0} {1}".format(months, months_text) if months else "",
229
        "{0} {1}".format(days, days_text) if days else "",
230
        "{0} {1}".format(hours, hours_text) if hours else "",
231
        "{0} {1}".format(minutes, minutes_text) if minutes else "",
232
        "{0} {1}".format(seconds, seconds_text) if seconds else ""
233
    ]))
234
    # Granularity
235
    result = ", ".join(result.split(", ")[:2])
236
    if future is False:
237
        result += " ago"
238
    else:
239
        result = "in " + result
240
    return result
241