Esempio n. 1
0
def now(db, args):
    parser = OptionParser(usage='''usage: %prog now [TIMESHEET]

Print the current sheet, whether it's active, and if so, how long it
has been active and what notes are associated with the current
period.

If a specific timesheet is given, display the same information for that
timesheet instead.''')
    parser.add_option('-s',
                      '--simple',
                      dest='simple',
                      action='store_true',
                      help='Only display the name \
of the current timesheet.')
    parser.add_option('-n',
                      '--notes',
                      dest='notes',
                      action='store_true',
                      help='Only display the notes \
associated with the current period.')
    opts, args = parser.parse_args(args=args)

    if opts.simple:
        print dbutil.get_current_sheet(db)
        return

    if args:
        sheet = cmdutil.complete(dbutil.get_sheet_names(db), args[0],
                                 'timesheet')
    else:
        sheet = dbutil.get_current_sheet(db)

    entry_count = dbutil.get_entry_count(db, sheet)
    if entry_count == 0:
        raise SystemExit, '%(prog)s: error: sheet is empty. For program \
usage, see "%(prog)s --help".' % {
            'prog': os.path.basename(sys.argv[0])
        }

    running = dbutil.get_active_info(db, sheet)
    notes = ''
    if running is None:
        active = 'not active'
    else:
        duration = str(timedelta(seconds=running[0]))
        if running[1]:
            notes = running[1].rstrip('.')
            active = '%s (%s)' % (duration, notes)
        else:
            active = duration
    if opts.notes:
        print notes
    else:
        print '%s: %s' % (sheet, active)
Esempio n. 2
0
def switch(db, timesheet, verbose=False):
    """Switch to a new timesheet

    Usage: t switch [options] <timesheet>

    Switch to a new timesheet. This causes all future operation (except switch)
    to operate on that timesheet. The default timesheet is called
    "default".

    Options:
      -v, --verbose  Print the name and number of entries of the timesheet.
    """
    # optimization: check that the given timesheet is not already
    # current. updates are far slower than selects.
    if dbutil.get_current_sheet(db) != timesheet:
        db.execute('''
        update
            meta
        set
            value = ?
        where
            key = 'current_sheet'
        ''', (timesheet,))

    if verbose:
        entry_count = dbutil.get_entry_count(db, timesheet)
        if entry_count == 0:
            print('switched to empty timesheet "%s"' % timesheet)
        else:
            print(ngettext(
                'switched to timesheet "%s" (1 entry)' % timesheet,
                'switched to timesheet "%s" (%s entries)' % (
                    timesheet, entry_count), entry_count))
Esempio n. 3
0
def kill(db, timesheet=None):
    """Delete a timesheet

    Usage: t (kill | delete) [<timesheet>]

    Delete a timesheet. If no timesheet is specified, delete the current
    timesheet and switch to the default timesheet.
    """
    if timesheet:
        to_delete = timesheet
        switch_to_default = False
    else:
        to_delete = dbutil.get_current_sheet(db)
        switch_to_default = True
    try:
        yes_answers = ('y', 'yes')
        # Use print to display the prompt since it intelligently decodes
        # unicode strings.
        print(('delete timesheet %s?' % to_delete), end=' ')
        confirm = input('').strip().lower() in yes_answers
    except (KeyboardInterrupt, EOFError):
        confirm = False
        print()
    if not confirm:
        print('canceled')
        return
    db.execute('delete from entry where sheet = ?', (to_delete,))
    if switch_to_default:
        switch(db, timesheet='default')
Esempio n. 4
0
def kill(db, args):
    parser = OptionParser(usage='''usage: %prog kill [TIMESHEET]

Delete a timesheet. If no timesheet is specified, delete the current
timesheet and switch to the default timesheet.''')
    opts, args = parser.parse_args(args=args)
    current = dbutil.get_current_sheet(db)
    if args:
        to_delete = args[0]
        switch_to_default = False
    else:
        to_delete = current
        switch_to_default = True
    try:
        yes_answers = ('y', 'yes')
        # Use print to display the prompt since it intelligently decodes
        # unicode strings.
        print(u'delete timesheet %s?' % to_delete),
        confirm = raw_input('').strip().lower() in yes_answers
    except (KeyboardInterrupt, EOFError):
        confirm = False
        print
    if not confirm:
        print 'canceled'
        return
    db.execute(u'delete from entry where sheet = ?', (to_delete, ))
    if switch_to_default:
        switch(db, ['default'])
Esempio n. 5
0
def run_command(db, cmd, args):
    func = get_command_by_name(db, cmd)
    try:
        if commands[func].locking:
            db.execute(u'begin')
        commands[func](db, args)
        if commands[func].locking:
            db.execute(u'commit')
        current_sheet = dbutil.get_current_sheet(db)
        if not commands[func].read_only:
            if db.config.has_option(current_sheet,
                                    'reporting_url') and db.config.has_option(
                                        'auth', 'username'):
                current_info = dbutil.get_active_info(db, current_sheet)
                report_to_url(
                    db.config.get(current_sheet, 'reporting_url'),
                    db.config.get('auth', 'username'),
                    current_info[1] if current_info else '',
                    (datetime.utcnow() - timedelta(seconds=current_info[0])
                     ).strftime("%Y-%m-%d %H:%M:%S") if current_info else '',
                    cmd, args)
            elif db.config.has_option(current_sheet, 'reporting_url'):
                print "Please specify a username in your configuration."
    except:
        if commands[func].locking:
            db.execute(u'rollback')
        raise
Esempio n. 6
0
def run_command(db, cmd, args):
    func = get_command_by_name(db, cmd)
    try:
        if commands[func].locking:
            db.execute(u'begin')
        commands[func](db, args)
        if commands[func].locking:
            db.execute(u'commit')
        current_sheet = dbutil.get_current_sheet(db)
        if not commands[func].read_only:
            if db.config.has_option(current_sheet, 'reporting_url'):
                current_info = dbutil.get_active_info(db, current_sheet)
                status_string = dbutil.get_status_string(db,
                                                         current_sheet,
                                                         exclude=['billable'])
                report_to_url(
                    db.config.get(current_sheet,
                                  'reporting_url'), None, status_string,
                    (datetime.utcnow() - timedelta(seconds=current_info[0])
                     ).strftime("%Y-%m-%d %H:%M:%S") if current_info else '',
                    datetime.now() - timedelta(seconds=current_info[0])
                    if current_info else timedelta(seconds=0),
                    current_info[0] if current_info else 0, cmd, args)
    except Exception:
        import traceback
        traceback.print_exc()
        if commands[func].locking:
            db.execute(u'rollback')
        raise
