sopel.modules.remind.TimeReminder.__init__()   C
last analyzed

Complexity

Conditions 9

Size

Total Lines 47
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 37
dl 0
loc 47
rs 6.6586
c 0
b 0
f 0
cc 9
nop 9

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
# coding=utf-8
2
"""
3
remind.py - Sopel Reminder Module
4
Copyright 2011, Sean B. Palmer, inamidst.com
5
Licensed under the Eiffel Forum License 2.
6
7
https://sopel.chat
8
"""
9
from __future__ import unicode_literals, absolute_import, print_function, division
10
11
import codecs
12
import collections
13
from datetime import datetime
14
import os
15
import re
16
import time
17
18
import pytz
19
20
from sopel import tools, module
21
from sopel.tools.time import get_timezone, format_time, validate_timezone
22
23
24
def get_filename(bot):
25
    """Get the remind database's filename
26
27
    :param bot: instance of Sopel
28
    :type bot: :class:`sopel.bot.Sopel`
29
    :return: the remind database's filename
30
    :rtype: str
31
32
    The remind database filename is based on the bot's nick and its
33
    configured ``core.host``, and it is located in the ``bot``'s ``homedir``.
34
    """
35
    name = bot.nick + '-' + bot.config.core.host + '.reminders.db'
36
    return os.path.join(bot.config.core.homedir, name)
37
38
39
def load_database(filename):
40
    """Load the remind database from a file
41
42
    :param str filename: absolute path to the remind database file
43
    :return: a :class:`dict` of reminders stored by timestamp
44
    :rtype: dict
45
46
    The remind database is a plain text file (utf-8 encoded) with tab-separated
47
    columns of data: time, channel, nick, and message. This function reads this
48
    file and outputs a :class:`dict` where keys are the timestamps of the
49
    reminders, and values are list of 3-value tuple of reminder data:
50
    ``(channel, nick, message)``.
51
52
    .. note::
53
54
        This function ignores microseconds from the timestamp, if any, meaning
55
        that ``523549800.245`` will be read as ``523549800``.
56
57
    .. note::
58
59
        If ``filename`` is not an existing file, this function returns an
60
        empty :class:`dict`.
61
62
    """
63
    if not os.path.isfile(filename):
64
        # no file to read
65
        return {}
66
67
    data = {}
68
    with codecs.open(filename, 'r', encoding='utf-8') as database:
69
        for line in database:
70
            unixtime, channel, nick, message = line.split('\t', 3)
71
            message = message.rstrip('\n')
72
            timestamp = int(float(unixtime))  # ignore microseconds
73
            reminder = (channel, nick, message)
74
            try:
75
                data[timestamp].append(reminder)
76
            except KeyError:
77
                data[timestamp] = [reminder]
78
    return data
79
80
81
def dump_database(filename, data):
82
    """Dump the remind database into a file
83
84
    :param str filename: absolute path to the remind database file
85
    :param dict data: remind database to dump
86
87
    The remind database is dumped into a plain text file (utf-8 encoded) as
88
    tab-separated columns of data: unixtime, channel, nick, and message.
89
90
    If the file does not exist, it is created.
91
    """
92
    with codecs.open(filename, 'w', encoding='utf-8') as database:
93
        for unixtime, reminders in tools.iteritems(data):
94
            for channel, nick, message in reminders:
95
                line = '%s\t%s\t%s\t%s\n' % (unixtime, channel, nick, message)
96
                database.write(line)
97
98
99
def create_reminder(bot, trigger, duration, message):
100
    """Create a reminder into the ``bot``'s database and reply to the sender
101
102
    :param bot: the bot's instance
103
    :type bot: :class:`~sopel.bot.SopelWrapper`
104
    :param trigger: the object that triggered the call
105
    :type trigger: :class:`~sopel.trigger.Trigger`
106
    :param int duration: duration from now, in seconds, until ``message``
107
                         must be reminded
108
    :param str message: message to be reminded
109
    :return: the reminder's timestamp
110
    :rtype: :class:`int`
111
    """
