File Encryption / Decryption Tool

 import os

import json

import hashlib

import base64

import secrets

import getpass

from datetime import datetime

from pathlib import Path


try:

    from cryptography.fernet import Fernet, InvalidToken

    from cryptography.hazmat.primitives import hashes

    from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

    CRYPTO_OK = True

except ImportError:

    CRYPTO_OK = False


# ============================================================

# CONFIGURATION

# ============================================================


KEY_FILE      = "encryption.key"

VAULT_LOG     = "encryption_vault.json"

ENC_EXTENSION = ".encrypted"

SALT_SIZE     = 16       # bytes

ITERATIONS    = 480000   # PBKDF2 iterations (NIST recommended)


# ============================================================

# HELPERS

# ============================================================


def format_size(size_bytes):

    for unit in ["B", "KB", "MB", "GB"]:

        if size_bytes < 1024:

            return f"{size_bytes:.1f} {unit}"

        size_bytes /= 1024

    return f"{size_bytes:.1f} GB"



def file_checksum(filepath):

    """Return MD5 checksum of a file."""

    h = hashlib.md5()

    with open(filepath, "rb") as f:

        while chunk := f.read(8192):

            h.update(chunk)

    return h.hexdigest()



# ============================================================

# KEY MANAGEMENT

# ============================================================


def generate_key():

    """Generate a new random Fernet key."""

    return Fernet.generate_key()



def derive_key_from_password(password: str, salt: bytes = None):

    """

    Derive a Fernet-compatible key from a password using PBKDF2-HMAC-SHA256.

    Returns (key, salt).

    """

    if salt is None:

        salt = secrets.token_bytes(SALT_SIZE)


    kdf = PBKDF2HMAC(

        algorithm=hashes.SHA256(),

        length=32,

        salt=salt,

        iterations=ITERATIONS,

    )

    key = base64.urlsafe_b64encode(kdf.derive(password.encode()))

    return key, salt



def save_key(key: bytes, filepath=KEY_FILE):

    """Save a key to a file."""

    with open(filepath, "wb") as f:

        f.write(key)

    print(f"  Key saved: {filepath}")

    print("  IMPORTANT: Keep this key file safe. Without it, decryption is impossible!")



def load_key(filepath=KEY_FILE):

    """Load a key from file."""

    if not Path(filepath).exists():

        print(f"  Key file not found: {filepath}")

        return None

    with open(filepath, "rb") as f:

        return f.read().strip()



# ============================================================

# VAULT LOG

# ============================================================


def load_vault():

    if Path(VAULT_LOG).exists():

        try:

            with open(VAULT_LOG, "r") as f:

                return json.load(f)

        except:

            pass

    return {"files": []}



def save_vault(vault):

    with open(VAULT_LOG, "w") as f:

        json.dump(vault, f, indent=2)



def log_operation(vault, operation, filepath, output_path, method):

    vault["files"].append({

        "operation":   operation,

        "source":      str(filepath),

        "output":      str(output_path),

        "method":      method,

        "timestamp":   datetime.now().strftime("%d-%m-%Y %H:%M:%S"),

        "size":        format_size(Path(output_path).stat().st_size)

                       if Path(output_path).exists() else "?"

    })

    vault["files"] = vault["files"][-100:]

    save_vault(vault)



# ============================================================

# ENCRYPT FILE

# ============================================================


def encrypt_file(filepath, key: bytes, output_path=None,

                 salt: bytes = None):

    """

    Encrypt a file using Fernet.

    If salt is provided, it's prepended to the encrypted output

    (used for password-based encryption).

    Returns output path or None on failure.

    """

    filepath = Path(filepath)

    if not filepath.exists():

        print(f"  File not found: {filepath}")

        return None


    if output_path is None:

        output_path = filepath.with_suffix(filepath.suffix + ENC_EXTENSION)

    output_path = Path(output_path)


    try:

        fernet = Fernet(key)


        with open(filepath, "rb") as f:

            data = f.read()


        encrypted = fernet.encrypt(data)


        with open(output_path, "wb") as f:

            if salt:

                # Write salt length (2 bytes) + salt + encrypted data

                f.write(len(salt).to_bytes(2, "big"))

                f.write(salt)

            f.write(encrypted)


        orig_size = format_size(filepath.stat().st_size)

        enc_size  = format_size(output_path.stat().st_size)

        checksum  = file_checksum(filepath)


        print(f"\n  Encrypted successfully!")

        print(f"  Input  : {filepath.name}  ({orig_size})")

        print(f"  Output : {output_path.name}  ({enc_size})")

        print(f"  Checksum (original): {checksum}")

        return output_path


    except Exception as e:

        print(f"  Encryption failed: {e}")

        return None



# ============================================================

# DECRYPT FILE

# ============================================================


def decrypt_file(filepath, key: bytes, output_path=None,

                 password_based=False):

    """

    Decrypt a Fernet-encrypted file.

    If password_based=True, reads the salt from the file header.

    Returns output path or None on failure.

    """

    filepath = Path(filepath)

    if not filepath.exists():

        print(f"  File not found: {filepath}")

        return None


    # Determine output path

    if output_path is None:

        name = filepath.name

        if name.endswith(ENC_EXTENSION):

            out_name = name[:-len(ENC_EXTENSION)]

        else:

            out_name = name + ".decrypted"

        output_path = filepath.parent / out_name

    output_path = Path(output_path)


    try:

        with open(filepath, "rb") as f:

            raw = f.read()


        if password_based:

            # Read salt from header

            salt_len  = int.from_bytes(raw[:2], "big")

            salt      = raw[2:2 + salt_len]

            encrypted = raw[2 + salt_len:]


            # Re-derive key from password

            password = getpass.getpass("  Password: ")

            key, _   = derive_key_from_password(password, salt)

        else:

            encrypted = raw


        fernet    = Fernet(key)

        decrypted = fernet.decrypt(encrypted)


        with open(output_path, "wb") as f:

            f.write(decrypted)


        orig_size = format_size(filepath.stat().st_size)

        dec_size  = format_size(output_path.stat().st_size)

        checksum  = file_checksum(output_path)


        print(f"\n  Decrypted successfully!")

        print(f"  Input  : {filepath.name}  ({orig_size})")

        print(f"  Output : {output_path.name}  ({dec_size})")

        print(f"  Checksum (restored): {checksum}")

        return output_path


    except InvalidToken:

        print("  Decryption failed: Wrong key or password, or file is corrupted.")

        return None

    except Exception as e:

        print(f"  Decryption failed: {e}")

        return None



# ============================================================

# ENCRYPT / DECRYPT FOLDER

# ============================================================


def process_folder(folder_path, key, operation,

                   extensions=None, password_based=False, salt=None):

    folder = Path(folder_path)

    if not folder.is_dir():

        print("  Invalid folder.")

        return


    if operation == "encrypt":

        if extensions:

            files = [f for f in folder.rglob("*")

                     if f.is_file() and f.suffix.lower() in extensions

                     and not f.name.endswith(ENC_EXTENSION)]

        else:

            files = [f for f in folder.rglob("*")

                     if f.is_file() and not f.name.endswith(ENC_EXTENSION)]

    else:

        files = [f for f in folder.rglob("*")

                 if f.is_file() and f.name.endswith(ENC_EXTENSION)]


    if not files:

        print(f"  No files found to {operation}.")

        return


    print(f"\n  Found {len(files)} file(s) to {operation}.")

    confirm = input(f"  Proceed? (y/n): ").strip().lower()

    if confirm != "y":

        print("  Cancelled.")

        return


    success = 0

    failed  = 0

    vault   = load_vault()


    for f in files:

        print(f"\n  [{operation.upper()}] {f.name}")

        if operation == "encrypt":

            result = encrypt_file(f, key, salt=salt)

        else:

            result = decrypt_file(f, key, password_based=password_based)


        if result:

            success += 1

            log_operation(vault, operation, f, result,

                          "password" if password_based else "key-file")

        else:

            failed += 1


    save_vault(vault)

    print(f"\n  Done: {success} succeeded, {failed} failed.")



# ============================================================

# ENCRYPT TEXT STRING

# ============================================================