Esempio n. 7
0
def kill(db, args):
    parser = OptionParser(usage='''usage: %prog kill [TIMESHEET]

Delete a timesheet. If no timesheet is specified, delete the current
timesheet and switch to the default timesheet.''')
    opts, args = parser.parse_args(args=args)
    current = dbutil.get_current_sheet(db)
    if args:
        to_delete = args[0]
        switch_to_default = False
    else:
        to_delete = current
        switch_to_default = True
    try:
        yes_answers = ('y', 'yes')
        # Use print to display the prompt since it intelligently decodes
        # unicode strings.
        print (u'delete timesheet %s?' % to_delete),
        confirm = raw_input('').strip().lower() in yes_answers
    except (KeyboardInterrupt, EOFError):
        confirm = False
        print
    if not confirm:
        print 'canceled'
        return
    db.execute(u'delete from entry where sheet = ?', (to_delete,))
    if switch_to_default:
        switch(db, ['default'])
Esempio n. 8
0
def now(db, args):
    parser = OptionParser(usage='''usage: %prog now [TIMESHEET]

Print the current sheet, whether it's active, and if so, how long it
has been active and what notes are associated with the current
period.

If a specific timesheet is given, display the same information for that
timesheet instead.''')
    parser.add_option('-s', '--simple', dest='simple',
                      action='store_true', help='Only display the name \
of the current timesheet.')
    parser.add_option('-n', '--notes', dest='notes',
                      action='store_true', help='Only display the notes \
associated with the current period.')
    opts, args = parser.parse_args(args=args)

    if opts.simple:
        print dbutil.get_current_sheet(db)
        return

    if args:
        sheet = cmdutil.complete(dbutil.get_sheet_names(db), args[0],
                                 'timesheet')
    else:
        sheet = dbutil.get_current_sheet(db)

    entry_count = dbutil.get_entry_count(db, sheet)
    if entry_count == 0:
        raise SystemExit, '%(prog)s: error: sheet is empty. For program \
usage, see "%(prog)s --help".' % {'prog': os.path.basename(sys.argv[0])}

    running = dbutil.get_active_info(db, sheet)
    notes = ''
    if running is None:
        active = 'not active'
    else:
        duration = str(timedelta(seconds=running[0]))
        if running[1]:
            notes = running[1].rstrip('.')
            active = '%s (%s)' % (duration, notes)
        else:
            active = duration
    if opts.notes:
        print notes
    else:
        print '%s: %s' % (sheet, active)
Esempio n. 9
0
def now(db, timesheet=None, simple=False, notes=False):
    """Show the status of the current timesheet

    Usage: t (now | info) [options] [<timesheet>]

    Print the current sheet, whether it's active, and if so, how long it
    has been active and what notes are associated with the current
    period.

    If a specific timesheet is given, display the same information for that
    timesheet instead.

    Options:
      -s, --simple  Only display the name of the current timesheet.
      -n, --notes   Only display the notes associated with the current period.
    """
    if simple:
        print(dbutil.get_current_sheet(db))
        return

    if timesheet:
        sheet = cmdutil.complete(dbutil.get_sheet_names(db), timesheet,
                                 'timesheet')
    else:
        sheet = dbutil.get_current_sheet(db)

    entry_count = dbutil.get_entry_count(db, sheet)
    if entry_count == 0:
        raise SystemExit('%(prog)s: error: sheet is empty. For program \
usage, see "%(prog)s --help".' % {'prog': os.path.basename(sys.argv[0])})

    running = dbutil.get_active_info(db, sheet)
    _notes = ''
    if running is None:
        active = 'not active'
    else:
        duration = str(timedelta(seconds=running[0]))
        if running[1]:
            _notes = running[1].rstrip('.')
            active = '%s (%s)' % (duration, _notes)
        else:
            active = duration
    if notes:
        print(_notes)
    else:
        print('%s: %s' % (sheet, active))
Esempio n. 10
0
def display(db, args):
    # arguments
    parser = OptionParser(usage='''usage: %prog display [TIMESHEET]

Display the data from a timesheet in the range of dates specified, either
in the normal timebook fashion (using --format=plain) or as
comma-separated value format spreadsheet (using --format=csv), which
ignores the final entry if active.

If a specific timesheet is given, display the same information for that
timesheet instead.''')
    parser.add_option('-s',
                      '--start',
                      dest='start',
                      type='string',
                      metavar='DATE',
                      help='Show only entries \
starting after 00:00 on this date. The date should be of the format \
YYYY-MM-DD.')
    parser.add_option('-e',
                      '--end',
                      dest='end',
                      type='string',
                      metavar='DATE',
                      help='Show only entries \
ending before 00:00 on this date. The date should be of the format \
YYYY-MM-DD.')
    parser.add_option('-f',
                      '--format',
                      dest='format',
                      type='string',
                      default='plain',
                      help="Select whether to output in the normal timebook \
style (--format=plain) or csv --format=csv")
    opts, args = parser.parse_args(args=args)

    # grab correct sheet
    if args:
        sheet = cmdutil.complete(dbutil.get_sheet_names(db), args[0],
                                 'timesheet')
    else:
        sheet = dbutil.get_current_sheet(db)

    #calculate "where"
    where = ''
    fmt = '%Y-%m-%d'
    if opts.start is not None:
        start = cmdutil.parse_date_time(opts.start)
        where += ' and start_time >= %s' % start
    if opts.end is not None:
        end = cmdutil.parse_date_time(opts.end)
        where += ' and end_time <= %s' % end
    if opts.format == 'plain':
        format_timebook(db, sheet, where)
    elif opts.format == 'csv':
        format_csv(db, sheet, where)
    else:
        raise SystemExit, 'Invalid format: %s' % opts.format
