Coverage for compiler_admin / commands / time / verify.py: 91%
89 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-28 05:48 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-28 05:48 +0000
1import sys
3import click
5from compiler_admin.services.harvest import HarvestTime
6from compiler_admin.services.time import TimeSummary
7from compiler_admin.services.toggl import TogglTime
9SUMMARIZERS = {
10 "harvest": HarvestTime().summarize,
11 "toggl": TogglTime().summarize,
12}
15def detect_file_type(file_path: str) -> str:
16 """Detect the type of a time entry CSV file.
18 Args:
19 file_path (str): The path to the CSV file.
21 Returns:
22 str: The type of the file (harvest or toggl).
23 """
24 with open(file_path, "r") as f:
25 header = f.readline().lower()
26 if "hours" in header:
27 return "harvest"
28 elif "duration" in header:
29 return "toggl"
30 else:
31 raise ValueError(f"Unknown file type for {file_path}")
34def _diff_summaries(summary1: TimeSummary, summary2: TimeSummary):
35 diffs = []
36 if summary1.earliest_date != summary2.earliest_date: 36 ↛ 37line 36 didn't jump to line 37 because the condition on line 36 was never true
37 diffs.append(f"Earliest date: {summary1.earliest_date} vs {summary2.earliest_date}")
38 if summary1.latest_date != summary2.latest_date: 38 ↛ 39line 38 didn't jump to line 39 because the condition on line 38 was never true
39 diffs.append(f"Latest date: {summary1.latest_date} vs {summary2.latest_date}")
40 if summary1.total_rows != summary2.total_rows: 40 ↛ 41line 40 didn't jump to line 41 because the condition on line 40 was never true
41 diffs.append(f"Total rows: {summary1.total_rows} vs {summary2.total_rows}")
42 if summary1.total_hours != summary2.total_hours: 42 ↛ 45line 42 didn't jump to line 45 because the condition on line 42 was always true
43 diffs.append(f"Total hours: {summary1.total_hours} vs {summary2.total_hours}")
44 # Detailed diff for hours_per_project
45 if summary1.hours_per_project != summary2.hours_per_project: 45 ↛ 54line 45 didn't jump to line 54 because the condition on line 45 was always true
46 all_projects = sorted(set(summary1.hours_per_project.keys()) | set(summary2.hours_per_project.keys()))
47 for project in all_projects:
48 hours1 = summary1.hours_per_project.get(project, 0.0)
49 hours2 = summary2.hours_per_project.get(project, 0.0)
50 if hours1 != hours2:
51 diffs.append(f" Project '{project}' hours: {hours1} vs {hours2}")
53 # Detailed diff for hours_per_user_project
54 if summary1.hours_per_user_project != summary2.hours_per_user_project: 54 ↛ 66line 54 didn't jump to line 66 because the condition on line 54 was always true
55 all_users = sorted(set(summary1.hours_per_user_project.keys()) | set(summary2.hours_per_user_project.keys()))
56 for user in all_users:
57 projects1 = summary1.hours_per_user_project.get(user, {})
58 projects2 = summary2.hours_per_user_project.get(user, {})
59 all_user_projects = sorted(set(projects1.keys()) | set(projects2.keys()))
60 for project in all_user_projects:
61 hours1 = projects1.get(project, 0.0)
62 hours2 = projects2.get(project, 0.0)
63 if hours1 != hours2:
64 diffs.append(f" User '{user}', Project '{project}' hours: {hours1} vs {hours2}")
66 return diffs
69@click.command()
70@click.argument("files", nargs=-1, type=click.Path(exists=True))
71def verify(files: list[str]):
72 """Verify time entry CSV files."""
73 if not 1 <= len(files) <= 2:
74 click.echo("Please provide one or two files to verify.")
75 return
77 summaries = []
78 for file_path in files:
79 try:
80 file_type = detect_file_type(file_path)
81 summarizer = SUMMARIZERS[file_type]
82 summary = summarizer(file_path)
83 summaries.append(summary)
84 except (ValueError, KeyError) as e:
85 click.echo(f"Error processing file {file_path}: {e}", err=True)
86 return
88 toggl_time = TogglTime()
90 if len(summaries) == 1:
91 click.echo(f"Summary for: {files[0]}")
92 summary: TimeSummary = summaries[0]
93 click.echo(f" Date range: {summary.earliest_date} - {summary.latest_date}")
94 click.echo()
95 click.echo(f" Total entries: {summary.total_rows}")
96 click.echo(f" Total hours: {summary.total_hours}")
97 for project, hours in summary.hours_per_project.items():
98 click.echo(f" {project}: {hours}")
99 click.echo()
100 for user, project_hours in summary.hours_per_user_project.items():
101 click.echo(f" {user}:")
102 for project, hours in project_hours.items():
103 click.echo(f" {project}: {hours}")
104 elif len(summaries) == 2: 104 ↛ exitline 104 didn't return from function 'verify' because the condition on line 104 was always true
105 summary1, summary2 = summaries
106 file1_type = detect_file_type(files[0])
107 file2_type = detect_file_type(files[1])
109 if file1_type == "toggl" and file2_type == "harvest": 109 ↛ 110line 109 didn't jump to line 110 because the condition on line 109 was never true
110 summary1 = toggl_time.normalize_summary(summary1)
111 elif file1_type == "harvest" and file2_type == "toggl":
112 summary2 = toggl_time.normalize_summary(summary2)
114 if summary1 == summary2:
115 click.echo("Summaries match.")
116 else:
117 click.echo("Summaries do not match:", err=True)
118 diffs = _diff_summaries(summary1, summary2)
119 for diff in diffs:
120 click.echo(f"- {diff}", err=True)
121 sys.exit(1)