fix: resolve CI type checking issues
- Add return type annotations to __hash__ (-> int) and __eq__ (-> bool) in HistoryEntry - Add TextIO import and type annotations for file parameters - Add type ignore comment for fuzzywuzzy import - Add HistoryEntry import and list type annotations in time_analysis - Add assert statements for Optional[datetime] timestamps - Add TypedDict classes for type-safe pattern dictionaries - Add CommandPattern import and list[CommandPattern] type annotation - Add -> None return types to all test methods - Remove unused HistoryEntry import (F401)
This commit is contained in:
@@ -1 +1,175 @@
|
||||
bmFtZTogQ0kKCm9uOgogIHB1c2g6CiAgICBicmFuY2hlczogW21haW5dCiAgcHVsbF9yZXF1ZXN0OgogICAgYnJhbmNoZXM6IFttYWluXQoKam9iczogCiAgdGVzdDoKICAgIHJ1bnMtb246IHVidW50dS1sYXRlc3QKICAgIHN0ZXBzOgogICAgICAtIHVzZXM6IGFjdGlvbnMvY2hlY2tvdXQdjNFgKICAgICAgdXNlczogYWN0aW9ucy9zZXR1cC1weXRob25fdjUKICAgICAgd2l0aDoKICAgICAgICBweXRob24tdmVyc2lvbjogJzMuMTEnCiAgICAtIHJ1bjogcGlwIGluc3RhbGwgLWUgIltcImRldlwiXSIKICAgIC0gcnVuOiBweXRlc3QgdGVzdHMvIC12CiAgICAtIHJ1bjogcnVmZiBjaGVjayAu
|
||||
"""Core module for shell history loading and parsing."""
|
||||
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Optional, TextIO
|
||||
|
||||
|
||||
@dataclass
|
||||
class HistoryEntry:
|
||||
"""Represents a single entry in shell history."""
|
||||
command: str
|
||||
timestamp: Optional[datetime] = None
|
||||
line_number: int = 0
|
||||
shell_type: str = "unknown"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.command)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if isinstance(other, HistoryEntry):
|
||||
return self.command == other.command
|
||||
return False
|
||||
|
||||
|
||||
@dataclass
|
||||
class HistoryStore:
|
||||
"""Stores and manages shell history entries."""
|
||||
entries: list[HistoryEntry] = field(default_factory=list)
|
||||
command_frequency: dict[str, int] = field(default_factory=dict)
|
||||
|
||||
def add_entry(self, entry: HistoryEntry) -> None:
|
||||
"""Add a history entry to the store."""
|
||||
self.entries.append(entry)
|
||||
self.command_frequency[entry.command] = self.command_frequency.get(entry.command, 0) + 1
|
||||
|
||||
def get_frequency(self, command: str) -> int:
|
||||
"""Get the frequency of a command."""
|
||||
return self.command_frequency.get(command, 0)
|
||||
|
||||
def get_most_frequent(self, limit: int = 10) -> list[tuple[str, int]]:
|
||||
"""Get the most frequently used commands."""
|
||||
sorted_freq = sorted(self.command_frequency.items(), key=lambda x: x[1], reverse=True)
|
||||
return sorted_freq[:limit]
|
||||
|
||||
def get_unique_commands(self) -> list[str]:
|
||||
"""Get list of unique commands."""
|
||||
return list(self.command_frequency.keys())
|
||||
|
||||
|
||||
class HistoryLoader:
|
||||
"""Loads and parses shell history from various shell formats."""
|
||||
|
||||
BASH_TIMESTAMP_PATTERN = re.compile(r'^#(\d{10})$')
|
||||
ZSH_TIMESTAMP_PATTERN = re.compile(r'^: (\d{10}):(\d+);(.*)$')
|
||||
|
||||
def __init__(self, history_path: Optional[str] = None):
|
||||
"""Initialize the history loader.
|
||||
|
||||
Args:
|
||||
history_path: Optional path to history file. If not provided,
|
||||
will attempt to detect from environment.
|
||||
"""
|
||||
self.history_path = history_path
|
||||
self.shell_type = self._detect_shell()
|
||||
|
||||
def _detect_shell(self) -> str:
|
||||
"""Detect the shell type from environment."""
|
||||
shell = os.environ.get("SHELL", "")
|
||||
if "zsh" in shell:
|
||||
return "zsh"
|
||||
elif "bash" in shell:
|
||||
return "bash"
|
||||
return "bash"
|
||||
|
||||
def _get_default_history_path(self) -> Optional[str]:
|
||||
"""Get the default history file path based on shell type."""
|
||||
home = os.path.expanduser("~")
|
||||
if self.shell_type == "zsh":
|
||||
return os.environ.get("HISTFILE", f"{home}/.zsh_history")
|
||||
return os.environ.get("HISTFILE", f"{home}/.bash_history")
|
||||
|
||||
def load(self) -> HistoryStore:
|
||||
"""Load history from the configured path.
|
||||
|
||||
Returns:
|
||||
HistoryStore containing all parsed entries.
|
||||
"""
|
||||
path = self.history_path or self._get_default_history_path()
|
||||
|
||||
if not path or not os.path.exists(path):
|
||||
raise FileNotFoundError(
|
||||
f"History file not found: {path}. "
|
||||
"Set HISTFILE environment variable or use --history flag."
|
||||
)
|
||||
|
||||
store = HistoryStore()
|
||||
|
||||
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
||||
if self.shell_type == "zsh":
|
||||
self._parse_zsh_history(f, store)
|
||||
else:
|
||||
self._parse_bash_history(f, store)
|
||||
|
||||
return store
|
||||
|
||||
def _parse_bash_history(self, file: TextIO, store: HistoryStore) -> None:
|
||||
"""Parse bash history format."""
|
||||
line_number = 0
|
||||
pending_timestamp: Optional[datetime] = None
|
||||
|
||||
for line in file:
|
||||
line = line.rstrip("\n")
|
||||
line_number += 1
|
||||
|
||||
timestamp_match = self.BASH_TIMESTAMP_PATTERN.match(line)
|
||||
if timestamp_match:
|
||||
ts = int(timestamp_match.group(1))
|
||||
pending_timestamp = datetime.fromtimestamp(ts)
|
||||
continue
|
||||
|
||||
if line and not line.startswith("#"):
|
||||
command = line
|
||||
entry = HistoryEntry(
|
||||
command=command,
|
||||
timestamp=pending_timestamp,
|
||||
line_number=line_number,
|
||||
shell_type="bash"
|
||||
)
|
||||
store.add_entry(entry)
|
||||
pending_timestamp = None
|
||||
|
||||
def _parse_zsh_history(self, file: TextIO, store: HistoryStore) -> None:
|
||||
"""Parse zsh history format."""
|
||||
line_number = 0
|
||||
|
||||
for line in file:
|
||||
line = line.rstrip("\n")
|
||||
line_number += 1
|
||||
|
||||
timestamp_match = self.ZSH_TIMESTAMP_PATTERN.match(line)
|
||||
if timestamp_match:
|
||||
ts = int(timestamp_match.group(1))
|
||||
command = timestamp_match.group(3)
|
||||
timestamp = datetime.fromtimestamp(ts)
|
||||
|
||||
entry = HistoryEntry(
|
||||
command=command,
|
||||
timestamp=timestamp,
|
||||
line_number=line_number,
|
||||
shell_type="zsh"
|
||||
)
|
||||
store.add_entry(entry)
|
||||
elif line:
|
||||
entry = HistoryEntry(
|
||||
command=line,
|
||||
timestamp=None,
|
||||
line_number=line_number,
|
||||
shell_type="zsh"
|
||||
)
|
||||
store.add_entry(entry)
|
||||
|
||||
@staticmethod
|
||||
def from_file(path: str) -> HistoryStore:
|
||||
"""Convenience method to load history from a specific file.
|
||||
|
||||
Args:
|
||||
path: Path to history file.
|
||||
|
||||
Returns:
|
||||
HistoryStore containing parsed entries.
|
||||
"""
|
||||
loader = HistoryLoader(history_path=path)
|
||||
return loader.load()
|
||||
|
||||
Reference in New Issue
Block a user