Esempio n. 11
0
def display(db, args):
    # arguments
    parser = optparse.OptionParser(usage='''usage: %prog display [TIMESHEET]

Display the data from a timesheet in the range of dates specified, either
in the normal timebook fashion (using --format=plain) or as
comma-separated value format spreadsheet (using --format=csv), which
ignores the final entry if active.

If a specific timesheet is given, display the same information for that
timesheet instead.''')
    parser.add_option('-s', '--start', dest='start', type='string',
                      metavar='DATE', help='Show only entries \
starting after 00:00 on this date. The date should be of the format \
YYYY-MM-DD.')
    parser.add_option('-e', '--end', dest='end', type='string',
                      metavar='DATE', help='Show only entries \
ending before 00:00 on this date. The date should be of the format \
YYYY-MM-DD.')
    parser.add_option('-f', '--format', dest='format', type='string',
                  default='plain',
                  help="Select whether to output in the normal timebook \
style (--format=plain) or csv --format=csv")
    parser.add_option('-i', '--show-ids', dest='show_ids',
            action='store_true', default=False)
    parser.add_option('--summary', dest='summary',
            action='store_true', default=False)
    opts, args = parser.parse_args(args=args)

    # grab correct sheet
    if args:
        sheet = cmdutil.complete(dbutil.get_sheet_names(db), args[0],
                                 'timesheet')
    else:
        sheet = dbutil.get_current_sheet(db)

    #calculate "where"
    where = ''
    if opts.start is not None:
        start = cmdutil.parse_date_time(opts.start)
        where += ' and start_time >= %s' % start
    else:
        where += ''' and start_time >
            STRFTIME(\'%s\', \'now\', \'-6 days\', \'start of day\')
        '''
    if opts.end is not None:
        end = cmdutil.parse_date_time(opts.end)
        where += ' and end_time <= %s' % end
    if opts.format == 'plain':
        format_timebook(
            db, sheet, where, show_ids=opts.show_ids, summary=opts.summary
        )
    elif opts.format == 'csv':
        format_csv(db, sheet, where, show_ids=opts.show_ids)
    else:
        raise SystemExit('Invalid format: %s' % opts.format)
Esempio n. 12
0
def pre_hook(db, func_name, args, kwargs):
    current_sheet = dbutil.get_current_sheet(db)
    keys_to_check = [
        'pre_%s_hook' % func_name,
        'pre_hook',
    ]
    for key_name in keys_to_check:
        if db.config.has_option(current_sheet, key_name):
            command = shlex.split(db.config.get(current_sheet, key_name), )
            res = subprocess.call(command + args, )
            if res != 0:
                raise exceptions.PreHookException(
                    "%s (%s)(%s)" % (command, func_name, ', '.join(args)))
    return True
Esempio n. 13
0
def display(db, args):
    # arguments
    parser = OptionParser(usage='''usage: %prog display [TIMESHEET]

Display the data from a timesheet in the range of dates specified, either
in the normal timebook fashion (using --format=plain) or as
comma-separated value format spreadsheet (using --format=csv), which
ignores the final entry if active.

If a specific timesheet is given, display the same information for that
timesheet instead.''')
    parser.add_option('-s', '--start', dest='start', type='string',
                      metavar='DATE', help='Show only entries \
starting after 00:00 on this date. The date should be of the format \
YYYY-MM-DD.')
    parser.add_option('-e', '--end', dest='end', type='string',
                      metavar='DATE', help='Show only entries \
ending before 00:00 on this date. The date should be of the format \
YYYY-MM-DD.')
    parser.add_option('-f', '--format', dest='format', type='string',
                  default='plain',
                  help='''Select whether to output in the normal timebook
style (--format=plain), csv --format=csv or group --format=group (groups 
periods by custom words in description and calculates total hours per group)''')
    opts, args = parser.parse_args(args=args)

    # grab correct sheet
    if args:
        sheet = cmdutil.complete(dbutil.get_sheet_names(db), args[0],
                                 'timesheet')
    else:
        sheet = dbutil.get_current_sheet(db)

    #calculate "where"
    where = ''
    fmt = '%Y-%m-%d'
    if opts.start is not None:
        start = cmdutil.parse_date_time(opts.start)
        where += ' and start_time >= %s' % start
    if opts.end is not None:
        end = cmdutil.parse_date_time(opts.end)
        where += ' and end_time <= %s' % end
    if opts.format == 'plain':
        format_timebook(db, sheet, where)
    elif opts.format == 'csv':
        format_csv(db, sheet, where)
    elif opts.format == 'group':
        format_timebook(db, sheet, where, group='on')
    else:
        raise SystemExit, 'Invalid format: %s' % opts.format
Esempio n. 14
0
def hours(db, args, extra=None):
    payperiod_class = 'MonthlyOnSecondToLastFriday'
    current_sheet = dbutil.get_current_sheet(db)
    if db.config.has_option(current_sheet, 'payperiod_type'):
        payperiod_class = db.config.get(current_sheet, 'payperiod_type')

    parser = optparse.OptionParser()
    parser.add_option("--param", type="string", dest="param", default=None)
    parser.add_option(
        "--payperiod-type", type="string",
        dest="payperiod_type", default=payperiod_class
    )
    (options, args, ) = parser.parse_args()

    ppu = PayPeriodUtil(db, options.payperiod_type)
    hour_info = ppu.get_hours_details()
    if options.param and options.param in hour_info.keys():
        param = hour_info[options.param]
        if isinstance(param, datetime):
            param = int(time.mktime(param.timetuple()))
        print param
    else:
        print "Period: %s through %s" % (
                hour_info['begin_period'].strftime("%Y-%m-%d"),
                hour_info['end_period'].strftime("%Y-%m-%d"),
                )

        if(hour_info['actual'] > hour_info['expected']):
            print "%.2f hour SURPLUS" % (
                    hour_info['actual'] - hour_info['expected'],
                )
            print "%s hours unpaid" % (hour_info['unpaid'],)
            print "%s hours vacation" % (hour_info['vacation'], )
            print "%s hours adjustment" % (hour_info['adjustments'], )
            print ""
            print "You should have left at %s today to maintain hours." % (
                    hour_info['out_time'].strftime("%H:%M"),
                )
        else:
            print "%.2f hour DEFICIT" % (
                    hour_info['expected'] - hour_info['actual']
                )
            print "%s hours unpaid" % (hour_info['unpaid'])
            print "%s hours vacation" % (hour_info['vacation'], )
            print "%s hours adjustment" % (hour_info['adjustments'], )
            print ""
            print "You should leave at %s today to maintain hours." % (
                    hour_info['out_time'].strftime("%H:%M"),
                )
