Coverage for src/sync_stats.py: 100%

66 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-16 04:41 +0000

1"""Sync statistics module for tracking sync operations and generating summaries.""" 

2 

3import datetime 

4from dataclasses import dataclass, field 

5 

6 

7@dataclass 

8class DriveStats: 

9 """Statistics for drive synchronization operations. 

10 

11 Tracks download counts, sizes, durations, and other metrics 

12 for drive sync operations. 

13 """ 

14 

15 files_downloaded: int = 0 

16 files_skipped: int = 0 

17 files_removed: int = 0 

18 bytes_downloaded: int = 0 

19 duration_seconds: float = 0.0 

20 errors: list[str] = field(default_factory=list) 

21 

22 def has_activity(self) -> bool: 

23 """Check if there was any sync activity. 

24 

25 Returns: 

26 True if any files were downloaded, skipped, or removed 

27 """ 

28 return self.files_downloaded > 0 or self.files_skipped > 0 or self.files_removed > 0 

29 

30 def has_errors(self) -> bool: 

31 """Check if there were any errors. 

32 

33 Returns: 

34 True if errors list is not empty 

35 """ 

36 return len(self.errors) > 0 

37 

38 

39@dataclass 

40class PhotoStats: 

41 """Statistics for photo synchronization operations. 

42 

43 Tracks download counts, hardlink usage, sizes, durations, 

44 and other metrics for photo sync operations. 

45 """ 

46 

47 photos_downloaded: int = 0 

48 photos_hardlinked: int = 0 

49 photos_skipped: int = 0 

50 bytes_downloaded: int = 0 

51 bytes_saved_by_hardlinks: int = 0 

52 albums_synced: list[str] = field(default_factory=list) 

53 duration_seconds: float = 0.0 

54 errors: list[str] = field(default_factory=list) 

55 

56 def has_activity(self) -> bool: 

57 """Check if there was any sync activity. 

58 

59 Returns: 

60 True if any photos were downloaded, hardlinked, or skipped 

61 """ 

62 return self.photos_downloaded > 0 or self.photos_hardlinked > 0 or self.photos_skipped > 0 

63 

64 def has_errors(self) -> bool: 

65 """Check if there were any errors. 

66 

67 Returns: 

68 True if errors list is not empty 

69 """ 

70 return len(self.errors) > 0 

71 

72 

73@dataclass 

74class SyncSummary: 

75 """Overall synchronization summary combining drive and photo stats. 

76 

77 Contains statistics for both drive and photo syncs, along with 

78 timing information for the overall sync operation. 

79 """ 

80 

81 drive_stats: DriveStats | None = None 

82 photo_stats: PhotoStats | None = None 

83 sync_start_time: datetime.datetime = field(default_factory=datetime.datetime.now) 

84 sync_end_time: datetime.datetime | None = None 

85 

86 def has_activity(self) -> bool: 

87 """Check if there was any sync activity overall. 

88 

89 Returns: 

90 True if either drive or photos had activity 

91 """ 

92 drive_activity = self.drive_stats.has_activity() if self.drive_stats else False 

93 photo_activity = self.photo_stats.has_activity() if self.photo_stats else False 

94 return drive_activity or photo_activity 

95 

96 def has_errors(self) -> bool: 

97 """Check if there were any errors in the sync. 

98 

99 Returns: 

100 True if either drive or photos had errors 

101 """ 

102 drive_errors = self.drive_stats.has_errors() if self.drive_stats else False 

103 photo_errors = self.photo_stats.has_errors() if self.photo_stats else False 

104 return drive_errors or photo_errors 

105 

106 def total_duration_seconds(self) -> float: 

107 """Calculate total sync duration. 

108 

109 Returns: 

110 Total duration in seconds 

111 """ 

112 if self.sync_end_time: 

113 return (self.sync_end_time - self.sync_start_time).total_seconds() 

114 return 0.0 

115 

116 

117def format_bytes(bytes_count: int) -> str: 

118 """Format byte count as human-readable string. 

119 

120 Args: 

121 bytes_count: Number of bytes 

122 

123 Returns: 

124 Formatted string (e.g., "1.5 GB", "234 MB") 

125 """ 

126 if bytes_count == 0: 

127 return "0 B" 

128 

129 units = ["B", "KB", "MB", "GB", "TB"] 

130 unit_index = 0 

131 size = float(bytes_count) 

132 

133 while size >= 1024.0 and unit_index < len(units) - 1: 

134 size /= 1024.0 

135 unit_index += 1 

136 

137 return f"{size:.1f} {units[unit_index]}" 

138 

139 

140def format_duration(seconds: float) -> str: 

141 """Format duration as human-readable string. 

142 

143 Args: 

144 seconds: Duration in seconds 

145 

146 Returns: 

147 Formatted string (e.g., "4m 32s", "1h 15m") 

148 """ 

149 if seconds < 60: 

150 return f"{int(seconds)}s" 

151 

152 minutes = int(seconds // 60) 

153 remaining_seconds = int(seconds % 60) 

154 

155 if minutes < 60: 

156 return f"{minutes}m {remaining_seconds}s" 

157 

158 hours = minutes // 60 

159 remaining_minutes = minutes % 60 

160 return f"{hours}h {remaining_minutes}m"