diff --git a/.github/ISSUE_TEMPLATE/documentation.yaml b/.github/ISSUE_TEMPLATE/documentation.yaml index d94fc138..48a71ed8 100644 --- a/.github/ISSUE_TEMPLATE/documentation.yaml +++ b/.github/ISSUE_TEMPLATE/documentation.yaml @@ -12,23 +12,23 @@ body: Please fill out the points below, as it will make our process much easier. - type: textarea - id: affected-pages + id: what-could-be-better attributes: - label: Affected Page(s) + label: Description description: > - Please tell us which page, or pages, from the documentation site - (https://dvklopfenstein.github.io/timetracker/) are affected in this issue - placeholder: "example: https://dvklopfenstein.github.io/timetracker/" + Please write a short description of what you hope can be clarified or + further explained. validations: required: true - type: textarea - id: what-could-be-better + id: affected-pages attributes: - label: What Could Be Better? + label: Affected Page(s) description: > - Please write a short description of what you hope can be clarified or - further explained. + Please tell us which page, or pages, from the documentation site + (https://dvklopfenstein.github.io/timetracker/) are affected in this issue + placeholder: "example: https://dvklopfenstein.github.io/timetracker/" validations: required: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 757f2ebb..cbe6bb7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ # Summary * [**Unreleased**](#unreleased) +* [**Release 2025-11-17 v0.8a7**](#release-2025-11-17-v08a7) Easily start timer using the end time of the last time unit * [**Release 2025-08-18 v0.8a6**](#release-2025-08-18-v08a6) Added to documentation * [**Release 2025-08-09 v0.8a5**](#release-2025-08-09-v08a5) Creating Microsoft Word docx files is now optional * [**Release 2025-07-15 v0.8a4**](#release-2025-07-15-v08a4) Made options and output more concise @@ -38,6 +39,10 @@ ## Unreleased +## Release 2025-11-17 v0.8a7 +* ADDED ability to start timer 1 second after the end of the previous time slot + * usage: `git start --last` + ## Release 2025-08-18 v0.8a6 * CHANGED basic usage documentation to add a tip * CHANGED Simplify basic usage by docs by moving selected basic usage docs to advanced docs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..f2da2745 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,127 @@ + + +# Contributing to timetracker-csv + +We welcome contributions to timetracker-csv, +whether it's through reporting bugs, +improving the documentation, +testing releases, +engaging in discussion on features and bugs, +or writing code. + +## Table of Contents + * [Code of Conduct](#code-of-conduct) + * [Reporting Bugs](#reporting-bugs) + * [Editing Documentation](#editing-documentation) + * [Testing](#testing) + * [Submitting feature requests and ideas](#submitting-feature-requests-and-ideas) + * [Developing timetracker-csv](#developing) + +## Code of Conduct + +Before starting, please read the +[Code of Conduct](https://github.com/dvklopfenstein/timetracker/blob/main/CODE_OF_CONDUCT.md). + +## Reporting Bugs + +Please report bugs by +[opening a new issue](https://github.com/dvklopfenstein/timetracker/issues/new/choose) +and describing it as well as possible. +Many bugs are specific to a particular operating system and Python version, so please include that information! + +## Editing Documentation + +If you find a typo or a mistake in the docs, +please fix it right away and send a pull request. +If you're unsure what to change but still see a problem, you can +[open a new issue](https://github.com/dvklopfenstein/timetracker/issues/new/choose) +with the "Documentation change" type. + +To edit the documentation, edit the `docs/*.md` files on the **main** branch. +You can see the result by running `make mkdoc` inside the project's root directory, +then navigating your browser to [localhost:8000](http://localhost:8000). + +### External editors and tips and tricks + +If you'd like to share a timetracker-csv command line trick that you find useful, +you may find it worthwhile to add it to the +["Tips and Tricks" section](tips-and-tricks.md). + +## Testing + +Much of the work of maintaining timetracker-csv involves testing rather than coding. +We welcome tests. + +## Submitting feature requests and ideas + +If you have a feature request or idea for timetracker-csv, please +[open a new issue](https://github.com/dvklopfenstein/timetracker/issues/new/choose) +and describe the goal of the feature, and any relevant use cases. +We'll discuss the issue with you, and decide if it's a good fit for the project. + +When discussing new features, please keep in mind our design goals. timetracker-csv strives to +[do one thing well](https://en.wikipedia.org/wiki/Unix_philosophy). To us, that means: + +* being _nimble_ +* having a simple interface +* avoiding duplicating functionality + +## Developing + +Pull requests should be made on the `main` branch. + +### Updating automated tests + +When resolving bugs or adding new functionality, +please add tests to prevent that functionality from breaking in the future. +If you notice any functionality that isn't covered in the tests, +feel free to submit a test-only pull request as well. + +For testing, timetracker-csv uses [pytest](https://docs.pytest.org) for unit tests. +All tests are in the `tests` folder. + +### Submitting pull requests + +When you're ready, feel free to submit a pull request (PR). +The continuous integration pipeline will +run automated tests on your PR within a matter of minutes and +will report back any issues +it has found with your code across a variety of environments. + +The pull request template contains a checklist full of housekeeping items. Please fill them out as necessary when you submit. + +If a pull request contains failing tests, +it probably will not be reviewed, +and it definitely will not be approved. +However, if you need help resolving a failing test, please mention that in your PR. + +### Finding things to work on + +You can search the +[timetracker-csv GitHub issues](https://github.com/dvklopfenstein/timetracker/issues) by +[label](https://github.com/dvklopfenstein/timetracker/labels) +for things to work on. Here are some labels worth searching: + +* [bug](https://github.com/dvklopfenstein/timetracker/labels/bug) +* [documentation](https://github.com/dvklopfenstein/timetracker/labels/documentation) +* [duplicate](https://github.com/dvklopfenstein/timetracker/labels/duplicate) +* [enhancement](https://github.com/dvklopfenstein/timetracker/labels/enhancement) +* [good first issue](https://github.com/dvklopfenstein/timetracker/labels/good%20first%20issue) +* [help wanted](https://github.com/dvklopfenstein/timetracker/labels/help%20wanted) +* [invalid](https://github.com/dvklopfenstein/timetracker/labels/invalid) +* [question](https://github.com/dvklopfenstein/timetracker/labels/question) +* [wontfix](https://github.com/dvklopfenstein/timetracker/labels/wontfix) + +### A note for new programmers and programmers new to Python + +If you have a question, please don't hesitate to ask! +Python is known for its welcoming community and openness to novice programmers, +so feel free to fork the code and play around with it! +If you create something you want to share with us, please create a pull request. +We never expect pull requests to be perfect, idiomatic, instantly mergeable code. +We can work through it together! + +Copyright (C) 2025, DV Klopfenstein, PhD. All rights reserved diff --git a/README.md b/README.md index 38477c66..f8ea3bfe 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,13 @@

Timetracker-csv

Pandas-friendly time tracking from the CLI, repo by repo

-PyPI - Version | -DOI | +PyPI - Version +DOI License +Python Version from PEP 621 TOML

+
 ┌────────────────────────────┐
 │ 🕒 Timetracker CLI Tool    │
 │ Track time → CSV → pandas  │
diff --git a/makefile b/makefile
index 0dd63d93..c38db2ec 100644
--- a/makefile
+++ b/makefile
@@ -90,6 +90,9 @@ vim_ver:
 	vim -p timetracker/__init__.py pyproject.toml CHANGELOG.md
 
 # -----------------------------------------------------------------------------
+./doc/mkdocs/source/contributing.md: CONTRIBUTING.md
+	cp $< $@
+
 clean_build:
 	rm -rf build/
 	rm -rf timetracker_csv.egg-info/
@@ -102,6 +105,7 @@ cltt:
 	rm -rf .timetracker/
 
 clean:
+	make ./doc/mkdocs/source/contributing.md
 	make clean_build
 	make clean_pycache
 	rm -f test_timetracker.csv
diff --git a/pyproject.toml b/pyproject.toml
index baddf239..7cefefd2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
 [project]
 name = "timetracker-csv"
 description = "Pandas-friendly time tracking from the CLI"
-version = '0.8.6'
+version = '0.8.7'
 license = "AGPL-3.0-or-later"
 authors = [
   {name = 'DV Klopfenstein, PhD', email = 'dvklopfenstein@protonmail.com'},
diff --git a/timetracker/__init__.py b/timetracker/__init__.py
index c120169a..071c5d72 100644
--- a/timetracker/__init__.py
+++ b/timetracker/__init__.py
@@ -2,6 +2,6 @@
 
 __copyright__ = 'Copyright (C) 2025-present, DV Klopfenstein, PhD. All rights reserved'
 __author__ = 'DV Klopfenstein, PhD'
-__version__ = '0.8.6'
+__version__ = '0.8.7'
 
 # Copyright (C) 2025-present, DV Klopfenstein, PhD. All rights reserved
diff --git a/timetracker/cli.py b/timetracker/cli.py
index d6079035..09e60158 100755
--- a/timetracker/cli.py
+++ b/timetracker/cli.py
@@ -196,6 +196,8 @@ def _add_subparser_start(subparsers):
             help='start tracking at a '
                  'specific(ex: 4pm, "Tue 4pm") or '
                  'elapsed time(ex: 10min, -10min, 4hr)')
+        parser.add_argument('-l', '--last', action='store_true',
+            help='start timer using the end of the last time period')
         return parser
 
     def _add_subparser_stop(self, subparsers):
diff --git a/timetracker/cmd/start.py b/timetracker/cmd/start.py
index 3d9835ac..259babfa 100755
--- a/timetracker/cmd/start.py
+++ b/timetracker/cmd/start.py
@@ -3,6 +3,8 @@
 __copyright__ = 'Copyright (C) 2025-present, DV Klopfenstein, PhD. All rights reserved.'
 __author__ = "DV Klopfenstein, PhD"
 
+import os.path as op
+from timetracker.csvfile import CsvFile
 from timetracker.epoch.epoch import get_dt_at
 from timetracker.cmd import common
 
@@ -13,15 +15,17 @@ def cli_run_start(fnamecfg, args):
         fnamecfg,
         args.name,
         start_at=args.at,
-        force=args.force)
+        last=args.last,
+        force=args.force,
+        uname=args.name)
 
