From c6d96f9ce2895ec8dbae54a33cf9d0b1e61fdcb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Mandera?= Date: Mon, 10 Mar 2014 14:46:38 +0100 Subject: [PATCH 1/9] Add shebang line, fix line terminators, file privileges * add shebang line to allow execution directly from commandline * fix line terminator to make it work on linux * remove +x from COPYING --- COPYING | 0 Punch.py | 1151 +++++++++++++++++++++++++++--------------------------- 2 files changed, 576 insertions(+), 575 deletions(-) mode change 100755 => 100644 COPYING diff --git a/COPYING b/COPYING old mode 100755 new mode 100644 diff --git a/Punch.py b/Punch.py index ef11a5c..a0307fa 100755 --- a/Punch.py +++ b/Punch.py @@ -1,575 +1,576 @@ -''' -Created on Mar 5, 2009 - -@author: Keith Lawless (keith at keithlawless dot com) - - Copyright 2009 Keith Lawless - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -''' -from os.path import abspath, exists, join -from os import pathsep, getenv -import cPickle -import os.path -import os -import shutil -import sys -import time - -from optparse import OptionParser - - -# -# Define some exceptions that our application can raise. These are used -# to exit the program gracefully, and control the error message displayed -# to the user when the program exits. -# - -class PunchCommandError(ValueError): - """Used to indicate that an invalid command was passed to Punch""" - - -class ToDoConfigNotFoundError(IOError): - """Used to indicate that todo.cfg was not found on the path""" - - -class ToDoFileNotFoundError(IOError): - """Used to indicate that todo.txt was not found on the path""" - - -class TaskFileNotFoundError(IOError): - """Used to indicate that the user specified task file was not found""" - - -class TaskNotFoundError(IOError): - """Used to indicate that the task number specified does not exist in the task file""" - -class NoOpenTaskError(IOError): - """Used to indicate that an 'out' command was issued, but the last task was already closed out.""" - -class DateFormatError(IOError): - """Used to indicate that a poorly formatted date was passed where a date was expected.""" - -class Punch(object): - - timestampFormat = '%Y%m%dT%H%M%S' - - def __init__(self, optlist, args): - self.optlist = optlist - self.args = args - - def execute(self): - """Execute the command - either 'in' or 'out'""" - if( self.args[0] == 'in' ): - self.execute_in() - elif( self.args[0] == 'out' ): - self.execute_out() - elif( self.args[0] in ['wh','what'] ): - self.execute_wh() - elif( self.args[0] in ['report', 'rep'] ): - self.execute_rep() - elif( self.args[0] in ['archive', 'ar'] ): - self.execute_ar() - else: - raise PunchCommandError - - def search_file(self, files, paths): - file_found = 0 - for filename in files: - for path in paths: - if path != None: - if exists(join(path, filename)): - file_found = 1 - break - if file_found: - break - if file_found: - return abspath(join(path, filename)) - else: - return None - - def parse_config(self): - """Parse the user's todo.cfg file and place the elements into a dictionary""" - try: - paths = [ getenv("HOME"), "."] - files = [ "todo.cfg", ".todo.cfg" ] - if getenv("TODOTXT_CFG_FILE") == None: - configFileName = self.search_file(files, paths) - else: - configFileName = getenv("TODOTXT_CFG_FILE") - if configFileName == None: - raise ToDoConfigNotFoundError - configFile = open( configFileName ) - self.propDict = dict() - for propLine in configFile: - propDef = propLine.strip() - if len(propDef) == 0: - continue - if propDef[0] in ( '#' ): - continue - if propDef[0:6] == 'export': - propDef = propDef[7:] - punctuation = [ propDef.find(c) for c in '= ' ] + [ len(propDef) ] - found = min( [ pos for pos in punctuation if pos != -1 ] ) - name= propDef[:found].rstrip() - value= propDef[found:].lstrip(":= ").rstrip() - self.propDict[name]= value.strip('"') - configFile.close() - - # Add the users environment variables to the propDict, unless - # a value has already been set. - for key in os.environ.keys(): - if self.propDict.has_key(key) == False: - self.propDict[key] = os.environ[key] - - except IOError: - raise ToDoConfigNotFoundError - - def resolve(self,value): - """Replace variables in a config entry with the actual value.""" - token = value.find('$') - if( token != -1 ): - terminus = token + value[token:].find('/') - ref = value[token+1:terminus] - refValue = self.propDict[ref] - value = refValue + value[terminus:] - - return value - - def open_todo(self): - """Open the user's todo.txt file.""" - try: - self.taskFile = open( self.resolve( self.propDict['TODO_FILE']), 'U' ) - except IOError: - raise ToDoFileNotFoundError - - def open_file(self,filename): - """Open a file given a filename.""" - try: - name = self.resolve( self.propDict['TODO_DIR'] + "/" + filename ) - self.taskFile = open( name, 'U' ) - except IOError: - raise TaskFileNotFoundError - - def close_task_file(self): - """Close the file taskFile - either todo.txt or a user supplied file.""" - self.taskFile.close() - - def open_punch_file(self,mode='a'): - """Open the output file - punch.dat - in the user's TODO_DIR.""" - name = self.resolve( self.propDict['TODO_DIR'] + "/punch.dat" ) - - if not os.path.exists(name): - open( name, 'w' ).close() - - self.punchFile = open( name, mode ) - - def close_punch_file(self): - """Close the output file - punch.csv.""" - self.punchFile.close() - - def open_punch_backup_file(self): - """Open the backup file - punch.dat.backup - in the user's TODO_DIR.""" - name = self.resolve( self.propDict['TODO_DIR'] + "/punch.dat.backup" ) - self.backupFile = open( name, 'w' ) - - def close_punch_backup_file(self): - """Close the output file - punch.csv.""" - self.backupFile.close() - - def backup_punch_file(self): - self.open_punch_file('r') - self.open_punch_backup_file() - shutil.copyfileobj(self.punchFile,self.backupFile) - self.close_punch_backup_file() - self.close_punch_file() - - def open_archive_file(self,mode='a'): - """Open the archive file - punch.archive - in the user's TODO_DIR.""" - name = self.resolve( self.propDict['TODO_DIR'] + "/punch.archive" ) - self.archiveFile = open( name, mode ) - - def close_archive_file(self): - """Close the archive file - punch.archive.""" - self.archiveFile.close() - - def get_last_punch_rec(self): - """Returns last line in the output file as a list of fields.""" - lastrec = [] - try: - self.open_punch_file('r') - lines = self.punchFile.readlines() - if( len(lines) > 0): - lastline = (lines[len(lines)-1]).strip() - lastrec = lastline.split('\t') - else: - lastrec = [] - self.close_punch_file() - except IOError: - lastrec = [] - - return lastrec - - def punch_rec_complete(self,rec): - """Returns true if the punch record is complete - that - is, contains a task, start timestamp, and end timestamp""" - - if len(rec) == 0: - isComplete = True - elif len(rec) == 3: - isComplete = True - else: - isComplete = False - - return isComplete - - def last_punch_line_complete(self): - lastrec = self.get_last_punch_rec() - return self.punch_rec_complete(lastrec) - - def get_time(self): - return time.strftime( self.timestampFormat, time.localtime()) - - def translate_time_to_secs(self,timestamp): - return time.strptime( timestamp[0:15], self.timestampFormat ) - - def get_duration(self,startTimestamp,endTimestamp): - minutes = self.get_duration_in_minutes(startTimestamp, endTimestamp) - return self.format_minutes(minutes) - - def get_duration_in_minutes(self,startTimestamp,endTimestamp): - start = self.translate_time_to_secs( startTimestamp ) - end = self.translate_time_to_secs( endTimestamp ) - - minutes = ( time.mktime(end) - time.mktime(start) ) // 60 - - return minutes - - def format_minutes(self,minutes): - retString = '(' - - if( minutes > 60 ): - hours = minutes // 60 - minutes = minutes - (hours * 60) - retString = retString + str(int(hours)) + ' hours ' - - retString = retString + str(int(minutes)) + ' minutes)' - - return retString - - - def add_literal_line(self,line): - """ - Add a new line to punch.dat containing task,start-timestamp - where task is a literal string (usually in the format '+project'). - """ - - # If previous output line wasn't closed by issuing an 'out' command, then - # do so now. - if self.last_punch_line_complete() == False: - self.add_out_line() - - rec = '%s\t%s' % (line, self.get_time()) - self.open_punch_file() - self.punchFile.write(rec) - self.close_punch_file() - print "Start timer on: " + line - - def add_in_line(self,line_num): - """ - Add a new line to punch.csv containing task,start-timestamp - where task is line 'line_num' from self.taskFile - """ - - # If previous output line wasn't closed by issuing an 'out' command, then - # do so now. - if self.last_punch_line_complete() == False: - self.add_out_line() - - lines = self.taskFile.readlines() - if( line_num > len(lines)): - raise TaskNotFoundError - line = lines[line_num-1].strip() - rec = '%s\t%s' % (line, self.get_time()) - self.open_punch_file() - self.punchFile.write(rec) - self.close_punch_file() - print "Start timer on: " + line - - def add_out_line(self): - """ - Add the 'out' timestamp to the last line of the file - and append the EOL. - """ - - # If last output line was already closed by issuing an 'out' command, then - # raise an exception. - lastrec = self.get_last_punch_rec() - if self.punch_rec_complete(lastrec): - raise NoOpenTaskError - - rec = '\t%s\n' % self.get_time() - - self.open_punch_file() - self.punchFile.write(rec) - self.close_punch_file() - - print "Stop timer on: " + lastrec[0] - - def execute_in(self): - """The logic for the 'in' command.""" - self.parse_config() - - """If only argument is passed, then it is an error.""" - if( len(self.args) == 1 ): - raise PunchCommandError - - """ - If only two arguments are passed, then there are three possibilities: - (1) An integer was passed, referencing a line in todo.txt (ie. punch in 7) - (2) A project name was passed, using the special '+project-name' syntax - (3) The user made a mistake. - """ - if( len(self.args) == 2 ): - # Check to see if the argument is number. - try: - line_num = int(self.args[1]) - except: - line_num = -1 - - if( line_num > -1 ): - self.open_todo() - self.add_in_line(line_num) - self.close_task_file() - else: - project = self.args[1].strip() - if( project[0] == '+' ): - self.add_literal_line(project) - else: - raise PunchCommandError - - """ - If three arguments are passed, then the last argument must be a task file (eg. projects.txt) - """ - if( len(self.args) == 3): - # Check to see if the argument is number. - try: - line_num = int(self.args[1]) - except: - line_num = -1 - - if( line_num > -1 ): - self.open_file(self.args[2]) - self.add_in_line(line_num) - self.close_task_file() - else: - raise PunchCommandError - - def execute_out(self): - """The logic for the 'out' command.""" - self.parse_config() - if( len(self.args) == 1 ): - self.add_out_line() - else: - raise PunchCommandError - - def execute_wh(self): - """The logic for the 'what' command.""" - self.parse_config() - if( len(self.args) == 1 ): - lastrec = self.get_last_punch_rec() - if( len(lastrec) == 2 ): - duration = self.get_duration(lastrec[1], self.get_time()) - print "Active task: " + lastrec[0] + ' ' + duration - else: - print "No task is active." - else: - raise PunchCommandError - - def execute_rep(self): - """The logic for the 'report' command.""" - self.parse_config() - if( len(self.args) == 1 ): - dateDict = dict() - totalTimeDict = dict() - self.open_punch_file('r') - lines = self.punchFile.readlines() - - if( len(lines) == 0 ): - print "There are no tasks in the data file." - else: - for line in lines: - rec = line.split('\t') - if( len(rec) == 3 ): - task = rec[0] - start = rec[1] - end = rec[2] - duration = self.get_duration_in_minutes(start,end) - dateKey = time.strftime( '%Y%m%d', self.translate_time_to_secs(start)) - - # Create a tree of dates that have time reported against them - if( dateKey in dateDict.keys()): - dateValue = dateDict[dateKey] - else: - dateValue = dict() - - # Create a simple dictionary of total elapsed time per date - if( dateKey in totalTimeDict.keys()): - totalTimeValue = int(totalTimeDict[dateKey]) - else: - totalTimeValue = 0 - - # For each date in the tree, store a subtree with - # unique tasks for the date - if( task in dateValue.keys()): - timeList = dateValue[task] - else: - timeList = list() - - # Populate the tree nodes. - timeList.append(duration) - dateValue[task] = timeList - dateDict[dateKey] = dateValue - - # Store total elapsed time for the entire date. - totalTimeValue = totalTimeValue + duration - totalTimeDict[dateKey] = totalTimeValue - - # Returned keys are untyped. Copy into a list of strings so we can sort. - dateNoneList = dateDict.keys() - dateList = list() - for dateThing in dateNoneList: - dateList.append(str(dateThing)) - dateList.sort() - - for dateKey in dateList: - print dateKey[0:4] + '-' + dateKey[4:6] + '-' + dateKey[6:] + ' ' + self.format_minutes(totalTimeDict[dateKey]) +':' - taskDict = dateDict[dateKey] - taskNoneList = taskDict.keys() - taskList = list() - for taskThing in taskNoneList: - taskList.append(str(taskThing)) - taskList.sort() - for taskKey in taskList: - minuteList = taskDict[taskKey] - sum = 0.0 - for m in minuteList: - sum = sum + m - print '\t' + taskKey + ' ' + self.format_minutes(sum) - # Giant else statement ends here. :) - - self.close_punch_file() - else: - raise PunchCommandError - - def execute_ar(self): - """The logic for the 'archive' command.""" - self.parse_config() - if( len(self.args) == 2 ): - #Make sure date argument can be parsed into a date - #in the past. - try: - archiveTs = args[1] + "T23:59:59" - archiveDate = time.strptime( archiveTs, '%Y-%m-%dT%H:%M:%S' ) - archiveTime = time.mktime( archiveDate ) - except: - raise DateFormatError - - #Back up the punch file - self.backup_punch_file() - - #Read the punch file into memory - self.open_punch_file('r') - lines = self.punchFile.readlines() - self.close_punch_file() - - #Open the archive file in append mode - self.open_archive_file() - - #Open the punch file in (destructive) write mode - self.open_punch_file('w') - - #Iterate through tasks in memory, either writing to the - #archive file or the (new) punch file, based on start timestamp - for line in lines: - rec = line.split('\t') - if( self.punch_rec_complete(rec)): - startTime = time.mktime( time.strptime( rec[1], self.timestampFormat )) - if( startTime < archiveTime ): - self.archiveFile.write(line) - else: - self.punchFile.write(line) - else: - self.punchFile.write(line) - - #Close the files. - self.close_punch_file() - self.close_archive_file() - - else: - raise PunchCommandError - -# -# The entry point for the script. -# - -if __name__ == '__main__': - try: - usage = \ -""" -Punch.py [-h] command [line-number] [filename] [archive-date] - - Commands: - 'in' : start the timer for a todo task [line-number] - 'out' : stop the timer for the current task - 'what' : print the current 'active' task. shortcut is 'wh' - 'report' : print a report. shortcut is 'rep' - 'archive' : archive all time records previous to [archive-date] inclusive - - line-number is the number of the item in the todo.txt file (or filename) -""" - - version = \ -""" - Punch.py - A time tracker for todo.sh - Version 1.2 - Author: Keith Lawless (keith@keithlawless.com) - Last updated: July 6,2009 - License: GPL, http://www.gnu.org/copyleft/gpl.html -""" - - parser = OptionParser(usage=usage,version=version) - optlist, args = parser.parse_args() - - if (( len(args) < 1 ) or ( len(args) > 3 )): - raise PunchCommandError - else: - punch = Punch(optlist,args) - punch.execute() - except PunchCommandError: - print usage - except ToDoConfigNotFoundError: - print "Error: Could not find configuration file (todo.cfg)" - except ToDoFileNotFoundError: - print "Error: Could not find todo.txt" - except TaskFileNotFoundError: - print "Error: Could not find file." - except TaskNotFoundError: - print "Error: Item number not found in file." - except NoOpenTaskError: - print "Error: No incomplete task found." - except DateFormatError: - print "Error: Could not translate your input into a date." - \ No newline at end of file +#!/usr/bin/env python +''' +Created on Mar 5, 2009 + +@author: Keith Lawless (keith at keithlawless dot com) + + Copyright 2009 Keith Lawless + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +''' +from os.path import abspath, exists, join +from os import pathsep, getenv +import cPickle +import os.path +import os +import shutil +import sys +import time + +from optparse import OptionParser + + +# +# Define some exceptions that our application can raise. These are used +# to exit the program gracefully, and control the error message displayed +# to the user when the program exits. +# + +class PunchCommandError(ValueError): + """Used to indicate that an invalid command was passed to Punch""" + + +class ToDoConfigNotFoundError(IOError): + """Used to indicate that todo.cfg was not found on the path""" + + +class ToDoFileNotFoundError(IOError): + """Used to indicate that todo.txt was not found on the path""" + + +class TaskFileNotFoundError(IOError): + """Used to indicate that the user specified task file was not found""" + + +class TaskNotFoundError(IOError): + """Used to indicate that the task number specified does not exist in the task file""" + +class NoOpenTaskError(IOError): + """Used to indicate that an 'out' command was issued, but the last task was already closed out.""" + +class DateFormatError(IOError): + """Used to indicate that a poorly formatted date was passed where a date was expected.""" + +class Punch(object): + + timestampFormat = '%Y%m%dT%H%M%S' + + def __init__(self, optlist, args): + self.optlist = optlist + self.args = args + + def execute(self): + """Execute the command - either 'in' or 'out'""" + if( self.args[0] == 'in' ): + self.execute_in() + elif( self.args[0] == 'out' ): + self.execute_out() + elif( self.args[0] in ['wh','what'] ): + self.execute_wh() + elif( self.args[0] in ['report', 'rep'] ): + self.execute_rep() + elif( self.args[0] in ['archive', 'ar'] ): + self.execute_ar() + else: + raise PunchCommandError + + def search_file(self, files, paths): + file_found = 0 + for filename in files: + for path in paths: + if path != None: + if exists(join(path, filename)): + file_found = 1 + break + if file_found: + break + if file_found: + return abspath(join(path, filename)) + else: + return None + + def parse_config(self): + """Parse the user's todo.cfg file and place the elements into a dictionary""" + try: + paths = [ getenv("HOME"), "."] + files = [ "todo.cfg", ".todo.cfg" ] + if getenv("TODOTXT_CFG_FILE") == None: + configFileName = self.search_file(files, paths) + else: + configFileName = getenv("TODOTXT_CFG_FILE") + if configFileName == None: + raise ToDoConfigNotFoundError + configFile = open( configFileName ) + self.propDict = dict() + for propLine in configFile: + propDef = propLine.strip() + if len(propDef) == 0: + continue + if propDef[0] in ( '#' ): + continue + if propDef[0:6] == 'export': + propDef = propDef[7:] + punctuation = [ propDef.find(c) for c in '= ' ] + [ len(propDef) ] + found = min( [ pos for pos in punctuation if pos != -1 ] ) + name= propDef[:found].rstrip() + value= propDef[found:].lstrip(":= ").rstrip() + self.propDict[name]= value.strip('"') + configFile.close() + + # Add the users environment variables to the propDict, unless + # a value has already been set. + for key in os.environ.keys(): + if self.propDict.has_key(key) == False: + self.propDict[key] = os.environ[key] + + except IOError: + raise ToDoConfigNotFoundError + + def resolve(self,value): + """Replace variables in a config entry with the actual value.""" + token = value.find('$') + if( token != -1 ): + terminus = token + value[token:].find('/') + ref = value[token+1:terminus] + refValue = self.propDict[ref] + value = refValue + value[terminus:] + + return value + + def open_todo(self): + """Open the user's todo.txt file.""" + try: + self.taskFile = open( self.resolve( self.propDict['TODO_FILE']), 'U' ) + except IOError: + raise ToDoFileNotFoundError + + def open_file(self,filename): + """Open a file given a filename.""" + try: + name = self.resolve( self.propDict['TODO_DIR'] + "/" + filename ) + self.taskFile = open( name, 'U' ) + except IOError: + raise TaskFileNotFoundError + + def close_task_file(self): + """Close the file taskFile - either todo.txt or a user supplied file.""" + self.taskFile.close() + + def open_punch_file(self,mode='a'): + """Open the output file - punch.dat - in the user's TODO_DIR.""" + name = self.resolve( self.propDict['TODO_DIR'] + "/punch.dat" ) + + if not os.path.exists(name): + open( name, 'w' ).close() + + self.punchFile = open( name, mode ) + + def close_punch_file(self): + """Close the output file - punch.csv.""" + self.punchFile.close() + + def open_punch_backup_file(self): + """Open the backup file - punch.dat.backup - in the user's TODO_DIR.""" + name = self.resolve( self.propDict['TODO_DIR'] + "/punch.dat.backup" ) + self.backupFile = open( name, 'w' ) + + def close_punch_backup_file(self): + """Close the output file - punch.csv.""" + self.backupFile.close() + + def backup_punch_file(self): + self.open_punch_file('r') + self.open_punch_backup_file() + shutil.copyfileobj(self.punchFile,self.backupFile) + self.close_punch_backup_file() + self.close_punch_file() + + def open_archive_file(self,mode='a'): + """Open the archive file - punch.archive - in the user's TODO_DIR.""" + name = self.resolve( self.propDict['TODO_DIR'] + "/punch.archive" ) + self.archiveFile = open( name, mode ) + + def close_archive_file(self): + """Close the archive file - punch.archive.""" + self.archiveFile.close() + + def get_last_punch_rec(self): + """Returns last line in the output file as a list of fields.""" + lastrec = [] + try: + self.open_punch_file('r') + lines = self.punchFile.readlines() + if( len(lines) > 0): + lastline = (lines[len(lines)-1]).strip() + lastrec = lastline.split('\t') + else: + lastrec = [] + self.close_punch_file() + except IOError: + lastrec = [] + + return lastrec + + def punch_rec_complete(self,rec): + """Returns true if the punch record is complete - that + is, contains a task, start timestamp, and end timestamp""" + + if len(rec) == 0: + isComplete = True + elif len(rec) == 3: + isComplete = True + else: + isComplete = False + + return isComplete + + def last_punch_line_complete(self): + lastrec = self.get_last_punch_rec() + return self.punch_rec_complete(lastrec) + + def get_time(self): + return time.strftime( self.timestampFormat, time.localtime()) + + def translate_time_to_secs(self,timestamp): + return time.strptime( timestamp[0:15], self.timestampFormat ) + + def get_duration(self,startTimestamp,endTimestamp): + minutes = self.get_duration_in_minutes(startTimestamp, endTimestamp) + return self.format_minutes(minutes) + + def get_duration_in_minutes(self,startTimestamp,endTimestamp): + start = self.translate_time_to_secs( startTimestamp ) + end = self.translate_time_to_secs( endTimestamp ) + + minutes = ( time.mktime(end) - time.mktime(start) ) // 60 + + return minutes + + def format_minutes(self,minutes): + retString = '(' + + if( minutes > 60 ): + hours = minutes // 60 + minutes = minutes - (hours * 60) + retString = retString + str(int(hours)) + ' hours ' + + retString = retString + str(int(minutes)) + ' minutes)' + + return retString + + + def add_literal_line(self,line): + """ + Add a new line to punch.dat containing task,start-timestamp + where task is a literal string (usually in the format '+project'). + """ + + # If previous output line wasn't closed by issuing an 'out' command, then + # do so now. + if self.last_punch_line_complete() == False: + self.add_out_line() + + rec = '%s\t%s' % (line, self.get_time()) + self.open_punch_file() + self.punchFile.write(rec) + self.close_punch_file() + print "Start timer on: " + line + + def add_in_line(self,line_num): + """ + Add a new line to punch.csv containing task,start-timestamp + where task is line 'line_num' from self.taskFile + """ + + # If previous output line wasn't closed by issuing an 'out' command, then + # do so now. + if self.last_punch_line_complete() == False: + self.add_out_line() + + lines = self.taskFile.readlines() + if( line_num > len(lines)): + raise TaskNotFoundError + line = lines[line_num-1].strip() + rec = '%s\t%s' % (line, self.get_time()) + self.open_punch_file() + self.punchFile.write(rec) + self.close_punch_file() + print "Start timer on: " + line + + def add_out_line(self): + """ + Add the 'out' timestamp to the last line of the file + and append the EOL. + """ + + # If last output line was already closed by issuing an 'out' command, then + # raise an exception. + lastrec = self.get_last_punch_rec() + if self.punch_rec_complete(lastrec): + raise NoOpenTaskError + + rec = '\t%s\n' % self.get_time() + + self.open_punch_file() + self.punchFile.write(rec) + self.close_punch_file() + + print "Stop timer on: " + lastrec[0] + + def execute_in(self): + """The logic for the 'in' command.""" + self.parse_config() + + """If only argument is passed, then it is an error.""" + if( len(self.args) == 1 ): + raise PunchCommandError + + """ + If only two arguments are passed, then there are three possibilities: + (1) An integer was passed, referencing a line in todo.txt (ie. punch in 7) + (2) A project name was passed, using the special '+project-name' syntax + (3) The user made a mistake. + """ + if( len(self.args) == 2 ): + # Check to see if the argument is number. + try: + line_num = int(self.args[1]) + except: + line_num = -1 + + if( line_num > -1 ): + self.open_todo() + self.add_in_line(line_num) + self.close_task_file() + else: + project = self.args[1].strip() + if( project[0] == '+' ): + self.add_literal_line(project) + else: + raise PunchCommandError + + """ + If three arguments are passed, then the last argument must be a task file (eg. projects.txt) + """ + if( len(self.args) == 3): + # Check to see if the argument is number. + try: + line_num = int(self.args[1]) + except: + line_num = -1 + + if( line_num > -1 ): + self.open_file(self.args[2]) + self.add_in_line(line_num) + self.close_task_file() + else: + raise PunchCommandError + + def execute_out(self): + """The logic for the 'out' command.""" + self.parse_config() + if( len(self.args) == 1 ): + self.add_out_line() + else: + raise PunchCommandError + + def execute_wh(self): + """The logic for the 'what' command.""" + self.parse_config() + if( len(self.args) == 1 ): + lastrec = self.get_last_punch_rec() + if( len(lastrec) == 2 ): + duration = self.get_duration(lastrec[1], self.get_time()) + print "Active task: " + lastrec[0] + ' ' + duration + else: + print "No task is active." + else: + raise PunchCommandError + + def execute_rep(self): + """The logic for the 'report' command.""" + self.parse_config() + if( len(self.args) == 1 ): + dateDict = dict() + totalTimeDict = dict() + self.open_punch_file('r') + lines = self.punchFile.readlines() + + if( len(lines) == 0 ): + print "There are no tasks in the data file." + else: + for line in lines: + rec = line.split('\t') + if( len(rec) == 3 ): + task = rec[0] + start = rec[1] + end = rec[2] + duration = self.get_duration_in_minutes(start,end) + dateKey = time.strftime( '%Y%m%d', self.translate_time_to_secs(start)) + + # Create a tree of dates that have time reported against them + if( dateKey in dateDict.keys()): + dateValue = dateDict[dateKey] + else: + dateValue = dict() + + # Create a simple dictionary of total elapsed time per date + if( dateKey in totalTimeDict.keys()): + totalTimeValue = int(totalTimeDict[dateKey]) + else: + totalTimeValue = 0 + + # For each date in the tree, store a subtree with + # unique tasks for the date + if( task in dateValue.keys()): + timeList = dateValue[task] + else: + timeList = list() + + # Populate the tree nodes. + timeList.append(duration) + dateValue[task] = timeList + dateDict[dateKey] = dateValue + + # Store total elapsed time for the entire date. + totalTimeValue = totalTimeValue + duration + totalTimeDict[dateKey] = totalTimeValue + + # Returned keys are untyped. Copy into a list of strings so we can sort. + dateNoneList = dateDict.keys() + dateList = list() + for dateThing in dateNoneList: + dateList.append(str(dateThing)) + dateList.sort() + + for dateKey in dateList: + print dateKey[0:4] + '-' + dateKey[4:6] + '-' + dateKey[6:] + ' ' + self.format_minutes(totalTimeDict[dateKey]) +':' + taskDict = dateDict[dateKey] + taskNoneList = taskDict.keys() + taskList = list() + for taskThing in taskNoneList: + taskList.append(str(taskThing)) + taskList.sort() + for taskKey in taskList: + minuteList = taskDict[taskKey] + sum = 0.0 + for m in minuteList: + sum = sum + m + print '\t' + taskKey + ' ' + self.format_minutes(sum) + # Giant else statement ends here. :) + + self.close_punch_file() + else: + raise PunchCommandError + + def execute_ar(self): + """The logic for the 'archive' command.""" + self.parse_config() + if( len(self.args) == 2 ): + #Make sure date argument can be parsed into a date + #in the past. + try: + archiveTs = args[1] + "T23:59:59" + archiveDate = time.strptime( archiveTs, '%Y-%m-%dT%H:%M:%S' ) + archiveTime = time.mktime( archiveDate ) + except: + raise DateFormatError + + #Back up the punch file + self.backup_punch_file() + + #Read the punch file into memory + self.open_punch_file('r') + lines = self.punchFile.readlines() + self.close_punch_file() + + #Open the archive file in append mode + self.open_archive_file() + + #Open the punch file in (destructive) write mode + self.open_punch_file('w') + + #Iterate through tasks in memory, either writing to the + #archive file or the (new) punch file, based on start timestamp + for line in lines: + rec = line.split('\t') + if( self.punch_rec_complete(rec)): + startTime = time.mktime( time.strptime( rec[1], self.timestampFormat )) + if( startTime < archiveTime ): + self.archiveFile.write(line) + else: + self.punchFile.write(line) + else: + self.punchFile.write(line) + + #Close the files. + self.close_punch_file() + self.close_archive_file() + + else: + raise PunchCommandError + +# +# The entry point for the script. +# + +if __name__ == '__main__': + try: + usage = \ +""" +Punch.py [-h] command [line-number] [filename] [archive-date] + + Commands: + 'in' : start the timer for a todo task [line-number] + 'out' : stop the timer for the current task + 'what' : print the current 'active' task. shortcut is 'wh' + 'report' : print a report. shortcut is 'rep' + 'archive' : archive all time records previous to [archive-date] inclusive + + line-number is the number of the item in the todo.txt file (or filename) +""" + + version = \ +""" + Punch.py - A time tracker for todo.sh + Version 1.2 + Author: Keith Lawless (keith@keithlawless.com) + Last updated: July 6,2009 + License: GPL, http://www.gnu.org/copyleft/gpl.html +""" + + parser = OptionParser(usage=usage,version=version) + optlist, args = parser.parse_args() + + if (( len(args) < 1 ) or ( len(args) > 3 )): + raise PunchCommandError + else: + punch = Punch(optlist,args) + punch.execute() + except PunchCommandError: + print usage + except ToDoConfigNotFoundError: + print "Error: Could not find configuration file (todo.cfg)" + except ToDoFileNotFoundError: + print "Error: Could not find todo.txt" + except TaskFileNotFoundError: + print "Error: Could not find file." + except TaskNotFoundError: + print "Error: Item number not found in file." + except NoOpenTaskError: + print "Error: No incomplete task found." + except DateFormatError: + print "Error: Could not translate your input into a date." + From 386b7a0de443e7a9855859e98b49055196fb3fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Mandera?= Date: Tue, 11 Mar 2014 09:24:36 +0100 Subject: [PATCH 2/9] Give more informative error when todo.cfg not found Suggest setting TODOTXT_CFG_FILE when can't find todo.txt configuration file. --- Punch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Punch.py b/Punch.py index a0307fa..7b04035 100755 --- a/Punch.py +++ b/Punch.py @@ -562,7 +562,8 @@ def execute_ar(self): except PunchCommandError: print usage except ToDoConfigNotFoundError: - print "Error: Could not find configuration file (todo.cfg)" + print "Error: Could not find configuration file. Environment \ +variable TODOTXT_CFG_FILE must point to your todo.cfg." except ToDoFileNotFoundError: print "Error: Could not find todo.txt" except TaskFileNotFoundError: From 6f58d6f208f179afd50dc5de40335fdfba0a46fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Mandera?= Date: Tue, 11 Mar 2014 12:32:21 +0100 Subject: [PATCH 3/9] Removed whitespaces at blank lines. --- Punch.py | 156 +++++++++++++++++++++++++++---------------------------- 1 file changed, 78 insertions(+), 78 deletions(-) diff --git a/Punch.py b/Punch.py index 7b04035..8c3fc4f 100755 --- a/Punch.py +++ b/Punch.py @@ -5,7 +5,7 @@ @author: Keith Lawless (keith at keithlawless dot com) Copyright 2009 Keith Lawless - + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or @@ -41,11 +41,11 @@ class PunchCommandError(ValueError): """Used to indicate that an invalid command was passed to Punch""" - + class ToDoConfigNotFoundError(IOError): """Used to indicate that todo.cfg was not found on the path""" - + class ToDoFileNotFoundError(IOError): """Used to indicate that todo.txt was not found on the path""" @@ -53,24 +53,24 @@ class ToDoFileNotFoundError(IOError): class TaskFileNotFoundError(IOError): """Used to indicate that the user specified task file was not found""" - + class TaskNotFoundError(IOError): """Used to indicate that the task number specified does not exist in the task file""" class NoOpenTaskError(IOError): """Used to indicate that an 'out' command was issued, but the last task was already closed out.""" - + class DateFormatError(IOError): """Used to indicate that a poorly formatted date was passed where a date was expected.""" - + class Punch(object): timestampFormat = '%Y%m%dT%H%M%S' - + def __init__(self, optlist, args): self.optlist = optlist self.args = args - + def execute(self): """Execute the command - either 'in' or 'out'""" if( self.args[0] == 'in' ): @@ -85,7 +85,7 @@ def execute(self): self.execute_ar() else: raise PunchCommandError - + def search_file(self, files, paths): file_found = 0 for filename in files: @@ -100,7 +100,7 @@ def search_file(self, files, paths): return abspath(join(path, filename)) else: return None - + def parse_config(self): """Parse the user's todo.cfg file and place the elements into a dictionary""" try: @@ -128,13 +128,13 @@ def parse_config(self): value= propDef[found:].lstrip(":= ").rstrip() self.propDict[name]= value.strip('"') configFile.close() - + # Add the users environment variables to the propDict, unless # a value has already been set. for key in os.environ.keys(): if self.propDict.has_key(key) == False: self.propDict[key] = os.environ[key] - + except IOError: raise ToDoConfigNotFoundError @@ -146,16 +146,16 @@ def resolve(self,value): ref = value[token+1:terminus] refValue = self.propDict[ref] value = refValue + value[terminus:] - + return value - + def open_todo(self): """Open the user's todo.txt file.""" try: self.taskFile = open( self.resolve( self.propDict['TODO_FILE']), 'U' ) except IOError: raise ToDoFileNotFoundError - + def open_file(self,filename): """Open a file given a filename.""" try: @@ -163,33 +163,33 @@ def open_file(self,filename): self.taskFile = open( name, 'U' ) except IOError: raise TaskFileNotFoundError - + def close_task_file(self): """Close the file taskFile - either todo.txt or a user supplied file.""" self.taskFile.close() - + def open_punch_file(self,mode='a'): """Open the output file - punch.dat - in the user's TODO_DIR.""" name = self.resolve( self.propDict['TODO_DIR'] + "/punch.dat" ) - + if not os.path.exists(name): open( name, 'w' ).close() - + self.punchFile = open( name, mode ) - + def close_punch_file(self): """Close the output file - punch.csv.""" self.punchFile.close() - + def open_punch_backup_file(self): """Open the backup file - punch.dat.backup - in the user's TODO_DIR.""" name = self.resolve( self.propDict['TODO_DIR'] + "/punch.dat.backup" ) self.backupFile = open( name, 'w' ) - + def close_punch_backup_file(self): """Close the output file - punch.csv.""" self.backupFile.close() - + def backup_punch_file(self): self.open_punch_file('r') self.open_punch_backup_file() @@ -201,11 +201,11 @@ def open_archive_file(self,mode='a'): """Open the archive file - punch.archive - in the user's TODO_DIR.""" name = self.resolve( self.propDict['TODO_DIR'] + "/punch.archive" ) self.archiveFile = open( name, mode ) - + def close_archive_file(self): """Close the archive file - punch.archive.""" self.archiveFile.close() - + def get_last_punch_rec(self): """Returns last line in the output file as a list of fields.""" lastrec = [] @@ -220,85 +220,85 @@ def get_last_punch_rec(self): self.close_punch_file() except IOError: lastrec = [] - + return lastrec def punch_rec_complete(self,rec): """Returns true if the punch record is complete - that is, contains a task, start timestamp, and end timestamp""" - + if len(rec) == 0: isComplete = True elif len(rec) == 3: isComplete = True else: isComplete = False - + return isComplete def last_punch_line_complete(self): lastrec = self.get_last_punch_rec() return self.punch_rec_complete(lastrec) - + def get_time(self): return time.strftime( self.timestampFormat, time.localtime()) - + def translate_time_to_secs(self,timestamp): return time.strptime( timestamp[0:15], self.timestampFormat ) - + def get_duration(self,startTimestamp,endTimestamp): minutes = self.get_duration_in_minutes(startTimestamp, endTimestamp) return self.format_minutes(minutes) - + def get_duration_in_minutes(self,startTimestamp,endTimestamp): start = self.translate_time_to_secs( startTimestamp ) end = self.translate_time_to_secs( endTimestamp ) - + minutes = ( time.mktime(end) - time.mktime(start) ) // 60 - + return minutes def format_minutes(self,minutes): retString = '(' - + if( minutes > 60 ): hours = minutes // 60 minutes = minutes - (hours * 60) retString = retString + str(int(hours)) + ' hours ' - + retString = retString + str(int(minutes)) + ' minutes)' return retString - - + + def add_literal_line(self,line): """ Add a new line to punch.dat containing task,start-timestamp where task is a literal string (usually in the format '+project'). """ - + # If previous output line wasn't closed by issuing an 'out' command, then # do so now. if self.last_punch_line_complete() == False: self.add_out_line() - + rec = '%s\t%s' % (line, self.get_time()) self.open_punch_file() self.punchFile.write(rec) self.close_punch_file() print "Start timer on: " + line - + def add_in_line(self,line_num): """ Add a new line to punch.csv containing task,start-timestamp where task is line 'line_num' from self.taskFile """ - + # If previous output line wasn't closed by issuing an 'out' command, then # do so now. if self.last_punch_line_complete() == False: self.add_out_line() - + lines = self.taskFile.readlines() if( line_num > len(lines)): raise TaskNotFoundError @@ -308,35 +308,35 @@ def add_in_line(self,line_num): self.punchFile.write(rec) self.close_punch_file() print "Start timer on: " + line - + def add_out_line(self): """ Add the 'out' timestamp to the last line of the file and append the EOL. """ - + # If last output line was already closed by issuing an 'out' command, then # raise an exception. lastrec = self.get_last_punch_rec() if self.punch_rec_complete(lastrec): raise NoOpenTaskError - + rec = '\t%s\n' % self.get_time() - + self.open_punch_file() self.punchFile.write(rec) self.close_punch_file() - + print "Stop timer on: " + lastrec[0] - + def execute_in(self): """The logic for the 'in' command.""" self.parse_config() - + """If only argument is passed, then it is an error.""" if( len(self.args) == 1 ): raise PunchCommandError - + """ If only two arguments are passed, then there are three possibilities: (1) An integer was passed, referencing a line in todo.txt (ie. punch in 7) @@ -349,7 +349,7 @@ def execute_in(self): line_num = int(self.args[1]) except: line_num = -1 - + if( line_num > -1 ): self.open_todo() self.add_in_line(line_num) @@ -360,7 +360,7 @@ def execute_in(self): self.add_literal_line(project) else: raise PunchCommandError - + """ If three arguments are passed, then the last argument must be a task file (eg. projects.txt) """ @@ -370,14 +370,14 @@ def execute_in(self): line_num = int(self.args[1]) except: line_num = -1 - + if( line_num > -1 ): self.open_file(self.args[2]) self.add_in_line(line_num) self.close_task_file() else: raise PunchCommandError - + def execute_out(self): """The logic for the 'out' command.""" self.parse_config() @@ -398,7 +398,7 @@ def execute_wh(self): print "No task is active." else: raise PunchCommandError - + def execute_rep(self): """The logic for the 'report' command.""" self.parse_config() @@ -407,7 +407,7 @@ def execute_rep(self): totalTimeDict = dict() self.open_punch_file('r') lines = self.punchFile.readlines() - + if( len(lines) == 0 ): print "There are no tasks in the data file." else: @@ -419,42 +419,42 @@ def execute_rep(self): end = rec[2] duration = self.get_duration_in_minutes(start,end) dateKey = time.strftime( '%Y%m%d', self.translate_time_to_secs(start)) - + # Create a tree of dates that have time reported against them if( dateKey in dateDict.keys()): dateValue = dateDict[dateKey] else: dateValue = dict() - + # Create a simple dictionary of total elapsed time per date if( dateKey in totalTimeDict.keys()): totalTimeValue = int(totalTimeDict[dateKey]) else: totalTimeValue = 0 - + # For each date in the tree, store a subtree with # unique tasks for the date if( task in dateValue.keys()): timeList = dateValue[task] else: timeList = list() - + # Populate the tree nodes. timeList.append(duration) dateValue[task] = timeList dateDict[dateKey] = dateValue - + # Store total elapsed time for the entire date. totalTimeValue = totalTimeValue + duration totalTimeDict[dateKey] = totalTimeValue - + # Returned keys are untyped. Copy into a list of strings so we can sort. dateNoneList = dateDict.keys() dateList = list() for dateThing in dateNoneList: dateList.append(str(dateThing)) dateList.sort() - + for dateKey in dateList: print dateKey[0:4] + '-' + dateKey[4:6] + '-' + dateKey[6:] + ' ' + self.format_minutes(totalTimeDict[dateKey]) +':' taskDict = dateDict[dateKey] @@ -470,7 +470,7 @@ def execute_rep(self): sum = sum + m print '\t' + taskKey + ' ' + self.format_minutes(sum) # Giant else statement ends here. :) - + self.close_punch_file() else: raise PunchCommandError @@ -487,21 +487,21 @@ def execute_ar(self): archiveTime = time.mktime( archiveDate ) except: raise DateFormatError - + #Back up the punch file self.backup_punch_file() - + #Read the punch file into memory self.open_punch_file('r') lines = self.punchFile.readlines() self.close_punch_file() - + #Open the archive file in append mode self.open_archive_file() - + #Open the punch file in (destructive) write mode self.open_punch_file('w') - + #Iterate through tasks in memory, either writing to the #archive file or the (new) punch file, based on start timestamp for line in lines: @@ -514,14 +514,14 @@ def execute_ar(self): self.punchFile.write(line) else: self.punchFile.write(line) - + #Close the files. self.close_punch_file() self.close_archive_file() - + else: raise PunchCommandError - + # # The entry point for the script. # @@ -531,17 +531,17 @@ def execute_ar(self): usage = \ """ Punch.py [-h] command [line-number] [filename] [archive-date] - + Commands: 'in' : start the timer for a todo task [line-number] 'out' : stop the timer for the current task 'what' : print the current 'active' task. shortcut is 'wh' 'report' : print a report. shortcut is 'rep' 'archive' : archive all time records previous to [archive-date] inclusive - + line-number is the number of the item in the todo.txt file (or filename) """ - + version = \ """ Punch.py - A time tracker for todo.sh @@ -550,7 +550,7 @@ def execute_ar(self): Last updated: July 6,2009 License: GPL, http://www.gnu.org/copyleft/gpl.html """ - + parser = OptionParser(usage=usage,version=version) optlist, args = parser.parse_args() @@ -574,4 +574,4 @@ def execute_ar(self): print "Error: No incomplete task found." except DateFormatError: print "Error: Could not translate your input into a date." - + From 05e2d3cf047cd32170216d5a813e5e546ad2d741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Mandera?= Date: Tue, 11 Mar 2014 13:16:53 +0100 Subject: [PATCH 4/9] PEP8 compliance (checked with pep8 command line tool) * removed whitespaces at blank lines * split long lines * normalized blank lines * removed spaces before and after brackets * spaces around commas and operators * fixed indentation * None/True/False matching with 'is' and 'is not' * removed trailing spaces * .has_key() replaced with 'in' --- Punch.py | 260 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 137 insertions(+), 123 deletions(-) diff --git a/Punch.py b/Punch.py index 8c3fc4f..6157351 100755 --- a/Punch.py +++ b/Punch.py @@ -55,13 +55,19 @@ class TaskFileNotFoundError(IOError): class TaskNotFoundError(IOError): - """Used to indicate that the task number specified does not exist in the task file""" + """Used to indicate that the task number specified does + not exist in the task file""" + class NoOpenTaskError(IOError): - """Used to indicate that an 'out' command was issued, but the last task was already closed out.""" + """Used to indicate that an 'out' command was issued, + but the last task was already closed out.""" + class DateFormatError(IOError): - """Used to indicate that a poorly formatted date was passed where a date was expected.""" + """Used to indicate that a poorly formatted date + was passed where a date was expected.""" + class Punch(object): @@ -73,15 +79,15 @@ def __init__(self, optlist, args): def execute(self): """Execute the command - either 'in' or 'out'""" - if( self.args[0] == 'in' ): + if(self.args[0] == 'in'): self.execute_in() - elif( self.args[0] == 'out' ): + elif(self.args[0] == 'out'): self.execute_out() - elif( self.args[0] in ['wh','what'] ): + elif(self.args[0] in ['wh', 'what']): self.execute_wh() - elif( self.args[0] in ['report', 'rep'] ): + elif(self.args[0] in ['report', 'rep']): self.execute_rep() - elif( self.args[0] in ['archive', 'ar'] ): + elif(self.args[0] in ['archive', 'ar']): self.execute_ar() else: raise PunchCommandError @@ -90,7 +96,7 @@ def search_file(self, files, paths): file_found = 0 for filename in files: for path in paths: - if path != None: + if path is not None: if exists(join(path, filename)): file_found = 1 break @@ -99,49 +105,50 @@ def search_file(self, files, paths): if file_found: return abspath(join(path, filename)) else: - return None + return None def parse_config(self): - """Parse the user's todo.cfg file and place the elements into a dictionary""" + """Parse the user's todo.cfg file and place + the elements into a dictionary""" try: - paths = [ getenv("HOME"), "."] - files = [ "todo.cfg", ".todo.cfg" ] - if getenv("TODOTXT_CFG_FILE") == None: + paths = [getenv("HOME"), "."] + files = ["todo.cfg", ".todo.cfg"] + if getenv("TODOTXT_CFG_FILE") is None: configFileName = self.search_file(files, paths) else: configFileName = getenv("TODOTXT_CFG_FILE") - if configFileName == None: + if configFileName is None: raise ToDoConfigNotFoundError - configFile = open( configFileName ) + configFile = open(configFileName) self.propDict = dict() for propLine in configFile: propDef = propLine.strip() if len(propDef) == 0: continue - if propDef[0] in ( '#' ): + if propDef[0] in ('#'): continue if propDef[0:6] == 'export': - propDef = propDef[7:] - punctuation = [ propDef.find(c) for c in '= ' ] + [ len(propDef) ] - found = min( [ pos for pos in punctuation if pos != -1 ] ) - name= propDef[:found].rstrip() - value= propDef[found:].lstrip(":= ").rstrip() - self.propDict[name]= value.strip('"') + propDef = propDef[7:] + punctuation = [propDef.find(c) for c in '= '] + [len(propDef)] + found = min([pos for pos in punctuation if pos != -1]) + name = propDef[:found].rstrip() + value = propDef[found:].lstrip(":= ").rstrip() + self.propDict[name] = value.strip('"') configFile.close() # Add the users environment variables to the propDict, unless # a value has already been set. for key in os.environ.keys(): - if self.propDict.has_key(key) == False: + if key in self.propDict is False: self.propDict[key] = os.environ[key] except IOError: - raise ToDoConfigNotFoundError + raise ToDoConfigNotFoundError - def resolve(self,value): + def resolve(self, value): """Replace variables in a config entry with the actual value.""" token = value.find('$') - if( token != -1 ): + if(token != -1): terminus = token + value[token:].find('/') ref = value[token+1:terminus] refValue = self.propDict[ref] @@ -152,30 +159,31 @@ def resolve(self,value): def open_todo(self): """Open the user's todo.txt file.""" try: - self.taskFile = open( self.resolve( self.propDict['TODO_FILE']), 'U' ) + self.taskFile = open(self.resolve(self.propDict['TODO_FILE']), 'U') except IOError: raise ToDoFileNotFoundError - def open_file(self,filename): - """Open a file given a filename.""" + def open_file(self, filename): + """Open a file given a filename.""" try: - name = self.resolve( self.propDict['TODO_DIR'] + "/" + filename ) - self.taskFile = open( name, 'U' ) + name = self.resolve(self.propDict['TODO_DIR'] + "/" + filename) + self.taskFile = open(name, 'U') except IOError: raise TaskFileNotFoundError def close_task_file(self): - """Close the file taskFile - either todo.txt or a user supplied file.""" + """Close the file taskFile - either todo.txt + or a user supplied file.""" self.taskFile.close() - def open_punch_file(self,mode='a'): + def open_punch_file(self, mode='a'): """Open the output file - punch.dat - in the user's TODO_DIR.""" - name = self.resolve( self.propDict['TODO_DIR'] + "/punch.dat" ) + name = self.resolve(self.propDict['TODO_DIR'] + "/punch.dat") if not os.path.exists(name): - open( name, 'w' ).close() + open(name, 'w').close() - self.punchFile = open( name, mode ) + self.punchFile = open(name, mode) def close_punch_file(self): """Close the output file - punch.csv.""" @@ -183,8 +191,8 @@ def close_punch_file(self): def open_punch_backup_file(self): """Open the backup file - punch.dat.backup - in the user's TODO_DIR.""" - name = self.resolve( self.propDict['TODO_DIR'] + "/punch.dat.backup" ) - self.backupFile = open( name, 'w' ) + name = self.resolve(self.propDict['TODO_DIR'] + "/punch.dat.backup") + self.backupFile = open(name, 'w') def close_punch_backup_file(self): """Close the output file - punch.csv.""" @@ -193,14 +201,14 @@ def close_punch_backup_file(self): def backup_punch_file(self): self.open_punch_file('r') self.open_punch_backup_file() - shutil.copyfileobj(self.punchFile,self.backupFile) + shutil.copyfileobj(self.punchFile, self.backupFile) self.close_punch_backup_file() self.close_punch_file() - def open_archive_file(self,mode='a'): + def open_archive_file(self, mode='a'): """Open the archive file - punch.archive - in the user's TODO_DIR.""" - name = self.resolve( self.propDict['TODO_DIR'] + "/punch.archive" ) - self.archiveFile = open( name, mode ) + name = self.resolve(self.propDict['TODO_DIR'] + "/punch.archive") + self.archiveFile = open(name, mode) def close_archive_file(self): """Close the archive file - punch.archive.""" @@ -212,7 +220,7 @@ def get_last_punch_rec(self): try: self.open_punch_file('r') lines = self.punchFile.readlines() - if( len(lines) > 0): + if(len(lines) > 0): lastline = (lines[len(lines)-1]).strip() lastrec = lastline.split('\t') else: @@ -223,7 +231,7 @@ def get_last_punch_rec(self): return lastrec - def punch_rec_complete(self,rec): + def punch_rec_complete(self, rec): """Returns true if the punch record is complete - that is, contains a task, start timestamp, and end timestamp""" @@ -241,27 +249,27 @@ def last_punch_line_complete(self): return self.punch_rec_complete(lastrec) def get_time(self): - return time.strftime( self.timestampFormat, time.localtime()) + return time.strftime(self.timestampFormat, time.localtime()) - def translate_time_to_secs(self,timestamp): - return time.strptime( timestamp[0:15], self.timestampFormat ) + def translate_time_to_secs(self, timestamp): + return time.strptime(timestamp[0:15], self.timestampFormat) - def get_duration(self,startTimestamp,endTimestamp): + def get_duration(self, startTimestamp, endTimestamp): minutes = self.get_duration_in_minutes(startTimestamp, endTimestamp) return self.format_minutes(minutes) - def get_duration_in_minutes(self,startTimestamp,endTimestamp): - start = self.translate_time_to_secs( startTimestamp ) - end = self.translate_time_to_secs( endTimestamp ) + def get_duration_in_minutes(self, startTimestamp, endTimestamp): + start = self.translate_time_to_secs(startTimestamp) + end = self.translate_time_to_secs(endTimestamp) - minutes = ( time.mktime(end) - time.mktime(start) ) // 60 + minutes = (time.mktime(end) - time.mktime(start)) // 60 return minutes - def format_minutes(self,minutes): + def format_minutes(self, minutes): retString = '(' - if( minutes > 60 ): + if(minutes > 60): hours = minutes // 60 minutes = minutes - (hours * 60) retString = retString + str(int(hours)) + ' hours ' @@ -270,16 +278,15 @@ def format_minutes(self,minutes): return retString - - def add_literal_line(self,line): + def add_literal_line(self, line): """ Add a new line to punch.dat containing task,start-timestamp where task is a literal string (usually in the format '+project'). """ - # If previous output line wasn't closed by issuing an 'out' command, then - # do so now. - if self.last_punch_line_complete() == False: + # If previous output line wasn't closed by issuing an 'out' command, + # then do so now. + if self.last_punch_line_complete() is False: self.add_out_line() rec = '%s\t%s' % (line, self.get_time()) @@ -288,21 +295,21 @@ def add_literal_line(self,line): self.close_punch_file() print "Start timer on: " + line - def add_in_line(self,line_num): + def add_in_line(self, line_num): """ Add a new line to punch.csv containing task,start-timestamp where task is line 'line_num' from self.taskFile """ - # If previous output line wasn't closed by issuing an 'out' command, then - # do so now. - if self.last_punch_line_complete() == False: + # If previous output line wasn't closed by issuing an 'out' command, + # then do so now. + if self.last_punch_line_complete() is False: self.add_out_line() lines = self.taskFile.readlines() - if( line_num > len(lines)): + if(line_num > len(lines)): raise TaskNotFoundError - line = lines[line_num-1].strip() + line = lines[line_num-1].strip() rec = '%s\t%s' % (line, self.get_time()) self.open_punch_file() self.punchFile.write(rec) @@ -315,8 +322,8 @@ def add_out_line(self): and append the EOL. """ - # If last output line was already closed by issuing an 'out' command, then - # raise an exception. + # If last output line was already closed by issuing an 'out' command, + # then raise an exception. lastrec = self.get_last_punch_rec() if self.punch_rec_complete(lastrec): raise NoOpenTaskError @@ -327,51 +334,53 @@ def add_out_line(self): self.punchFile.write(rec) self.close_punch_file() - print "Stop timer on: " + lastrec[0] + print "Stop timer on: " + lastrec[0] def execute_in(self): """The logic for the 'in' command.""" self.parse_config() """If only argument is passed, then it is an error.""" - if( len(self.args) == 1 ): + if(len(self.args) == 1): raise PunchCommandError """ - If only two arguments are passed, then there are three possibilities: - (1) An integer was passed, referencing a line in todo.txt (ie. punch in 7) + If only two arguments are passed, then there are three possibilities: + (1) An integer was passed, referencing a line in + todo.txt (ie. punch in 7) (2) A project name was passed, using the special '+project-name' syntax (3) The user made a mistake. """ - if( len(self.args) == 2 ): + if(len(self.args) == 2): # Check to see if the argument is number. try: line_num = int(self.args[1]) except: line_num = -1 - if( line_num > -1 ): + if(line_num > -1): self.open_todo() self.add_in_line(line_num) self.close_task_file() else: project = self.args[1].strip() - if( project[0] == '+' ): + if(project[0] == '+'): self.add_literal_line(project) else: raise PunchCommandError """ - If three arguments are passed, then the last argument must be a task file (eg. projects.txt) + If three arguments are passed, then the last argument + must be a task file (eg. projects.txt) """ - if( len(self.args) == 3): + if(len(self.args) == 3): # Check to see if the argument is number. try: line_num = int(self.args[1]) except: line_num = -1 - if( line_num > -1 ): + if(line_num > -1): self.open_file(self.args[2]) self.add_in_line(line_num) self.close_task_file() @@ -381,60 +390,64 @@ def execute_in(self): def execute_out(self): """The logic for the 'out' command.""" self.parse_config() - if( len(self.args) == 1 ): + if(len(self.args) == 1): self.add_out_line() else: - raise PunchCommandError + raise PunchCommandError def execute_wh(self): """The logic for the 'what' command.""" self.parse_config() - if( len(self.args) == 1 ): + if(len(self.args) == 1): lastrec = self.get_last_punch_rec() - if( len(lastrec) == 2 ): + if(len(lastrec) == 2): duration = self.get_duration(lastrec[1], self.get_time()) print "Active task: " + lastrec[0] + ' ' + duration else: print "No task is active." else: - raise PunchCommandError + raise PunchCommandError def execute_rep(self): """The logic for the 'report' command.""" self.parse_config() - if( len(self.args) == 1 ): + if(len(self.args) == 1): dateDict = dict() totalTimeDict = dict() self.open_punch_file('r') lines = self.punchFile.readlines() - if( len(lines) == 0 ): + if(len(lines) == 0): print "There are no tasks in the data file." else: for line in lines: rec = line.split('\t') - if( len(rec) == 3 ): + if(len(rec) == 3): task = rec[0] start = rec[1] end = rec[2] - duration = self.get_duration_in_minutes(start,end) - dateKey = time.strftime( '%Y%m%d', self.translate_time_to_secs(start)) - - # Create a tree of dates that have time reported against them - if( dateKey in dateDict.keys()): + duration = self.get_duration_in_minutes(start, end) + dateKey = time.strftime( + '%Y%m%d', + self.translate_time_to_secs(start)) + + # Create a tree of dates that have time + # reported against them + if(dateKey in dateDict.keys()): dateValue = dateDict[dateKey] else: dateValue = dict() - # Create a simple dictionary of total elapsed time per date - if( dateKey in totalTimeDict.keys()): + # Create a simple dictionary of total + # elapsed time per date + if(dateKey in totalTimeDict.keys()): totalTimeValue = int(totalTimeDict[dateKey]) else: totalTimeValue = 0 - # For each date in the tree, store a subtree with + # For each date in the tree, store a subtree with # unique tasks for the date - if( task in dateValue.keys()): + if(task in dateValue.keys()): timeList = dateValue[task] else: timeList = list() @@ -448,7 +461,8 @@ def execute_rep(self): totalTimeValue = totalTimeValue + duration totalTimeDict[dateKey] = totalTimeValue - # Returned keys are untyped. Copy into a list of strings so we can sort. + # Returned keys are untyped. + # Copy into a list of strings so we can sort. dateNoneList = dateDict.keys() dateList = list() for dateThing in dateNoneList: @@ -456,7 +470,9 @@ def execute_rep(self): dateList.sort() for dateKey in dateList: - print dateKey[0:4] + '-' + dateKey[4:6] + '-' + dateKey[6:] + ' ' + self.format_minutes(totalTimeDict[dateKey]) +':' + print dateKey[0:4] + '-' + dateKey[4:6] + '-'\ + + dateKey[6:] + ' '\ + + self.format_minutes(totalTimeDict[dateKey]) + ':' taskDict = dateDict[dateKey] taskNoneList = taskDict.keys() taskList = list() @@ -478,13 +494,13 @@ def execute_rep(self): def execute_ar(self): """The logic for the 'archive' command.""" self.parse_config() - if( len(self.args) == 2 ): + if(len(self.args) == 2): #Make sure date argument can be parsed into a date #in the past. try: archiveTs = args[1] + "T23:59:59" - archiveDate = time.strptime( archiveTs, '%Y-%m-%dT%H:%M:%S' ) - archiveTime = time.mktime( archiveDate ) + archiveDate = time.strptime(archiveTs, '%Y-%m-%dT%H:%M:%S') + archiveTime = time.mktime(archiveDate) except: raise DateFormatError @@ -506,9 +522,10 @@ def execute_ar(self): #archive file or the (new) punch file, based on start timestamp for line in lines: rec = line.split('\t') - if( self.punch_rec_complete(rec)): - startTime = time.mktime( time.strptime( rec[1], self.timestampFormat )) - if( startTime < archiveTime ): + if(self.punch_rec_complete(rec)): + startTime = time.mktime( + time.strptime(rec[1], self.timestampFormat)) + if(startTime < archiveTime): self.archiveFile.write(line) else: self.punchFile.write(line) @@ -528,36 +545,34 @@ def execute_ar(self): if __name__ == '__main__': try: - usage = \ -""" + usage = """ Punch.py [-h] command [line-number] [filename] [archive-date] - Commands: - 'in' : start the timer for a todo task [line-number] - 'out' : stop the timer for the current task - 'what' : print the current 'active' task. shortcut is 'wh' - 'report' : print a report. shortcut is 'rep' - 'archive' : archive all time records previous to [archive-date] inclusive +Commands: +'in' : start the timer for a todo task [line-number] +'out' : stop the timer for the current task +'what' : print the current 'active' task. shortcut is 'wh' +'report' : print a report. shortcut is 'rep' +'archive' : archive all time records previous to [archive-date] inclusive - line-number is the number of the item in the todo.txt file (or filename) +line-number is the number of the item in the todo.txt file (or filename) """ - version = \ -""" - Punch.py - A time tracker for todo.sh - Version 1.2 - Author: Keith Lawless (keith@keithlawless.com) - Last updated: July 6,2009 - License: GPL, http://www.gnu.org/copyleft/gpl.html + version = """ +Punch.py - A time tracker for todo.sh +Version 1.2 +Author: Keith Lawless (keith@keithlawless.com) +Last updated: July 6,2009 +License: GPL, http://www.gnu.org/copyleft/gpl.html """ - parser = OptionParser(usage=usage,version=version) + parser = OptionParser(usage=usage, version=version) optlist, args = parser.parse_args() - if (( len(args) < 1 ) or ( len(args) > 3 )): + if ((len(args) < 1) or (len(args) > 3)): raise PunchCommandError else: - punch = Punch(optlist,args) + punch = Punch(optlist, args) punch.execute() except PunchCommandError: print usage @@ -574,4 +589,3 @@ def execute_ar(self): print "Error: No incomplete task found." except DateFormatError: print "Error: Could not translate your input into a date." - From 3c8414cb0f6d8d7b12671c63ac01ad55a1d97720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Mandera?= Date: Sat, 15 Mar 2014 01:38:29 +0100 Subject: [PATCH 5/9] Vim temporary files in .gitignore .gitignore now includes only Vim temporary files --- .gitignore | 31 +++---------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index 209c29e..d772925 100644 --- a/.gitignore +++ b/.gitignore @@ -1,28 +1,3 @@ -.idea/* -*.py[co] - -# Packages -*.egg -*.egg-info -dist -build -eggs -parts -bin -var -sdist -develop-eggs -.installed.cfg - -# Installer logs -pip-log.txt - -# Unit test / coverage reports -.coverage -.tox - -#Translations -*.mo - -#Mr Developer -.mr.developer.cfg +*~ +*.swp +*.swo From 88e06bcc090595ce03acc42ef2dbb72a60ad32b7 Mon Sep 17 00:00:00 2001 From: Livia Date: Wed, 1 Feb 2017 19:25:14 +0100 Subject: [PATCH 6/9] compatibility with Python 3, usage as plugin: todo.sh punch in 1 --- Punch.py | 64 ++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/Punch.py b/Punch.py index 6157351..19e626b 100755 --- a/Punch.py +++ b/Punch.py @@ -20,11 +20,9 @@ along with this program. If not, see . ''' -from os.path import abspath, exists, join -from os import pathsep, getenv -import cPickle -import os.path -import os +from __future__ import print_function # (at top of module) +from os.path import abspath, exists, join, basename +from os import pathsep, getenv, environ import shutil import sys import time @@ -138,9 +136,9 @@ def parse_config(self): # Add the users environment variables to the propDict, unless # a value has already been set. - for key in os.environ.keys(): + for key in environ.keys(): if key in self.propDict is False: - self.propDict[key] = os.environ[key] + self.propDict[key] = environ[key] except IOError: raise ToDoConfigNotFoundError @@ -180,7 +178,7 @@ def open_punch_file(self, mode='a'): """Open the output file - punch.dat - in the user's TODO_DIR.""" name = self.resolve(self.propDict['TODO_DIR'] + "/punch.dat") - if not os.path.exists(name): + if not exists(name): open(name, 'w').close() self.punchFile = open(name, mode) @@ -293,7 +291,7 @@ def add_literal_line(self, line): self.open_punch_file() self.punchFile.write(rec) self.close_punch_file() - print "Start timer on: " + line + print("Start timer on: " + line) def add_in_line(self, line_num): """ @@ -314,7 +312,7 @@ def add_in_line(self, line_num): self.open_punch_file() self.punchFile.write(rec) self.close_punch_file() - print "Start timer on: " + line + print("Start timer on: " + line) def add_out_line(self): """ @@ -334,7 +332,7 @@ def add_out_line(self): self.punchFile.write(rec) self.close_punch_file() - print "Stop timer on: " + lastrec[0] + print("Stop timer on: " + lastrec[0]) def execute_in(self): """The logic for the 'in' command.""" @@ -402,9 +400,9 @@ def execute_wh(self): lastrec = self.get_last_punch_rec() if(len(lastrec) == 2): duration = self.get_duration(lastrec[1], self.get_time()) - print "Active task: " + lastrec[0] + ' ' + duration + print("Active task: " + lastrec[0] + ' ' + duration) else: - print "No task is active." + print("No task is active.") else: raise PunchCommandError @@ -418,7 +416,7 @@ def execute_rep(self): lines = self.punchFile.readlines() if(len(lines) == 0): - print "There are no tasks in the data file." + print("There are no tasks in the data file.") else: for line in lines: rec = line.split('\t') @@ -470,9 +468,9 @@ def execute_rep(self): dateList.sort() for dateKey in dateList: - print dateKey[0:4] + '-' + dateKey[4:6] + '-'\ + print(dateKey[0:4] + '-' + dateKey[4:6] + '-'\ + dateKey[6:] + ' '\ - + self.format_minutes(totalTimeDict[dateKey]) + ':' + + self.format_minutes(totalTimeDict[dateKey]) + ':') taskDict = dateDict[dateKey] taskNoneList = taskDict.keys() taskList = list() @@ -484,7 +482,7 @@ def execute_rep(self): sum = 0.0 for m in minuteList: sum = sum + m - print '\t' + taskKey + ' ' + self.format_minutes(sum) + print('\t' + taskKey + ' ' + self.format_minutes(sum)) # Giant else statement ends here. :) self.close_punch_file() @@ -568,24 +566,40 @@ def execute_ar(self): parser = OptionParser(usage=usage, version=version) optlist, args = parser.parse_args() + if ((len(args) < 1) : + raise PunchCommandError + #experimental: install at todo.sh plugin + if args[0] == 'install': + import subprocess + sys.exit(subprocess.call('ln -s %s %s/%s' %(__file__, + os.environ.get('TODO_ACTIONS_DIR','~/.todo.actions.d', + 'punch'))) + + #verify if this script has been called as a plugin of todo.sh cli + if args[0] == basename(__file__): + args.pop(0) + elif args[0] == 'usage': + print(usage) + sys.exit(0) + if ((len(args) < 1) or (len(args) > 3)): raise PunchCommandError else: punch = Punch(optlist, args) punch.execute() except PunchCommandError: - print usage + print(usage) except ToDoConfigNotFoundError: - print "Error: Could not find configuration file. Environment \ -variable TODOTXT_CFG_FILE must point to your todo.cfg." + print("Error: Could not find configuration file. Environment \ +variable TODOTXT_CFG_FILE must point to your todo.cfg.") except ToDoFileNotFoundError: - print "Error: Could not find todo.txt" + print("Error: Could not find todo.txt") except TaskFileNotFoundError: - print "Error: Could not find file." + print("Error: Could not find file.") except TaskNotFoundError: - print "Error: Item number not found in file." + print("Error: Item number not found in file.") except NoOpenTaskError: - print "Error: No incomplete task found." + print("Error: No incomplete task found.") except DateFormatError: - print "Error: Could not translate your input into a date." + print("Error: Could not translate your input into a date.") From ffa8baec2c3617c72ba21a88983c3649b7e8169c Mon Sep 17 00:00:00 2001 From: Livia Date: Wed, 1 Feb 2017 19:44:06 +0100 Subject: [PATCH 7/9] fixed wrong parenthesis --- Punch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Punch.py b/Punch.py index 19e626b..b26ba8d 100755 --- a/Punch.py +++ b/Punch.py @@ -566,14 +566,14 @@ def execute_ar(self): parser = OptionParser(usage=usage, version=version) optlist, args = parser.parse_args() - if ((len(args) < 1) : + if len(args) < 1 : raise PunchCommandError #experimental: install at todo.sh plugin if args[0] == 'install': import subprocess sys.exit(subprocess.call('ln -s %s %s/%s' %(__file__, - os.environ.get('TODO_ACTIONS_DIR','~/.todo.actions.d', + os.environ.get('TODO_ACTIONS_DIR','~/.todo.actions.d'), 'punch'))) #verify if this script has been called as a plugin of todo.sh cli From 1a8f056bc05478133b7a5af534184e2038c34127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Mandera?= Date: Thu, 2 Feb 2017 08:06:07 +0100 Subject: [PATCH 8/9] Remove spaces at the end of lines --- Punch.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Punch.py b/Punch.py index b26ba8d..9caf9dc 100755 --- a/Punch.py +++ b/Punch.py @@ -572,17 +572,17 @@ def execute_ar(self): #experimental: install at todo.sh plugin if args[0] == 'install': import subprocess - sys.exit(subprocess.call('ln -s %s %s/%s' %(__file__, - os.environ.get('TODO_ACTIONS_DIR','~/.todo.actions.d'), + sys.exit(subprocess.call('ln -s %s %s/%s' %(__file__, + os.environ.get('TODO_ACTIONS_DIR','~/.todo.actions.d'), 'punch'))) - + #verify if this script has been called as a plugin of todo.sh cli if args[0] == basename(__file__): args.pop(0) elif args[0] == 'usage': print(usage) sys.exit(0) - + if ((len(args) < 1) or (len(args) > 3)): raise PunchCommandError else: From 6c41e6f9e2587b4e8f3fd5ab2d78b8928bc62337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Mandera?= Date: Thu, 2 Feb 2017 08:11:14 +0100 Subject: [PATCH 9/9] Update README.md --- README.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ba91880..6b2dfcc 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,14 @@ -punch -===== - Punch for Todo.txt +================== + +This project was originally forked by +[adewinter](https://github.com/adewinter/punch) from +http://code.google.com/p/punch-time-tracking/: -DESC -==== -This is a project forked from http://code.google.com/p/punch-time-tracking/: +> Punch is a time-tracking add-on for todo.txt - a command line to-do list list +> utility. Punch works alongside the todo.txt script files popularized by Life +> Hacker and todotxt.org. All time tracking info is kept in a separate file, so +> no harm is done to the todo.txt system. It does use your todo.cfg file and +> todo.txt file to streamline time tracking. - " Punch is a time-tracking add-on for todo.txt - a command line to-do list list utility. Punch works alongside the todo.txt script files popularized by Life Hacker and todotxt.org. All time tracking info is kept in a separate file, so no harm is done to the todo.txt system. It does use your todo.cfg file and todo.txt file to streamline time tracking. " \ No newline at end of file +This is a cleaned, Python 3 compatible version.