Esempio n. 15
0
def hours(db, args, extra=None):
    payperiod_class = 'MonthlyOnSecondToLastFriday'
    current_sheet = dbutil.get_current_sheet(db)
    if db.config.has_option(current_sheet, 'payperiod_type'):
        payperiod_class = db.config.get(current_sheet, 'payperiod_type')

    parser = optparse.OptionParser()
    parser.add_option("--param", type="string", dest="param", default=None)
    parser.add_option("--payperiod-type",
                      type="string",
                      dest="payperiod_type",
                      default=payperiod_class)
    (
        options,
        args,
    ) = parser.parse_args()

    ppu = PayPeriodUtil(db, options.payperiod_type)
    hour_info = ppu.get_hours_details()
    if options.param and options.param in hour_info.keys():
        param = hour_info[options.param]
        if isinstance(param, datetime):
            param = int(time.mktime(param.timetuple()))
        print param
    else:
        print "Period: %s through %s" % (
            hour_info['begin_period'].strftime("%Y-%m-%d"),
            hour_info['end_period'].strftime("%Y-%m-%d"),
        )

        if (hour_info['actual'] > hour_info['expected']):
            print "%.2f hour SURPLUS" % (hour_info['actual'] -
                                         hour_info['expected'], )
            print "%s hours unpaid" % (hour_info['unpaid'], )
            print "%s hours vacation" % (hour_info['vacation'], )
            print "%s hours adjustment" % (hour_info['adjustments'], )
            print ""
            print "You should have left at %s today to maintain hours." % (
                hour_info['out_time'].strftime("%H:%M"), )
        else:
            print "%.2f hour DEFICIT" % (hour_info['expected'] -
                                         hour_info['actual'])
            print "%s hours unpaid" % (hour_info['unpaid'])
            print "%s hours vacation" % (hour_info['vacation'], )
            print "%s hours adjustment" % (hour_info['adjustments'], )
            print ""
            print "You should leave at %s today to maintain hours." % (
                hour_info['out_time'].strftime("%H:%M"), )
Esempio n. 16
0
def in_(db, args, extra=None):
    parser = OptionParser(usage='''usage: %prog in [NOTES...]

Start the timer for the current timesheet. Must be called before out.
Notes may be specified for this period. This is exactly equivalent to
%prog in; %prog alter''')
    parser.add_option('-s', '--switch', dest='switch', type='string',
                      help='Switch to another timesheet before \
starting the timer.')
    parser.add_option('-d', '--db', dest='database', type='string',
                      help='Switch to another databae from config file')
    parser.add_option('-o', '--out', dest='out', action='store_true',
                      default=False, help='''Clocks out before clocking \
in''')
    parser.add_option('-a', '--at', dest='at', type='string',
                      help='''Set time of clock-in''')
    parser.add_option('-r', '--resume', dest='resume', action='store_true',
                      default=False, help='''Clocks in with status of \
last active period''')
    opts, args = parser.parse_args(args=args)
    if opts.switch:
        sheet = opts.switch
        switch(db, [sheet])
    else:
        sheet = dbutil.get_current_sheet(db)
    if opts.resume and args:
        parser.error('"--resume" already sets a note, and is incompatible \
with arguments.')
    timestamp = cmdutil.parse_date_time_or_now(opts.at)
    if opts.out:
        clock_out(db, timestamp=timestamp)
    running = dbutil.get_active_info(db, sheet)
    if running is not None:
        raise SystemExit, 'error: timesheet already active'
    most_recent_clockout = dbutil.get_most_recent_clockout(db, sheet)
    description = u' '.join(args) or None
    if most_recent_clockout:
        (previous_timestamp, previous_description) = most_recent_clockout
        if timestamp < previous_timestamp:
            raise SystemExit, \
                  'error: time periods could end up overlapping'
        if opts.resume:
            description = previous_description
    db.execute(u'''
    insert into entry (
        sheet, start_time, description, extra
    ) values (?,?,?,?)
    ''', (sheet, timestamp, description, extra))
Esempio n. 17
0
def display(db, timesheet=None, format='plain', start=None, end=None):
    """Display a timesheet, by default the current one

    Usage: t (display | export | format | show) [options] [<timesheet>]

    Display the data from a timesheet in the range of dates specified, either
    in the normal timebook fashion (using --format=plain) or as
    comma-separated value format spreadsheet (using --format=csv), which
    ignores the final entry if active.

    If a specific timesheet is given, display the same information for that
    timesheet instead.

    Options:
      -s <date>, --start <date>
                        Show only entries starting after 00:00 on this date.
                        The date should be of the format YYYY-MM-DD.
      -e <date>, --end <date>
                        Show only entries ending before 00:00 on this date.
                        The date should be of the format YYYY-MM-DD.
      -f (plain|csv), --format=(plain|csv)
                        Select whether to output in the normal timebook style
                        (--format=plain) or CSV (--format=csv) [default: plain].

    """
    # grab correct sheet
    if timesheet:
        sheet = cmdutil.complete(dbutil.get_sheet_names(db), timesheet,
                                 'timesheet')
    else:
        sheet = dbutil.get_current_sheet(db)

    #calculate "where"
    where = ''
    if start is not None:
        start_date = cmdutil.parse_date_time(start)
        where += ' and start_time >= %s' % start_date
    if end is not None:
        end_date = cmdutil.parse_date_time(end)
        where += ' and end_time <= %s' % end_date
    if format == 'plain':
        format_timebook(db, sheet, where)
    elif format == 'csv':
        format_csv(db, sheet, where)
    else:
        raise SystemExit('Invalid format: %s' % format)
Esempio n. 18
0
def in_(db, description='', switch=None, out=False, at=None, resume=False,
        extra=None):
    """Start the timer for the current timesheet

    Usage: t (in | start) [options] [<description>...]

    Start the timer for the current timesheet. Must be called before out.
    Notes may be specified for this period. This is exactly equivalent to
    `t in; t alter`.

    Options:
      -s <timesheet>, --switch <timesheet>
                    Switch to another timesheet before starting the timer.
      -o, --out     Clock out before clocking in.
      -a <time>, --at <time>
                    Set time of clock-in.
      -r, --resume  Clock in with description of last active period.
    """
    sheet = switch
    if sheet:
        commands['switch'](db, timesheet=sheet)
    else:
        sheet = dbutil.get_current_sheet(db)
    if resume and description:
        raise SystemExit('"--resume" already sets a description')
    timestamp = cmdutil.parse_date_time_or_now(at)
    if out:
        clock_out(db, timestamp=timestamp)
    running = dbutil.get_active_info(db, sheet)
    if running is not None:
        raise SystemExit('error: timesheet already active')
    most_recent_clockout = dbutil.get_most_recent_clockout(db, sheet)
    description = ' '.join(description) or None
    if most_recent_clockout:
        (previous_timestamp, previous_description) = most_recent_clockout
        if timestamp < previous_timestamp:
            raise SystemExit('error: time periods could end up overlapping')
        if resume:
            description = previous_description
    db.execute('''
    insert into entry (
        sheet, start_time, description, extra
    ) values (?,?,?,?)
    ''', (sheet, timestamp, description, extra))
