from datetime import datetime from typing import List, Optional from pydantic import BaseModel, Field from typing_extensions import Literal class WorkDetail(BaseModel): group_name: str duration_seconds: float class WorkSession(BaseModel): start_time: datetime end_time: datetime details: List[WorkDetail] synchronized: bool = False id: Optional[str] = None youtrack_created_at: Optional[datetime] = None description: Optional[str] = None youtrack_duration_minutes: Optional[float] = None class Config: json_encoders = { datetime: lambda v: v.isoformat() } @property def total_minutes(self) -> float: return sum(d.duration_seconds for d in self.details) / 60.0 def is_modified(self) -> bool: """Проверяет, изменилась ли длительность сессии после последней синхронизации""" if self.youtrack_duration_minutes is None: return not self.synchronized # Несинхронизированная сессия = изменённая return abs(self.total_minutes - self.youtrack_duration_minutes) > 0.5 # погрешность 30 сек class TrackedTask(BaseModel): task_id: str project_id: Optional[str] = None estimated_seconds: Optional[float] = None start_time: datetime end_time: Optional[datetime] = None sessions: List[WorkSession] = Field(default_factory=list) @property def total_worked_seconds(self) -> float: return sum( detail.duration_seconds for session in self.sessions for detail in session.details ) @property def remaining_seconds(self) -> Optional[float]: if self.estimated_seconds is None: return None return max(0.0, self.estimated_seconds - self.total_worked_seconds) class WindowPolicy(BaseModel): group_name: str window_patterns: List[str] = Field(default_factory=list) process_names: List[str] = Field(default_factory=list) policy_type: Literal["work", "blocked"] class TrackerConfig(BaseModel): tracking_enabled: bool = False current_task_id: Optional[str] = None window_policies: List[WindowPolicy] = Field(default_factory=list) idle_threshold_seconds: int = 60 # Порог перехода в idle new_session_after_idle_seconds: int = 600 # 10 минут для новой сессии @staticmethod def default_policies() -> List[WindowPolicy]: return [ WindowPolicy( group_name="IDE", window_patterns=["PyCharm", "IntelliJ", "Visual Studio Code", "Code", "Sublime", "Vim", "Neovim"], process_names=["pycharm64.exe", "idea64.exe", "code.exe", "sublime_text.exe", "nvim.exe", "vim.exe"], policy_type="work" ), WindowPolicy( group_name="Gaming & Distractions", window_patterns=["Steam", "Discord", "YouTube", "Twitch", "Spotify", "Netflix"], process_names=["steam.exe", "discord.exe", "spotify.exe"], policy_type="blocked" ), ] def __init__(self, **data): super().__init__(**data) if not self.window_policies: self.window_policies = self.default_policies() class YouTrackTimeTrackerEntryDuration(BaseModel): minutes: int class YouTrackTimeTrackerEntry(BaseModel): id: str duration: YouTrackTimeTrackerEntryDuration @property def minutes(self) -> int: return self.duration.minutes