diff --git a/src/storage/models.py b/src/storage/models.py new file mode 100644 index 0000000..0332216 --- /dev/null +++ b/src/storage/models.py @@ -0,0 +1,170 @@ +"""Data models for DevTrace.""" + +from datetime import datetime +from dataclasses import dataclass, field +from typing import Optional, Dict, Any + + +@dataclass +class Session: + """Represents a development session.""" + id: int + name: str + directory: str + start_time: Optional[datetime] = None + end_time: Optional[datetime] = None + notes: Optional[str] = None + + @property + def duration(self) -> Optional[float]: + """Calculate session duration in seconds.""" + if self.start_time and self.end_time: + return (self.end_time - self.start_time).total_seconds() + return None + + +@dataclass +class FileEvent: + """Represents a file system event.""" + id: int + session_id: int + event_type: str + file_path: str + timestamp: Optional[datetime] = None + details: Optional[str] = None + content_hash: Optional[str] = None + + @property + def filename(self) -> str: + """Get just the filename from the path.""" + return self.file_path.split("/")[-1] + + @property + def extension(self) -> str: + """Get the file extension.""" + return self.filename.split(".")[-1] if "." in self.filename else "" + + +@dataclass +class CommandEvent: + """Represents a shell command execution.""" + id: int + session_id: int + command: str + timestamp: Optional[datetime] = None + exit_code: Optional[int] = None + working_directory: Optional[str] = None + details: Optional[str] = None + + @property + def was_successful(self) -> Optional[bool]: + """Check if command was successful.""" + if self.exit_code is None: + return None + return self.exit_code == 0 + + @property + def short_command(self) -> str: + """Get truncated command for display.""" + max_len = 50 + if len(self.command) <= max_len: + return self.command + return self.command[:max_len] + "..." + + +@dataclass +class GitEvent: + """Represents a git operation.""" + id: int + session_id: int + operation: str + branch: Optional[str] = None + commit_hash: Optional[str] = None + timestamp: Optional[datetime] = None + details: Optional[str] = None + diff: Optional[str] = None + + @property + def short_hash(self) -> str: + """Get short commit hash.""" + if self.commit_hash: + return self.commit_hash[:7] + return "" + + +@dataclass +class SearchResult: + """Represents a search result.""" + event_type: str + event_id: int + score: float + details: Dict[str, Any] + content: str = "" + + +@dataclass +class TimelineEvent: + """Unified event representation for timeline display.""" + id: int + session_id: int + event_type: str + timestamp: Optional[datetime] + details: Dict[str, Any] + display_text: str + icon: str + + @classmethod + def from_file_event(cls, event: FileEvent) -> "TimelineEvent": + """Create TimelineEvent from FileEvent.""" + icons = { + "created": "📄", + "modified": "✏️", + "deleted": "🗑️", + "moved": "📁" + } + return cls( + id=event.id, + session_id=event.session_id, + event_type="file", + timestamp=event.timestamp, + details={"file_path": event.file_path, "content_hash": event.content_hash}, + display_text=f"{event.event_type} {event.filename}", + icon=icons.get(event.event_type, "📄") + ) + + @classmethod + def from_command_event(cls, event: CommandEvent) -> "TimelineEvent": + """Create TimelineEvent from CommandEvent.""" + status = "✅" if event.was_successful else "❌" if event.was_successful is False else "⏳" + return cls( + id=event.id, + session_id=event.session_id, + event_type="command", + timestamp=event.timestamp, + details={"command": event.command, "exit_code": event.exit_code}, + display_text=f"{event.short_command}", + icon=f"⚡ {status}" + ) + + @classmethod + def from_git_event(cls, event: GitEvent) -> "TimelineEvent": + """Create TimelineEvent from GitEvent.""" + icons = { + "commit": "💾", + "branch": "🌿", + "merge": "🔀", + "checkout": "🔀", + "push": "📤", + "pull": "📥", + "rebase": "📜", + "reset": "⏪" + } + return cls( + id=event.id, + session_id=event.session_id, + event_type="git", + timestamp=event.timestamp, + details={"operation": event.operation, "branch": event.branch, "commit": event.commit_hash}, + display_text=f"{event.operation} {event.branch or event.short_hash}", + icon=icons.get(event.operation, "🔧") + )