Esempio n. 19
0
def insert(db, args):
    try:
        start = datetime.strptime(args[0], "%Y-%m-%d %H:%M")
        end = datetime.strptime(args[1], "%Y-%m-%d %H:%M")
        memo = args[2]

        sql = """INSERT INTO entry (sheet, start_time, end_time, description)
            VALUES (?, ?, ?, ?)"""
        args = (
                    dbutil.get_current_sheet(db),
                    int(time.mktime(start.timetuple())),
                    int(time.mktime(end.timetuple())),
                    memo
                )
        db.execute(sql, args)
    except (ValueError, IndexError, ) as e:
        print "Insert requires three arguments, START END DESCRIPTION. \
Please use the date format \"YYYY-MM-DD HH:MM\""
        logger.exception(e)
Esempio n. 20
0
def pre_hook(db, func_name, args, kwargs):
    current_sheet = dbutil.get_current_sheet(db)
    keys_to_check = [
                'pre_%s_hook' % func_name,
                'pre_hook',
            ]
    for key_name in keys_to_check:
        if db.config.has_option(current_sheet, key_name):
            command = shlex.split(
                        db.config.get(current_sheet, key_name),
                    )
            res = subprocess.call(
                    command + args,
                )
            if res != 0:
                raise exceptions.PreHookException(
                        "%s (%s)(%s)" % (command, func_name, ', '.join(args))
                    )
    return True
Esempio n. 21
0
def insert(db, args):
    try:
        start = datetime.strptime(args[0], "%Y-%m-%d %H:%M")
        end = datetime.strptime(args[1], "%Y-%m-%d %H:%M")
        memo = args[2]

        sql = """INSERT INTO entry (sheet, start_time, end_time, description)
            VALUES (?, ?, ?, ?)"""
        args = (dbutil.get_current_sheet(db),
                int(time.mktime(start.timetuple())),
                int(time.mktime(end.timetuple())), memo)
        db.execute(sql, args)
    except (
            ValueError,
            IndexError,
    ) as e:
        print "Insert requires three arguments, START END DESCRIPTION. \
Please use the date format \"YYYY-MM-DD HH:MM\""

        logger.exception(e)
Esempio n. 22
0
def switch(db, args):
    parser = OptionParser(usage='''usage: %prog switch TIMESHEET

Switch to a new timesheet. This causes all future operation (except switch)
to operate on that timesheet. The default timesheet is called
"default".''')
    parser.add_option('-v',
                      '--verbose',
                      dest='verbose',
                      action='store_true',
                      help='Print the name and \
number of entries of the timesheet.')
    opts, args = parser.parse_args(args=args)
    if len(args) != 1:
        parser.error('no timesheet given')

    sheet = args[0]

    # optimization: check that the given timesheet is not already
    # current. updates are far slower than selects.
    if dbutil.get_current_sheet(db) != sheet:
        db.execute(
            u'''
        update
            meta
        set
            value = ?
        where
            key = 'current_sheet'
        ''', (args[0], ))

    if opts.verbose:
        entry_count = dbutil.get_entry_count(db, sheet)
        if entry_count == 0:
            print u'switched to empty timesheet "%s"' % sheet
        else:
            print ngettext(
                u'switched to timesheet "%s" (1 entry)' % sheet,
                u'switched to timesheet "%s" (%s entries)' %
                (sheet, entry_count), entry_count)
Esempio n. 23
0
def run_command(db, cmd, args):
    func = get_command_by_name(db, cmd)
    try:
        if commands[func].locking:
            db.execute(u'begin')
        commands[func](db, args)
        if commands[func].locking:
            db.execute(u'commit')
        current_sheet = dbutil.get_current_sheet(db)
        if not commands[func].read_only:
            if db.config.has_option(
                        current_sheet,
                        'reporting_url'
                    ):
                current_info = dbutil.get_active_info(db, current_sheet)
                status_string = dbutil.get_status_string(
                    db,
                    current_sheet,
                    exclude=['billable']
                )
                report_to_url(
                        db.config.get(current_sheet, 'reporting_url'),
                        None,
                        status_string,
                        (
                            datetime.utcnow()
                            - timedelta(seconds=current_info[0])
                        ).strftime("%Y-%m-%d %H:%M:%S")
                        if current_info else '',
                        datetime.now() - timedelta(seconds=current_info[0]) if current_info else timedelta(seconds=0),
                        current_info[0] if current_info else 0,
                        cmd,
                        args
                    )
    except Exception:
        import traceback
        traceback.print_exc()
        if commands[func].locking:
            db.execute(u'rollback')
        raise
Esempio n. 24
0
def switch(db, args):
    parser = OptionParser(usage='''usage: %prog switch TIMESHEET

Switch to a new timesheet. This causes all future operation (except switch)
to operate on that timesheet. The default timesheet is called
"default".''')
    parser.add_option('-v', '--verbose', dest='verbose',
                      action='store_true', help='Print the name and \
number of entries of the timesheet.')
    opts, args = parser.parse_args(args=args)
    if len(args) != 1:
        parser.error('no timesheet given')

    sheet = args[0]

    # optimization: check that the given timesheet is not already
    # current. updates are far slower than selects.
    if dbutil.get_current_sheet(db) != sheet:
        db.execute(u'''
        update
            meta
        set
            value = ?
        where
            key = 'current_sheet'
        ''', (args[0],))

    if opts.verbose:
        entry_count = dbutil.get_entry_count(db, sheet)
        if entry_count == 0:
            print u'switched to empty timesheet "%s"' % sheet
        else:
            print ngettext(
                u'switched to timesheet "%s" (1 entry)' % sheet,
                u'switched to timesheet "%s" (%s entries)' % (
                    sheet, entry_count), entry_count)