-def _run_start(fnamecfg, name=None, start_at=None, **kwargs):
+def _run_start(fnamecfg, name=None, start_at=None, last=None, **kwargs):
     """Initialize timetracking on a project"""
     cfg = common.get_cfg(fnamecfg)
     cfgproj = cfg.cfg_loc
-    return run_start(cfgproj, name, start_at, **kwargs)
+    return run_start(cfgproj, name, start_at, last, **kwargs)
 
-def run_start(cfgproj, name=None, start_at=None, **kwargs):
+def run_start(cfgproj, name=None, start_at=None, last=None, **kwargs):
     """Initialize timetracking on a project"""
     startobj = cfgproj.get_starttime_obj(name)
     if startobj is None:
@@ -37,7 +41,8 @@ def run_start(cfgproj, name=None, start_at=None, **kwargs):
 
     # Set (if not started) or reset (if start is forced) starting time
     if not startobj.started() or force:
-        starttime = get_dt_at(start_at, kwargs.get('now'), kwargs.get('defaultdt'))
+        starttime = _get_starttime(start_at, last, cfgproj, kwargs)
+        #print(f'STARTTIME: {starttime}')
         if starttime is None:
             return startobj
         startobj.wr_starttime(starttime, kwargs.get('activity'), kwargs.get('tag'))
