"""Сервис переноса трудочасов из Clockify в Redmine.""" import logging from colorama import Fore from classes import Activity, AggregatedActivity from utils import format_date, format_hours logger = logging.getLogger(__name__) def aggregate_activities(activities: list[Activity]) -> list[AggregatedActivity]: """Объединяет одинаковые активности и суммирует их трудозатраты.""" grouped_activities = {} for activity in activities: key = ( activity.author_id, activity.task_id, activity.tag_id, activity.project_id, activity.vetro_project_id, activity.date_start.date(), ) grouped_activities.setdefault(key, []).append(activity) result = [] for grouped in grouped_activities.values(): descriptions = list( dict.fromkeys( description.rstrip(".") for activity in grouped if (description := activity.description.strip()) ) ) result.append( AggregatedActivity( **{ **grouped[0].__dict__, "description": ". ".join(descriptions), "time_spent": sum(activity.time_spent for activity in grouped), }, source_activities=tuple(grouped), ) ) return result class TrackingService: """Координирует загрузку, сохранение и перенос активностей в Redmine.""" def __init__(self, clock, red, db, is_debug: bool, is_dry_run: bool = False): self.clock = clock self.red = red self.db = db self.is_debug = is_debug self.is_dry_run = is_dry_run def track_period(self, start, end, page_size: int | None): """Переносит активности за период и возвращает список найденных активностей.""" self._save_current_user() activities = sorted( self.clock.get_activities( **{ "start": format_date(start), "end": format_date(end), "page-size": page_size, }, ), key=lambda x: x.date_start, ) if not activities: logger.info(Fore.YELLOW + "Нет задач для занесения трудочасов" + Fore.RESET) return [] activities_to_track = aggregate_activities( [ activity for activity in activities if not activity.is_tracked and activity.date_end ] ) if self.is_dry_run: self._log_preview(activities, activities_to_track) return activities self.db.insert_activities(activities) self.red.track_activities( activities_to_track, on_success=None if self.is_debug else self._save_tracked_activity, ) return activities def _save_current_user(self): """Сохраняет связку текущих пользователей Clockify и Redmine.""" user = self.clock.get_user() clock_user = { "clockify_user_id": user["id"], "clockify_email": user["email"], "clockify_username": user["username"], } user = self.red.get_user() red_user = { "redmine_user_id": user["id"], "redmine_email": user["email"], "first_name": user["first_name"], "last_name": user["last_name"], "redmine_username": user["username"], } self.db.insert_user(**clock_user, **red_user) def _save_tracked_activity(self, activity, redmine_time_entry_id: int): """Сохраняет активность как успешно перенесенную в Redmine.""" source_activities = list(activity.source_activities or (activity,)) for source_activity in source_activities: source_activity.is_tracked = True self.db.insert_activities(source_activities) self.db.insert_time_entry( activity, source_activities, redmine_time_entry_id, ) def _log_preview(self, activities, activities_to_track): """Выводит план переноса активностей без сохранения и отправки.""" total_hours = sum(activity.time_spent for activity in activities_to_track) logger.info(f"{Fore.YELLOW}Предпросмотр переноса трудочасов{Fore.RESET}") logger.info(f"Найдено активностей: {len(activities)}") logger.info(f"Готово к переносу: {len(activities_to_track)}") logger.info(f"Всего часов: {format_hours(total_hours)}") for activity in activities_to_track: logger.info( f"{activity.date_start.date()} | #{activity.task_id} | " f"{format_hours(activity.time_spent)} | {activity.description}" )