202 lines
6.8 KiB
Python
Executable File
202 lines
6.8 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
# Author: Josh Holtrop
|
|
# Simple script to show me my week's hours as far as my Ubuntu box knows them
|
|
|
|
import os
|
|
import sys
|
|
import re
|
|
import argparse
|
|
from datetime import *
|
|
|
|
LOG_FILE = '/var/log/auth.log'
|
|
ADJUSTMENTS_FILE = os.path.expanduser('~/.hours')
|
|
ISO_DATE_FMT = '%Y-%m-%d'
|
|
HOURS_PER_DAY = 8.0
|
|
|
|
now = datetime.now()
|
|
monday = (now - timedelta(now.weekday())).date()
|
|
|
|
def get_date_from_day_spec(day_spec):
|
|
if re.match(r'\d+', day_spec):
|
|
for i in range(7):
|
|
d = monday + timedelta(i)
|
|
if d.day == int(day_spec):
|
|
return d.strftime(ISO_DATE_FMT)
|
|
return ''
|
|
days = {
|
|
'mon': 0,
|
|
'tue': 1,
|
|
'wed': 2,
|
|
'thu': 3,
|
|
'fri': 4,
|
|
'sat': 5,
|
|
'sun': 6
|
|
}
|
|
day_spec = day_spec.lower()
|
|
if len(day_spec) > 3:
|
|
day_spec = day_spec[:3]
|
|
if day_spec in days:
|
|
return (monday + timedelta(days[day_spec])).strftime(ISO_DATE_FMT)
|
|
return ''
|
|
|
|
def get_adjustments():
|
|
adjustments = {}
|
|
if not os.path.isfile(ADJUSTMENTS_FILE):
|
|
return adjustments
|
|
f = open(ADJUSTMENTS_FILE, 'r')
|
|
while True:
|
|
line = f.readline()
|
|
if line == '':
|
|
break
|
|
m = re.match(r'adj\s+(\S+)\s+(\S+)', line)
|
|
if m is not None:
|
|
adjustments[m.group(1)] = float(m.group(2))
|
|
f.close()
|
|
return adjustments
|
|
|
|
def save_adjustments(adjustments):
|
|
f = open(ADJUSTMENTS_FILE, 'w')
|
|
for a in adjustments:
|
|
if datetime.strptime(a, ISO_DATE_FMT).date() >= monday:
|
|
if adjustments[a] != 0.0:
|
|
f.write('adj %s %f\n' % (a, adjustments[a]))
|
|
f.close()
|
|
|
|
def get_dt_from_log_line(line):
|
|
m = re.match(r'(\S\S\S)\s+(\d+)\s+(\d+):(\d+):(\d+)\s+.*', line)
|
|
if m is not None:
|
|
month_name, day, t_hr, t_min, t_sec = m.group(1, 2, 3, 4, 5)
|
|
try:
|
|
month = datetime.strptime(month_name, '%b').month
|
|
except:
|
|
return None
|
|
year = now.year
|
|
if month == 12 and now.month == 1:
|
|
year -= 1
|
|
dt = datetime(*map(int, [year, month, day, t_hr, t_min, t_sec]))
|
|
return dt
|
|
return None
|
|
|
|
def main(argv):
|
|
goal_hours = 40
|
|
adjustments = get_adjustments()
|
|
parser = argparse.ArgumentParser('hours')
|
|
parser.add_argument('-t', '--total', type=float, metavar='TOT',
|
|
help='set target number of hours for the week')
|
|
parser.add_argument('-a', '--adjust', type=float, metavar='ADJ',
|
|
help="adjust given day's (default today) hours by ADJ")
|
|
parser.add_argument('-d', '--day',
|
|
help='specify which day to adjust hours for with --adj')
|
|
parser.add_argument('-s', '--show-schedule', action='store_true',
|
|
help='show schedule for the rest of the week')
|
|
args = parser.parse_args(argv[1:])
|
|
if args.total is not None:
|
|
goal_hours = args.total
|
|
if args.adjust is not None:
|
|
adj_date = now.strftime(ISO_DATE_FMT)
|
|
if args.day is not None:
|
|
adj_date = get_date_from_day_spec(args.day)
|
|
if adj_date == '':
|
|
sys.stderr.write('Unknown DAY format.\n')
|
|
sys.stderr.write('Specify DAY as an integer or as a day name\n')
|
|
sys.exit(2)
|
|
if adj_date in adjustments:
|
|
adjustments[adj_date] += args.adjust
|
|
else:
|
|
adjustments[adj_date] = args.adjust
|
|
save_adjustments(adjustments)
|
|
sys.stdout.write('Adjusted %s by %.1f\n' % (adj_date, args.adjust))
|
|
|
|
times = []
|
|
for i in range(7):
|
|
times.append([None, None])
|
|
|
|
f = open(LOG_FILE, 'r')
|
|
while True:
|
|
line = f.readline()
|
|
if line == '':
|
|
break
|
|
if re.search(r'gnome-screensaver.*unlocked.login.keyring', line):
|
|
# found a login line
|
|
dt = get_dt_from_log_line(line)
|
|
idx = (dt.date() - monday).days
|
|
if times[idx][0] is None or dt < times[idx][0]:
|
|
times[idx][0] = dt
|
|
elif re.search(r'lock-screen:', line):
|
|
# found a logout line
|
|
dt = get_dt_from_log_line(line)
|
|
idx = (dt.date() - monday).days
|
|
if times[idx][1] is None or dt > times[idx][1]:
|
|
times[idx][1] = dt
|
|
f.close()
|
|
|
|
now_idx = (now.date() - monday).days
|
|
if times[now_idx][1] is None or now > times[now_idx][1]:
|
|
times[now_idx][1] = now
|
|
|
|
def fmt_time_dt(dt):
|
|
s = dt.strftime('%I:%M') + dt.strftime('%p')[0].lower()
|
|
if s[0] == '0':
|
|
s = s[1:]
|
|
return s
|
|
|
|
border = lambda: sys.stdout.write('-' * 55 + '\n')
|
|
|
|
total_hours = 0
|
|
sched_hours = 0
|
|
border()
|
|
for time in times:
|
|
if time[0] is not None:
|
|
sys.stdout.write('%-14s' % (time[0].strftime('%d %A') + ':'))
|
|
sys.stdout.write(fmt_time_dt(time[0]))
|
|
sys.stdout.write(' - ')
|
|
if time[1] is not None:
|
|
sys.stdout.write(fmt_time_dt(time[1]))
|
|
seconds = (time[1] - time[0]).seconds
|
|
hours = round(seconds / 60.0 / 60.0, 1)
|
|
iso_spec = time[1].strftime(ISO_DATE_FMT)
|
|
adj = 0.0
|
|
if iso_spec in adjustments:
|
|
adj = adjustments[iso_spec]
|
|
hours += adj
|
|
sys.stdout.write(' (%.1f hours)' % hours)
|
|
if adj != 0.0:
|
|
sys.stdout.write(
|
|
' [%s%.1f]' % ('+' if adj > 0.0 else '', adj))
|
|
if (iso_spec == now.strftime(ISO_DATE_FMT)
|
|
and hours < HOURS_PER_DAY):
|
|
sched_hours += HOURS_PER_DAY
|
|
sys.stdout.write(', %.1f at %s' % (HOURS_PER_DAY,
|
|
fmt_time_dt(time[0] + timedelta(0,
|
|
int((HOURS_PER_DAY - adj) * 60 * 60)))))
|
|
else:
|
|
sched_hours += hours
|
|
total_hours += hours
|
|
else:
|
|
sys.stdout.write('???')
|
|
sys.stdout.write('\n')
|
|
|
|
# schedule for remainder of week
|
|
if args.show_schedule:
|
|
d = now.date() + timedelta(1)
|
|
while sched_hours < goal_hours:
|
|
hours_to_do = min(goal_hours - sched_hours, HOURS_PER_DAY)
|
|
sys.stdout.write('%-14s' % (d.strftime('%d %A') + ':'))
|
|
sys.stdout.write('* %.1f hours *' % hours_to_do)
|
|
sys.stdout.write('\n')
|
|
sched_hours += hours_to_do
|
|
d += timedelta(1)
|
|
|
|
border()
|
|
sys.stdout.write('Total: %.1f hours' % round(total_hours, 1))
|
|
|
|
if total_hours < goal_hours:
|
|
out_time = now + timedelta(0, (goal_hours - total_hours) * 60 * 60)
|
|
sys.stdout.write('; %.1f at: %s %s' % (goal_hours,
|
|
out_time.strftime('%a %d'), fmt_time_dt(out_time)))
|
|
sys.stdout.write('\n')
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv))
|