Skip to content

Instantly share code, notes, and snippets.

@ZoomTen
Last active June 3, 2021 13:46
Show Gist options
  • Save ZoomTen/f46a167eeb04377640623abc5813697f to your computer and use it in GitHub Desktop.
Save ZoomTen/f46a167eeb04377640623abc5813697f to your computer and use it in GitHub Desktop.
Activity logger that I run every night before I go to bed
#!/usr/bin/python
from appdirs import AppDirs
from argparse import ArgumentParser
from configparser import ConfigParser
from datetime import datetime, date
from textwrap import TextWrapper as tw
import os
import sys
import sqlite3
import shutil
import re
appname = "PyActivityLog"
appversion = "1.3.2"
app_displayname = "Zumi's Activity Manager"
def prompt_yn(prompt, default_yes=False):
y = ['yes', 'y', 'yeah', 'yea', 'ye', 'ya']
n = ['no', 'nah', 'n', 'nein']
if default_yes:
prompt = prompt + " [Y/n] "
else:
prompt = prompt + " [y/N] "
choice = input(prompt).lower()
if choice in y:
return True
elif choice in n:
return False
elif choice == '':
if default_yes:
return True
else:
return False
else:
print("Choose yes/y or no/n")
def eprint(*args, **kwargs):
return None #print(*args, file=sys.stderr, **kwargs)
class ActivityLog():
def __init__(self):
self.commands = {
"checkdate": self.checkdate,
"setdate": self.setdate,
"log": self.push,
"dump": self.dump,
"dumptoday": self.dumpdate,
"todo": self.addtodo,
"todolist": self.listtodo,
"list": self.list,
"listtoday": self.listtoday,
"peek": self.peek,
"addproject": self.addproject,
"subproject": self.addsubproject,
"projects": self.listprojects,
"info": self.info,
"dumpstats": self.dumpstats
}
dirs = AppDirs(appname)
self.config = dirs.user_config_dir
if not os.path.exists(self.config):
if prompt_yn("Config dir: {} doesn't exist. Create?".format(self.config), default_yes=True):
os.mkdir(self.config)
print("Created config dir.")
else:
print("Can't continue without config dir, exiting.")
exit(0)
self.conffile = os.path.join(self.config, 'config.ini')
self.confini = ConfigParser()
# fallback is 80 cols / 25 lines
self.term_size = shutil.get_terminal_size((72,25))
# TODO: Currently hardcoded config path
if not os.path.exists(self.conffile):
# Create defaults
self.data = dirs.user_data_dir
self.confini['Settings'] = {'Date':'today', 'DataPath':self.data}
with open(self.conffile, 'w') as c:
self.confini.write(c)
else:
self.confini.read(self.conffile)
if self.confini['Settings']['DataPath']:
self.data = self.confini['Settings']['DataPath']
else:
self.data = dirs.user_data_dir
if not os.path.exists(self.data):
if prompt_yn("Data dir: {} doesn't exist. Create?".format(self.data), default_yes=True):
os.mkdir(self.data)
print("Created data dir.")
else:
print("Can't continue without data dir, exiting.")
exit(0)
self.datafile = os.path.join(self.data, 'activity.db')
self.db = sqlite3.connect(self.datafile)
if self.db.execute('''
Select Count(name) From sqlite_master
Where type='table' And name='activities'
''').fetchone()[0] == 0:
self.db.execute('''
Create Table activities(
date Text Not Null,
time Text Not Null,
activity Text Not Null
);
''')
eprint("Created activities table")
if self.db.execute('''
Select Count(name) From sqlite_master
Where type='table' And name='todos'
''').fetchone()[0] == 0:
self.db.execute('''
Create Table todos(
at_date Text,
at_time Text,
activity Text Not Null,
done Int Not Null Default 0,
completed_on Text,
done_by Int
);
''')
eprint("Created todos table")
if self.db.execute('''
Select Count(name) From sqlite_master
Where type='table' And name='projects'
''').fetchone()[0] == 0:
self.db.execute('''
Create Table projects(
added_on Text,
target_date Text,
project_title Text,
project_desc Text,
done Int Not Null Default 0,
done_by Int
);
''')
eprint("Created projects table")
if self.db.execute('''
Select Count(name) From sqlite_master
Where type='table' And name='subprojects'
''').fetchone()[0] == 0:
self.db.execute('''
Create Table subprojects(
added_on Text,
target_date Text,
subproject Text,
done Int Not Null Default 0,
belongs_to Int Not Null Default 0,
subproject_index Int Not Null,
done_by Int
);
''')
eprint("Created subprojects table")
if self.confini['Settings']['Date'].lower() == 'today':
self.thedate = date.today().isoformat()
else:
try:
self.thedate = date.fromisoformat(self.confini['Settings']['Date'])
except ValueError as e:
eprint("Error in config file {}: Date must be 'today' or a date in ISO format. ({})".format(self.conffile, e))
eprint("Defaulting to 'today'.")
self.thedate = date.today().isoformat()
# activitylog decorator
def al_command(func):
def wrapper(self, *args, **kwargs):
eprint("{} version {}".format(app_displayname, appversion))
#eprint("action: {}".format(func.__name__))
eprint("-"*10)
eprint("Using config : {}".format(self.conffile))
eprint("Using data : {}".format(self.datafile))
eprint("-"*10)
eprint()
func(self, *args, **kwargs)
return self.db.close()
return wrapper
def register_subparsers(self, sp):
for name, command in self.commands.items():
if command == self.checkdate:
ap = sp.add_parser(name, help="Check the current log date", description="Check the current log date.")
if command == self.setdate:
ap = sp.add_parser(name, help="Set the current log date", description="Set the current log date.")
ap.add_argument('entry_date')
if command == self.push:
ap = sp.add_parser(name, help="Log an activity entry", description="Log an activity entry. Use [todo#XXX] to mark a completed to-do entry. Use [proj#XXX.XXX] to mark a subproject as complete. Use [proj#XXX] to mark a project as complete.")
ap.add_argument('entry_string')
ap.add_argument('--date', '-d', help="Date for the entry")
ap.add_argument('--time', '-t', help="Time for the entry")
if command == self.dump:
ap = sp.add_parser(name, help="Dump the activity database", description="Dump the activity database into CSV formatted output.")
if command == self.dumpdate:
ap = sp.add_parser(name, help="Dump the activity database for the day", description="Dump the activity database for the specified day into CSV formatted output.")
if command == self.list:
ap = sp.add_parser(name, help="List every logged activity", description="List every logged activity as neatly formatted output.")
ap.add_argument('--count', '-c', help="Count how many activities are there for the day", action='store_true')
if command == self.listtoday:
ap = sp.add_parser(name, help="List every activity for the day", description="List every logged activity as neatly formatted output for the specified day.")
ap.add_argument('--count', '-c', help="Count how many activities are there for the day", action='store_true')
if command == self.addtodo:
ap = sp.add_parser(name, help="Add a to-do entry", description="Add a to-do entry.")
ap.add_argument('entry_string')
ap.add_argument('--on', '-d', help="Date for the specified to-do")
ap.add_argument('--at', '-t', help="Time for the specified to-do")
if command == self.listtodo:
ap = sp.add_parser(name, help="List to-dos", description="Neatly format a to-do list.")
ap.add_argument('--all', '-a', help="Include entries marked as done", action='store_true')
ap.add_argument('--num', '-n', help="View one to-do. Overrides --all.", type=int)
ap.add_argument('--left', '-l', help="View to-do's left to be done. Overrides --all.", action='store_true')
if command == self.peek:
ap = sp.add_parser(name, help="Look at a specified activity ID", description="Look at a specified activity ID, useful to see which activity was referred by todolist -a.")
ap.add_argument('id')
if command == self.addproject:
ap = sp.add_parser(name, help="Add a project", description="Add a project.")
ap.add_argument('project_string')
ap.add_argument('--by', '-d', help="Target overall completion date for the project")
ap.add_argument('--desc', '-e', help="Describe the project")
if command == self.addsubproject:
ap = sp.add_parser(name, help="Add a subproject / task to the project", description="Add a subproject / task to the project.")
ap.add_argument('project_id', type=int)
ap.add_argument('subproject_string')
ap.add_argument('--by', '-d', help="Target overall completion date for the subproject")
if command == self.listprojects:
ap = sp.add_parser(name, help="List current projects", description="List current projects as neatly formatted output.")
ap.add_argument('--all', '-a', help="Include projects marked as done", action='store_true')
ap.add_argument('--num', '-n', help="View one project. Overrides --all.", type=int)
if command == self.info:
ap = sp.add_parser(name, help="View stats", description="View information and stats about your activities.")
if command == self.dumpstats:
ap = sp.add_parser(name, help="Dump the activity counter", description="Dump the activity counter for each day as CSV-formatted output.")
@al_command
def info(self, args):
total = self.db.execute('''
Select Count(*) From activities;
''').fetchone()[0]
day_total = self.db.execute('''
Select Count(*) From activities
Where date = ?;
''',(self.thedate,)).fetchone()[0]
day_high = self.db.execute('''
Select date, Max(count) From (
Select date As date, Count(*) as count
From activities
Group By date
);
''').fetchone()
day_low = self.db.execute('''
Select date, Min(count) From (
Select date As date, Count(*) as count
From activities
Group By date
);
''').fetchone()
day_avg = self.db.execute('''
Select Avg(count) From (
Select date As date, Count(*) as count
From activities
Group By date
);
''').fetchone()[0]
todo_done = self.db.execute('''
Select Count(*) From todos
Where done_by Not Null;
''').fetchone()[0]
project_done = self.db.execute('''
Select Count(*) From projects
Where done_by Not Null;
''').fetchone()[0]
subproject_done = self.db.execute('''
Select Count(*) From subprojects
Where done_by Not Null;
''').fetchone()[0]
print("Total activities logged so far : {}".format(total))
print("Total activities logged for today : {}".format(day_total))
print("---")
print("Most productive day : {} ({} activities)".format(day_high[0], day_high[1]))
print("Least productive day : {} ({} activities)".format(day_low[0], day_low[1]))
print("---")
if day_avg:
print("Average \"productivity\" : {:.2f}".format(day_avg))
print("---")
print("Completed todos : {}".format(todo_done))
print("Completed projects : {}".format(project_done))
print("Completed subprojects : {}".format(subproject_done))
@al_command
def dumpstats(self, args):
eprint("CSV dump of per-day activity count")
query = self.db.execute('''
Select date As date, Count(*) As count From activities
Group By date;
''')
self.csv_dump_common(query)
@al_command
def checkdate(self, args):
print("Currently set date is {} ({})".format(self.thedate, self.confini['Settings']['Date']))
@al_command
def setdate(self, args):
date_input = args.entry_date.strip()
if date_input.lower() == 'today':
self.confini['Settings']['Date'] = date_input
self.confini['Settings']['Date'] = date_input
else:
try:
isodate = date.fromisoformat(date_input)
except ValueError as e:
eprint("Error: Date must be 'today' or a date in ISO format ({})".format(e))
exit(1)
finally:
self.confini['Settings']['Date'] = date_input
with open(self.conffile, 'w') as c:
self.confini.write(c)
print("Date successfully changed to {}".format(date_input))
@al_command
def push(self, args):
if args.date and args.time:
d = args.date
t = args.time
else:
d = self.thedate
t = datetime.now().time().isoformat('seconds')
i = self.db.execute('''
Insert Into activities(date, time, activity)
Values (?, ?, ?);
''', (d, t, args.entry_string))
r = i.lastrowid
print("Logged activity [{} {}]:".format(d, t))
has_todo = re.findall(r'\[todo#(\d+)\]', args.entry_string)
a_str = re.sub(r'\[todo#(\d+)\]', '', args.entry_string).strip()
has_proj = re.findall(r'\[proj#(\d+).?(\d+)?\]', a_str)
a_str = re.sub(r'\[proj#(\d+).?(\d+)?\]', '', a_str).strip()
wrap_act = tw(width=self.term_size.columns, initial_indent=' '*4,subsequent_indent=' '*4)
print(wrap_act.fill('\033[1m{}\033[0m'.format(a_str)))
if has_todo:
for todo_id in has_todo:
todo_id = int(todo_id)
if self.db.execute('Select 1 From todos Where rowid=?',(todo_id,)).fetchone():
self.db.execute('''
Update todos
Set done = 1, completed_on = ?, done_by = ?
Where rowid = ?
''', ("{} {}".format(d,t), i.lastrowid, todo_id))
print(wrap_act.fill('[Completed todo #{}]'.format(todo_id)))
if has_proj:
for proj, subproj in has_proj:
proj_id = int(proj)
if subproj == '':
if self.db.execute('Select 1 From projects Where rowid=?',(proj_id,)).fetchone():
self.db.execute('''
Update projects
Set done = 1, done_by = ?
Where rowid = ?
''', (i.lastrowid, proj_id))
print(wrap_act.fill('[Completed project #{}]'.format(proj_id)))
else:
subproj_id = int(subproj)
if self.db.execute('Select 1 From subprojects Where belongs_to=? And subproject_index=?',(proj_id,subproj_id,)).fetchone():
self.db.execute('''
Update subprojects
Set done = 1, done_by = ?
Where belongs_to=? And subproject_index=?
''', (i.lastrowid, proj_id, subproj_id))
print(wrap_act.fill('[Completed subproject #{}.{}]'.format(proj_id, subproj_id)))
self.db.commit()
def vali_date(self, date_string):
date_input = date_string.strip()
try:
isodate = date.fromisoformat(date_input)
except ValueError as e:
eprint("Error: Date must be a date in ISO format ({})".format(e))
exit(1)
finally:
return date_input
@al_command
def addtodo(self, args):
d = None
t = None
if args.on:
d = self.vali_date(args.on)
if args.at:
time_input = args.at.strip()
if re.match(r'[012][0-9]:\d{2}', time_input):
t = time_input
else:
eprint("Error: Time must be in HH:MM format")
exit(1)
i = self.db.execute('''
Insert Into todos(at_date, at_time, activity)
Values (?, ?, ?);
''', (d, t, args.entry_string))
if d is None:
d = "no date"
if t is None:
t = "no time"
r = i.lastrowid
wrap_act = tw(width=self.term_size.columns, initial_indent=' '*4,subsequent_indent=' '*4)
print("Created todo {}:".format(i.lastrowid))
print(wrap_act.fill('\033[1m{}\033[0m'.format(args.entry_string)))
print(wrap_act.fill('(due {} at {})'.format(d,t)))
self.db.commit()
@al_command
def listtodo(self, args):
eprint("Todo list")
if args.num:
query = self.db.execute('''
Select rowid, * From todos
Where rowid=?;
''',(args.num,))
elif args.left:
query = self.db.execute('''
Select Count(rowid) From todos
Where done=0;
''')
elif args.all:
query = self.db.execute('''
Select rowid, * From todos
Order By at_date Asc, at_time Asc;
''')
else:
query = self.db.execute('''
Select rowid, * From todos
Where done=0
Order By at_date Asc, at_time Asc;
''')
if args.left:
print(list(query)[0][0])
else:
for i in list(query):
row_id = i[0]
at_date = i[1]
at_time = i[2]
activity = i[3]
done = i[4]
completed_on = i[5]
done_by = i[6]
if args.all:
if done > 0:
done_string = "[X]"
else:
done_string = "[ ]"
else:
done_string = "-"
if at_date:
due_string = "(due {} at --:--)".format(at_date)
if at_time:
due_string = "(due {} at {})".format(at_date, at_time)
elif at_time:
due_string = "(due ---------- at {})".format(at_time)
else:
due_string = "------------------------ "
if completed_on:
complete_string = "(completed on {})".format(completed_on)
if done_by:
complete_string = "(completed on {} by activity #{})".format(completed_on, done_by)
else:
complete_string = ""
prefix = "{} #{:<4} ".format(done_string, row_id)
wrap_mes = tw(width=self.term_size.columns, initial_indent=prefix, subsequent_indent=' '*len(prefix))
wrap_due = tw(width=self.term_size.columns, initial_indent=' '*(len(prefix)+4), subsequent_indent=' '*(len(prefix)+4))
print(wrap_mes.fill('\033[1m{}\033[0m'.format(activity)))
print(wrap_due.fill(due_string))
print(wrap_due.fill(complete_string))
if completed_on: print()
def csv_dump_common(self, query):
# used by dumptoday and dump
for i in query.description:
print(i[0], end=';')
print()
for i in list(query):
for j in i:
print(j, end=';')
print()
@al_command
def dumpdate(self, args):
eprint("CSV dump of activities on {}".format(self.thedate))
query = self.db.execute('''
Select * From activities
Where date=?
Order By time;
''', (self.thedate,))
self.csv_dump_common(query)
@al_command
def dump(self, args):
eprint("CSV dump of all activities")
query = self.db.execute('''
Select * From activities
Order By date, time;
''')
self.csv_dump_common(query)
def list_common(self, query_entry):
# used by listtoday and list
a_num = query_entry[0]
a_date = query_entry[1]
a_time = query_entry[2]
a_has_todo = re.findall(r'\[todo#(\d+)\]', query_entry[3])
a_has_proj = re.findall(r'\[proj#(\d+).?(\d+)?\]', query_entry[3])
a_str = re.sub(r'\[todo#(\d+)\]', '', query_entry[3]).strip()
a_str = re.sub(r'\[proj#(\d+).?(\d+)?\]', '', a_str).strip()
prefix = "{:>5}> ({} {}) ".format(a_num, a_date, a_time)
wrap_act = tw(width=self.term_size.columns, initial_indent=prefix, subsequent_indent=' '*len(prefix))
print(wrap_act.fill('\033[1m{}\033[0m'.format(a_str)))
if a_has_todo:
for todo in a_has_todo:
wrap_todo = tw(width=self.term_size.columns, initial_indent=' '*29, subsequent_indent=' '*29)
print(wrap_todo.fill('[Completes todo #{}]'.format(todo)))
if a_has_proj:
for proj, subproj in a_has_proj:
wrap_proj = tw(width=self.term_size.columns, initial_indent=' '*29, subsequent_indent=' '*29)
if subproj == '':
print(wrap_proj.fill('[Completes project #{}]'.format(proj)))
else:
print(wrap_proj.fill('[Completes subproject #{}.{}]'.format(proj, subproj)))
@al_command
def listtoday(self, args):
query = self.db.execute('''
Select rowid, * From activities
Where date=?
Order By time;
''', (self.thedate,))
activities = list(query)
if args.count:
print(len(activities))
else:
print("\033[1mList of activities on {}\033[0m".format(self.thedate))
for i in activities:
self.list_common(i)
@al_command
def list(self, args):
query = self.db.execute('''
Select rowid, * From activities
Order By date, time;
''')
activities = list(query)
if args.count:
print(len(activities))
else:
print("\033[1mList of all activities\033[0m")
for i in activities:
self.list_common(i)
@al_command
def peek(self, args):
query = self.db.execute('''
Select rowid, * From activities
Where rowid=?;
''',(args.id,))
peek_result = query.fetchone()
if peek_result:
a_num = peek_result[0]
a_date = peek_result[1]
a_time = peek_result[2]
a_has_todo = re.search(r'\[todo#(\d+)\]', peek_result[3])
a_str = re.sub(r'\[todo#(\d+)\]', '', peek_result[3]).strip()
print("Activity {} [{} {}]:".format(a_num, a_date, a_time))
wrap_act = tw(width=self.term_size.columns, initial_indent=' '*4,subsequent_indent=' '*4)
print(wrap_act.fill('\033[1m{}\033[0m'.format(a_str)))
if a_has_todo:
print(wrap_act.fill('[Completes todo #{}]'.format(a_has_todo.groups()[0])))
else:
print('Activity {} not found'.format(args.id))
@al_command
def addproject(self, args):
by = None
if args.by:
by = self.vali_date(args.by)
desc = None
if args.desc:
desc = args.desc
d = self.thedate
t = datetime.now().time().isoformat('seconds')
i = self.db.execute('''
Insert Into projects(added_on, target_date, project_title, project_desc)
Values (?, ?, ?, ?);
''', ('{} {}'.format(d,t), by, args.project_string, desc))
r = i.lastrowid
print("Created project {} [{} {}]".format(r, d, t))
if by:
print("(Due by {})".format(by))
wrap_act = tw(width=self.term_size.columns, initial_indent=' '*4,subsequent_indent=' '*4)
print(wrap_act.fill('\033[1m{}\033[0m'.format(args.project_string)))
if desc:
wrap_desc = tw(width=self.term_size.columns, initial_indent=' '*6,subsequent_indent=' '*6)
print(wrap_desc.fill('{}'.format(desc)))
self.db.commit()
@al_command
def addsubproject(self, args):
d = self.thedate
t = datetime.now().time().isoformat('seconds')
by = None
if args.by:
by = self.vali_date(args.by)
get_project_id = self.db.execute('''
Select rowid From projects
Where rowid=?;
''',(args.project_id,))
project_id_valid = get_project_id.fetchone()
if project_id_valid:
get_last_subproject_id = self.db.execute('''
Select Max(subproject_index) From subprojects
Where belongs_to=?;
''',(args.project_id,))
last_subproject_id = get_last_subproject_id.fetchone()[0]
if last_subproject_id is not None:
current_index = int(last_subproject_id) + 1
else:
current_index = 0
i = self.db.execute('''
Insert Into subprojects(added_on, target_date, subproject, belongs_to, subproject_index)
Values (?, ?, ?, ?, ?);
''', ('{} {}'.format(d,t), by, args.subproject_string, args.project_id, current_index))
print("Created subproject {}.{} [{} {}]".format(args.project_id, current_index, d, t))
if by:
print("(Due by {})".format(by))
wrap_act = tw(width=self.term_size.columns, initial_indent=' '*4,subsequent_indent=' '*4)
print(wrap_act.fill('\033[1m{}\033[0m'.format(args.subproject_string)))
self.db.commit()
else:
print("Can't find project {} -- project_id must be valid".format(args.project_id))
@al_command
def listprojects(self, args):
query_string = 'Select rowid, * From projects '
if args.num:
query_string += 'Where rowid=?;'
projects_list = self.db.execute(query_string, (args.num,))
elif args.all:
query_string += ';'
projects_list = self.db.execute(query_string)
else:
query_string += 'Where done=0;'
projects_list = self.db.execute(query_string)
for proj_id, added, target, title, desc, done, done_by in projects_list:
if args.all or args.num:
if done:
done_string = "[x]"
else:
done_string = "[ ]"
else:
done_string = "-"
complete_string = ""
prefix = "{} #{:<4} ".format(done_string, proj_id)
wrap_mes = tw(width=self.term_size.columns, initial_indent=prefix, subsequent_indent=' '*len(prefix))
wrap_desc = tw(width=self.term_size.columns, initial_indent=' '*len(prefix), subsequent_indent=' '*len(prefix))
print(wrap_mes.fill('\033[1m{}\033[0m'.format(title)))
if done_by:
print(wrap_desc.fill("(completed by activity #{})".format(done_by)))
if target:
print(wrap_desc.fill('\033[3m(Due by {})\033[0m'.format(target)))
if desc:
print()
print(wrap_desc.fill('{}'.format(desc)))
subprojects = self.db.execute('''
Select * From subprojects
Where belongs_to = ?;
''', (proj_id,))
subproject_list = []
first_subproject = subprojects.fetchone()
subproject_list.append(first_subproject)
if first_subproject:
print()
print(wrap_desc.fill('==== Subprojects ===='))
for sp in subprojects.fetchall():
subproject_list.append(sp)
for sp in subproject_list:
if sp:
added, target, subproject, done, belongs_to, index, done_by = sp
if done:
done_string = "[X]"
else:
done_string = "[ ]"
prefix = " {} #{:<7} ".format(done_string, '{}.{}'.format(belongs_to, index))
wrap_sub = tw(width=self.term_size.columns, initial_indent=prefix, subsequent_indent=' '*len(prefix))
wrap_due = tw(width=self.term_size.columns, initial_indent=' '*len(prefix), subsequent_indent=' '*len(prefix))
if args.all:
print(wrap_sub.fill('\033[1m{}\033[0m'.format(subproject)))
if done_by:
print(wrap_due.fill("(completed by activity #{})".format(done_by)))
if target:
print(wrap_due.fill('\033[3m(due by {})\033[0m'.format(target)))
elif not done:
print(wrap_sub.fill('\033[1m{}\033[0m'.format(subproject)))
if done_by:
print(wrap_due.fill("(completed by activity #{})".format(done_by)))
if target:
print(wrap_due.fill('\033[3m(due by {})\033[0m'.format(target)))
print()
al = ActivityLog()
ap = ArgumentParser(description="Simple activity logger. Might help your productivity.")
sp = ap.add_subparsers(dest='action', help='Action to take', required=True)
al.register_subparsers(sp)
args = ap.parse_args()
al.commands[args.action](args)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment