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