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

1import sys 

2 

3import click 

4 

5from compiler_admin.services.harvest import HarvestTime 

6from compiler_admin.services.time import TimeSummary 

7from compiler_admin.services.toggl import TogglTime 

8 

9SUMMARIZERS = { 

10 "harvest": HarvestTime().summarize, 

11 "toggl": TogglTime().summarize, 

12} 

13 

14 

15def detect_file_type(file_path: str) -> str: 

16 """Detect the type of a time entry CSV file. 

17 

18 Args: 

19 file_path (str): The path to the CSV file. 

20 

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}") 

32 

33 

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}") 

52 

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}") 

65 

66 return diffs 

67 

68 

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 

76 

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 

87 

88 toggl_time = TogglTime() 

89 

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]) 

108 

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) 

113 

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)