@@ -53,6 +58,25 @@ def run_start(cfgproj, name=None, start_at=None, **kwargs):
             print(f'Run `trk start --at {start_at} --force` to force restart')
     return startobj
 
+
+def _get_starttime(start_at, last, cfgproj, kwargs):
+    ##if start_at != 'last':
+    if not last:
+        return get_dt_at(start_at, kwargs.get('now'), kwargs.get('defaultdt'))
+    fcsv = cfgproj.get_filename_csv(kwargs.get('uname'), kwargs.get('dirhome'))
+    if op.exists(fcsv):
+        return _get_next_starttime(fcsv)
+    return get_dt_at(None, kwargs.get('now'), kwargs.get('defaultdt'))
+
+def _get_next_starttime(fcsv):
+    csvfile = CsvFile(fcsv)
+    ntd = csvfile.rd_last_line()
+    if ntd:
+        #print(f'  READ: {fcsv}')
+        #print(ntd)
+        return csvfile.get_next_start_datetime(ntd, seconds=1)
+    return None
+
 def _get_msg(start_at, force):
     if force:
         return "start reset to"
diff --git a/timetracker/csvfile.py b/timetracker/csvfile.py
index 8d053073..e46c6157 100755
--- a/timetracker/csvfile.py
+++ b/timetracker/csvfile.py
@@ -4,11 +4,12 @@
 __copyright__ = 'Copyright (C) 2025-present, DV Klopfenstein, PhD. All rights reserved.'
 __author__ = "DV Klopfenstein, PhD"
 
