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
DISKFORMAT = DATETIMEFORMAT
class Error(Exception):
pass
# Our output format
def user_timedelta(seconds):
"""Format timedelta for the user"""
@ -59,15 +61,17 @@ def user_timedelta(seconds):
return (hours, minutes, seconds)
def ctt_dir():
home = os.environ['HOME']
ctt_dir = os.path.join(home, ".ctt")
return ctt_dir
def project_dir(project):
project_dir = os.path.join(ctt_dir(), project)
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__)
class ListProjects(object):
"""Return existing projects"""
@ -33,7 +34,6 @@ class ListProjects(object):
def commandline(cls, args):
cls.print_projects()
@classmethod
def print_projects(cls):
for project in cls.list_projects():

View file

@ -21,16 +21,13 @@
#
#
import calendar
import datetime
import logging
import time
import os
import os.path
import re
import sys
import glob
import collections
@ -39,6 +36,7 @@ import ctt.listprojects
log = logging.getLogger(__name__)
class Report(object):
"""Create a report on tracked time"""
@ -84,7 +82,6 @@ class Report(object):
cls.summary(total_time)
@staticmethod
def print_report_time_entries(report_data, output_format, summary):
''' Print time entries from report_data report using output_format.
@ -109,7 +106,7 @@ class Report(object):
report, report_data = reports[project]
if summary:
for time in report_data:
if not time in summary_report:
if time not in summary_report:
summary_report[time] = report_data[time]
else:
summary_report[time].extend(report_data[time])
@ -117,20 +114,22 @@ class Report(object):
report.header()
Report.print_report_time_entries(report_data,
output_format, summary)
if summary:
Report.print_report_time_entries(summary_report,
output_format, summary)
# For summary do not print time entries.
# if summary:
# Report.print_report_time_entries(summary_report,
# output_format, summary)
def _init_date(self, start_date, end_date):
"""Setup date - either default or user given values"""
now = datetime.datetime.now()
first_day_this_month = now.replace(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_this_month = now.replace(
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)
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_end_date = last_day_this_month
@ -140,18 +139,21 @@ class Report(object):
try:
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:
self.start_date = default_start_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:
self.end_date = default_end_date
except ValueError as 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:
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):
log.debug("Dirname: %s" % dirname)
try:
dir_datetime = datetime.datetime.strptime(dirname, ctt.DISKFORMAT)
dir_datetime = datetime.datetime.strptime(
dirname, ctt.DISKFORMAT)
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
if dir_datetime >= self.start_date and dir_datetime <= self.end_date:
filename = os.path.join(self.project_dir, dirname, ctt.FILE_DELTA)
comment_filename = os.path.join(self.project_dir, dirname, ctt.FILE_COMMENT)
if (dir_datetime >= self.start_date and
dir_datetime <= self.end_date):
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
comment = None
@ -183,10 +191,11 @@ class Report(object):
comment = fd.read().rstrip('\n')
# 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
self._report_db[dirname] = {}
if comment:
self._report_db[dirname]['comment'] = comment
@ -194,7 +203,8 @@ class Report(object):
with open(filename, "r") as fd:
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:
log.debug("Skipping: %s" % dirname)
@ -223,7 +233,6 @@ class Report(object):
return count
def _get_report_entry(self, time, entry):
''' Get one time entry data.
'''
@ -245,7 +254,6 @@ class Report(object):
report['comment'] = False
return report
def report(self):
"""Return total time tracked"""
@ -254,7 +262,7 @@ class Report(object):
for time in time_keys:
entry = self._report_db[time]
report = self._get_report_entry(time, entry)
if not time in entries:
if time not in entries:
entries[time] = [report]
else:
entries[time].append(report)

View file

@ -22,7 +22,9 @@
import os
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):
def setUp(self):

View file

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

View file

@ -104,32 +104,34 @@ class ReportTestCase(ctt.test.CttTestCase):
report_data = rep.report()
reports[project] = (rep, report_data)
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-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"
)
rep.print_reports(reports, ctt.REPORTFORMAT, summary=False)
output = self._get_output()
self.assertEqual(output, expected_output)
def test_print_reports_summary(self):
reports = collections.OrderedDict()
for project in ('foo1', 'foo2'):
rep = report.Report(project, ('2016-04-07',), ('2016-04-08',),
ctt.REPORTFORMAT, None, None)
report_data = rep.report()
reports[project] = (rep, report_data)
expected_output = (
"2016-04-07-0810 (0:00:10): foo2\n"
"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()
self.assertEqual(output, expected_output)
# Summary should not print time entries
# def test_print_reports_summary(self):
# reports = collections.OrderedDict()
# for project in ('foo1', 'foo2'):
# rep = report.Report(project, ('2016-04-07',), ('2016-04-08',),
# ctt.REPORTFORMAT, None, None)
# report_data = rep.report()
# reports[project] = (rep, report_data)
# expected_output = (
# "2016-04-07-0810 (0:00:10): foo2\n"
# "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()
# self.assertEqual(output, expected_output)
def test__init_date(self):
rep = report.Report('foo1', ('2016-04-07', ), ('2016-04-07', ),
@ -141,7 +143,7 @@ class ReportTestCase(ctt.test.CttTestCase):
@unittest.expectedFailure
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)
def test__init_date_defaults(self):
@ -150,15 +152,17 @@ class ReportTestCase(ctt.test.CttTestCase):
now = datetime.datetime.now()
expected_start_date = now.replace(day=1, hour=0, minute=0, second=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)
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.end_date, expected_end_date)
@unittest.expectedFailure
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)
def test__init_report_db(self):

View file

@ -28,6 +28,7 @@ import os
import datetime
import shutil
class TrackerTestCase(ctt.test.CttTestCase):
def setUp(self):
@ -69,7 +70,7 @@ class TrackerTestCase(ctt.test.CttTestCase):
@unittest.expectedFailure
def test__init__fail(self):
project = 'foo1'
tracker = tr.Tracker(project, start_datetime=('2016-04-090900',))
tr.Tracker(project, start_datetime=('2016-04-090900', ))
def test_delta(self):
project = 'foo1'
@ -126,7 +127,7 @@ class TrackerTestCase(ctt.test.CttTestCase):
tracker = tr.Tracker(project, start_datetime=(start_dt,),
comment=True)
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._tracked_time = True
expected_comment = "test"

View file

@ -22,17 +22,17 @@
import datetime
import logging
import time
import os
import os.path
import sys
import ctt
log = logging.getLogger(__name__)
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_dir = ctt.project_dir(project)
@ -42,12 +42,14 @@ class Tracker:
# Setup default values
try:
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:
self.start_datetime = None
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:
self.end_datetime = None
except ValueError as e:
@ -56,7 +58,6 @@ class Tracker:
if self.start_datetime and self.end_datetime:
self._tracked_time = True
@classmethod
def commandline(cls, args):
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)
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)

View file

@ -23,7 +23,6 @@
import argparse
import logging
import os.path
import sys
log = logging.getLogger(__name__)
@ -34,13 +33,15 @@ log = logging.getLogger(__name__)
# Record tags
def parse_argv(argv, version):
parser = {}
parser['loglevel'] = argparse.ArgumentParser(add_help=False)
parser['loglevel'].add_argument('-d', '--debug',
help='set log level to debug', action='store_true',
parser['loglevel'].add_argument(
'-d', '--debug', help='set log level to debug', action='store_true',
default=False)
parser['loglevel'].add_argument('-v', '--verbose',
parser['loglevel'].add_argument(
'-v', '--verbose',
help='set log level to info, be more verbose',
action='store_true', default=False)
@ -48,38 +49,61 @@ def parse_argv(argv, version):
parents=[parser['loglevel']])
parser['sub'] = parser['main'].add_subparsers(title="Commands")
parser['listprojects'] = parser['sub'].add_parser('listprojects',
parents=[parser['loglevel']])
parser['listprojects'] = parser['sub'].add_parser(
'listprojects', parents=[parser['loglevel']])
parser['listprojects'].set_defaults(func=ListProjects.commandline)
parser['track'] = parser['sub'].add_parser('track',
parents=[parser['loglevel']])
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")
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")
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")
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',
parents=[parser['loglevel']])
parser['report'].set_defaults(func=Report.commandline)
parser['report'].add_argument("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,
parser['report'].add_argument(
"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")
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")
parser['report'].add_argument("-a", "--all", help="List entries for all projects", action='store_true')
parser['report'].add_argument("-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,
parser['report'].add_argument(
"-a", "--all", help="List entries for all projects",
action='store_true')
parser['report'].add_argument(
"-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")
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",
# action="store_true")
@ -114,6 +138,5 @@ if __name__ == "__main__":
from ctt.report import Report
from ctt.listprojects import ListProjects
parse_argv(sys.argv[1:], ctt.VERSION)
sys.exit(0)

View file

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