Esempio n. 25
0
def run_command(db, cmd, args):
    func = get_command_by_name(db, cmd)
    try:
        if commands[func].locking:
            db.execute(u'begin')
        commands[func](db, args)
        if commands[func].locking:
            db.execute(u'commit')
        current_sheet = dbutil.get_current_sheet(db)
        if not commands[func].read_only:
            if db.config.has_option(
                        current_sheet,
                        'reporting_url'
                    ) and db.config.has_option(
                        'auth',
                        'username'
                    ):
                current_info = dbutil.get_active_info(db, current_sheet)
                report_to_url(
                        db.config.get(current_sheet, 'reporting_url'),
                        db.config.get('auth', 'username'),
                        current_info[1] if current_info else '',
                        (
                            datetime.utcnow()
                            - timedelta(seconds=current_info[0])
                        ).strftime("%Y-%m-%d %H:%M:%S")
                        if current_info else '',
                        cmd,
                        args
                    )
            elif db.config.has_option(current_sheet, 'reporting_url'):
                print "Please specify a username in your configuration."
    except:
        if commands[func].locking:
            db.execute(u'rollback')
        raise
Esempio n. 26
0
def display(db, args):
    # arguments
    parser = optparse.OptionParser(usage='''usage: %prog display [TIMESHEET]

Display the data from a timesheet in the range of dates specified, either
in the normal timebook fashion (using --format=plain) or as
comma-separated value format spreadsheet (using --format=csv), which
ignores the final entry if active.

If a specific timesheet is given, display the same information for that
timesheet instead.''')
    parser.add_option('-s',
                      '--start',
                      dest='start',
                      type='string',
                      metavar='DATE',
                      help='Show only entries \
starting after 00:00 on this date. The date should be of the format \
YYYY-MM-DD.')
    parser.add_option('-e',
                      '--end',
                      dest='end',
                      type='string',
                      metavar='DATE',
                      help='Show only entries \
ending before 00:00 on this date. The date should be of the format \
YYYY-MM-DD.')
    parser.add_option('-f',
                      '--format',
                      dest='format',
                      type='string',
                      default='plain',
                      help="Select whether to output in the normal timebook \
style (--format=plain) or csv --format=csv or eu timesheet csv --format=eu")
    parser.add_option('-i',
                      '--show-ids',
                      dest='show_ids',
                      action='store_true',
                      default=False)
    parser.add_option('--summary',
                      dest='summary',
                      action='store_true',
                      default=False)
    parser.add_option('-m',
                      '--month',
                      dest='month',
                      type='int',
                      default=0,
                      help='Month to export int[1 .. 12]')
    opts, args = parser.parse_args(args=args)

    # grab correct sheet
    if args:
        sheet = cmdutil.complete(dbutil.get_sheet_names(db), args[0],
                                 'timesheet')
    else:
        sheet = dbutil.get_current_sheet(db)

    # calculate "where"
    where = ''
    if opts.month > 0:
        # if month option is used, overwrite start and end date
        y = datetime.now().year
        opts.start = "%d-%d-01" % (y, opts.month)
        opts.end = "%d-%d-%d" % (y, opts.month,
                                 calendar.monthrange(y, opts.month)[1])

    if opts.start is not None:
        start = cmdutil.parse_date_time(opts.start)
        where += ' and start_time >= %s' % start
    else:
        where += ''' and start_time >
            STRFTIME(\'%s\', \'now\', \'-6 days\', \'start of day\')
        '''
    if opts.end is not None:
        end = cmdutil.parse_date_time(opts.end)
        where += ' and end_time <= %s' % end
    if opts.format == 'plain':
        format_timebook(db,
                        sheet,
                        where,
                        show_ids=opts.show_ids,
                        summary=opts.summary)
    elif opts.format == 'csv':
        format_csv(db, sheet, where, show_ids=opts.show_ids)
    elif opts.format == 'eu':
        format_eu(db,
                  sheet,
                  where,
                  show_ids=opts.show_ids,
                  sdate=datetime.strptime(opts.start, '%Y-%m-%d'),
                  edate=datetime.strptime(opts.end, '%Y-%m-%d'))
    else:
        raise SystemExit('Invalid format: %s' % opts.format)
Esempio n. 27
0
def backdate(db, args):
    try:
        try:
            now_dt = datetime.now()
            offset = cmdutil.get_time_offset(args[0])
            start = datetime(now_dt.year, now_dt.month, now_dt.day,
                             now_dt.hour, now_dt.minute,
                             now_dt.second) - offset
        except ValueError:
            start = datetime.fromtimestamp(cmdutil.parse_date_time(args[0]))
        args = args[1:]

        active = dbutil.get_current_start_time(db)
        if active:
            clock_out(db)

        sql = """
            SELECT id 
            FROM entry
            WHERE 
                sheet = ?
                AND
                end_time > ?
        """
        sql_args = (
            dbutil.get_current_sheet(db),
            int(time.mktime(start.timetuple())),
        )
        db.execute(sql, sql_args)

        rows = db.fetchall()

        if len(rows) > 1:
            raise exceptions.CommandError(
                '%s overlaps %s entries. '
                'Please select a later time to backdate to.' %
                (start, len(rows)))

        sql = """
            UPDATE entry
            SET end_time = ?
            WHERE 
                sheet = ?
                AND
                end_time > ?
        """
        sql_args = (
            int(time.mktime(start.timetuple())),
            dbutil.get_current_sheet(db),
            int(time.mktime(start.timetuple())),
        )
        db.execute(sql, sql_args)

        # Clock in
        args.extend(['--at', str(start)])
        in_(db, args)
    except IndexError as e:
        print(
            "Backdate requires at least one argument: START. "
            "Please use either the format \"YYY-MM-DD HH:MM\" or "
            "a time offset like '1h 20m'.")
        logger.exception(e)
