Merge remote-tracking branch 'upstream/master' into ignore-non-matching-patterns

This commit is contained in:
Darko Poljak 2017-02-15 21:30:50 +01:00
commit 2238ea7042
10 changed files with 218 additions and 172 deletions

View File

@ -38,9 +38,11 @@ WWW = 'http://www.nico.schottelius.org/software/ctt/'
# to ensure cross-os compatibility # to ensure cross-os compatibility
DISKFORMAT = DATETIMEFORMAT DISKFORMAT = DATETIMEFORMAT
class Error(Exception): class Error(Exception):
pass pass
# Our output format # Our output format
def user_timedelta(seconds): def user_timedelta(seconds):
"""Format timedelta for the user""" """Format timedelta for the user"""
@ -59,15 +61,17 @@ def user_timedelta(seconds):
return (hours, minutes, seconds) return (hours, minutes, seconds)
def ctt_dir(): def ctt_dir():
home = os.environ['HOME'] home = os.environ['HOME']
ctt_dir = os.path.join(home, ".ctt") ctt_dir = os.path.join(home, ".ctt")
return ctt_dir return ctt_dir
def project_dir(project): def project_dir(project):
project_dir = os.path.join(ctt_dir(), project) project_dir = os.path.join(ctt_dir(), project)
return project_dir return project_dir
return os.listdir(ctt_dir) # return os.listdir(ctt_dir)

View File

@ -26,6 +26,7 @@ import os
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class ListProjects(object): class ListProjects(object):
"""Return existing projects""" """Return existing projects"""
@ -33,7 +34,6 @@ class ListProjects(object):
def commandline(cls, args): def commandline(cls, args):
cls.print_projects() cls.print_projects()
@classmethod @classmethod
def print_projects(cls): def print_projects(cls):
for project in cls.list_projects(): for project in cls.list_projects():

View File

