|
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
|
|
|
|