Files
TimeTracker/src/citrus_time_tracker/models.py

107 lines
3.6 KiB
Python
Raw Normal View History

2026-03-10 01:40:10 +03:00
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
2026-03-12 19:54:31 +03:00
id: Optional[str] = None
youtrack_created_at: Optional[datetime] = None
description: Optional[str] = None
youtrack_duration_minutes: Optional[float] = None
2026-03-12 18:50:39 +03:00
class Config:
json_encoders = {
datetime: lambda v: v.isoformat()
}
2026-03-10 01:40:10 +03:00
2026-03-12 19:54:31 +03:00
@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 сек
2026-03-10 01:40:10 +03:00
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)
2026-03-12 19:41:29 +03:00
idle_threshold_seconds: int = 60 # Порог перехода в idle
new_session_after_idle_seconds: int = 600 # 10 минут для новой сессии
2026-03-10 01:40:10 +03:00
@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:
2026-03-12 18:50:39 +03:00
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