Esempio n. 28
0
def modify(db, args):
    if len(args) < 1:
        raise exceptions.CommandError(
            "You must select the ID number of an entry \
of you'd like to modify. Use 'modify latest' to modify the latest entry of the current sheet."
        )
    if args[0] == "latest":
        db.execute(
            u"""
            SELECT id
            FROM entry WHERE sheet = ?
            ORDER BY id DESC LIMIT 1
        """, (dbutil.get_current_sheet(db), ))
        row = db.fetchone()
        if not row:
            raise exceptions.CommandError("No entries for modification found.")
        id = row[0]
    else:
        id = args[0]
    db.execute(
        u"""
        SELECT start_time, end_time, description
        FROM entry WHERE id = ?
    """, (id, ))
    row = db.fetchone()
    if not row:
        raise exceptions.CommandError("The ID you specified does not exist.")
    start = datetime.fromtimestamp(row[0])
    try:
        end = datetime.fromtimestamp(row[1])
    except TypeError:
        end = None

    new_start_date = rawinput_date_format(
        "Start Date",
        "%Y-%m-%d",
        start,
    )
    new_start_time = rawinput_date_format("Start Time", "%H:%M", start)
    start = datetime(
        new_start_date.year,
        new_start_date.month,
        new_start_date.day,
        new_start_time.hour,
        new_start_time.minute,
    )
    new_end_date = rawinput_date_format(
        "End Date",
        "%Y-%m-%d",
        end,
    )
    if new_end_date:
        new_end_time = rawinput_date_format(
            "End Time",
            "%H:%M",
            end,
        )
        if new_end_date and new_end_time:
            end = datetime(
                new_end_date.year,
                new_end_date.month,
                new_end_date.day,
                new_end_time.hour,
                new_end_time.minute,
            )
    description = raw_input("Description (\"%s\"):\t" % (row[2]))
    if not description:
        description = row[2]

    sql = """
        UPDATE entry
        SET start_time = ?, end_time = ?, description = ? WHERE id = ?
        """
    args = (int(time.mktime(start.timetuple())),
            int(time.mktime(end.timetuple())) if end else None, description,
            id)
    db.execute(sql, args)
Esempio n. 29
0
def in_(db, args, extra=None, change=False):
    parser = optparse.OptionParser(usage='''usage: %prog in [NOTES...]

Start the timer for the current timesheet. Must be called before out.
Notes may be specified for this period. This is exactly equivalent to
%prog in; %prog alter''')
    parser.add_option(
        '-s',
        '--switch',
        dest='switch',
        type='string',
        help='Switch to another timesheet before starting the timer.')
    parser.add_option('-o',
                      '--out',
                      dest='out',
                      action='store_true',
                      default=False,
                      help='Clocks out before clocking in')
    parser.add_option('-a',
                      '--at',
                      dest='at',
                      type='string',
                      help='Set time of clock-in')
    parser.add_option('-t',
                      '--ticket',
                      dest='ticket_number',
                      type='string',
                      default=None,
                      help='Set ticket number')
    parser.add_option('--billable',
                      dest='billable',
                      action='store_true',
                      default=True,
                      help='Marks entry as billable')
    parser.add_option('--non-billable',
                      dest='billable',
                      action='store_false',
                      default=True,
                      help='Marks entry as non-billable')
    cmdutil.add_user_specified_attributes(db, parser)
    opts, args = parser.parse_args(args=args)
    metadata = cmdutil.collect_user_specified_attributes(db, opts)
    metadata['billable'] = 'yes' if opts.billable else 'no'
    if opts.ticket_number:
        metadata['ticket_number'] = opts.ticket_number
    if opts.switch:
        sheet = opts.switch
        switch(db, [sheet])
    else:
        sheet = dbutil.get_current_sheet(db)
    timestamp = cmdutil.parse_date_time_or_now(opts.at)
    if opts.out:
        clock_out(db, timestamp=timestamp)
    running = dbutil.get_active_info(db, sheet)
    if running is not None:
        raise SystemExit('error: timesheet already active')
    most_recent_clockout = dbutil.get_most_recent_clockout(db, sheet)
    description = u' '.join(args) or None
    if most_recent_clockout:
        (id, start_time, prev_timestamp, prev_desc) = most_recent_clockout
        prev_meta = dbutil.get_entry_meta(db, id)
        if timestamp < prev_timestamp:
            raise SystemExit('error: time periods could end up overlapping')
        current_sheet = dbutil.get_current_sheet(db)
        if change and db.config.has_option(current_sheet, 'autocontinue'):
            if not description:
                description = prev_desc
            for p_key, p_value in prev_meta.items():
                if p_key not in metadata.keys() or not metadata[p_key]:
                    metadata[p_key] = p_value

    db.execute(
        u'''
    insert into entry (
        sheet, start_time, description, extra
    ) values (?,?,?,?)
    ''', (sheet, timestamp, description, extra))
    entry_id = db.cursor.lastrowid
    dbutil.update_entry_meta(db, entry_id, metadata)
Esempio n. 30
0
def in_(db, args, extra=None):
    parser = OptionParser(usage='''usage: %prog in [NOTES...]

Start the timer for the current timesheet. Must be called before out.
Notes may be specified for this period. This is exactly equivalent to
%prog in; %prog alter''')
    parser.add_option('-s',
                      '--switch',
                      dest='switch',
                      type='string',
                      help='Switch to another timesheet before \
starting the timer.')
    parser.add_option('-o',
                      '--out',
                      dest='out',
                      action='store_true',
                      default=False,
                      help='''Clocks out before clocking \
in''')
    parser.add_option('-a',
                      '--at',
                      dest='at',
                      type='string',
                      help='''Set time of clock-in''')
    parser.add_option('-r',
                      '--resume',
                      dest='resume',
                      action='store_true',
                      default=False,
                      help='''Clocks in with status of \
last active period''')
    opts, args = parser.parse_args(args=args)
    if opts.switch:
        sheet = opts.switch
        switch(db, [sheet])
    else:
        sheet = dbutil.get_current_sheet(db)
    if opts.resume and args:
        parser.error('"--resume" already sets a note, and is incompatible \
with arguments.')
    timestamp = cmdutil.parse_date_time_or_now(opts.at)
    if opts.out:
        clock_out(db, timestamp=timestamp)
    running = dbutil.get_active_info(db, sheet)
    if running is not None:
        raise SystemExit, 'error: timesheet already active'
    most_recent_clockout = dbutil.get_most_recent_clockout(db, sheet)
    description = u' '.join(args) or None
    if most_recent_clockout:
        (previous_timestamp, previous_description) = most_recent_clockout
        if timestamp < previous_timestamp:
            raise SystemExit, \
                  'error: time periods could end up overlapping'
        if opts.resume:
            description = previous_description
    db.execute(
        u'''
    insert into entry (
        sheet, start_time, description, extra
    ) values (?,?,?,?)
    ''', (sheet, timestamp, description, extra))