112
    timestamp = int(time.time()) + duration
113
    reminder = (trigger.sender, trigger.nick, message)
114
    try:
115
        bot.rdb[timestamp].append(reminder)
116
    except KeyError:
117
        bot.rdb[timestamp] = [reminder]
118
119
    dump_database(bot.rfn, bot.rdb)
120
    return timestamp
121
122
123
def setup(bot):
124
    """Load the remind database"""
125
    bot.rfn = get_filename(bot)
126
    bot.rdb = load_database(bot.rfn)
127
128
129
def shutdown(bot):
130
    """Dump the remind database before shutdown"""
131
    dump_database(bot.rfn, bot.rdb)
132
    bot.rdb = {}
133
    del bot.rfn
134
    del bot.rdb
135
136
137
@module.interval(2.5)
138
def remind_monitoring(bot):
139
    """Check for reminder"""
140
    now = int(time.time())
141
    unixtimes = [int(key) for key in bot.rdb]
142
    oldtimes = [t for t in unixtimes if t <= now]
143
    if oldtimes:
144
        for oldtime in oldtimes:
145
            for (channel, nick, message) in bot.rdb[oldtime]:
146
                if message:
147
                    bot.say(nick + ': ' + message, channel)
148
                else:
149
                    bot.say(nick + '!', channel)
150
            del bot.rdb[oldtime]
151
        dump_database(bot.rfn, bot.rdb)
152
153
154
SCALING = collections.OrderedDict([
155
    ('years', 365.25 * 24 * 3600),
156
    ('year', 365.25 * 24 * 3600),
157
    ('yrs', 365.25 * 24 * 3600),
158
    ('y', 365.25 * 24 * 3600),
159
160
    ('months', 29.53059 * 24 * 3600),
161
    ('month', 29.53059 * 24 * 3600),
162
    ('mo', 29.53059 * 24 * 3600),
163
164
    ('weeks', 7 * 24 * 3600),
165
    ('week', 7 * 24 * 3600),
166
    ('wks', 7 * 24 * 3600),
167
    ('wk', 7 * 24 * 3600),
168
    ('w', 7 * 24 * 3600),
169
170
    ('days', 24 * 3600),
171
    ('day', 24 * 3600),
172
    ('d', 24 * 3600),
173
174
    ('hours', 3600),
175
    ('hour', 3600),
176
    ('hrs', 3600),
177
    ('hr', 3600),
178
    ('h', 3600),
179
180
    ('minutes', 60),
181
    ('minute', 60),
182
    ('mins', 60),
183
    ('min', 60),
184
    ('m', 60),
185
186
    ('seconds', 1),
187
    ('second', 1),
188
    ('secs', 1),
189
    ('sec', 1),
190
    ('s', 1),
191
])
192
193
PERIODS = '|'.join(SCALING.keys())
194
195
196
@module.commands('in')
197
@module.example('.in 3h45m Go to class')
198
def remind_in(bot, trigger):
199
    """Gives you a reminder in the given amount of time."""
200
    if not trigger.group(2):
201
        bot.say("Missing arguments for reminder command.")
202
        return module.NOLIMIT
203
    if trigger.group(3) and not trigger.group(4):
204
        bot.say("No message given for reminder.")
205
        return module.NOLIMIT
206
    duration = 0
207
    message = filter(None, re.split(r'(\d+(?:\.\d+)? ?(?:(?i)' + PERIODS + ')) ?',
208
                                    trigger.group(2))[1:])
209
    reminder = ''
210
    stop = False
211
    for piece in message:
212
        grp = re.match(r'(\d+(?:\.\d+)?) ?(.*) ?', piece)
213
        if grp and not stop:
214
            length = float(grp.group(1))
215
            factor = SCALING.get(grp.group(2).lower(), 60)
216
            duration += length * factor
217
        else:
218
            reminder = reminder + piece
219
            stop = True
220
    if duration == 0:
