Skip to content

Commit e86870f

Browse files
Merge pull request #55 from dvklopfenstein/dev
Added functions to collect various groups of project csvs; Added running `git add` during `trk init`
2 parents f07e3ac + e13de9f commit e86870f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1052
-794
lines changed

.timetracker/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
start_*.txt

.timetracker/config

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# TimeTracker project configuration file
2+
3+
project = "timetracker"
4+
5+
[csv]
6+
filename = "./timetracker_timetracker_$USER$.csv"

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
# Summary
44
* [**Unreleased**](#unreleased)
5+
* [**Release 2025-05-27 v0.5a9**](#release-2025-05-27-v05a9) Added functions to collect various groups of csv files
56
* [**Release 2025-05-18 v0.5a7**](#release-2025-05-18-v05a7) Add starttime to `report`; Bug fix in `cancel`
67
* [**Release 2025-05-16 v0.5a6**](#release-2025-05-16-v05a6) Add options to `projects` command; Fix `report` command
78
* [**Release 2025-05-15 v0.5a5**](#release-2025-05-15-v05a5) Report command tested and updated; Get csv files for a single user tested and implemented
@@ -30,6 +31,16 @@
3031

3132
## Unreleased
3233

34+
## Release 2025-05-27 v0.5a9
35+
* ADDED `trk hours` options `--global` to show hours for all projects for a single username
36+
* ADDED `trk hours` options `--global` `--all-users` for hours for all projects for all usernames
37+
* ADDED function get_csv_local_uname
38+
* ADDED function get_csvs_local_all
39+
* ADDED function get_csvs_global_uname
40+
* ADDED function get_csvs_global_all
41+
* ADDED running `git add .timetracker/config .timetracker/.gitignore` during initialization
42+
* FIXED exit gracefully if `start --at` datetime cannot be parsed
43+
3344
## Release 2025-05-18 v0.5a7
3445
* ADDED start time to `report` stdout
3546
* FIXED incorrect param order in `cancel`

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ build-backend = "setuptools.build_meta"
77
[project]
88
name = "timetracker-csv"
99
description = "Pandas-friendly time tracking from the CLI"
10-
version = "0.5a7"
10+
version = "0.5a9"
1111
license = "AGPL-3.0-or-later"
1212
authors = [
13-
{name = 'DV Klopfenstein, PhD', email = 'dvklopfenstein@protonmail.com'},
13+
{name = 'DV Klopfenstein, PhD', email = 'dvklopfenstein@protonmail.com'},
1414
]
1515
readme = {file="README.md", content-type="text/markdown"}
1616

tests/pkgtttest/expcsvs.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""Query expected csvs"""
2+
3+
from re import compile as re_compile
4+
from timetracker.csvget import NTCSV
5+
6+
7+
class ExpCsvs:
8+
"""Query expected csvs"""
9+
10+
FCSV = re_compile(r'home/(?P<username>\w+)/proj/(?P<project>\w+)')
11+
12+
def __init__(self, orig_ntcsvs, pull_copies):
13+
self.exp_ntcsvs = orig_ntcsvs + self._init_ntcsvs(pull_copies, orig_ntcsvs)
14+
15+
def chk_get_csvs_global_all(self, usr2ntcsvs):
16+
"""Check the csvs resulting from `get_csvs_global_uname`"""
17+
for usr_act, ntcsvs_act in usr2ntcsvs.items():
18+
ntcsvs_exp = self._exp_get_csvs_global_all(usr_act)
19+
fcsvs_exp = set(nt.fcsv for nt in ntcsvs_exp)
20+
fcsvs_act = set(nt.fcsv for nt in ntcsvs_act)
21+
assert fcsvs_act == fcsvs_exp, self._err(fcsvs_act, fcsvs_exp)
22+
for ntcsv in ntcsvs_act:
23+
assert usr_act in ntcsv.fcsv, f'FILE OUTSIDE OF ACCOUNT({usr_act}): {ntcsv.fcsv}'
24+
#print(f'GLB ALL ACT {usr_act:7} {ntcsv}')
25+
26+
def chk_get_csvs_global_uname(self, usr2ntcsvs):
27+
"""Check the csvs resulting from `get_csvs_global_uname`"""
28+
for usr_act, ntcsvs_act in usr2ntcsvs.items():
29+
ntcsvs_exp = self._exp_get_csvs_global_uname(usr_act)
30+
assert ntcsvs_act == ntcsvs_exp, self._err(ntcsvs_act, ntcsvs_exp)
31+
for ntcsv in ntcsvs_act:
32+
assert ntcsv.username == usr_act
33+
#print(f'ACT {usr_act:7} {ntcsv}')
34+
35+
def chk_get_csvs_local_all(self, usrprj2ntcsvs):
36+
"""Check the csvs resulting from `get_csvs_global_uname`"""
37+
for (usr_act, prj_act), ntcsvs_act in usrprj2ntcsvs.items():
38+
ntcsvs_exp = self._exp_get_csvs_local_all(usr_act, prj_act)
39+
fcsvs_exp = set(nt.fcsv for nt in ntcsvs_exp)
40+
fcsvs_act = set(nt.fcsv for nt in ntcsvs_act)
41+
assert fcsvs_act == fcsvs_exp, self._err(fcsvs_act, fcsvs_exp)
42+
for ntcsv in ntcsvs_act:
43+
assert usr_act in ntcsv.fcsv, f'FILE OUTSIDE OF ACCOUNT({usr_act}): {ntcsv.fcsv}'
44+
#print(f'GLB ALL ACT {usr_act:7} {ntcsv}')
45+
46+
def chk_get_csvs_local_uname(self, usrprj2ntcsv):
47+
"""Check the csvs resulting from `get_csvs_global_uname`"""
48+
for (usr_act, prj_act), ntcsv_act in usrprj2ntcsv.items():
49+
ntcsv_exp = self._exp_get_csvs_local_uname(usr_act, prj_act)
50+
assert ntcsv_act == ntcsv_exp, self._err([ntcsv_act], [ntcsv_exp])
51+
52+
# -------------------------------------------------------------------------------
53+
def _exp_get_csvs_global_uname(self, uname):
54+
nts = set()
55+
for ntcsv in self._exp_get_csvs_global_all(uname):
56+
if f'_{uname}.csv' in ntcsv.fcsv:
57+
nts.add(ntcsv)
58+
return nts
59+
60+
def _exp_get_csvs_global_all(self, login):
61+
nts = set()
62+
for ntcsv in self.exp_ntcsvs:
63+
if ntcsv.fcsv is not None and f'home/{login}/proj' in ntcsv.fcsv:
64+
nts.add(ntcsv)
65+
return nts
66+
67+
def _exp_get_csvs_local_all(self, login, project):
68+
nts = set()
69+
for ntcsv in self.exp_ntcsvs:
70+
if ntcsv.fcsv is not None and f'home/{login}/proj/{project}' in ntcsv.fcsv:
71+
nts.add(ntcsv)
72+
return nts
73+
74+
def _exp_get_csvs_local_uname(self, login, project):
75+
for ntcsv in self._exp_get_csvs_local_all(login, project):
76+
if ntcsv.fcsv is not None and f'_{login}.csv' in ntcsv.fcsv:
77+
return ntcsv
78+
return None
79+
80+
@staticmethod
81+
def _err(act, exp):
82+
errmsg = [f'ACT[{len(act)}] != EXP[{len(exp)}]',]
83+
errmsg.append('ACT:')
84+
for idx, elem in enumerate(sorted(act)):
85+
errmsg.append(f'ACT {idx}) {elem}')
86+
errmsg.append('EXP:')
87+
for idx, elem in enumerate(sorted(exp)):
88+
errmsg.append(f'EXP {idx}) {elem}')
89+
return '\n'.join(errmsg)
90+
91+
# -------------------------------------------------------------------------------
92+
def _init_ntcsvs(self, csv_pull_copies, orig_ntcsvs):
93+
watchers = set((nt.username, nt.project) for nt in orig_ntcsvs)
94+
ntcsvs = []
95+
for fcsv in csv_pull_copies:
96+
mtch = self.FCSV.search(fcsv)
97+
assert mtch is not None, 'EXPECTED FILENAME: home/USER/proj/PROJECT'
98+
username = mtch['username']
99+
project = mtch['project']
100+
assert (username, project) in watchers
101+
ntd = NTCSV(fcsv=fcsv, username=username, project=project)
102+
ntcsvs.append(ntd)
103+
return ntcsvs

tests/pkgtttest/mkprojs.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -90,17 +90,8 @@ def _get_expdirs(tmphome, project, dirgit, trksubdir):
9090
dirgit=join(dirproj, '.git') if dirgit else None,
9191
dirtrk=join(dirproj, trksubdir),
9292
dirdoc=join(dirproj, 'doc'))
93-
####prt_expdirs(ntexpdirs, "tests/pkgtttest/mkprojs:mk_projdirs")
9493
return ntexpdirs
9594

96-
##def mk_projdirs_wcfgs(tmp_home, project, trksubdir='.timetracker'):
97-
## """Make sub-directories & cfgs in a temporary directory for use in tests"""
98-
## ##dirproj = mk_projdirs(tmp_home, project)
99-
## ntexpdirs = mk_projdirs(tmp_home, project)
100-
## fname_cfgproj = join(ntexpdirs.dirproj, trksubdir, 'config')
101-
## cfg = run_init(fname_cfgproj, '.', project, dirhome=tmp_home)
102-
## return ntexpdirs.dirproj, cfg.cfg_loc, cfg.cfg_glb
103-
10495
def findhome(home):
10596
"""Do a find on the given homedir and print using debug logging"""
10697
debug(findhome_str(home))
@@ -126,6 +117,11 @@ def get_files(basedir):
126117
files_all.append(join(root, file))
127118
return files_all
128119

120+
def prt_files(basedir, prefix=''):
121+
"""Print files in basedir and below"""
122+
for fname in get_files(basedir):
123+
print(f'{prefix}{fname}')
124+
129125
def get_type2files(basedir):
130126
"""Recursively walk dirs to get files & group by type (config, csv)"""
131127
type2files = defaultdict(set)

tests/pkgtttest/runprojs.py

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
"""Create and manage a set of user timetracker projects"""
33

44
from os.path import join
5+
from os.path import dirname
6+
from os.path import relpath
57
from logging import basicConfig
68
from logging import DEBUG
79
from collections import namedtuple
810
from timetracker.ntcsv import get_ntcsv
911
from timetracker.utils import yellow
1012
from timetracker.cfg.cfg import Cfg
1113
from timetracker.cfg.doc_local import get_docproj
12-
#from timetracker.cfg.cfg_global import CfgGlobal
1314
from timetracker.cfg.cfg_global import get_cfgglobal
1415
from timetracker.cfg.docutils import get_value
1516
from timetracker.cmd.init import run_init
@@ -21,7 +22,10 @@
2122
from tests.pkgtttest.consts import SEP3
2223
from tests.pkgtttest.runfncs import proj_setup
2324
from tests.pkgtttest.userprojs import UserProjects
24-
from tests.pkgtttest.mkprojs import prt_type2files
25+
from tests.pkgtttest.upstream import Upstream
26+
from tests.pkgtttest.mkprojs import get_type2files
27+
from tests.pkgtttest.mkprojs import get_files
28+
from tests.pkgtttest.mkprojs import prt_files
2529
from tests.pkgtttest.dts import get_iter_weekday
2630
from tests.pkgtttest.dts import td2hours
2731
from tests.pkgtttest.dts import I1266 as DT2525
@@ -30,23 +34,49 @@
3034
class RunProjs:
3135
"""Manage all users and their projects"""
3236

33-
##def __init__(self, tmproot, userprojs, fcfg_global=None):
34-
def __init__(self, tmproot, userprojs, fcfg_explicit=None, fcfg_doc=None):
37+
def __init__(self, tmproot, userprojs):
3538
self.tmproot = tmproot
3639
self.dirhome = join(tmproot, 'home')
3740
self.ups = UserProjects(userprojs)
38-
##self.cfg_global = CfgGlobal(fcfg_global)
39-
self.cfg_global = get_cfgglobal(fcfg_explicit, self.dirhome, fcfg_doc)
40-
self.cfg = Cfg("phoneyproj.cfg", self.cfg_global)
41-
self.prj2mgrprj = {e:MngUsrProj(self.dirhome, self.cfg_global, *e) for e in userprojs}
41+
self.prj2mgrprj = {(usr,prj):MngUsrProj(self.dirhome, usr, prj) for usr, prj in userprojs}
42+
self.upstream = Upstream(join(self.tmproot, 'upstream'))
43+
self.orig_ntcsvs = self.ups.get_expcsvs(self.tmproot)
44+
45+
def all_push(self):
46+
"""simulate a git-like push"""
47+
for (_, prj), obj in self.prj2mgrprj.items():
48+
dirproj = dirname(dirname(obj.cfg.cfg_loc.filename))
49+
files = get_files(dirproj)
50+
for absfname in files:
51+
relfname = relpath(absfname, dirproj)
52+
self.upstream.push(prj, absfname, relfname)
53+
54+
def all_pull(self):
55+
"""simulate a git-like pull"""
56+
pull_copies_all = []
57+
for (_, prj), obj in self.prj2mgrprj.items():
58+
dirproj = dirname(dirname(obj.cfg.cfg_loc.filename))
59+
pull_copies_cur = self.upstream.pull(prj, dirproj)
60+
pull_copies_all.extend(pull_copies_cur)
61+
return pull_copies_all
62+
63+
def prt_userfiles(self, msg='FILES FOR ALL USERNAMES', prefix='USERFILE: '):
64+
"""Print files for each username"""
65+
if prefix:
66+
prefix = f'{msg}: '
67+
print(f'\n{msg}:')
68+
for uname in self.ups.usernames:
69+
print(f'FILES FOR USERNAME: {uname}:')
70+
userhome = join(self.dirhome, uname)
71+
prt_files(userhome, prefix)
4272

4373
def get_user2glbcfg(self):
4474
"""For each username, get their one global config file"""
4575
user2glbcfg = {}
4676
for (usr, _), obj in self.prj2mgrprj.items():
4777
docproj = get_docproj(obj.fcfgproj)
4878
fcfg_doc = get_value(docproj, 'global_config', 'filename')
49-
cfg_glb = get_cfgglobal(dirhome=self.dirhome, fcfg_doc=fcfg_doc)
79+
cfg_glb = get_cfgglobal(dirhome=obj.home, fcfg_doc=fcfg_doc)
5080
assert cfg_glb is not None
5181
if usr not in user2glbcfg:
5282
user2glbcfg[usr] = cfg_glb
@@ -58,11 +88,9 @@ def run_setup(self):
5888
"""Initialize and fill timeslots for multiple users and projects"""
5989
print(yellow(f"{SEP1}`run_init` on each project"))
6090
basicConfig(level=DEBUG)
61-
prt_type2files(self.tmproot)
6291

6392
print(yellow('`run_start` and `run_stop` to fill each researcher & project'))
6493
self._run_start_stop_all()
65-
prt_type2files(self.tmproot)
6694

6795
print(yellow('Check projects listed in CfgGlobal'))
6896

@@ -72,12 +100,12 @@ def _run_start_stop_all(self):
72100
mgrprj = self.prj2mgrprj[usrprj]
73101
mgrprj.add_timeslots(times)
74102

75-
def chk_projects(self, exp_projects):
103+
def chk_proj_configs(self, exp_fcfgprojs):
76104
"""Check the projects"""
77-
act_projs = self.cfg_global.get_projects()
78-
home = self.dirhome
79-
exp_projs = [[prj, join(home, rcfg)] for prj, rcfg in exp_projects]
80-
assert act_projs == exp_projs, self._errmsg(act_projs, exp_projs)
105+
type2files = get_type2files(self.tmproot)
106+
exp_fcfgprojs = set(join(self.dirhome, f) for f in exp_fcfgprojs)
107+
act_fcfgprojs = set(type2files['config'])
108+
assert set(act_fcfgprojs) == set(exp_fcfgprojs), self._errmsg(act_fcfgprojs, exp_fcfgprojs)
81109

82110
def run_hoursprojs(self):
83111
"""print hours, iterating through all users & their projects"""
@@ -87,7 +115,7 @@ def run_hoursprojs(self):
87115

88116
print(f'{SEP3}run_setup: run_hours project({usrprj[0]}) username({usrprj[1]})')
89117
# run_hours nt: RdCsvs: results errors ntcsvs
90-
run1 = run_hours(self.cfg, usr, dirhome=mgrprj.home)
118+
run1 = run_hours(mgrprj.cfg, usr, dirhome=mgrprj.home)
91119
assert td2hours(run1.results) == self._get_total_hours(usr), (
92120
f'ACT({td2hours(run1.results)}) != EXP({self._get_total_hours(usr)}) '
93121
f'project({usrprj[0]}) username({usrprj[1]})')
@@ -102,13 +130,13 @@ def _get_total_hours(self, usr):
102130
return sum(e for (u, _), (_, e) in self.ups.userprojs.items() if u == usr)
103131

104132
@staticmethod
105-
def _errmsg(act_projs, exp_projs):
133+
def _errmsg(act_fcfgprojs, exp_fcfgprojs):
106134
txt = ['\nEXP:',]
107-
for prj, fcfg in exp_projs:
108-
txt.append(f' {prj:14} {fcfg}')
135+
for fcfg in sorted(exp_fcfgprojs):
136+
txt.append(f' {fcfg}')
109137
txt.append('\nACT:',)
110-
for prj, fcfg in act_projs:
111-
txt.append(f' {prj:14} {fcfg}')
138+
for fcfg in sorted(act_fcfgprojs):
139+
txt.append(f' {fcfg}')
112140
return '\n'.join(txt)
113141

114142

@@ -117,18 +145,20 @@ class MngUsrProj:
117145
"""Manage one user and the project"""
118146

119147
# pylint: disable=unknown-option-value,too-many-arguments,too-many-positional-arguments
120-
def __init__(self, tmproot, cfg_global, user, projname, dircsv=None):
121-
self.cfg_global = cfg_global
148+
def __init__(self, tmproot, user, projname, dircsv=None):
122149
self.home = join(tmproot, user)
123150
self.user = user
124151
self.projname = projname
152+
self.cfg_global = get_cfgglobal(None, self.home, fcfg_doc=None)
125153
##print(f'\nMngUsrProj({self.home:29}, {user:7}, {projname})')
126-
self.fcfgproj, _, self.exp = proj_setup(self.home, projname, dircur='dirproj')
127-
self.cfg = run_init(self.fcfgproj,
154+
self.fcfgproj, finder, self.exp = proj_setup(self.home, projname, dircur='dirproj')
155+
self.cfg = Cfg(self.fcfgproj)
156+
run_init(self.cfg,
157+
finder.dirgit,
128158
dircsv=dircsv,
129159
project=self.projname,
130160
dirhome=self.home,
131-
cfg_global=cfg_global,
161+
cfg_global=self.cfg_global,
132162
quiet=True)
133163

134164
def get_args_hours(self):

0 commit comments

Comments
 (0)