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)
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))
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')
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'])
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
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
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'])
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)
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))
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
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)
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
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
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"), )
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"), )
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))
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)
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))
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)
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
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)
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)
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
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)
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
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)
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)
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)
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)
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))
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)
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)
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'
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)