Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .github/ISSUE_TEMPLATE/documentation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
127 changes: 127 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<!--
Copyright (C) 2025 DV Klopfenstein, PhD
License: https://www.gnu.org/licenses/agpl-3.0.en.html#license-text
-->

# 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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
<h1 align="center">Timetracker-csv</h1>
<h3 align="center">Pandas-friendly time tracking from the CLI, repo by repo</h3>
<h3 align="center">
<a href="https://pypi.org/project/timetracker-csv"><img src="https://img.shields.io/pypi/v/timetracker-csv" alt="PyPI - Version"></a> |
<a href="https://doi.org/10.5281/zenodo.14803226"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.14803226.svg" alt="DOI"></a> |
<a href="https://pypi.org/project/timetracker-csv"><img src="https://img.shields.io/pypi/v/timetracker-csv" alt="PyPI - Version"></a>
<a href="https://doi.org/10.5281/zenodo.14803226"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.14803226.svg" alt="DOI"></a>
<a href="https://www.gnu.org/licenses/agpl-3.0.en.html"><img src="https://img.shields.io/github/license/dvklopfenstein/timetracker" alt="License"></a>
<img alt="Python Version from PEP 621 TOML" src="https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Fdvklopfenstein%2Ftimetracker%2Frefs%2Fheads%2Fmain%2Fpyproject.toml">
</h3>
<pre align="center" style="font-family: monospace; font-size: larger; border: 1px solid #ccc; padding: 10px; display: inline-block;">

┌────────────────────────────┐
│ 🕒 Timetracker CLI Tool │
│ Track time → CSV → pandas │
Expand Down
4 changes: 4 additions & 0 deletions makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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'},
Expand Down
2 changes: 1 addition & 1 deletion timetracker/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions timetracker/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
34 changes: 29 additions & 5 deletions timetracker/cmd/start.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand All @@ -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'))
Expand All @@ -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"
Expand Down
37 changes: 34 additions & 3 deletions timetracker/csvfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -55,20 +57,49 @@ 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

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]),
Expand Down
Loading