PYTHONPython

utils

real world projects / password manager / pwm

PYTHON
utils.py🐍
"""
Utility functions for password manager.
"""

import sys
import time
import threading
from typing import Optional, Callable
from functools import wraps

# Try to import clipboard
try:
    import pyperclip
    HAS_CLIPBOARD = True
except ImportError:
    HAS_CLIPBOARD = False


def copy_to_clipboard(text: str, clear_after: int = 30) -> bool:
    """
    Copy text to clipboard and optionally clear after timeout.
    
    Args:
        text: Text to copy
        clear_after: Seconds before clearing (0 to disable)
    
    Returns:
        True if successful
    """
    if not HAS_CLIPBOARD:
        return False
    
    try:
        pyperclip.copy(text)
        
        if clear_after > 0:
            # Start timer to clear clipboard
            def clear():
                time.sleep(clear_after)
                try:
                    current = pyperclip.paste()
                    if current == text:
                        pyperclip.copy("")
                except:
                    pass
            
            thread = threading.Thread(target=clear, daemon=True)
            thread.start()
        
        return True
    except:
        return False


def clear_clipboard():
    """Clear the clipboard."""
    if HAS_CLIPBOARD:
        try:
            pyperclip.copy("")
        except:
            pass


def get_from_clipboard() -> str:
    """Get text from clipboard."""
    if not HAS_CLIPBOARD:
        return ""
    
    try:
        return pyperclip.paste()
    except:
        return ""


def secure_input(prompt: str = "", hide: bool = True) -> str:
    """
    Get secure input from user (hidden for passwords).
    
    Args:
        prompt: Prompt to display
        hide: Whether to hide input
    
    Returns:
        User input
    """
    if hide:
        try:
            import getpass
            return getpass.getpass(prompt)
        except:
            pass
    
    return input(prompt)


def confirm(prompt: str, default: bool = False) -> bool:
    """
    Ask user for confirmation.
    
    Args:
        prompt: Question to ask
        default: Default answer
    
    Returns:
        User's choice
    """
    suffix = " [Y/n]" if default else " [y/N]"
    
    while True:
        response = input(prompt + suffix + ": ").strip().lower()
        
        if not response:
            return default
        
        if response in ("y", "yes"):
            return True
        
        if response in ("n", "no"):
            return False
        
        print("Please answer 'yes' or 'no'")


def format_time_ago(timestamp: str) -> str:
    """
    Format timestamp as relative time.
    
    Args:
        timestamp: ISO format timestamp
    
    Returns:
        Human-readable relative time
    """
    from datetime import datetime
    
    try:
        dt = datetime.fromisoformat(timestamp)
        now = datetime.utcnow()
        delta = now - dt
        
        seconds = delta.total_seconds()
        
        if seconds < 60:
            return "just now"
        
        minutes = seconds / 60
        if minutes < 60:
            return f"{int(minutes)} minutes ago"
        
        hours = minutes / 60
        if hours < 24:
            return f"{int(hours)} hours ago"
        
        days = hours / 24
        if days < 30:
            return f"{int(days)} days ago"
        
        months = days / 30
        if months < 12:
            return f"{int(months)} months ago"
        
        years = months / 12
        return f"{int(years)} years ago"
        
    except:
        return timestamp


def mask_password(password: str, show_length: bool = True) -> str:
    """
    Mask a password for display.
    
    Args:
        password: Password to mask
        show_length: Whether to indicate length
    
    Returns:
        Masked string
    """
    if show_length:
        return "*" * min(len(password), 16) + f" ({len(password)} chars)"
    return "********"


def truncate(text: str, max_length: int = 50, suffix: str = "...") -> str:
    """
    Truncate text to max length.
    
    Args:
        text: Text to truncate
        max_length: Maximum length
        suffix: Suffix to add if truncated
    
    Returns:
        Truncated text
    """
    if len(text) <= max_length:
        return text
    
    return text[:max_length - len(suffix)] + suffix


def rate_limit(calls: int, period: float) -> Callable:
    """
    Decorator to rate limit function calls.
    
    Args:
        calls: Maximum calls allowed
        period: Time period in seconds
    
    Returns:
        Decorator function
    """
    def decorator(func: Callable) -> Callable:
        timestamps = []
        lock = threading.Lock()
        
        @wraps(func)
        def wrapper(*args, **kwargs):
            with lock:
                now = time.time()
                
                # Remove old timestamps
                timestamps[:] = [t for t in timestamps if now - t < period]
                
                if len(timestamps) >= calls:
                    raise RuntimeError(
                        f"Rate limit exceeded: {calls} calls per {period}s"
                    )
                
                timestamps.append(now)
            
            return func(*args, **kwargs)
        
        return wrapper
    
    return decorator


def retry(max_attempts: int = 3, delay: float = 1.0) -> Callable:
    """
    Decorator to retry function on failure.
    
    Args:
        max_attempts: Maximum retry attempts
        delay: Delay between retries
    
    Returns:
        Decorator function
    """
    def decorator(func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_error: Exception = RuntimeError("No attempts made")
            
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_error = e
                    if attempt < max_attempts - 1:
                        time.sleep(delay)
            
            raise last_error
        
        return wrapper
    
    return decorator
PreviousNext