def encrypt_text(key: bytes):

    print("\n  Enter text to encrypt (type END on new line to finish):")

    lines = []

    while True:

        line = input()

        if line.strip().upper() == "END":

            break

        lines.append(line)

    text = "\n".join(lines)


    fernet    = Fernet(key)

    encrypted = fernet.encrypt(text.encode())

    encoded   = encrypted.decode()


    print(f"\n  Encrypted text (copy this):")

    print(f"  {encoded}")


    save = input("\n  Save to file? (y/n): ").strip().lower()

    if save == "y":

        fname = f"encrypted_text_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"

        with open(fname, "w") as f:

            f.write(encoded)

        print(f"  Saved: {fname}")



def decrypt_text(key: bytes):

    print("\n  Paste encrypted text:")

    enc_text = input("  > ").strip()


    try:

        fernet    = Fernet(key)

        decrypted = fernet.decrypt(enc_text.encode()).decode()

        print(f"\n  Decrypted text:")

        print(f"  {decrypted}")

    except InvalidToken:

        print("  Decryption failed: Invalid token or wrong key.")

    except Exception as e:

        print(f"  Error: {e}")



# ============================================================

# DISPLAY VAULT LOG

# ============================================================


def display_vault():

    vault = load_vault()

    files = vault.get("files", [])


    if not files:

        print("\n  No operations logged yet.")

        return


    print("\n" + "="*65)

    print(f"  ENCRYPTION VAULT LOG  ({len(files)} entries)")

    print("="*65)

    print(f"  {'OP':<10} {'TIMESTAMP':<22} {'METHOD':<10} FILE")

    print("  " + "-"*60)


    for entry in reversed(files[-20:]):

        op     = entry["operation"].upper()

        ts     = entry["timestamp"]

        method = entry["method"]

        fname  = Path(entry["output"]).name

        print(f"  {op:<10} {ts:<22} {method:<10} {fname}")


    print("="*65)



# ============================================================

# SETUP: GET OR CREATE KEY

# ============================================================


def setup_key_menu():

    print("\n  KEY SETUP")

    print("  1. Generate new random key (save to file)")

    print("  2. Use existing key file")

    print("  3. Use password (derive key)")


    choice = input("\n  Choice (1/2/3): ").strip()


    if choice == "1":

        key = generate_key()

        fname = input(f"  Save key as [{KEY_FILE}]: ").strip() or KEY_FILE

        save_key(key, fname)

        return key, False, None


    elif choice == "2":

        fname = input(f"  Key file path [{KEY_FILE}]: ").strip() or KEY_FILE

        key = load_key(fname)

        if key:

            print(f"  Key loaded from: {fname}")

            return key, False, None

        return None, False, None


    elif choice == "3":

        password = getpass.getpass("  Enter password: ")

        confirm  = getpass.getpass("  Confirm password: ")

        if password != confirm:

            print("  Passwords do not match.")

            return None, False, None

        key, salt = derive_key_from_password(password)

        print("  Key derived from password successfully.")

        print(f"  Strength tip: Use 12+ chars with mixed letters, numbers, symbols.")

        return key, True, salt


    return None, False, None



# ============================================================

# MAIN MENU

# ============================================================


def print_menu(key_loaded, method):

    status = f"Key loaded ({method})" if key_loaded else "No key loaded"

    print("\n" + "-"*50)

    print(f"  FILE ENCRYPTION TOOL  [{status}]")

    print("-"*50)

    print("  1. Setup key (generate / load / password)")

    print("  2. Encrypt a file")

    print("  3. Decrypt a file")

    print("  4. Encrypt all files in folder")

    print("  5. Decrypt all .encrypted files in folder")

    print("  6. Encrypt text string")

    print("  7. Decrypt text string")

    print("  8. View vault log")

    print("  0. Exit")

    print("-"*50)