221
        return bot.reply("Sorry, didn't understand the input.")
222
223
    if duration % 1:
224
        duration = int(duration) + 1
225
    else:
226
        duration = int(duration)
227
    timezone = get_timezone(
228
        bot.db, bot.config, None, trigger.nick, trigger.sender)
229
    timestamp = create_reminder(bot, trigger, duration, reminder)
230
231 View Code Duplication
    if duration >= 60:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
232
        human_time = format_time(
233
            bot.db,
234
            bot.config,
235
            timezone,
236
            trigger.nick,
237
            trigger.sender,
238
            datetime.utcfromtimestamp(timestamp))
239
        bot.reply('Okay, will remind at %s' % human_time)
240
    else:
241
        bot.reply('Okay, will remind in %s secs' % duration)
242
243
244
REGEX_AT = re.compile(
245
    # hours:minutes
246
    r'(?P<hours>\d+):(?P<minutes>\d+)'
247
    # optional seconds
248
    r'(?::(?P<seconds>\d+))?'
249
    # optional timezone
250
    r'(?P<tz>[^\s\d]+)?'
251
    # optional date (start)
252
    r'(?:\s+'
253
    # - date 1 (at least one digit)
254
    r'(?P<date1>\d{1,4})'
255
    # - separator (one character)
256
    r'(?P<sep>[./-])'
257
    # - date 2 (at least one digit)
258
    r'(?P<date2>\d{1,4})'
259
    # - optional sep + date 3 (at least one digit)
260
    r'(?:(?P=sep)(?P<date3>\d{1,4}))?'
261
    r')?'  # (end)
262
    # at least one space + message
263
    r'\s+(?P<message>.*)'
264
)
265
266
267
class TimeReminder(object):
268
    """Time reminder for the ``at`` command"""
269
    def __init__(self,
270
                 hour,
271
                 minute,
272
                 second,
273
                 timezone,
274
                 date1,
275
                 date2,
276
                 date3,
277
                 message):
278
        self.hour = hour
279
        self.minute = minute
280
        self.second = second
281
        self.timezone = pytz.timezone(timezone)
282
        self.message = message
283
284
        year = None
285
        month = None
286
        day = None
287
288
        if date1 and date2 and date3:
289
            if len(date1) == 4:
290
                # YYYY-mm-dd
291
                year = int(date1)
292
                month = int(date2)
293
                day = int(date3)
294
            else:
295
                # dd-mm-YYYY or dd/mm/YY
296
                year = int(date3)
297
                month = int(date2)
298
                day = int(date1)
299
        elif date1 and date2:
300
            if len(date1) == 4:
301
                # YYYY-mm
302
                year = int(date1)
303
                month = int(date2)
304
            elif len(date2) == 4:
305
                # mm-YYYY
306
                year = int(date2)
307
                month = int(date1)
308
            else:
309
                # dd/mm
310
                month = int(date2)
311
                day = int(date1)
312
313
        self.year = year
314
        self.month = month
315
        self.day = day
316
317
    def __eq__(self, other):
318
        return all(
319
            getattr(self, attr) == getattr(other, attr, None)
320
            for attr in [
321
                'hour',
322
                'minute',
323
                'second',
324
                'timezone',
325
                'year',
326
                'month',
327
                'day',
328
                'message',
329
            ]
330
        )
331
332
    def get_duration(self, today=None):
333
        """Get the duration between the reminder and ``today``
334
335
        :param today: aware datetime to compare to; defaults to current time
336
        :type today: aware :class:`datetime.datetime`
337
        :return: The duration, in second, between ``today`` and the reminder.
338
        :rtype: :class:`int`
339
340
        This method returns the number of seconds given by the
341
        :class:`datetime.timedelta` obtained by the difference between the
342
        reminder and ``today``.
343
344
        If the delta between the reminder and ``today`` is negative, Python
345
        will represent it as a negative number of days, and a positive number
346
        of seconds: since it returns the number of seconds, any past reminder
347
        will be transformed into a future reminder the next day.
348
349
        .. seealso::
350
351
            The :mod:`datetime` built-in module's documentation explains what
352
            is an "aware" datetime.
353
354
        """