@ -21,16 +21,13 @@
# #
# #
import calendar
import datetime import datetime
import logging import logging
import time
import os import os
import os.path import os.path
import re import re
import sys
import glob import glob
import collections import collections
@ -39,6 +36,7 @@ import ctt.listprojects
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class Report(object): class Report(object):
"""Create a report on tracked time""" """Create a report on tracked time"""
@ -84,7 +82,6 @@ class Report(object):
cls.summary(total_time) cls.summary(total_time)
@staticmethod @staticmethod
def print_report_time_entries(report_data, output_format, summary): def print_report_time_entries(report_data, output_format, summary):
''' Print time entries from report_data report using output_format. ''' Print time entries from report_data report using output_format.
@ -109,7 +106,7 @@ class Report(object):
report, report_data = reports[project] report, report_data = reports[project]
if summary: if summary:
for time in report_data: for time in report_data:
if not time in summary_report: if time not in summary_report:
summary_report[time] = report_data[time] summary_report[time] = report_data[time]
else: else:
summary_report[time].extend(report_data[time]) summary_report[time].extend(report_data[time])
@ -117,20 +114,22 @@ class Report(object):
report.header() report.header()
Report.print_report_time_entries(report_data, Report.print_report_time_entries(report_data,
output_format, summary) output_format, summary)
if summary: # For summary do not print time entries.
Report.print_report_time_entries(summary_report, # if summary:
output_format, summary) # Report.print_report_time_entries(summary_report,
# output_format, summary)
def _init_date(self, start_date, end_date): def _init_date(self, start_date, end_date):
"""Setup date - either default or user given values""" """Setup date - either default or user given values"""
now = datetime.datetime.now() now = datetime.datetime.now()
first_day_this_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) first_day_this_month = now.replace(
next_month = first_day_this_month.replace(day=28) + datetime.timedelta(days=4) day=1, hour=0, minute=0, second=0, microsecond=0)
next_month = first_day_this_month.replace(
day=28) + datetime.timedelta(days=4)
first_day_next_month = next_month.replace(day=1) first_day_next_month = next_month.replace(day=1)
last_day_this_month = first_day_next_month - datetime.timedelta(seconds=1) last_day_this_month = first_day_next_month - datetime.timedelta(
seconds=1)
default_start_date = first_day_this_month default_start_date = first_day_this_month
default_end_date = last_day_this_month default_end_date = last_day_this_month
@ -140,18 +139,21 @@ class Report(object):
try: try:
if start_date: if start_date:
self.start_date = datetime.datetime.strptime(start_date[0], ctt.DATEFORMAT) self.start_date = datetime.datetime.strptime(
start_date[0], ctt.DATEFORMAT)
else: else:
self.start_date = default_start_date self.start_date = default_start_date
if end_date: if end_date:
self.end_date = datetime.datetime.strptime(end_date[0], ctt.DATEFORMAT) self.end_date = datetime.datetime.strptime(
end_date[0], ctt.DATEFORMAT)
else: else:
self.end_date = default_end_date self.end_date = default_end_date
except ValueError as e: except ValueError as e:
raise ctt.Error(e) raise ctt.Error(e)
self.end_date = self.end_date.replace(hour=23,minute=59,second=59) self.end_date = self.end_date.replace(
hour=23, minute=59, second=59)
if self.start_date >= self.end_date: if self.start_date >= self.end_date:
raise ctt.Error("End date must be after start date (%s >= %s)" % raise ctt.Error("End date must be after start date (%s >= %s)" %
@ -167,14 +169,20 @@ class Report(object):
for dirname in os.listdir(self.project_dir): for dirname in os.listdir(self.project_dir):
log.debug("Dirname: %s" % dirname) log.debug("Dirname: %s" % dirname)
try: try:
dir_datetime = datetime.datetime.strptime(dirname, ctt.DISKFORMAT) dir_datetime = datetime.datetime.strptime(
dirname, ctt.DISKFORMAT)
except ValueError: except ValueError:
log.warning("Invalid time entry {entry} for project {project}, skipping.".format(entry=dirname, project=self.project)) log.warning("Invalid time entry {entry} for project "
"{project}, skipping.".format(
entry=dirname, project=self.project))
continue continue
if dir_datetime >= self.start_date and dir_datetime <= self.end_date: if (dir_datetime >= self.start_date and
filename = os.path.join(self.project_dir, dirname, ctt.FILE_DELTA) dir_datetime <= self.end_date):
comment_filename = os.path.join(self.project_dir, dirname, ctt.FILE_COMMENT) filename = os.path.join(
self.project_dir, dirname, ctt.FILE_DELTA)
comment_filename = os.path.join(
self.project_dir, dirname, ctt.FILE_COMMENT)
# Check for matching comment # Check for matching comment
comment = None comment = None
@ -183,10 +191,11 @@ class Report(object):
comment = fd.read().rstrip('\n') comment = fd.read().rstrip('\n')
# If regular expression given, but not matching, skip entry # If regular expression given, but not matching, skip entry
if self.regexp and not re.search(self.regexp, comment, self.search_flags): if (self.regexp and
not re.search(self.regexp, comment,
self.search_flags)):
continue continue
self._report_db[dirname] = {} self._report_db[dirname] = {}
if comment: if comment:
self._report_db[dirname]['comment'] = comment self._report_db[dirname]['comment'] = comment
@ -194,7 +203,8 @@ class Report(object):
with open(filename, "r") as fd: with open(filename, "r") as fd:
self._report_db[dirname]['delta'] = fd.read().rstrip('\n') self._report_db[dirname]['delta'] = fd.read().rstrip('\n')
log.debug("Recording: %s: %s" % (dirname, self._report_db[dirname]['delta'])) log.debug("Recording: %s: %s"
% (dirname, self._report_db[dirname]['delta']))
else: else:
log.debug("Skipping: %s" % dirname) log.debug("Skipping: %s" % dirname)
@ -223,7 +233,6 @@ class Report(object):
return count return count
def _get_report_entry(self, time, entry): def _get_report_entry(self, time, entry):
''' Get one time entry data. ''' Get one time entry data.
''' '''
@ -245,7 +254,6 @@ class Report(object):
report['comment'] = False report['comment'] = False
return report return report
def report(self): def report(self):
"""Return total time tracked""" """Return total time tracked"""
@ -254,7 +262,7 @@ class Report(object):
for time in time_keys: for time in time_keys:
entry = self._report_db[time] entry = self._report_db[time]
report = self._get_report_entry(time, entry) report = self._get_report_entry(time, entry)
if not time in entries: if time not in entries:
entries[time] = [report] entries[time] = [report]
else: else:
entries[time].append(report) entries[time].append(report)

View File

@ -22,7 +22,9 @@
import os import os
import unittest import unittest
fixtures_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "fixtures")) fixtures_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),
"fixtures"))
class CttTestCase(unittest.TestCase): class CttTestCase(unittest.TestCase):
def setUp(self): def setUp(self):