+import os
 from os.path import exists
 from collections import namedtuple
 from datetime import timedelta
 # https://docs.python.org/3/library/csv.html
-from csv import writer
+import csv
 
 from timetracker.csvutils import get_hdr_itr
 from timetracker.epoch.calc import dt_from_str
@@ -47,6 +48,7 @@ def wr_csvline(self, dta, delta, csvfields):
             with open(self.fcsv, 'w', encoding='utf8') as csvfile:
                 self.wr_hdrs(csvfile)
         # Print time information into csv
+        _writer = csv.writer
         with open(self.fcsv, 'a', encoding='utf8') as csvfile:
             # timedelta(days=0, seconds=0, microseconds=0,
             #           milliseconds=0, minutes=0, hours=0, weeks=0)
@@ -55,7 +57,7 @@ def wr_csvline(self, dta, delta, csvfields):
             data = [str(dta),
                     str(delta),
                     csvfields.activity, csvfields.message, csvfields.tags]
-            writer(csvfile, lineterminator='\n').writerow(data)
+            _writer(csvfile, lineterminator='\n').writerow(data)
             return data
         return None
 
@@ -63,12 +65,41 @@ def wr_hdrs(self, prt):
         """Write header"""
         print(','.join(self.hdrs), file=prt)
 
+    @staticmethod
+    def get_next_start_datetime(ntd, **tdkws):
+        """Get the next start_datetime after the last line in a timetracker csv file"""
+        # https://docs.python.org/3/library/datetime.html#datetime.timedelta
+        return ntd.start_datetime + ntd.duration + timedelta(**tdkws)
+
+    def rd_last_line(self):
+        """Return the last line in the csv file"""
+        try:
+            fptr = open(self.fcsv, mode='rb')  #encoding='utf8')
+        except (PermissionError, OSError) as err:
+            print(type(err).__name__, err.args)
+        else:
+            with fptr as csvstrm:
+                try:
+                    csvstrm.seek(-2, os.SEEK_END)
+                    while csvstrm.read(1) != b'\n':
+                        csvstrm.seek(-2, os.SEEK_CUR)
+                except OSError:
+                    csvstrm.seek(0)
+                csv_line   = csvstrm.readline().decode()
+                csv_reader = csv.reader([csv_line])
+                row = next(csv_reader)
+                nts = self._get_ntdata([row])
+                if nts:
+                    assert len(nts) == 1, nts
+                    return nts[0]
+        return None
+
     # ------------------------------------------------------------------
     def _get_ntdata(self, csvlines):
         """Get data where start and stop are datetimes; timdelta is calculated from them"""
         nto = self.nto
         def _get_nt(row):
-            assert len(row) == 5, f'{self.fcsv} ROW[{len(row)}]: {row}'
+            assert len(row) == 5, row
             return nto(
                 start_datetime=dt_from_str(row[0]),
                 duration=td_from_str(row[1]),