355
        if not today:
356
            today = datetime.now(self.timezone)
357
        else:
358
            today = today.astimezone(self.timezone)
359
360
        year = self.year if self.year is not None else today.year
361
        month = self.month if self.month is not None else today.month
362
        day = self.day if self.day is not None else today.day
363
364
        at_time = datetime(
365
            year, month, day,
366
            self.hour, self.minute, self.second,
367
            tzinfo=today.tzinfo)
368
369
        timediff = at_time - today
370
        duration = timediff.seconds
371
372
        if timediff.days > 0:
373
            duration = duration + (86400 * timediff.days)
374
375
        return duration
376
377
378
def parse_regex_match(match, default_timezone=None):
379
    """Parse a time reminder from ``match``
380
381
    :param match: :obj:`~.REGEX_AT`'s matching result
382
    :param default_timezone: timezone used when ``match`` doesn't have one;
383
                             defaults to ``UTC``
384
    :rtype: :class:`TimeReminder`
385
    """
386
    try:
387
        timezone = validate_timezone(match.group('tz') or 'UTC')
388
    except ValueError:
389
        timezone = default_timezone or 'UTC'
390
391
    return TimeReminder(
392
        int(match.group('hours')),
393
        int(match.group('minutes')),
394
        int(match.group('seconds') or '0'),
395
        timezone,
396
        match.group('date1'),
397
        match.group('date2'),
398
        match.group('date3'),
399
        match.group('message')
400
    )
401
402
403
@module.commands('at')
404
@module.example('.at 13:47 Do your homework!', user_help=True)
405
@module.example('.at 03:14:07 2038-01-19 End of signed 32-bit int timestamp',
406
                user_help=True)
407
@module.example('.at 00:01 25/12 Open your gift!', user_help=True)
408
def remind_at(bot, trigger):
409
    """Gives you a reminder at the given time.
410
411
    Takes ``hh:mm:ssTimezone Date message`` where seconds, Timezone, and Date
412
    are optional.
413
414
    Timezone is any timezone Sopel takes elsewhere; the best choices are those
415
    from the tzdb; a list of valid options is available at
416
    <https://sopel.chat/tz>.
417
418
    The Date can be expressed in one of these formats: YYYY-mm-dd, dd-mm-YYYY,
419
    dd-mm-YY, or dd-mm. The separator can be ``.``, ``-``, or ``/``.
420
    """
421
    if not trigger.group(2):
422
        bot.say("No arguments given for reminder command.")
423
        return module.NOLIMIT
424
    if trigger.group(3) and not trigger.group(4):
425
        bot.say("No message given for reminder.")
426
        return module.NOLIMIT
427
428
    match = REGEX_AT.match(trigger.group(2))
429
    if not match:
430
        bot.reply("Sorry, but I didn't understand your input.")
431
        return module.NOLIMIT
432
433
    default_timezone = get_timezone(bot.db, bot.config, None,
434
                                    trigger.nick, trigger.sender)
435
436
    reminder = parse_regex_match(match, default_timezone)
437
438
    try:
439
        duration = reminder.get_duration()
440
    except ValueError as error:
441
        bot.reply(
442
            "Sorry, but I didn't understand your input: %s" % str(error))
443
        return module.NOLIMIT
444
445
    # save reminder
446
    timestamp = create_reminder(bot, trigger, duration, reminder.message)
447
448 View Code Duplication
    if duration >= 60:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
449
        human_time = format_time(
450
            bot.db,
451
            bot.config,
452
            reminder.timezone.zone,
453
            trigger.nick,
454
            trigger.sender,
455
            datetime.utcfromtimestamp(timestamp))
456
        bot.reply('Okay, will remind at %s' % human_time)
457
    else:
458
        bot.reply('Okay, will remind in %s secs' % duration)
459