Files
shell-history-automation-tool/shellhist/cli/time_analysis.py

262 lines
7.5 KiB
Python

"""Time-based analysis command."""
from collections import defaultdict
from datetime import datetime, timedelta
from typing import Optional
import click
from rich.console import Console
from rich.table import Table
from shellhist.core import HistoryLoader
@click.command("analyze-time")
@click.option(
"--history",
"-H",
type=str,
help="Path to history file",
)
@click.option(
"--daily/--no-daily",
default=False,
help="Show commands run at similar times daily",
)
@click.option(
"--weekly/--no-weekly",
default=False,
help="Show commands run at similar times weekly",
)
@click.option(
"--time-range",
"-t",
type=str,
default="7d",
help="Time range filter (e.g., '7d', '24h', '30d')",
)
@click.option(
"--shell",
"-s",
type=click.Choice(["bash", "zsh"]),
help="Shell type for parsing",
)
@click.pass_context
def analyze_time_command(
ctx: click.Context,
history: Optional[str],
daily: bool,
weekly: bool,
time_range: str,
shell: Optional[str],
) -> None:
"""Analyze time-based patterns in your shell history.
Examples:
\b
shellhist analyze-time --daily
shellhist analyze-time --weekly --time-range 30d
shellhist analyze-time --daily --time-range 7d
"""
console = Console()
try:
loader = HistoryLoader(history_path=history)
store = loader.load()
if not store.entries:
console.print("[yellow]No entries found in history.[/yellow]")
return
entries_with_time = [
e for e in store.entries
if e.timestamp is not None
]
if not entries_with_time:
console.print(
"[yellow]No timestamps found in history. "
"Ensure your history includes timestamps.[/yellow]"
)
return
cutoff = _parse_time_range(time_range)
recent_entries = [
e for e in entries_with_time
if e.timestamp >= cutoff
]
if daily:
_analyze_daily_patterns(console, recent_entries)
elif weekly:
_analyze_weekly_patterns(console, recent_entries)
else:
_analyze_hourly_distribution(console, recent_entries)
except FileNotFoundError as e:
console.print(f"[red]Error: {e}[/red]")
ctx.exit(1)
except Exception as e:
console.print(f"[red]Error analyzing time patterns: {e}[/red]")
ctx.exit(1)
def _parse_time_range(time_range: str) -> datetime:
"""Parse time range string like '7d', '24h', '30d'."""
value = int(time_range[:-1])
unit = time_range[-1].lower()
now = datetime.now()
if unit == 'h':
return now - timedelta(hours=value)
elif unit == 'd':
return now - timedelta(days=value)
elif unit == 'w':
return now - timedelta(weeks=value)
else:
return now - timedelta(days=7)
def _analyze_hourly_distribution(console: Console, entries: list) -> None:
"""Analyze command distribution by hour of day."""
hourly = defaultdict(list)
for entry in entries:
hour = entry.timestamp.hour
hourly[hour].append(entry.command)
console.print("\n[bold cyan]Hourly Command Distribution[/bold cyan]")
table = Table(show_header=True, header_style="bold magenta")
table.add_column("Hour", width=10)
table.add_column("Commands", width=10)
table.add_column("Top Commands", width=60)
for hour in range(24):
cmds = hourly.get(hour, [])
if cmds:
top = defaultdict(int)
for cmd in cmds:
top[cmd] += 1
top_sorted = sorted(top.items(), key=lambda x: x[1], reverse=True)[:2]
top_str = ", ".join(f"{c}({n})" for c, n in top_sorted)
else:
top_str = "-"
hour_label = f"{hour:02d}:00"
table.add_row(hour_label, str(len(cmds)), top_str[:58])
console.print(table)
def _analyze_daily_patterns(console: Console, entries: list) -> None:
"""Analyze commands run at similar times daily."""
daily_patterns = defaultdict(list)
for entry in entries:
hour = entry.timestamp.hour
key = (entry.timestamp.weekday(), hour)
daily_patterns[key].append(entry.command)
console.print("\n[bold cyan]Daily Time Patterns[/bold cyan]")
day_names = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
patterns_found = []
for (weekday, hour), cmds in daily_patterns.items():
if len(cmds) >= 2:
top = defaultdict(int)
for cmd in cmds:
top[cmd] += 1
top_sorted = sorted(top.items(), key=lambda x: x[1], reverse=True)[:3]
patterns_found.append({
"day": day_names[weekday],
"hour": hour,
"count": len(cmds),
"top": top_sorted,
})
if not patterns_found:
console.print("[yellow]No significant daily patterns found.[/yellow]")
return
patterns_found.sort(key=lambda x: x["count"], reverse=True)
table = Table(show_header=True, header_style="bold magenta")
table.add_column("Day", width=8)
table.add_column("Hour", width=8)
table.add_column("Times", width=8)
table.add_column("Common Commands", width=60)
for p in patterns_found[:20]:
top_str = ", ".join(f"{c}({n})" for c, n in p["top"])
table.add_row(
p["day"],
f"{p['hour']:02d}:00",
str(p["count"]),
top_str[:58],
)
console.print(table)
def _analyze_weekly_patterns(console: Console, entries: list) -> None:
"""Analyze commands run at similar times weekly."""
weekly_patterns = defaultdict(list)
for entry in entries:
key = (
entry.timestamp.isocalendar().year,
entry.timestamp.isocalendar().week,
entry.timestamp.weekday(),
entry.timestamp.hour,
)
weekly_patterns[key].append(entry.command)
console.print("\n[bold cyan]Weekly Time Patterns[/bold cyan]")
day_names = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
patterns_found = []
for key, cmds in weekly_patterns.items():
year, week, weekday, hour = key
if len(cmds) >= 2:
top = defaultdict(int)
for cmd in cmds:
top[cmd] += 1
top_sorted = sorted(top.items(), key=lambda x: x[1], reverse=True)[:3]
patterns_found.append({
"week": week,
"day": day_names[weekday],
"hour": hour,
"count": len(cmds),
"top": top_sorted,
})
if not patterns_found:
console.print("[yellow]No significant weekly patterns found.[/yellow]")
return
patterns_found.sort(key=lambda x: x["count"], reverse=True)
table = Table(show_header=True, header_style="bold magenta")
table.add_column("Week", width=8)
table.add_column("Day", width=8)
table.add_column("Hour", width=8)
table.add_column("Times", width=8)
table.add_column("Common Commands", width=50)
for p in patterns_found[:20]:
top_str = ", ".join(f"{c}({n})" for c, n in p["top"])
table.add_row(
str(p["week"]),
p["day"],
f"{p['hour']:02d}:00",
str(p["count"]),
top_str[:48],
)
console.print(table)