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
« 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."""
3import datetime
4from dataclasses import dataclass, field
7@dataclass
8class DriveStats:
9 """Statistics for drive synchronization operations.
11 Tracks download counts, sizes, durations, and other metrics
12 for drive sync operations.
13 """
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)
22 def has_activity(self) -> bool:
23 """Check if there was any sync activity.
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
30 def has_errors(self) -> bool:
31 """Check if there were any errors.
33 Returns:
34 True if errors list is not empty
35 """
36 return len(self.errors) > 0
39@dataclass
40class PhotoStats:
41 """Statistics for photo synchronization operations.
43 Tracks download counts, hardlink usage, sizes, durations,
44 and other metrics for photo sync operations.
45 """
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)
56 def has_activity(self) -> bool:
57 """Check if there was any sync activity.
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
64 def has_errors(self) -> bool:
65 """Check if there were any errors.
67 Returns:
68 True if errors list is not empty
69 """
70 return len(self.errors) > 0
73@dataclass
74class SyncSummary:
75 """Overall synchronization summary combining drive and photo stats.
77 Contains statistics for both drive and photo syncs, along with
78 timing information for the overall sync operation.
79 """
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
86 def has_activity(self) -> bool:
87 """Check if there was any sync activity overall.
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
96 def has_errors(self) -> bool:
97 """Check if there were any errors in the sync.
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
106 def total_duration_seconds(self) -> float:
107 """Calculate total sync duration.
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
117def format_bytes(bytes_count: int) -> str:
118 """Format byte count as human-readable string.
120 Args:
121 bytes_count: Number of bytes
123 Returns:
124 Formatted string (e.g., "1.5 GB", "234 MB")
125 """
126 if bytes_count == 0:
127 return "0 B"
129 units = ["B", "KB", "MB", "GB", "TB"]
130 unit_index = 0
131 size = float(bytes_count)
133 while size >= 1024.0 and unit_index < len(units) - 1:
134 size /= 1024.0
135 unit_index += 1
137 return f"{size:.1f} {units[unit_index]}"
140def format_duration(seconds: float) -> str:
141 """Format duration as human-readable string.
143 Args:
144 seconds: Duration in seconds
146 Returns:
147 Formatted string (e.g., "4m 32s", "1h 15m")
148 """
149 if seconds < 60:
150 return f"{int(seconds)}s"
152 minutes = int(seconds // 60)
153 remaining_seconds = int(seconds % 60)
155 if minutes < 60:
156 return f"{minutes}m {remaining_seconds}s"
158 hours = minutes // 60
159 remaining_minutes = minutes % 60
160 return f"{hours}h {remaining_minutes}m"