View File

@ -25,6 +25,7 @@ import ctt
import ctt.listprojects as cttls import ctt.listprojects as cttls
import ctt.test import ctt.test
class ListProjectsTestCase(ctt.test.CttTestCase): class ListProjectsTestCase(ctt.test.CttTestCase):
def test_list_projects(self): def test_list_projects(self):

View File

@ -104,32 +104,34 @@ class ReportTestCase(ctt.test.CttTestCase):
report_data = rep.report() report_data = rep.report()
reports[project] = (rep, report_data) reports[project] = (rep, report_data)
expected_output = ( expected_output = (
"Report for foo1 between 2016-04-07 00:00:00 and 2016-04-08 23:59:59\n" "Report for foo1 between 2016-04-07 00:00:00 and "
"2016-04-08 23:59:59\n"
"2016-04-07-0826 (0:00:06): foo1\n" "2016-04-07-0826 (0:00:06): foo1\n"
"2016-04-08-1200 (1:23:20): foo1 12\n" "2016-04-08-1200 (1:23:20): foo1 12\n"
"Report for foo2 between 2016-04-07 00:00:00 and 2016-04-08 23:59:59\n" "Report for foo2 between 2016-04-07 00:00:00 and "
"2016-04-08 23:59:59\n"
"2016-04-07-0810 (0:00:10): foo2" "2016-04-07-0810 (0:00:10): foo2"
) )
rep.print_reports(reports, ctt.REPORTFORMAT, summary=False) rep.print_reports(reports, ctt.REPORTFORMAT, summary=False)
output = self._get_output() output = self._get_output()
self.assertEqual(output, expected_output) self.assertEqual(output, expected_output)
def test_print_reports_summary(self): # Summary should not print time entries
reports = collections.OrderedDict() # def test_print_reports_summary(self):
for project in ('foo1', 'foo2'): # reports = collections.OrderedDict()
rep = report.Report(project, ('2016-04-07',), ('2016-04-08',), # for project in ('foo1', 'foo2'):
ctt.REPORTFORMAT, None, None) # rep = report.Report(project, ('2016-04-07',), ('2016-04-08',),
report_data = rep.report() # ctt.REPORTFORMAT, None, None)
reports[project] = (rep, report_data) # report_data = rep.report()
expected_output = ( # reports[project] = (rep, report_data)
"2016-04-07-0810 (0:00:10): foo2\n" # expected_output = (
"2016-04-07-0826 (0:00:06): foo1\n" # "2016-04-07-0810 (0:00:10): foo2\n"
"2016-04-08-1200 (1:23:20): foo1 12" # "2016-04-07-0826 (0:00:06): foo1\n"
) # "2016-04-08-1200 (1:23:20): foo1 12"
rep.print_reports(reports, ctt.REPORTFORMAT, summary=True) # )
output = self._get_output() # rep.print_reports(reports, ctt.REPORTFORMAT, summary=True)
self.assertEqual(output, expected_output) # output = self._get_output()
# self.assertEqual(output, expected_output)
def test__init_date(self): def test__init_date(self):
rep = report.Report('foo1', ('2016-04-07', ), ('2016-04-07', ), rep = report.Report('foo1', ('2016-04-07', ), ('2016-04-07', ),
@ -141,7 +143,7 @@ class ReportTestCase(ctt.test.CttTestCase):
@unittest.expectedFailure @unittest.expectedFailure
def test__init_date_fail(self): def test__init_date_fail(self):
rep = report.Report('foo1', ('2016-04-08',), ('2016-04-07',), report.Report('foo1', ('2016-04-08', ), ('2016-04-07', ),
ctt.REPORTFORMAT, None, None) ctt.REPORTFORMAT, None, None)
def test__init_date_defaults(self): def test__init_date_defaults(self):
@ -150,15 +152,17 @@ class ReportTestCase(ctt.test.CttTestCase):
now = datetime.datetime.now() now = datetime.datetime.now()
expected_start_date = now.replace(day=1, hour=0, minute=0, second=0, expected_start_date = now.replace(day=1, hour=0, minute=0, second=0,
microsecond=0) microsecond=0)
next_month = expected_start_date.replace(day=28) + datetime.timedelta(days=4) next_month = expected_start_date.replace(day=28) + datetime.timedelta(
days=4)
first_day_next_month = next_month.replace(day=1) first_day_next_month = next_month.replace(day=1)
expected_end_date = first_day_next_month - datetime.timedelta(seconds=1) expected_end_date = first_day_next_month - datetime.timedelta(
seconds=1)
self.assertEqual(rep.start_date, expected_start_date) self.assertEqual(rep.start_date, expected_start_date)
self.assertEqual(rep.end_date, expected_end_date) self.assertEqual(rep.end_date, expected_end_date)
@unittest.expectedFailure @unittest.expectedFailure
def test__init_report_db_fail(self): def test__init_report_db_fail(self):
rep = report.Report('unexisting', ('2016-04-07',), ('2016-04-07',), report.Report('unexisting', ('2016-04-07',), ('2016-04-07',),
ctt.REPORTFORMAT, None, None) ctt.REPORTFORMAT, None, None)
def test__init_report_db(self): def test__init_report_db(self):

View File

@ -28,6 +28,7 @@ import os
import datetime import datetime
import shutil import shutil
class TrackerTestCase(ctt.test.CttTestCase): class TrackerTestCase(ctt.test.CttTestCase):
def setUp(self): def setUp(self):
@ -69,7 +70,7 @@ class TrackerTestCase(ctt.test.CttTestCase):
@unittest.expectedFailure @unittest.expectedFailure
def test__init__fail(self): def test__init__fail(self):
project = 'foo1' project = 'foo1'
tracker = tr.Tracker(project, start_datetime=('2016-04-090900',)) tr.Tracker(project, start_datetime=('2016-04-090900', ))
def test_delta(self): def test_delta(self):
project = 'foo1' project = 'foo1'
@ -126,7 +127,7 @@ class TrackerTestCase(ctt.test.CttTestCase):
tracker = tr.Tracker(project, start_datetime=(start_dt,), tracker = tr.Tracker(project, start_datetime=(start_dt,),
comment=True) comment=True)
end_dt = datetime.datetime(2016, 4, 9, hour=17, minute=45) end_dt = datetime.datetime(2016, 4, 9, hour=17, minute=45)
expected_delta = 15 * 60 # seconds # expected_delta = 15 * 60 # seconds
tracker.end_datetime = end_dt tracker.end_datetime = end_dt
tracker._tracked_time = True tracker._tracked_time = True
expected_comment = "test" expected_comment = "test"

View File

@ -22,17 +22,17 @@
import datetime import datetime
import logging import logging
import time
import os import os
import os.path import os.path
import sys
import ctt import ctt
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class Tracker: class Tracker:
def __init__(self, project, start_datetime = None, end_datetime = None, comment = True): def __init__(self, project, start_datetime=None, end_datetime=None,
comment=True):
self.project = project self.project = project
self.project_dir = ctt.project_dir(project) self.project_dir = ctt.project_dir(project)
@ -42,12 +42,14 @@ class Tracker:
# Setup default values # Setup default values
try: try:
if start_datetime: if start_datetime:
self.start_datetime = datetime.datetime.strptime(start_datetime[0], ctt.DATETIMEFORMAT) self.start_datetime = datetime.datetime.strptime(
start_datetime[0], ctt.DATETIMEFORMAT)
else: else:
self.start_datetime = None self.start_datetime = None
if end_datetime: if end_datetime:
self.end_datetime = datetime.datetime.strptime(end_datetime[0], ctt.DATETIMEFORMAT) self.end_datetime = datetime.datetime.strptime(
end_datetime[0], ctt.DATETIMEFORMAT)
else: else:
self.end_datetime = None self.end_datetime = None
except ValueError as e: except ValueError as e:
@ -56,7 +58,6 @@ class Tracker:
if self.start_datetime and self.end_datetime: if self.start_datetime and self.end_datetime:
self._tracked_time = True self._tracked_time = True
@classmethod @classmethod
def commandline(cls, args): def commandline(cls, args):
tracker = cls(args.project[0], args.start, args.end, args.comment) tracker = cls(args.project[0], args.start, args.end, args.comment)
@ -106,7 +107,8 @@ class Tracker:
time_dir = os.path.join(self.project_dir, subdirname) time_dir = os.path.join(self.project_dir, subdirname)
if os.path.exists(time_dir): if os.path.exists(time_dir):
raise ctt.Error("Already tracked time at this beginning for this project") raise ctt.Error(
"Already tracked time at this beginning for this project")
os.makedirs(time_dir, mode=0o700) os.makedirs(time_dir, mode=0o700)

View File

@ -23,7 +23,6 @@
import argparse import argparse
import logging import logging
import os.path
import sys import sys
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -34,13 +33,15 @@ log = logging.getLogger(__name__)
# Record tags # Record tags
def parse_argv(argv, version): def parse_argv(argv, version):
parser = {} parser = {}
parser['loglevel'] = argparse.ArgumentParser(add_help=False) parser['loglevel'] = argparse.ArgumentParser(add_help=False)
parser['loglevel'].add_argument('-d', '--debug', parser['loglevel'].add_argument(
help='set log level to debug', action='store_true', '-d', '--debug', help='set log level to debug', action='store_true',
default=False) default=False)
parser['loglevel'].add_argument('-v', '--verbose', parser['loglevel'].add_argument(
'-v', '--verbose',
help='set log level to info, be more verbose', help='set log level to info, be more verbose',
action='store_true', default=False) action='store_true', default=False)
@ -48,38 +49,61 @@ def parse_argv(argv, version):
parents=[parser['loglevel']]) parents=[parser['loglevel']])
parser['sub'] = parser['main'].add_subparsers(title="Commands") parser['sub'] = parser['main'].add_subparsers(title="Commands")
parser['listprojects'] = parser['sub'].add_parser(
parser['listprojects'] = parser['sub'].add_parser('listprojects', 'listprojects', parents=[parser['loglevel']])
parents=[parser['loglevel']])
parser['listprojects'].set_defaults(func=ListProjects.commandline) parser['listprojects'].set_defaults(func=ListProjects.commandline)
parser['track'] = parser['sub'].add_parser('track', parser['track'] = parser['sub'].add_parser('track',
parents=[parser['loglevel']]) parents=[parser['loglevel']])
parser['track'].set_defaults(func=Tracker.commandline) parser['track'].set_defaults(func=Tracker.commandline)
parser['track'].add_argument("--sd", "--start", help="start date (default: first of this month, format: %s)" % ctt.DATEFORMAT_PLAIN, parser['track'].add_argument(
"--sd", "--start",
help="start date (default: first of this month, format: %s)"
% ctt.DATEFORMAT_PLAIN,
nargs=1, dest="start") nargs=1, dest="start")
parser['track'].add_argument("--ed", "--end", help="end date (default: last of this month, format: %s)" % ctt.DATEFORMAT_PLAIN, parser['track'].add_argument(
"--ed", "--end",
help="end date (default: last of this month, format: %s)"
% ctt.DATEFORMAT_PLAIN,
nargs=1, default=None, dest="end") nargs=1, default=None, dest="end")
parser['track'].add_argument("-n", "--no-comment", help="disable comment prompting after tracking", parser['track'].add_argument(
"-n", "--no-comment", help="disable comment prompting after tracking",
action='store_false', dest="comment") action='store_false', dest="comment")
parser['track'].add_argument("project", help="project to track time for", nargs=1) parser['track'].add_argument(
"project", help="project to track time for", nargs=1)
parser['report'] = parser['sub'].add_parser('report', parser['report'] = parser['sub'].add_parser('report',
parents=[parser['loglevel']]) parents=[parser['loglevel']])
parser['report'].set_defaults(func=Report.commandline) parser['report'].set_defaults(func=Report.commandline)
parser['report'].add_argument("project", help="project to report time for", nargs='*') parser['report'].add_argument(
parser['report'].add_argument("--sd", "--start", help="start date (default: first of this month, format: %s)" % ctt.DATEFORMAT_PLAIN, "project", help="project to report time for", nargs='*')
parser['report'].add_argument(
"--sd", "--start",
help="start date (default: first of this month, format: %s)"
% ctt.DATEFORMAT_PLAIN,
nargs=1, dest="start") nargs=1, dest="start")
parser['report'].add_argument("--ed", "--end", help="end date (default: last of this month, format: %s)" % ctt.DATEFORMAT_PLAIN, parser['report'].add_argument(
"--ed", "--end",
help="end date (default: last of this month, format: %s)"
% ctt.DATEFORMAT_PLAIN,
nargs=1, default=None, dest="end") nargs=1, default=None, dest="end")
parser['report'].add_argument("-a", "--all", help="List entries for all projects", action='store_true') parser['report'].add_argument(
parser['report'].add_argument("-e", "--regexp", help="regular expression to match", "-a", "--all", help="List entries for all projects",
default=None) action='store_true')
parser['report'].add_argument("-i", "--ignore-case", help="ignore case distinctions", action="store_true") parser['report'].add_argument(
parser['report'].add_argument("-f", "--format", help="output format (default: %s)" % ctt.REPORTFORMAT, "-e", "--regexp", help="regular expression to match", default=None)
parser['report'].add_argument(
"-i", "--ignore-case", help="ignore case distinctions",
action="store_true")
parser['report'].add_argument(
"-f", "--format",
help="output format (default: %s)" % ctt.REPORTFORMAT,
default=ctt.REPORTFORMAT, dest="output_format") default=ctt.REPORTFORMAT, dest="output_format")
parser['report'].add_argument("-s", "--summary", help="hide project names and list time entries in chronological order", action="store_true") parser['report'].add_argument(
"-s", "--summary",
help="hide project names and list time entries in chronological order",
action="store_true")
# parser['track'].add_argument("-t", "--tag", help="Add tags", # parser['track'].add_argument("-t", "--tag", help="Add tags",
# action="store_true") # action="store_true")
@ -114,6 +138,5 @@ if __name__ == "__main__":
from ctt.report import Report from ctt.report import Report
from ctt.listprojects import ListProjects from ctt.listprojects import ListProjects
parse_argv(sys.argv[1:], ctt.VERSION) parse_argv(sys.argv[1:], ctt.VERSION)
sys.exit(0) sys.exit(0)

View File

@ -4,9 +4,10 @@ script to install ctt
""" """
import sys import sys
from setuptools import setup from setuptools import setup
sys.path.insert(0, 'lib/')
import ctt import ctt
sys.path.insert(0, 'lib/')
setup(name='ctt', setup(name='ctt',
version=ctt.VERSION, version=ctt.VERSION,
author=ctt.AUTHOR, author=ctt.AUTHOR,
@ -28,5 +29,5 @@ setup(name='ctt',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Operating System :: POSIX', 'Operating System :: POSIX',
'Programming Language :: Python', 'Programming Language :: Python',
'Requires-Python:: 3.x'] 'Requires-Python:: 3.x'
) ])