Esempio n. 31
0
def backdate(db, args):
    try:
        try:
            now_dt = datetime.now()
            offset = cmdutil.get_time_offset(args[0])
            start = datetime(
                now_dt.year,
                now_dt.month,
                now_dt.day,
                now_dt.hour,
                now_dt.minute,
                now_dt.second
            ) - offset
        except ValueError:
            start = datetime.fromtimestamp(cmdutil.parse_date_time(args[0]))
        args = args[1:]

        active = dbutil.get_current_start_time(db)
        if active:
            clock_out(db)

        sql = """
            SELECT id 
            FROM entry
            WHERE 
                sheet = ?
                AND
                end_time > ?
        """
        sql_args = (
            dbutil.get_current_sheet(db),
            int(time.mktime(start.timetuple())),
        )
        db.execute(sql, sql_args)

        rows = db.fetchall()

        if len(rows) > 1:
            raise exceptions.CommandError(
                '%s overlaps %s entries. '
                'Please select a later time to backdate to.' % (
                    start,
                    len(rows)
                )
            )

        sql = """
            UPDATE entry
            SET end_time = ?
            WHERE 
                sheet = ?
                AND
                end_time > ?
        """
        sql_args = (
            int(time.mktime(start.timetuple())),
            dbutil.get_current_sheet(db),
            int(time.mktime(start.timetuple())),
        )
        db.execute(sql, sql_args)

        # Clock in
        args.extend(
            ['--at', str(start)]
        )
        in_(db, args)
    except IndexError as e:
        print (
            "Backdate requires at least one argument: START. "
            "Please use either the format \"YYY-MM-DD HH:MM\" or "
            "a time offset like '1h 20m'."
        )
        logger.exception(e)
Esempio n. 32
0
def in_(db, args, extra=None, change=False):
    parser = optparse.OptionParser(usage='''usage: %prog in [NOTES...]

Start the timer for the current timesheet. Must be called before out.
Notes may be specified for this period. This is exactly equivalent to
%prog in; %prog alter''')
    parser.add_option('-s', '--switch', dest='switch', type='string',
            help='Switch to another timesheet before starting the timer.'
            )
    parser.add_option('-o', '--out', dest='out', action='store_true',
            default=False, help='Clocks out before clocking in'
            )
    parser.add_option('-a', '--at', dest='at', type='string',
            help='Set time of clock-in'
            )
    parser.add_option('-t', '--ticket', dest='ticket_number', type='string',
            default=None, help='Set ticket number'
            )
    parser.add_option('--billable', dest='billable', action='store_true',
            default=True, help='Marks entry as billable'
            )
    parser.add_option('--non-billable', dest='billable', action='store_false',
            default=True, help='Marks entry as non-billable'
            )
    cmdutil.add_user_specified_attributes(db, parser)
    opts, args = parser.parse_args(args=args)
    metadata = cmdutil.collect_user_specified_attributes(db, opts)
    metadata['billable'] = 'yes' if opts.billable else 'no'
    if opts.ticket_number:
        metadata['ticket_number'] = opts.ticket_number
    if opts.switch:
        sheet = opts.switch
        switch(db, [sheet])
    else:
        sheet = dbutil.get_current_sheet(db)
    timestamp = cmdutil.parse_date_time_or_now(opts.at)
    if opts.out:
        clock_out(db, timestamp=timestamp)
    running = dbutil.get_active_info(db, sheet)
    if running is not None:
        raise SystemExit('error: timesheet already active')
    most_recent_clockout = dbutil.get_most_recent_clockout(db, sheet)
    description = u' '.join(args) or None
    if most_recent_clockout:
        (id, start_time, prev_timestamp, prev_desc) = most_recent_clockout
        prev_meta = dbutil.get_entry_meta(db, id)
        if timestamp < prev_timestamp:
            raise SystemExit('error: time periods could end up overlapping')
        current_sheet = dbutil.get_current_sheet(db)
        if change and db.config.has_option(current_sheet, 'autocontinue'):
            if not description:
                description = prev_desc
            for p_key, p_value in prev_meta.items():
                if p_key not in metadata.keys() or not metadata[p_key]:
                    metadata[p_key] = p_value

    db.execute(u'''
    insert into entry (
        sheet, start_time, description, extra
    ) values (?,?,?,?)
    ''', (sheet, timestamp, description, extra))
    entry_id = db.cursor.lastrowid
    dbutil.update_entry_meta(db, entry_id, metadata)
Esempio n. 33
0
def test_switch(end, cmd, db):
    from timebook.dbutil import get_current_sheet
    assert get_current_sheet(db) == 'default'
    cmd(db, 'test')
    assert get_current_sheet(db) == 'test'
Esempio n. 34
0
def modify(db, args):
    if len(args) < 1:
        raise exceptions.CommandError("You must select the ID number of an entry \
of you'd like to modify. Use 'modify latest' to modify the latest entry of the current sheet.")
    if args[0] == "latest":
        db.execute(u"""
            SELECT id
            FROM entry WHERE sheet = ?
            ORDER BY id DESC LIMIT 1
        """, (dbutil.get_current_sheet(db), ))
        row = db.fetchone()
        if not row:
            raise exceptions.CommandError("No entries for modification found.")
        id = row[0]
    else:
        id = args[0]
    db.execute(u"""
        SELECT start_time, end_time, description
        FROM entry WHERE id = ?
    """, (id, ))
    row = db.fetchone()
    if not row:
        raise exceptions.CommandError("The ID you specified does not exist.")
    start = datetime.fromtimestamp(row[0])
    try:
        end = datetime.fromtimestamp(row[1])
    except TypeError:
        end = None

    new_start_date = rawinput_date_format(
                "Start Date",
                "%Y-%m-%d",
                start,
            )
    new_start_time = rawinput_date_format(
                "Start Time",
                "%H:%M",
                start
            )
    start = datetime(
                new_start_date.year,
                new_start_date.month,
                new_start_date.day,
                new_start_time.hour,
                new_start_time.minute,
            )
    new_end_date = rawinput_date_format(
                "End Date",
                "%Y-%m-%d",
                end,
            )
    if new_end_date:
        new_end_time = rawinput_date_format(
                    "End Time",
                    "%H:%M",
                    end,
                )
        if new_end_date and new_end_time:
            end = datetime(
                        new_end_date.year,
                        new_end_date.month,
                        new_end_date.day,
                        new_end_time.hour,
                        new_end_time.minute,
                    )
    description = raw_input("Description (\"%s\"):\t" % (
            row[2]
        ))
    if not description:
        description = row[2]

    sql = """
        UPDATE entry
        SET start_time = ?, end_time = ?, description = ? WHERE id = ?
        """
    args = (
            int(time.mktime(start.timetuple())),
            int(time.mktime(end.timetuple())) if end else None,
            description,
            id
        )
    db.execute(sql, args)