def main():

    print("\n" + "="*55)

    print("     FILE ENCRYPTION / DECRYPTION TOOL")

    print("="*55)


    if not CRYPTO_OK:

        print("\n  cryptography library not installed!")

        print("  Install it:  pip install cryptography")

        return


    print("\n  Uses Fernet symmetric encryption (AES-128-CBC + HMAC-SHA256)")

    print("  Supports: key file OR password-based encryption\n")


    current_key      = None

    password_based   = False

    current_salt     = None

    method_str       = ""


    # Auto-load key if exists

    if Path(KEY_FILE).exists():

        current_key = load_key(KEY_FILE)

        method_str  = "key-file"

        print(f"  Auto-loaded key from: {KEY_FILE}")


    vault = load_vault()


    while True:

        print_menu(current_key is not None, method_str)

        choice = input("  > ").strip()


        if choice == "1":

            current_key, password_based, current_salt = setup_key_menu()

            if current_key:

                method_str = "password" if password_based else "key-file"


        elif choice in ["2", "3", "4", "5", "6", "7"]:

            if not current_key:

                print("\n  No key loaded. Please set up a key first (Option 1).")

                continue


            if choice == "2":

                path = input("\n  File to encrypt: ").strip()

                out  = input("  Output path (Enter = auto): ").strip() or None

                result = encrypt_file(path, current_key, out,

                                      salt=current_salt)

                if result:

                    log_operation(vault, "encrypt", path, result, method_str)

                    save_vault(vault)


            elif choice == "3":

                path = input("\n  File to decrypt: ").strip()

                out  = input("  Output path (Enter = auto): ").strip() or None

                result = decrypt_file(path, current_key, out,

                                      password_based=password_based)

                if result:

                    log_operation(vault, "decrypt", path, result, method_str)

                    save_vault(vault)


            elif choice == "4":

                folder = input("\n  Folder path: ").strip()

                ext_in = input("  File types to encrypt (e.g. .txt .pdf, Enter=all): ").strip()

                exts   = [e if e.startswith(".") else f".{e}"

                          for e in ext_in.split()] if ext_in else None

                process_folder(folder, current_key, "encrypt",

                               extensions=exts,

                               salt=current_salt)


            elif choice == "5":

                folder = input("\n  Folder path: ").strip()

                process_folder(folder, current_key, "decrypt",

                               password_based=password_based)


            elif choice == "6":

                encrypt_text(current_key)


            elif choice == "7":

                decrypt_text(current_key)


        elif choice == "8":

            display_vault()


        elif choice == "0":

            print("\n  Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



# ============================================================

# RUN

# ============================================================


if __name__ == "__main__":

    main()

Log File Analyzer

import re

import os

import json

from datetime import datetime

from pathlib import Path

from collections import defaultdict, Counter


# ============================================================

# CONFIGURATION

# ============================================================


REPORT_FILE = "log_analysis_report.json"


# Common log level patterns

LOG_LEVELS = ["CRITICAL", "ERROR", "WARNING", "WARN", "INFO", "DEBUG", "TRACE", "FATAL"]


# Built-in format patterns

LOG_FORMATS = {

    "apache_access": r'(?P<ip>\S+) \S+ \S+ \[(?P<datetime>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+)[^"]*" (?P<status>\d{3}) (?P<size>\S+)',

    "apache_error":  r'\[(?P<datetime>[^\]]+)\] \[(?P<level>\w+)\] (?P<message>.*)',

    "nginx_access":  r'(?P<ip>\S+) - \S+ \[(?P<datetime>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+)[^"]*" (?P<status>\d{3}) (?P<size>\d+)',

    "python_log":    r'(?P<datetime>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}),\d+ (?P<level>\w+) (?P<logger>\S+) (?P<message>.*)',

    "syslog":        r'(?P<datetime>\w{3}\s+\d+ \d{2}:\d{2}:\d{2}) (?P<host>\S+) (?P<process>\S+): (?P<message>.*)',

    "generic":       r'(?P<datetime>\d{4}[-/]\d{2}[-/]\d{2}[T ]\d{2}:\d{2}:\d{2}).*?(?P<level>CRITICAL|ERROR|WARNING|WARN|INFO|DEBUG|TRACE|FATAL).*?(?P<message>.*)',

}


# ============================================================

# HELPERS

# ============================================================


def draw_bar(value, max_val, width=25, fill="█", empty="░"):

    if max_val == 0:

        return ""

    filled = int((min(value, max_val) / max_val) * width)

    return fill * filled + empty * (width - filled)



def format_size(size_bytes):

    for unit in ["B", "KB", "MB", "GB"]:

        if size_bytes < 1024:

            return f"{size_bytes:.1f} {unit}"

        size_bytes /= 1024

    return f"{size_bytes:.1f} GB"



def safe_read(filepath, encoding="utf-8"):

    try:

        with open(filepath, "r", encoding=encoding, errors="replace") as f:

            return f.readlines()

    except Exception as e:

        print(f"  Error reading file: {e}")

        return []



# ============================================================

# AUTO-DETECT LOG FORMAT

# ============================================================


def detect_format(lines):

    """Try each format pattern on first 20 lines. Return best match."""

    sample = lines[:20]

    scores = {}

    for fmt_name, pattern in LOG_FORMATS.items():

        matches = sum(1 for line in sample if re.search(pattern, line))

        scores[fmt_name] = matches


    best = max(scores, key=scores.get)

    if scores[best] > 0:

        return best, scores[best]

    return "generic", 0



# ============================================================

# CORE PARSER

# ============================================================


def parse_log(filepath, fmt_name=None, keyword_filter=None,

              level_filter=None, max_lines=0):

    """

    Parse a log file and return structured data.

    Returns dict with entries, stats, errors.

    """

    lines = safe_read(filepath)

    if not lines:

        return None


    total_lines = len(lines)

    print(f"\n  File    : {Path(filepath).name}")

    print(f"  Size    : {format_size(Path(filepath).stat().st_size)}")

    print(f"  Lines   : {total_lines:,}")


    # Auto-detect format

    if not fmt_name or fmt_name == "auto":

        fmt_name, score = detect_format(lines)

        print(f"  Format  : {fmt_name} (auto-detected, {score}/20 lines matched)")

    else:

        print(f"  Format  : {fmt_name}")


    pattern = LOG_FORMATS.get(fmt_name, LOG_FORMATS["generic"])


    # Limit lines if requested

    if max_lines > 0:

        lines = lines[:max_lines]

        print(f"  Parsing : first {max_lines:,} lines")


    # Parse each line

    entries       = []

    level_counts  = Counter()

    hour_counts   = Counter()

    ip_counts     = Counter()

    status_counts = Counter()

    path_counts   = Counter()

    message_list  = []

    parse_errors  = 0

    keyword_hits  = []


    for i, line in enumerate(lines):

        line = line.rstrip()

        if not line:

            continue


        match = re.search(pattern, line, re.IGNORECASE)


        if match:

            entry = match.groupdict()


            # Normalize level

            level = entry.get("level", "").upper()

            if not level:

                # Try to find level anywhere in line

                for lvl in LOG_LEVELS:

                    if lvl in line.upper():

                        level = lvl

                        break

            entry["level"] = level or "UNKNOWN"

            level_counts[entry["level"]] += 1


            # Extract hour from datetime

            dt_str = entry.get("datetime", "")

            for fmt in ["%Y-%m-%d %H:%M:%S", "%d/%b/%Y:%H:%M:%S",

                        "%b %d %H:%M:%S", "%Y/%m/%d %H:%M:%S"]:

                try:

                    dt = datetime.strptime(dt_str[:len(fmt)], fmt)

                    hour_counts[dt.strftime("%H:00")] += 1

                    entry["datetime_parsed"] = dt.strftime("%d-%m-%Y %H:%M:%S")

                    break

                except:

                    continue


            # Track IPs, status codes, paths (for web logs)

            if "ip" in entry and entry["ip"]:

                ip_counts[entry["ip"]] += 1

            if "status" in entry and entry["status"]:

                status_counts[entry["status"]] += 1

            if "path" in entry and entry["path"]:

                path_counts[entry["path"]] += 1


            # Track messages

            msg = entry.get("message", line[:120])

            message_list.append(msg)


            # Apply filters

            if level_filter and entry["level"] not in [l.upper() for l in level_filter]:

                continue

            if keyword_filter and keyword_filter.lower() not in line.lower():

                continue


            entry["line_no"] = i + 1

            entry["raw"]     = line[:200]

            entries.append(entry)


            # Keyword tracking

            if keyword_filter and keyword_filter.lower() in line.lower():

                keyword_hits.append({"line": i + 1, "text": line[:200]})


        else:

            parse_errors += 1


    parse_rate = round((1 - parse_errors / max(len(lines), 1)) * 100, 1)

    print(f"  Parsed  : {len(entries):,} entries  ({parse_rate}% success rate)")


    return {

        "filepath":     str(filepath),

        "filename":     Path(filepath).name,

        "total_lines":  total_lines,

        "parsed":       len(entries),

        "parse_errors": parse_errors,

        "format":       fmt_name,

        "entries":      entries,

        "level_counts": dict(level_counts),

        "hour_counts":  dict(hour_counts),

        "ip_counts":    dict(ip_counts),

        "status_counts":dict(status_counts),

        "path_counts":  dict(path_counts),

        "message_list": message_list,

        "keyword_hits": keyword_hits,

    }



# ============================================================

# DISPLAY: LEVEL SUMMARY

# ============================================================


def display_level_summary(result):

    counts   = result["level_counts"]

    total    = sum(counts.values())

    if not counts:

        print("\n  No log level data found.")

        return


    max_val  = max(counts.values())


    print("\n" + "="*55)

    print(f"  LOG LEVEL SUMMARY  —  {result['filename']}")

    print("="*55)

    print(f"  {'LEVEL':<12} {'COUNT':>8}  {'%':>6}  BAR")

    print("  " + "-"*51)


    order = ["FATAL", "CRITICAL", "ERROR", "WARNING", "WARN",

             "INFO", "DEBUG", "TRACE", "UNKNOWN"]


    for level in order:

        count = counts.get(level, 0)

        if count == 0:

            continue

        pct = count / total * 100

        bar = draw_bar(count, max_val)

        print(f"  {level:<12} {count:>8,}  {pct:>5.1f}%  {bar}")


    print("  " + "-"*51)

    print(f"  {'TOTAL':<12} {total:>8,}")

    print("="*55)


    # Highlight issues

    errors   = counts.get("ERROR", 0) + counts.get("CRITICAL", 0) + counts.get("FATAL", 0)

    warnings = counts.get("WARNING", 0) + counts.get("WARN", 0)


    if errors > 0:

        print(f"\n  *** {errors:,} ERROR/CRITICAL/FATAL entries found!")

    if warnings > 0:

        print(f"  *** {warnings:,} WARNING entries found.")

    if errors == 0 and warnings == 0:

        print("\n  All clear — no errors or warnings detected.")



# ============================================================

# DISPLAY: HOURLY ACTIVITY

# ============================================================


def display_hourly_activity(result):

    counts = result["hour_counts"]

    if not counts:

        print("\n  No timestamp data found for hourly analysis.")

        return


    max_val = max(counts.values())


    print("\n" + "="*55)

    print("  HOURLY ACTIVITY")

    print("="*55)


    for hour in sorted(counts.keys()):

        count = counts[hour]

        bar   = draw_bar(count, max_val, width=30)

        print(f"  {hour}  {count:>6,}  {bar}")


    peak_hour  = max(counts, key=counts.get)

    quiet_hour = min(counts, key=counts.get)

    print("="*55)

    print(f"  Peak hour  : {peak_hour}  ({counts[peak_hour]:,} entries)")

    print(f"  Quiet hour : {quiet_hour}  ({counts[quiet_hour]:,} entries)")



# ============================================================

# DISPLAY: TOP IPs / PATHS / STATUS CODES

# ============================================================


def display_web_stats(result, top_n=10):

    ip_counts     = result.get("ip_counts", {})

    status_counts = result.get("status_counts", {})

    path_counts   = result.get("path_counts", {})


    if not ip_counts and not status_counts and not path_counts:

        print("\n  No web log data (IPs / status codes / paths) found.")

        return


    print("\n" + "="*55)

    print("  WEB LOG STATISTICS")

    print("="*55)


    if ip_counts:

        top_ips = sorted(ip_counts.items(), key=lambda x: x[1], reverse=True)[:top_n]

        max_val = top_ips[0][1]

        print(f"\n  TOP {top_n} IP ADDRESSES:")

        print(f"  {'IP':<18} {'REQUESTS':>10}  BAR")

        print("  " + "-"*45)

        for ip, count in top_ips:

            bar = draw_bar(count, max_val, width=15)

            print(f"  {ip:<18} {count:>10,}  {bar}")


    if status_counts:

        print(f"\n  HTTP STATUS CODES:")

        print(f"  {'CODE':<8} {'COUNT':>10}  MEANING")

        print("  " + "-"*40)

        meanings = {

            "200": "OK", "201": "Created", "301": "Moved Permanently",

            "302": "Found", "304": "Not Modified", "400": "Bad Request",

            "401": "Unauthorized", "403": "Forbidden", "404": "Not Found",

            "500": "Internal Server Error", "502": "Bad Gateway",

            "503": "Service Unavailable"

        }

        for code, count in sorted(status_counts.items()):

            meaning = meanings.get(code, "")

            print(f"  {code:<8} {count:>10,}  {meaning}")


    if path_counts:

        top_paths = sorted(path_counts.items(), key=lambda x: x[1], reverse=True)[:top_n]

        max_val   = top_paths[0][1]

        print(f"\n  TOP {top_n} REQUESTED PATHS:")

        print(f"  {'PATH':<35} {'HITS':>8}  BAR")

        print("  " + "-"*55)

        for path, count in top_paths:

            display_path = path[:33] + ".." if len(path) > 35 else path

            bar = draw_bar(count, max_val, width=10)

            print(f"  {display_path:<35} {count:>8,}  {bar}")


    print("="*55)



# ============================================================

# DISPLAY: RECENT ERRORS

# ============================================================


def display_errors(result, top_n=20):

    entries = result["entries"]

    errors  = [e for e in entries

               if e.get("level") in ("ERROR", "CRITICAL", "FATAL")]


    if not errors:

        print("\n  No ERROR / CRITICAL / FATAL entries found.")

        return


    print("\n" + "="*65)

    print(f"  ERRORS & CRITICAL ENTRIES  ({len(errors):,} total, showing last {top_n})")

    print("="*65)


    for e in errors[-top_n:]:

        dt  = e.get("datetime_parsed") or e.get("datetime", "")[:19]

        msg = e.get("message") or e.get("raw", "")

        msg = msg[:80]

        print(f"\n  [{e['level']}]  Line {e.get('line_no','?')}  {dt}")

        print(f"  {msg}")


    print("="*65)



# ============================================================

# DISPLAY: KEYWORD SEARCH RESULTS

# ============================================================


def display_keyword_hits(result, keyword):

    hits = result.get("keyword_hits", [])

    if not hits:

        print(f"\n  No matches found for: '{keyword}'")

        return


    print("\n" + "="*65)

    print(f"  KEYWORD SEARCH: '{keyword}'  ({len(hits):,} matches)")

    print("="*65)


    for hit in hits[:30]:

        print(f"\n  Line {hit['line']:>6}:  {hit['text'][:100]}")


    if len(hits) > 30:

        print(f"\n  ... and {len(hits) - 30} more matches.")

    print("="*65)



# ============================================================

# DISPLAY: COMMON PATTERNS / REPEATED MESSAGES

# ============================================================


def display_patterns(result, top_n=15):

    messages = result.get("message_list", [])

    if not messages:

        print("\n  No messages extracted.")

        return


    # Normalize messages (strip numbers/IPs for better grouping)

    def normalize(msg):

        msg = re.sub(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', '<IP>', msg)

        msg = re.sub(r'\b\d+\b', '<N>', msg)

        msg = re.sub(r'"[^"]*"', '"<VAL>"', msg)

        return msg[:80].strip()


    normalized = [normalize(m) for m in messages if m.strip()]

    counts     = Counter(normalized)

    top        = counts.most_common(top_n)


    max_val    = top[0][1] if top else 1


    print("\n" + "="*65)

    print(f"  MOST REPEATED LOG PATTERNS  (Top {top_n})")

    print("="*65)

    print(f"  {'COUNT':>7}  MESSAGE PATTERN")

    print("  " + "-"*60)


    for pattern, count in top:

        bar = draw_bar(count, max_val, width=12)

        print(f"  {count:>7,}  {bar}  {pattern}")


    print("="*65)



# ============================================================

# FULL REPORT

# ============================================================


def full_report(result):

    display_level_summary(result)

    display_hourly_activity(result)

    display_web_stats(result)

    display_errors(result)

    display_patterns(result)



# ============================================================

# SAVE REPORT TO JSON

# ============================================================


def save_report(result):

    report = {

        "filename":      result["filename"],

        "analyzed_at":   datetime.now().strftime("%d-%m-%Y %H:%M:%S"),

        "total_lines":   result["total_lines"],

        "parsed":        result["parsed"],

        "format":        result["format"],

        "level_counts":  result["level_counts"],

        "top_ips":       dict(sorted(result["ip_counts"].items(),

                                     key=lambda x: x[1], reverse=True)[:20]),

        "status_codes":  result["status_counts"],

        "top_paths":     dict(sorted(result["path_counts"].items(),

                                     key=lambda x: x[1], reverse=True)[:20]),

        "hourly":        result["hour_counts"],

    }


    with open(REPORT_FILE, "w") as f:

        json.dump(report, f, indent=2)

    print(f"\n  Report saved: {REPORT_FILE}")



# ============================================================

# BATCH ANALYZE (FOLDER)

# ============================================================


def batch_analyze(folder):

    folder = Path(folder)

    if not folder.is_dir():

        print("  Invalid folder.")

        return


    log_files = (list(folder.glob("*.log")) +

                 list(folder.glob("*.txt")) +

                 list(folder.glob("*.out")))


    if not log_files:

        print(f"  No .log / .txt / .out files in {folder}")

        return


    print(f"\n  Found {len(log_files)} log file(s).\n")

    summary = []


    for f in log_files:

        result = parse_log(f)

        if result:

            errors   = (result["level_counts"].get("ERROR", 0) +

                        result["level_counts"].get("CRITICAL", 0) +

                        result["level_counts"].get("FATAL", 0))

            warnings = (result["level_counts"].get("WARNING", 0) +

                        result["level_counts"].get("WARN", 0))

            summary.append({

                "file":     f.name,

                "lines":    result["total_lines"],

                "errors":   errors,

                "warnings": warnings,

            })


    print("\n" + "="*60)

    print(f"  BATCH SUMMARY  ({len(summary)} files)")

    print("="*60)

    print(f"  {'FILE':<30} {'LINES':>8} {'ERRORS':>8} {'WARNINGS':>10}")

    print("  " + "-"*56)


    for s in summary:

        print(f"  {s['file']:<30} {s['lines']:>8,} {s['errors']:>8,} "

              f"{s['warnings']:>10,}")


    total_errors   = sum(s["errors"]   for s in summary)

    total_warnings = sum(s["warnings"] for s in summary)

    print("  " + "-"*56)

    print(f"  {'TOTAL':<30} {'':>8} {total_errors:>8,} {total_warnings:>10,}")

    print("="*60)



# ============================================================

# MAIN MENU

# ============================================================


def print_menu():

    print("\n" + "-"*48)

    print("  LOG FILE ANALYZER")

    print("-"*48)

    print("  1. Analyze log file (full report)")

    print("  2. Log level summary")

    print("  3. Hourly activity chart")

    print("  4. Web log stats (IPs / status / paths)")

    print("  5. Show errors & critical entries")

    print("  6. Search keyword in log")

    print("  7. Repeated message patterns")

    print("  8. Batch analyze a folder")

    print("  9. Save report to JSON")

    print("  0. Exit")

    print("-"*48)



def main():

    print("\n" + "="*55)

    print("     LOG FILE ANALYZER")

    print("="*55)

    print("\n  Supports: Apache, Nginx, Python, Syslog, Generic logs")

    print("  Auto-detects format from first 20 lines.\n")


    last_result = None


    while True:

        print_menu()

        choice = input("  > ").strip()


        if choice in ["1","2","3","4","5","6","7","9"]:

            if not last_result or choice == "1":

                path = input("\n  Log file path: ").strip()

                if not path or not Path(path).exists():

                    print("  File not found.")

                    continue


                fmt_choice = input(

                    "  Format (auto/apache_access/apache_error/"

                    "nginx_access/python_log/syslog/generic): "

                ).strip() or "auto"


                kw = None

                if choice == "6":

                    kw = input("  Keyword to search: ").strip()


                lvl_filter = None

                max_lines  = 0


                ml = input("  Max lines to parse (0=all): ").strip()

                max_lines = int(ml) if ml.isdigit() else 0


                last_result = parse_log(path, fmt_choice, kw, lvl_filter, max_lines)


                if not last_result:

                    continue


        if choice == "1":

            full_report(last_result)


        elif choice == "2":

            display_level_summary(last_result)


        elif choice == "3":

            display_hourly_activity(last_result)


        elif choice == "4":

            display_web_stats(last_result)


        elif choice == "5":

            n = input("  Show last N errors (default 20): ").strip()

            n = int(n) if n.isdigit() else 20

            display_errors(last_result, n)


        elif choice == "6":

            kw = input("  Keyword to search: ").strip()

            if kw:

                # Re-parse with keyword

                last_result = parse_log(last_result["filepath"],

                                        last_result["format"],

                                        kw)

                if last_result:

                    display_keyword_hits(last_result, kw)


        elif choice == "7":

            n = input("  Top N patterns (default 15): ").strip()

            n = int(n) if n.isdigit() else 15

            display_patterns(last_result, n)


        elif choice == "8":

            folder = input("\n  Folder path: ").strip()

            batch_analyze(folder)


        elif choice == "9":

            if last_result:

                save_report(last_result)

            else:

                print("  No analysis data. Parse a file first.")


        elif choice == "0":

            print("\n  Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



# ============================================================

# RUN

# ============================================================


if __name__ == "__main__":

    main()

Loan EMI Calculator

import math

import json

import csv

import os

from datetime import datetime, date

from pathlib import Path

from dateutil.relativedelta import relativedelta


# Try importing dateutil

try:

    from dateutil.relativedelta import relativedelta

    DATEUTIL_OK = True

except ImportError:

    DATEUTIL_OK = False


# ============================================================

# CONFIGURATION

# ============================================================


HISTORY_FILE  = "emi_history.json"

CURRENCY_SYM  = "₹"


# ============================================================

# FORMAT HELPERS

# ============================================================


def fmt(amount):

    return f"{CURRENCY_SYM}{amount:,.2f}"



def fmt_plain(amount):

    return f"{amount:,.2f}"



def draw_bar(value, max_val, width=30, fill="█", empty="░"):

    if max_val == 0:

        return ""

    filled = int((min(value, max_val) / max_val) * width)

    return fill * filled + empty * (width - filled)



# ============================================================

# CORE EMI CALCULATION

# ============================================================


def calculate_emi(principal, annual_rate, tenure_months):

    """

    EMI = P × r × (1+r)^n / ((1+r)^n - 1)

    Where:

        P = Principal

        r = Monthly interest rate (annual_rate / 12 / 100)

        n = Tenure in months

    """

    if annual_rate == 0:

        return round(principal / tenure_months, 2)


    r   = annual_rate / 12 / 100

    n   = tenure_months

    emi = principal * r * math.pow(1 + r, n) / (math.pow(1 + r, n) - 1)

    return round(emi, 2)



def calculate_summary(principal, annual_rate, tenure_months):

    """Return full summary of loan calculations."""

    emi           = calculate_emi(principal, annual_rate, tenure_months)

    total_payment = round(emi * tenure_months, 2)

    total_interest = round(total_payment - principal, 2)

    interest_pct  = round((total_interest / principal) * 100, 2)


    return {

        "principal":       principal,

        "annual_rate":     annual_rate,

        "tenure_months":   tenure_months,

        "tenure_years":    round(tenure_months / 12, 2),

        "emi":             emi,

        "total_payment":   total_payment,

        "total_interest":  total_interest,

        "interest_pct":    interest_pct,

    }



# ============================================================

# AMORTIZATION TABLE

# ============================================================


def generate_amortization(principal, annual_rate, tenure_months,

                          start_date=None):

    """

    Generate month-by-month amortization schedule.

    Returns list of dicts with payment breakdown per month.

    """

    emi       = calculate_emi(principal, annual_rate, tenure_months)

    r         = annual_rate / 12 / 100

    balance   = principal

    schedule  = []


    if start_date is None:

        start_date = date.today()


    for month in range(1, tenure_months + 1):

        interest_component  = round(balance * r, 2)

        principal_component = round(emi - interest_component, 2)


        # Last payment adjustment for rounding

        if month == tenure_months:

            principal_component = round(balance, 2)

            emi_actual          = round(principal_component + interest_component, 2)

        else:

            emi_actual = emi


        balance = round(balance - principal_component, 2)

        if balance < 0:

            balance = 0.0


        # Calculate payment date

        if DATEUTIL_OK:

            pay_date = (start_date + relativedelta(months=month)).strftime("%b %Y")

        else:

            m = (start_date.month + month - 1) % 12 + 1

            y = start_date.year + (start_date.month + month - 1) // 12

            pay_date = f"{date(y, m, 1).strftime('%b %Y')}"


        schedule.append({

            "month":     month,

            "pay_date":  pay_date,

            "emi":       emi_actual,

            "principal": principal_component,

            "interest":  interest_component,

            "balance":   balance,

        })


    return schedule



# ============================================================

# DISPLAY SUMMARY

# ============================================================


def display_summary(summary, label="LOAN SUMMARY"):

    p   = summary["principal"]

    ti  = summary["total_interest"]

    tp  = summary["total_payment"]


    # Pie-style ASCII breakdown

    p_bar  = draw_bar(p,  tp, width=25)

    ti_bar = draw_bar(ti, tp, width=25)


    print("\n" + "="*58)

    print(f"  {label}")

    print("="*58)

    print(f"  Principal Amount   : {fmt(summary['principal'])}")

    print(f"  Annual Interest    : {summary['annual_rate']}%")

    print(f"  Tenure             : {summary['tenure_months']} months "

          f"({summary['tenure_years']} years)")

    print("-"*58)

    print(f"  Monthly EMI        : {fmt(summary['emi'])}")

    print(f"  Total Payment      : {fmt(summary['total_payment'])}")

    print(f"  Total Interest     : {fmt(summary['total_interest'])}  "

          f"({summary['interest_pct']}% of principal)")

    print("-"*58)

    print(f"  Principal  {fmt_plain(p):>14}  {draw_bar(p,  tp, 20)}")

    print(f"  Interest   {fmt_plain(ti):>14}  {draw_bar(ti, tp, 20)}")

    print(f"  Total      {fmt_plain(tp):>14}")

    print("="*58)



# ============================================================

# DISPLAY AMORTIZATION TABLE

# ============================================================


def display_amortization(schedule, show_all=False):

    total_months = len(schedule)


    print("\n" + "="*72)

    print(f"  AMORTIZATION SCHEDULE  ({total_months} months)")

    print("="*72)

    print(f"  {'Mo':<4} {'Date':<10} {'EMI':>12} {'Principal':>12} "

          f"{'Interest':>12} {'Balance':>12}")

    print("  " + "-"*68)


    # Decide which months to show

    if show_all or total_months <= 24:

        rows = schedule

    else:

        # Show first 6, middle hint, last 6

        rows = schedule[:6]

        mid  = total_months // 2

        rows_display = schedule[:6]

        print_middle_hint = True

    

    if show_all or total_months <= 24:

        for row in schedule:

            print(f"  {row['month']:<4} {row['pay_date']:<10} "

                  f"{fmt_plain(row['emi']):>12} "

                  f"{fmt_plain(row['principal']):>12} "

                  f"{fmt_plain(row['interest']):>12} "

                  f"{fmt_plain(row['balance']):>12}")

    else:

        # First 6 months

        for row in schedule[:6]:

            print(f"  {row['month']:<4} {row['pay_date']:<10} "

                  f"{fmt_plain(row['emi']):>12} "

                  f"{fmt_plain(row['principal']):>12} "

                  f"{fmt_plain(row['interest']):>12} "

                  f"{fmt_plain(row['balance']):>12}")


        print(f"\n  ... {total_months - 12} more months ...\n")


        # Last 6 months

        for row in schedule[-6:]:

            print(f"  {row['month']:<4} {row['pay_date']:<10} "

                  f"{fmt_plain(row['emi']):>12} "

                  f"{fmt_plain(row['principal']):>12} "

                  f"{fmt_plain(row['interest']):>12} "

                  f"{fmt_plain(row['balance']):>12}")


    # Totals

    total_emi  = sum(r["emi"]       for r in schedule)

    total_prin = sum(r["principal"] for r in schedule)

    total_int  = sum(r["interest"]  for r in schedule)


    print("  " + "-"*68)

    print(f"  {'TOTAL':<15} {fmt_plain(total_emi):>12} "

          f"{fmt_plain(total_prin):>12} {fmt_plain(total_int):>12}")

    print("="*72)



# ============================================================

# YEARLY SUMMARY

# ============================================================


def display_yearly_summary(schedule):

    yearly = {}

    for row in schedule:

        year = row["pay_date"].split()[-1]

        if year not in yearly:

            yearly[year] = {"principal": 0, "interest": 0, "emi": 0}

        yearly[year]["principal"] += row["principal"]

        yearly[year]["interest"]  += row["interest"]

        yearly[year]["emi"]       += row["emi"]


    print("\n" + "="*58)

    print("  YEARLY SUMMARY")

    print("="*58)

    print(f"  {'YEAR':<8} {'EMI PAID':>12} {'PRINCIPAL':>12} {'INTEREST':>12}")

    print("  " + "-"*50)


    for year, vals in sorted(yearly.items()):

        print(f"  {year:<8} {fmt_plain(vals['emi']):>12} "

              f"{fmt_plain(vals['principal']):>12} "

              f"{fmt_plain(vals['interest']):>12}")


    print("="*58)



# ============================================================

# EMI COMPARISON (Multiple Rates / Tenures)

# ============================================================


def compare_options(principal):

    print(f"\n  EMI COMPARISON for {fmt(principal)}")

    print("  " + "="*70)


    rates   = [6, 7, 8, 9, 10, 11, 12, 14]

    tenures = [12, 24, 36, 60, 84, 120, 180, 240]


    # Header

    print(f"  {'RATE':<6}", end="")

    for t in tenures:

        y = t // 12

        print(f"  {str(y)+'yr':>9}", end="")

    print()

    print("  " + "-"*70)


    for rate in rates:

        print(f"  {rate}%   ", end="")

        for t in tenures:

            emi = calculate_emi(principal, rate, t)

            print(f"  {fmt_plain(emi):>9}", end="")

        print()


    print("  " + "="*70)

    print("  (Values show monthly EMI in same currency)")



# ============================================================

# PREPAYMENT IMPACT

# ============================================================


def prepayment_impact(principal, annual_rate, tenure_months, prepay_amount, prepay_month):

    """

    Calculate how a lump-sum prepayment affects

    total interest and remaining tenure.

    """

    # Without prepayment

    orig = calculate_summary(principal, annual_rate, tenure_months)


    # With prepayment — recalculate from prepay month

    schedule_before = generate_amortization(principal, annual_rate, tenure_months)


    if prepay_month >= tenure_months:

        print("  Prepayment month exceeds tenure.")

        return


    balance_at_prepay = schedule_before[prepay_month - 1]["balance"]

    new_principal     = max(0, balance_at_prepay - prepay_amount)


    if new_principal <= 0:

        print(f"\n  Prepayment fully closes the loan at month {prepay_month}!")

        interest_paid_so_far = sum(

            r["interest"] for r in schedule_before[:prepay_month]

        )

        total_paid = (prepay_month * orig["emi"]) + prepay_amount

        saved      = orig["total_payment"] - total_paid

        print(f"  Total paid     : {fmt(total_paid)}")

        print(f"  Interest saved : {fmt(orig['total_interest'] - interest_paid_so_far)}")

        return


    remaining_months = tenure_months - prepay_month

    new_summary      = calculate_summary(new_principal, annual_rate, remaining_months)


    interest_before  = sum(r["interest"] for r in schedule_before[:prepay_month])

    total_interest_new = interest_before + new_summary["total_interest"]

    interest_saved     = round(orig["total_interest"] - total_interest_new, 2)

    months_saved       = tenure_months - (prepay_month + remaining_months)


    print("\n" + "="*55)

    print("  PREPAYMENT IMPACT ANALYSIS")

    print("="*55)

    print(f"  Prepayment Amount  : {fmt(prepay_amount)} at month {prepay_month}")

    print(f"  Balance Before     : {fmt(balance_at_prepay)}")

    print(f"  Balance After      : {fmt(new_principal)}")

    print("-"*55)

    print(f"  Original Interest  : {fmt(orig['total_interest'])}")

    print(f"  New Total Interest : {fmt(total_interest_new)}")

    print(f"  Interest Saved     : {fmt(interest_saved)}")

    print(f"  New Monthly EMI    : {fmt(new_summary['emi'])}")

    print(f"  Old EMI            : {fmt(orig['emi'])}")

    print(f"  Months Saved       : {months_saved}")

    print("="*55)



# ============================================================

# EXPORT TO CSV

# ============================================================


def export_to_csv(schedule, summary, filename=None):

    if not filename:

        filename = f"emi_schedule_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"


    with open(filename, "w", newline="", encoding="utf-8") as f:

        writer = csv.writer(f)


        # Summary header

        writer.writerow(["LOAN EMI SCHEDULE"])

        writer.writerow(["Principal", fmt_plain(summary["principal"])])

        writer.writerow(["Annual Rate (%)", summary["annual_rate"]])

        writer.writerow(["Tenure (months)", summary["tenure_months"]])

        writer.writerow(["Monthly EMI", fmt_plain(summary["emi"])])

        writer.writerow(["Total Payment", fmt_plain(summary["total_payment"])])

        writer.writerow(["Total Interest", fmt_plain(summary["total_interest"])])

        writer.writerow([])


        # Table header

        writer.writerow(["Month", "Date", "EMI",

                         "Principal", "Interest", "Balance"])

        for row in schedule:

            writer.writerow([

                row["month"], row["pay_date"],

                fmt_plain(row["emi"]),

                fmt_plain(row["principal"]),

                fmt_plain(row["interest"]),

                fmt_plain(row["balance"])

            ])


    print(f"\n  Exported: {filename}")

    return filename



# ============================================================

# SAVE / LOAD HISTORY

# ============================================================


def save_to_history(summary):

    history = []

    if Path(HISTORY_FILE).exists():

        try:

            with open(HISTORY_FILE, "r") as f:

                history = json.load(f)

        except:

            pass


    summary["saved_at"] = datetime.now().strftime("%d-%m-%Y %H:%M")

    history.append(summary)

    history = history[-20:]


    with open(HISTORY_FILE, "w") as f:

        json.dump(history, f, indent=2)

    print("  Saved to history.")



def show_history():

    if not Path(HISTORY_FILE).exists():

        print("\n  No history yet.")

        return


    try:

        with open(HISTORY_FILE, "r") as f:

            history = json.load(f)

    except:

        print("  Could not load history.")

        return


    print("\n" + "="*65)

    print(f"  CALCULATION HISTORY  ({len(history)} entries)")

    print("="*65)

    print(f"  {'DATE':<18} {'PRINCIPAL':>14} {'RATE':>6} "

          f"{'MONTHS':>7} {'EMI':>12}")

    print("  " + "-"*60)


    for h in reversed(history):

        print(f"  {h.get('saved_at',''):<18} "

              f"{fmt_plain(h['principal']):>14}  "

              f"{h['annual_rate']:>5}%  "

              f"{h['tenure_months']:>6}  "

              f"{fmt_plain(h['emi']):>12}")

    print("="*65)



# ============================================================

# GET LOAN INPUT

# ============================================================


def get_loan_input():

    print("\n  Enter Loan Details")

    print("  " + "-"*40)


    try:

        principal = float(input("  Loan Amount       : ").strip().replace(",", ""))

        if principal <= 0:

            print("  Amount must be positive.")

            return None, None, None


        rate = float(input("  Annual Interest % : ").strip())

        if rate < 0 or rate > 100:

            print("  Rate must be between 0 and 100.")

            return None, None, None


        tenure_input = input("  Tenure (months or Xy for years): ").strip()

        if tenure_input.lower().endswith("y"):

            tenure_months = int(float(tenure_input[:-1]) * 12)

        else:

            tenure_months = int(tenure_input)


        if tenure_months <= 0:

            print("  Tenure must be positive.")

            return None, None, None


        return principal, rate, tenure_months


    except ValueError:

        print("  Invalid input.")

        return None, None, None



# ============================================================

# MAIN MENU

# ============================================================


def print_menu():

    print("\n" + "-"*48)

    print("  LOAN EMI CALCULATOR")

    print("-"*48)

    print("  1. Calculate EMI & summary")

    print("  2. Full amortization table")

    print("  3. Yearly payment summary")

    print("  4. Compare rates & tenures")

    print("  5. Prepayment impact analysis")

    print("  6. Export schedule to CSV")

    print("  7. View calculation history")

    print("  0. Exit")

    print("-"*48)



def main():

    print("\n" + "="*55)

    print("     LOAN EMI CALCULATOR")

    print("="*55)

    print("\n  Calculate EMI for Home, Car, Personal loans.")

    print("  Generate full amortization & prepayment analysis.\n")


    last_summary  = None

    last_schedule = None


    while True:

        print_menu()

        choice = input("  > ").strip()


        if choice in ["1", "2", "3", "5", "6"]:

            if choice in ["2", "3", "5", "6"] and last_summary:

                reuse = input("\n  Use last loan details? (y/n): ").strip().lower()

                if reuse == "n":

                    last_summary = last_schedule = None


            if not last_summary:

                p, r, t = get_loan_input()

                if p is None:

                    continue

                last_summary  = calculate_summary(p, r, t)

                last_schedule = generate_amortization(p, r, t)


        if choice == "1":

            display_summary(last_summary)

            save_to_history(last_summary.copy())


        elif choice == "2":

            display_summary(last_summary)

            show_all = input("\n  Show ALL months? (y/n): ").strip().lower()

            display_amortization(last_schedule, show_all == "y")


        elif choice == "3":

            display_yearly_summary(last_schedule)


        elif choice == "4":

            try:

                p = float(input("\n  Loan amount for comparison: ")

                          .strip().replace(",", ""))

                compare_options(p)

            except ValueError:

                print("  Invalid amount.")


        elif choice == "5":

            if not last_summary:

                p, r, t = get_loan_input()

                if p is None:

                    continue

                last_summary  = calculate_summary(p, r, t)

                last_schedule = generate_amortization(p, r, t)


            display_summary(last_summary)

            try:

                prepay_amt   = float(input("\n  Prepayment amount : ").strip().replace(",", ""))

                prepay_month = int(input("  At which month    : ").strip())

                prepayment_impact(

                    last_summary["principal"],

                    last_summary["annual_rate"],

                    last_summary["tenure_months"],

                    prepay_amt, prepay_month

                )

            except ValueError:

                print("  Invalid input.")


        elif choice == "6":

            if last_summary and last_schedule:

                fname = input("  Filename (Enter for auto): ").strip() or None

                export_to_csv(last_schedule, last_summary, fname)

            else:

                print("  No data to export. Calculate EMI first.")


        elif choice == "7":

            show_history()


        elif choice == "0":

            print("\n  Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



# ============================================================

# RUN

# ============================================================


if __name__ == "__main__":

    main()

Student Grade Calculator

import json

import os

import statistics

from datetime import datetime

from pathlib import Path


# ============================================================

# CONFIGURATION

# ============================================================


DATA_FILE    = "students_data.json"

PASSING_MARK = 40       # minimum marks to pass a subject

MAX_MARKS    = 100      # maximum marks per subject


# ============================================================

# GRADING SYSTEM

# ============================================================


def get_grade(marks, max_marks=100):

    """Return letter grade, grade point, and remarks."""

    pct = (marks / max_marks) * 100


    if pct >= 90:

        return "A+", 10.0, "Outstanding"

    elif pct >= 80:

        return "A",  9.0,  "Excellent"

    elif pct >= 70:

        return "B+", 8.0,  "Very Good"

    elif pct >= 60:

        return "B",  7.0,  "Good"

    elif pct >= 50:

        return "C+", 6.0,  "Above Average"

    elif pct >= 40:

        return "C",  5.0,  "Average"

    elif pct >= 35:

        return "D",  4.0,  "Pass (Marginal)"

    else:

        return "F",  0.0,  "Fail"



def get_class(percentage):

    """Return division/class based on overall percentage."""

    if percentage >= 75:

        return "First Class with Distinction"

    elif percentage >= 60:

        return "First Class"

    elif percentage >= 50:

        return "Second Class"

    elif percentage >= 40:

        return "Pass Class"

    else:

        return "Fail"



def calculate_gpa(subjects):

    """Calculate weighted GPA from a list of subject dicts."""

    total_points  = 0

    total_credits = 0

    for sub in subjects:

        _, gp, _ = get_grade(sub["marks_obtained"], sub["max_marks"])

        credits   = sub.get("credits", 1)

        total_points  += gp * credits

        total_credits += credits

    if total_credits == 0:

        return 0.0

    return round(total_points / total_credits, 2)



# ============================================================

# LOAD & SAVE DATA

# ============================================================


def load_data():

    if Path(DATA_FILE).exists():

        try:

            with open(DATA_FILE, "r") as f:

                return json.load(f)

        except:

            pass

    return {"students": []}



def save_data(data):

    with open(DATA_FILE, "w") as f:

        json.dump(data, f, indent=2)



# ============================================================

# ADD STUDENT

# ============================================================


def add_student(data):

    print("\n" + "="*50)

    print("  ADD NEW STUDENT")

    print("="*50)


    name     = input("  Student Name  : ").strip()

    roll_no  = input("  Roll Number   : ").strip()

    cls      = input("  Class/Semester: ").strip()

    section  = input("  Section       : ").strip() or "A"


    if not name or not roll_no:

        print("  Name and Roll Number are required.")

        return


    # Check duplicate roll number

    if any(s["roll_no"] == roll_no for s in data["students"]):

        print(f"  Roll number {roll_no} already exists.")

        return


    # Input subjects

    subjects = []

    print(f"\n  Enter subjects and marks (type 'done' to finish)")

    print(f"  Format: Subject Name | Marks Obtained | Max Marks | Credits")

    print(f"  Example: Mathematics | 85 | 100 | 4\n")


    while True:

        entry = input("  > ").strip()

        if entry.lower() == "done":

            if not subjects:

                print("  At least one subject is required.")

                continue

            break


        parts = [p.strip() for p in entry.split("|")]

        if len(parts) < 2:

            print("  Format: Subject | Marks | Max Marks (opt) | Credits (opt)")

            continue


        sub_name = parts[0]

        try:

            marks    = float(parts[1])

            max_m    = float(parts[2]) if len(parts) > 2 and parts[2] else MAX_MARKS

            credits  = float(parts[3]) if len(parts) > 3 and parts[3] else 1


            if marks < 0 or marks > max_m:

                print(f"  Invalid marks. Must be between 0 and {max_m}")

                continue


            grade, gp, remarks = get_grade(marks, max_m)

            subjects.append({

                "name":          sub_name,

                "marks_obtained": marks,

                "max_marks":     max_m,

                "credits":       credits,

                "grade":         grade,

                "grade_point":   gp,

                "remarks":       remarks,

                "passed":        marks >= (max_m * PASSING_MARK / MAX_MARKS)

            })

            print(f"  Added: {sub_name} — {marks}/{max_m}  Grade: {grade}  ({remarks})")


        except ValueError:

            print("  Invalid marks value.")

            continue


    # Calculate overall stats

    total_obtained = sum(s["marks_obtained"] for s in subjects)

    total_max      = sum(s["max_marks"]      for s in subjects)

    percentage     = round((total_obtained / total_max) * 100, 2) if total_max else 0

    gpa            = calculate_gpa(subjects)

    passed_all     = all(s["passed"] for s in subjects)

    division       = get_class(percentage)

    failed_subs    = [s["name"] for s in subjects if not s["passed"]]


    student = {

        "id":             len(data["students"]) + 1,

        "name":           name,

        "roll_no":        roll_no,

        "class":          cls,

        "section":        section,

        "subjects":       subjects,

        "total_obtained": total_obtained,

        "total_max":      total_max,

        "percentage":     percentage,

        "gpa":            gpa,

        "division":       division,

        "passed":         passed_all,

        "failed_subjects": failed_subs,

        "added_at":       datetime.now().strftime("%d-%m-%Y %H:%M:%S")

    }


    data["students"].append(student)

    save_data(data)


    print(f"\n  Student saved! Quick summary:")

    print(f"  Percentage : {percentage}%")

    print(f"  GPA        : {gpa}")

    print(f"  Result     : {'PASS' if passed_all else 'FAIL'}")

    print(f"  Division   : {division}")

    if failed_subs:

        print(f"  Failed in  : {', '.join(failed_subs)}")



# ============================================================

# PRINT REPORT CARD

# ============================================================


def print_report_card(student):

    w = 58

    print("\n" + "=" * w)

    print(f"  {'REPORT CARD':^{w-4}}")

    print("=" * w)

    print(f"  Name        : {student['name']}")

    print(f"  Roll No     : {student['roll_no']}")

    print(f"  Class       : {student['class']}  Section: {student['section']}")

    print(f"  Date        : {student['added_at'][:10]}")

    print("-" * w)

    print(f"  {'SUBJECT':<20} {'MAX':>5} {'OBT':>5} {'%':>6}  "

          f"{'GR':>3}  {'GP':>4}  REMARKS")

    print("-" * w)


    for sub in student["subjects"]:

        pct   = round((sub["marks_obtained"] / sub["max_marks"]) * 100, 1)

        pass_marker = " " if sub["passed"] else "*"

        print(f"  {sub['name']:<20} {sub['max_marks']:>5.0f} "

              f"{sub['marks_obtained']:>5.0f} {pct:>5.1f}%  "

              f"{sub['grade']:>3}  {sub['grade_point']:>4.1f}  "

              f"{sub['remarks']}{pass_marker}")


    print("-" * w)

    overall_pct = round((student["total_obtained"] / student["total_max"]) * 100, 1)

    print(f"  {'TOTAL':<20} {student['total_max']:>5.0f} "

          f"{student['total_obtained']:>5.0f} {overall_pct:>5.1f}%")

    print("=" * w)

    print(f"  GPA         : {student['gpa']} / 10.0")

    print(f"  Percentage  : {student['percentage']}%")

    print(f"  Division    : {student['division']}")

    result_str = "PASS" if student["passed"] else "FAIL"

    print(f"  Result      : {result_str}")

    if student.get("failed_subjects"):

        print(f"  Failed in   : {', '.join(student['failed_subjects'])}")

    print("=" * w)


    # Grade scale legend

    print(f"\n  Grade Scale: A+(90+)=10 | A(80+)=9 | B+(70+)=8 | "

          f"B(60+)=7 | C+(50+)=6 | C(40+)=5 | D(35+)=4 | F=0")

    print(f"  * = Failed subject (below passing marks)\n")



# ============================================================

# SEARCH STUDENT

# ============================================================


def search_student(data):

    query = input("\n  Search by name or roll number: ").strip().lower()

    results = [

        s for s in data["students"]

        if query in s["name"].lower() or query in s["roll_no"].lower()

    ]


    if not results:

        print(f"  No students found for: '{query}'")

        return None


    if len(results) == 1:

        return results[0]


    print(f"\n  Found {len(results)} students:")

    for i, s in enumerate(results, 1):

        print(f"  [{i}] {s['roll_no']}  {s['name']}  "

              f"Class: {s['class']}  {s['percentage']}%")

    try:

        idx = int(input("\n  Select number: ").strip()) - 1

        if 0 <= idx < len(results):

            return results[idx]

    except ValueError:

        pass

    return None



# ============================================================

# CLASS REPORT (ALL STUDENTS)

# ============================================================


def class_report(data):

    students = data["students"]

    if not students:

        print("\n  No students added yet.")

        return


    print("\n" + "="*70)

    print(f"  CLASS REPORT  ({len(students)} students)")

    print("="*70)

    print(f"  {'ROLL':<10} {'NAME':<22} {'%':>6}  "

          f"{'GPA':>5}  {'DIV':<30}  RESULT")

    print("  " + "-"*65)


    sorted_students = sorted(students,

                             key=lambda x: x["percentage"],

                             reverse=True)


    for rank, s in enumerate(sorted_students, 1):

        result = "PASS" if s["passed"] else "FAIL"

        print(f"  {s['roll_no']:<10} {s['name']:<22} "

              f"{s['percentage']:>5.1f}%  {s['gpa']:>5.2f}  "

              f"{s['division']:<30}  {result}")


    # Class statistics

    percentages = [s["percentage"] for s in students]

    gpas        = [s["gpa"]        for s in students]

    passed      = sum(1 for s in students if s["passed"])

    failed      = len(students) - passed


    print("  " + "-"*65)

    print(f"\n  CLASS STATISTICS")

    print(f"  Total Students : {len(students)}")

    print(f"  Passed         : {passed}  "

          f"({passed/len(students)*100:.1f}%)")

    print(f"  Failed         : {failed}  "

          f"({failed/len(students)*100:.1f}%)")

    print(f"  Highest %      : {max(percentages):.2f}%")

    print(f"  Lowest  %      : {min(percentages):.2f}%")

    print(f"  Average %      : {statistics.mean(percentages):.2f}%")

    print(f"  Median  %      : {statistics.median(percentages):.2f}%")

    print(f"  Average GPA    : {statistics.mean(gpas):.2f}")


    # Topper

    topper = max(students, key=lambda x: x["percentage"])

    print(f"\n  Class Topper   : {topper['name']} "

          f"({topper['roll_no']})  — {topper['percentage']}%")


    # Grade distribution

    grade_dist = {}

    for s in students:

        g, _, _ = get_grade(s["percentage"])

        grade_dist[g] = grade_dist.get(g, 0) + 1


    print(f"\n  Grade Distribution:")

    for grade in ["A+", "A", "B+", "B", "C+", "C", "D", "F"]:

        count = grade_dist.get(grade, 0)

        bar   = "█" * count

        print(f"  {grade:>3}: {count:>3}  {bar}")


    print("="*70)



# ============================================================

# SUBJECT-WISE ANALYSIS

# ============================================================


def subject_analysis(data):

    students = data["students"]

    if not students:

        print("\n  No data yet.")

        return


    # Collect all subjects

    sub_map = {}

    for s in students:

        for sub in s["subjects"]:

            name = sub["name"]

            if name not in sub_map:

                sub_map[name] = []

            sub_map[name].append(sub["marks_obtained"])


    if not sub_map:

        return


    print("\n" + "="*60)

    print("  SUBJECT-WISE ANALYSIS")

    print("="*60)

    print(f"  {'SUBJECT':<22} {'AVG':>6}  {'HIGH':>6}  "

          f"{'LOW':>6}  {'PASS%':>6}  STUDENTS")

    print("  " + "-"*56)


    for sub_name, marks_list in sorted(sub_map.items()):

        avg      = statistics.mean(marks_list)

        high     = max(marks_list)

        low      = min(marks_list)

        pass_pct = sum(1 for m in marks_list

                       if m >= PASSING_MARK) / len(marks_list) * 100

        print(f"  {sub_name:<22} {avg:>6.1f}  {high:>6.1f}  "

              f"{low:>6.1f}  {pass_pct:>5.1f}%  {len(marks_list)}")


    print("="*60)



# ============================================================

# DELETE STUDENT

# ============================================================


def delete_student(data):

    student = search_student(data)

    if not student:

        return

    confirm = input(f"\n  Delete {student['name']} ({student['roll_no']})? "

                    f"(yes/no): ").strip().lower()

    if confirm == "yes":

        data["students"] = [s for s in data["students"]

                            if s["roll_no"] != student["roll_no"]]

        save_data(data)

        print("  Student deleted.")

    else:

        print("  Cancelled.")



# ============================================================

# MAIN MENU

# ============================================================


def print_menu(data):

    count = len(data["students"])

    print("\n" + "-"*48)

    print(f"  STUDENT GRADE CALCULATOR  [{count} students]")

    print("-"*48)

    print("  1. Add new student with marks")

    print("  2. View student report card")

    print("  3. Search student")

    print("  4. Class report (all students)")

    print("  5. Subject-wise analysis")

    print("  6. Delete a student")

    print("  0. Exit")

    print("-"*48)



def main():

    print("\n" + "="*55)

    print("     STUDENT GRADE CALCULATOR")

    print("="*55)

    print(f"\n  Passing Marks : {PASSING_MARK}/{MAX_MARKS}")

    print(f"  Grading       : A+(90+) to F(<35)")

    print(f"  GPA Scale     : 0 - 10")


    data = load_data()

    if data["students"]:

        print(f"\n  Loaded {len(data['students'])} student(s) from {DATA_FILE}")


    while True:

        print_menu(data)

        choice = input("  > ").strip()


        if choice == "1":

            add_student(data)


        elif choice == "2":

            if not data["students"]:

                print("\n  No students added yet.")

                continue

            student = search_student(data)

            if student:

                print_report_card(student)


        elif choice == "3":

            if not data["students"]:

                print("\n  No students added yet.")

                continue

            student = search_student(data)

            if student:

                print(f"\n  Found: {student['name']}  |  "

                      f"Roll: {student['roll_no']}  |  "

                      f"Class: {student['class']}")

                print(f"  Percentage: {student['percentage']}%  |  "

                      f"GPA: {student['gpa']}  |  "

                      f"Result: {'PASS' if student['passed'] else 'FAIL'}")

                show = input("\n  Show full report card? (y/n): ").strip().lower()

                if show == "y":

                    print_report_card(student)


        elif choice == "4":

            class_report(data)


        elif choice == "5":

            subject_analysis(data)


        elif choice == "6":

            delete_student(data)


        elif choice == "0":

            print("\n  Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



# ============================================================

# RUN

# ============================================================


if __name__ == "__main__":

    main()