CSV ↔ JSON Converter

import csv

import json

import os

from datetime import datetime

from pathlib import Path

from collections import OrderedDict


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

# 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 detect_type(value):

    """Try to auto-detect and cast string values to int/float/bool."""

    if value is None or value == "":

        return None

    v = str(value).strip()

    if v.lower() in ("true", "yes"):

        return True

    if v.lower() in ("false", "no"):

        return False

    try:

        return int(v)

    except ValueError:

        pass

    try:

        return float(v)

    except ValueError:

        pass

    return v



def flatten_dict(d, parent_key="", sep="."):

    """Flatten nested JSON dict to single level for CSV."""

    items = []

    for k, v in d.items():

        new_key = f"{parent_key}{sep}{k}" if parent_key else k

        if isinstance(v, dict):

            items.extend(flatten_dict(v, new_key, sep).items())

        elif isinstance(v, list):

            items.append((new_key, json.dumps(v)))

        else:

            items.append((new_key, v))

    return dict(items)



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

# CSV → JSON

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


def csv_to_json(input_path, output_path=None,

                delimiter=",", auto_cast=True,

                indent=2, encoding="utf-8"):

    """

    Convert a CSV file to JSON.

    Returns the output path and record count.

    """

    input_path = Path(input_path)


    if not input_path.exists():

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

        return None, 0


    if not output_path:

        output_path = input_path.with_suffix(".json")

    output_path = Path(output_path)


    records = []

    errors  = []


    try:

        with open(input_path, "r", encoding=encoding, newline="") as f:

            reader = csv.DictReader(f, delimiter=delimiter)

            headers = reader.fieldnames


            if not headers:

                print("  CSV file has no headers.")

                return None, 0


            print(f"\n  Reading CSV: {input_path.name}")

            print(f"  Columns ({len(headers)}): {', '.join(headers)}")


            for i, row in enumerate(reader, 1):

                if auto_cast:

                    record = {k: detect_type(v) for k, v in row.items()}

                else:

                    record = dict(row)

                records.append(record)


                if i % 1000 == 0:

                    print(f"  Processed {i} rows...", end="\r")


    except UnicodeDecodeError:

        print("  Encoding error. Trying latin-1...")

        return csv_to_json(input_path, output_path, delimiter,

                           auto_cast, indent, "latin-1")

    except Exception as e:

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

        return None, 0


    # Write JSON

    try:

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

            json.dump(records, f, indent=indent, ensure_ascii=False)


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

        print(f"\n  Converted {len(records)} records")

        print(f"  Output   : {output_path}  ({size})")

        return output_path, len(records)


    except Exception as e:

        print(f"  Error writing JSON: {e}")

        return None, 0



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

# JSON → CSV

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


def json_to_csv(input_path, output_path=None,

                delimiter=",", flatten=True,

                encoding="utf-8"):

    """

    Convert a JSON file (array of objects) to CSV.

    Returns the output path and record count.

    """

    input_path = Path(input_path)


    if not input_path.exists():

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

        return None, 0


    if not output_path:

        output_path = input_path.with_suffix(".csv")

    output_path = Path(output_path)


    try:

        with open(input_path, "r", encoding=encoding) as f:

            data = json.load(f)

    except json.JSONDecodeError as e:

        print(f"  Invalid JSON: {e}")

        return None, 0

    except Exception as e:

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

        return None, 0


    # Handle both list and single object

    if isinstance(data, dict):

        # Try common wrapper keys

        for key in ["data", "records", "results", "items", "rows"]:

            if key in data and isinstance(data[key], list):

                data = data[key]

                print(f"  Extracted records from key: '{key}'")

                break

        else:

            data = [data]


    if not isinstance(data, list) or not data:

        print("  JSON does not contain a list of records.")

        return None, 0


    # Flatten nested dicts if requested

    if flatten:

        data = [flatten_dict(r) if isinstance(r, dict) else {"value": r}

                for r in data]


    # Collect all unique keys across all records

    all_keys = list(OrderedDict.fromkeys(

        k for record in data if isinstance(record, dict)

        for k in record.keys()

    ))


    print(f"\n  Reading JSON: {input_path.name}")

    print(f"  Records   : {len(data)}")

    print(f"  Columns ({len(all_keys)}): {', '.join(str(k) for k in all_keys[:10])}"

          f"{'...' if len(all_keys) > 10 else ''}")


    try:

        with open(output_path, "w", encoding=encoding,

                  newline="") as f:

            writer = csv.DictWriter(f, fieldnames=all_keys,

                                    delimiter=delimiter,

                                    extrasaction="ignore")

            writer.writeheader()


            for i, record in enumerate(data, 1):

                if isinstance(record, dict):

                    # Convert lists/dicts in values to strings

                    row = {k: (json.dumps(v) if isinstance(v, (dict, list)) else v)

                           for k, v in record.items()}

                    writer.writerow(row)

                if i % 1000 == 0:

                    print(f"  Written {i} rows...", end="\r")


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

        print(f"\n  Converted {len(data)} records")

        print(f"  Output   : {output_path}  ({size})")

        return output_path, len(data)


    except Exception as e:

        print(f"  Error writing CSV: {e}")

        return None, 0



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

# PREVIEW FILE

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


def preview_csv(filepath, rows=5):

    """Print first N rows of a CSV file."""

    try:

        with open(filepath, "r", encoding="utf-8", newline="") as f:

            reader = csv.reader(f)

            headers = next(reader, None)

            if not headers:

                print("  Empty CSV.")

                return


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

            print("  " + "-"*60)


            # Header

            col_w = min(18, max(8, 60 // len(headers)))

            header_line = "  " + "  ".join(str(h)[:col_w].ljust(col_w) for h in headers)

            print(header_line)

            print("  " + "-"*60)


            # Rows

            for i, row in enumerate(reader):

                if i >= rows:

                    break

                line = "  " + "  ".join(str(v)[:col_w].ljust(col_w) for v in row)

                print(line)


            print("  " + "-"*60)


    except Exception as e:

        print(f"  Preview error: {e}")



def preview_json(filepath, rows=3):

    """Print first N records of a JSON file."""

    try:

        with open(filepath, "r", encoding="utf-8") as f:

            data = json.load(f)


        if isinstance(data, list):

            preview = data[:rows]

        elif isinstance(data, dict):

            preview = [data]

        else:

            preview = [{"value": data}]


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

        print("  " + "-"*60)

        print(json.dumps(preview, indent=4, ensure_ascii=False)[:2000])

        if len(str(data)) > 2000:

            print("  ... (truncated)")

        print("  " + "-"*60)


    except Exception as e:

        print(f"  Preview error: {e}")



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

# FILE INFO

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


def file_info(filepath):

    p = Path(filepath)

    if not p.exists():

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

        return


    size  = format_size(p.stat().st_size)

    mtime = datetime.fromtimestamp(p.stat().st_mtime).strftime("%d-%m-%Y %H:%M")

    ext   = p.suffix.lower()


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

    print(f"  Size     : {size}")

    print(f"  Modified : {mtime}")

    print(f"  Type     : {ext.upper()[1:]} file")


    if ext == ".csv":

        try:

            with open(filepath, "r", encoding="utf-8", newline="") as f:

                reader = csv.reader(f)

                headers = next(reader, [])

                row_count = sum(1 for _ in reader)

            print(f"  Rows     : {row_count:,}")

            print(f"  Columns  : {len(headers)}")

            print(f"  Headers  : {', '.join(headers[:8])}"

                  f"{'...' if len(headers) > 8 else ''}")

        except:

            pass


    elif ext == ".json":

        try:

            with open(filepath, "r", encoding="utf-8") as f:

                data = json.load(f)

            if isinstance(data, list):

                print(f"  Records  : {len(data):,}")

                if data and isinstance(data[0], dict):

                    keys = list(data[0].keys())

                    print(f"  Keys     : {', '.join(str(k) for k in keys[:8])}"

                          f"{'...' if len(keys) > 8 else ''}")

            elif isinstance(data, dict):

                print(f"  Keys     : {', '.join(list(data.keys())[:8])}")

        except:

            pass



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

# BATCH CONVERT

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


def batch_convert(folder, direction):

    folder = Path(folder)

    if not folder.is_dir():

        print("  Invalid folder.")

        return


    if direction == "csv_to_json":

        files = list(folder.glob("*.csv"))

        ext_from, ext_to = "CSV", "JSON"

    else:

        files = list(folder.glob("*.json"))

        ext_from, ext_to = "JSON", "CSV"


    if not files:

        print(f"  No {ext_from} files found in {folder}")

        return


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

    success = 0


    for f in files:

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

        if direction == "csv_to_json":

            out, count = csv_to_json(f)

        else:

            out, count = json_to_csv(f)

        if out:

            success += 1


    print(f"\n  Batch done: {success}/{len(files)} converted successfully.")



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

# MAIN MENU

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


def print_menu():

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

    print("  CSV  <->  JSON  CONVERTER")

    print("-"*48)

    print("  1. CSV  →  JSON")

    print("  2. JSON →  CSV")

    print("  3. Preview a CSV file")

    print("  4. Preview a JSON file")

    print("  5. File info & stats")

    print("  6. Batch convert CSV → JSON (folder)")

    print("  7. Batch convert JSON → CSV (folder)")

    print("  0. Exit")

    print("-"*48)



def main():

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

    print("     CSV  <->  JSON  CONVERTER")

    print("="*55)

    print("\n  Bidirectional converter with auto type detection,")

    print("  nested JSON flattening, and batch processing.\n")


    while True:

        print_menu()

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


        # ── CSV → JSON ──────────────────────────────────────

        if choice == "1":

            print("\n  CSV to JSON Conversion")

            inp = input("  Input CSV file path: ").strip()

            if not inp:

                continue


            out = input("  Output JSON path (Enter = same name): ").strip() or None


            delim = input("  Delimiter (, or ; or Tab): ").strip()

            if delim.lower() == "tab":

                delim = "\t"

            elif delim not in [",", ";", "|", "\t"]:

                delim = ","


            cast = input("  Auto-detect types? (y/n, default y): ").strip().lower()

            cast = cast != "n"


            indent = input("  JSON indent spaces (default 2): ").strip()

            indent = int(indent) if indent.isdigit() else 2


            out_path, count = csv_to_json(inp, out, delim, cast, indent)


            if out_path and count:

                preview = input("\n  Preview output? (y/n): ").strip().lower()

                if preview == "y":

                    preview_json(out_path, rows=3)


        # ── JSON → CSV ──────────────────────────────────────

        elif choice == "2":

            print("\n  JSON to CSV Conversion")

            inp = input("  Input JSON file path: ").strip()

            if not inp:

                continue


            out = input("  Output CSV path (Enter = same name): ").strip() or None


            delim = input("  Delimiter (, or ; or Tab): ").strip()

            if delim.lower() == "tab":

                delim = "\t"

            elif delim not in [",", ";", "|", "\t"]:

                delim = ","


            flat = input("  Flatten nested objects? (y/n, default y): ").strip().lower()

            flat = flat != "n"


            out_path, count = json_to_csv(inp, out, delim, flat)


            if out_path and count:

                preview = input("\n  Preview output? (y/n): ").strip().lower()

                if preview == "y":

                    preview_csv(out_path, rows=5)


        # ── Preview CSV ─────────────────────────────────────

        elif choice == "3":

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

            rows = input("  Rows to preview (default 5): ").strip()

            rows = int(rows) if rows.isdigit() else 5

            preview_csv(path, rows)


        # ── Preview JSON ────────────────────────────────────

        elif choice == "4":

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

            rows = input("  Records to preview (default 3): ").strip()

            rows = int(rows) if rows.isdigit() else 3

            preview_json(path, rows)


        # ── File Info ───────────────────────────────────────

        elif choice == "5":

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

            file_info(path)


        # ── Batch CSV → JSON ────────────────────────────────

        elif choice == "6":

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

            batch_convert(folder, "csv_to_json")


        # ── Batch JSON → CSV ────────────────────────────────

        elif choice == "7":

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

            batch_convert(folder, "json_to_csv")


        elif choice == "0":

            print("\n  Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



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

# RUN

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


if __name__ == "__main__":

    main()

Personal Budget Tracker

import json

import os

from datetime import datetime

from pathlib import Path

from collections import defaultdict


try:

    import matplotlib.pyplot as plt

    import matplotlib.patches as mpatches

    MATPLOTLIB_OK = True

except ImportError:

    MATPLOTLIB_OK = False


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

# CONFIGURATION

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


DATA_FILE    = "budget_data.json"

CURRENCY     = "INR"

CURRENCY_SYM = "₹"


# Default categories

EXPENSE_CATEGORIES = [

    "Food", "Transport", "Rent", "Utilities",

    "Entertainment", "Healthcare", "Shopping",

    "Education", "EMI / Loan", "Other"

]

INCOME_CATEGORIES = [

    "Salary", "Freelance", "Business",

    "Investment", "Gift", "Other"

]


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

# 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 {"transactions": [], "budgets": {}}



def save_data(data):

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

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



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

# FORMAT AMOUNT

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


def fmt(amount):

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



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

# ADD TRANSACTION

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


def add_transaction(data, t_type):

    """Add an income or expense entry."""

    print(f"\n  Add {t_type.upper()}")

    print("  " + "-"*40)


    categories = INCOME_CATEGORIES if t_type == "income" else EXPENSE_CATEGORIES


    print("  Categories:")

    for i, cat in enumerate(categories, 1):

        print(f"  {i:>2}. {cat}")


    try:

        cat_idx = int(input("\n  Choose category (number): ").strip()) - 1

        if cat_idx < 0 or cat_idx >= len(categories):

            print("  Invalid category.")

            return

        category = categories[cat_idx]

    except ValueError:

        print("  Invalid input.")

        return


    try:

        amount = float(input("  Amount: ").strip())

        if amount <= 0:

            print("  Amount must be positive.")

            return

    except ValueError:

        print("  Invalid amount.")

        return


    note = input("  Note (optional): ").strip()


    date_input = input("  Date (DD-MM-YYYY) or Enter for today: ").strip()

    if date_input:

        try:

            date = datetime.strptime(date_input, "%d-%m-%Y").strftime("%d-%m-%Y")

        except ValueError:

            print("  Invalid date format. Using today.")

            date = datetime.now().strftime("%d-%m-%Y")

    else:

        date = datetime.now().strftime("%d-%m-%Y")


    entry = {

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

        "type":     t_type,

        "category": category,

        "amount":   amount,

        "note":     note,

        "date":     date,

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

    }


    data["transactions"].append(entry)

    save_data(data)


    icon = "+" if t_type == "income" else "-"

    print(f"\n  {icon} Saved: {category} — {fmt(amount)} on {date}")

    if note:

        print(f"  Note: {note}")



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

# VIEW TRANSACTIONS

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


def view_transactions(data, month=None, t_type=None):

    txns = data["transactions"]


    # Filter by month (MM-YYYY)

    if month:

        txns = [t for t in txns if t["date"][3:] == month]


    # Filter by type

    if t_type:

        txns = [t for t in txns if t["type"] == t_type]


    if not txns:

        print("\n  No transactions found.")

        return


    # Sort by date descending

    txns_sorted = sorted(txns,

                         key=lambda x: datetime.strptime(x["date"], "%d-%m-%Y"),

                         reverse=True)


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

    label = f"{t_type.upper() if t_type else 'ALL'}"

    label += f"  [{month}]" if month else ""

    print(f"  TRANSACTIONS — {label}  ({len(txns)} entries)")

    print("="*68)

    print(f"  {'#':<5} {'DATE':<13} {'TYPE':<10} {'CATEGORY':<16} "

          f"{'AMOUNT':>12}  NOTE")

    print("  " + "-"*63)


    for t in txns_sorted[:50]:    # show last 50

        icon = "+" if t["type"] == "income" else "-"

        note = t.get("note", "")[:20]

        print(f"  {t['id']:<5} {t['date']:<13} {t['type']:<10} "

              f"{t['category']:<16} {icon}{fmt(t['amount']):>12}  {note}")


    if len(txns_sorted) > 50:

        print(f"  ... and {len(txns_sorted) - 50} more. Filter by month to see all.")


    total_in  = sum(t["amount"] for t in txns if t["type"] == "income")

    total_out = sum(t["amount"] for t in txns if t["type"] == "expense")

    balance   = total_in - total_out


    print("  " + "-"*63)

    print(f"  Total Income  : {fmt(total_in)}")

    print(f"  Total Expense : {fmt(total_out)}")

    b_sign = "+" if balance >= 0 else ""

    print(f"  Balance       : {b_sign}{fmt(balance)}")

    print("="*68)



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

# MONTHLY SUMMARY

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


def monthly_summary(data):

    txns = data["transactions"]

    if not txns:

        print("\n  No data yet.")

        return


    # Group by month

    monthly = defaultdict(lambda: {"income": 0, "expense": 0,

                                    "by_cat": defaultdict(float)})

    for t in txns:

        month = t["date"][3:]     # MM-YYYY

        monthly[month][t["type"]] += t["amount"]

        if t["type"] == "expense":

            monthly[month]["by_cat"][t["category"]] += t["amount"]


    # Sort months chronologically

    def month_key(m):

        try:

            return datetime.strptime(m, "%m-%Y")

        except:

            return datetime.min


    sorted_months = sorted(monthly.keys(), key=month_key)


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

    print("  MONTHLY SUMMARY")

    print("="*60)

    print(f"  {'MONTH':<12} {'INCOME':>12} {'EXPENSE':>12} "

          f"{'BALANCE':>12}  STATUS")

    print("  " + "-"*56)


    for month in sorted_months:

        m     = monthly[month]

        bal   = m["income"] - m["expense"]

        sign  = "+" if bal >= 0 else ""

        status = "SURPLUS" if bal >= 0 else "DEFICIT"

        bar    = _mini_bar(m["income"], m["expense"])

        print(f"  {month:<12} {fmt(m['income']):>12} "

              f"{fmt(m['expense']):>12} "

              f"{sign}{fmt(bal):>12}  {status}  {bar}")


    # Overall totals

    total_in  = sum(m["income"]  for m in monthly.values())

    total_out = sum(m["expense"] for m in monthly.values())

    total_bal = total_in - total_out


    print("  " + "-"*56)

    print(f"  {'TOTAL':<12} {fmt(total_in):>12} "

          f"{fmt(total_out):>12} "

          f"{('+' if total_bal>=0 else '')}{fmt(total_bal):>12}")

    print("="*60)



def _mini_bar(income, expense):

    if max(income, expense) == 0:

        return ""

    scale = 12

    i_bar = int((income  / max(income, expense)) * scale)

    e_bar = int((expense / max(income, expense)) * scale)

    return f"I:{'█'*i_bar}  E:{'█'*e_bar}"



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

# CATEGORY BREAKDOWN

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


def category_breakdown(data, month=None):

    txns = data["transactions"]

    if month:

        txns = [t for t in txns if t["date"][3:] == month]


    expenses = [t for t in txns if t["type"] == "expense"]

    incomes  = [t for t in txns if t["type"] == "income"]


    if not expenses and not incomes:

        print("\n  No data for this period.")

        return


    def show_breakdown(items, label):

        cat_totals = defaultdict(float)

        for t in items:

            cat_totals[t["category"]] += t["amount"]

        total = sum(cat_totals.values())

        if not cat_totals:

            return

        sorted_cats = sorted(cat_totals.items(), key=lambda x: x[1], reverse=True)

        max_val = sorted_cats[0][1]


        print(f"\n  {label}  (Total: {fmt(total)})")

        print("  " + "-"*55)

        print(f"  {'CATEGORY':<18} {'AMOUNT':>12}  {'%':>5}  BAR")

        print("  " + "-"*55)

        for cat, amt in sorted_cats:

            pct = (amt / total * 100) if total else 0

            bar = "█" * int((amt / max_val) * 20)

            print(f"  {cat:<18} {fmt(amt):>12}  {pct:>4.1f}%  {bar}")


    title = f"CATEGORY BREAKDOWN"

    if month:

        title += f"  [{month}]"

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

    print(f"  {title}")

    print("="*58)


    show_breakdown(expenses, "EXPENSES")

    show_breakdown(incomes,  "INCOME SOURCES")



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

# BUDGET GOALS

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


def set_budget(data):

    print("\n  SET MONTHLY BUDGET LIMITS")

    print("  Categories:")

    for i, cat in enumerate(EXPENSE_CATEGORIES, 1):

        current = data["budgets"].get(cat, 0)

        current_str = f"  (current: {fmt(current)})" if current else ""

        print(f"  {i:>2}. {cat}{current_str}")


    try:

        idx = int(input("\n  Choose category: ").strip()) - 1

        if idx < 0 or idx >= len(EXPENSE_CATEGORIES):

            print("  Invalid.")

            return

        cat = EXPENSE_CATEGORIES[idx]

        amt = float(input(f"  Budget limit for {cat}: ").strip())

        data["budgets"][cat] = amt

        save_data(data)

        print(f"  Budget set: {cat} = {fmt(amt)}/month")

    except ValueError:

        print("  Invalid input.")



def check_budgets(data):

    budgets = data.get("budgets", {})

    if not budgets:

        print("\n  No budget limits set. Use option to set budgets.")

        return


    # Current month

    this_month = datetime.now().strftime("%m-%Y")

    txns = [t for t in data["transactions"]

            if t["type"] == "expense" and t["date"][3:] == this_month]


    cat_spent = defaultdict(float)

    for t in txns:

        cat_spent[t["category"]] += t["amount"]


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

    print(f"  BUDGET STATUS — {this_month}")

    print("="*58)

    print(f"  {'CATEGORY':<18} {'BUDGET':>12} {'SPENT':>12} "

          f"{'LEFT':>12}  STATUS")

    print("  " + "-"*54)


    for cat, budget in sorted(budgets.items()):

        spent = cat_spent.get(cat, 0)

        left  = budget - spent

        pct   = (spent / budget * 100) if budget else 0

        bar   = "█" * min(int(pct / 5), 20)


        if pct >= 100:

            status = "OVER BUDGET!"

        elif pct >= 80:

            status = "WARNING"

        elif pct >= 50:

            status = "ON TRACK"

        else:

            status = "OK"


        print(f"  {cat:<18} {fmt(budget):>12} {fmt(spent):>12} "

              f"{fmt(left):>12}  {status}")

        print(f"  {'':18} {pct:>5.1f}% used  {bar}")


    print("="*58)



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

# MATPLOTLIB CHARTS (Optional)

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


def plot_monthly_chart(data):

    if not MATPLOTLIB_OK:

        print("\n  matplotlib not installed.")

        print("  Install: pip install matplotlib")

        return


    txns = data["transactions"]

    if not txns:

        print("\n  No data to plot.")

        return


    monthly = defaultdict(lambda: {"income": 0, "expense": 0})

    for t in txns:

        month = t["date"][3:]

        monthly[month][t["type"]] += t["amount"]


    def month_key(m):

        try:

            return datetime.strptime(m, "%m-%Y")

        except:

            return datetime.min


    months  = sorted(monthly.keys(), key=month_key)[-12:]

    incomes  = [monthly[m]["income"]  for m in months]

    expenses = [monthly[m]["expense"] for m in months]

    balances = [i - e for i, e in zip(incomes, expenses)]


    x = range(len(months))

    width = 0.35


    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))

    fig.suptitle("Personal Budget Overview", fontsize=14, fontweight="bold")


    # Bar chart — income vs expense

    bars1 = ax1.bar([i - width/2 for i in x], incomes,

                    width, label="Income", color="#2ecc71", alpha=0.85)

    bars2 = ax1.bar([i + width/2 for i in x], expenses,

                    width, label="Expense", color="#e74c3c", alpha=0.85)

    ax1.set_title("Monthly Income vs Expense")

    ax1.set_xticks(list(x))

    ax1.set_xticklabels(months, rotation=45, ha="right")

    ax1.set_ylabel(f"Amount ({CURRENCY})")

    ax1.legend()

    ax1.yaxis.set_major_formatter(

        plt.FuncFormatter(lambda val, _: f"{CURRENCY_SYM}{val:,.0f}")

    )

    ax1.grid(axis="y", alpha=0.3)


    # Line chart — balance

    colors = ["#2ecc71" if b >= 0 else "#e74c3c" for b in balances]

    ax2.bar(list(x), balances, color=colors, alpha=0.8)

    ax2.axhline(0, color="black", linewidth=0.8, linestyle="--")

    ax2.set_title("Monthly Balance (Surplus / Deficit)")

    ax2.set_xticks(list(x))

    ax2.set_xticklabels(months, rotation=45, ha="right")

    ax2.set_ylabel(f"Balance ({CURRENCY})")

    ax2.yaxis.set_major_formatter(

        plt.FuncFormatter(lambda val, _: f"{CURRENCY_SYM}{val:,.0f}")

    )

    ax2.grid(axis="y", alpha=0.3)


    plt.tight_layout()

    plt.savefig("budget_chart.png", dpi=120, bbox_inches="tight")

    print("\n  Chart saved: budget_chart.png")

    plt.show()



def plot_expense_pie(data, month=None):

    if not MATPLOTLIB_OK:

        print("\n  matplotlib not installed.")

        print("  Install: pip install matplotlib")

        return


    txns = data["transactions"]

    if month:

        txns = [t for t in txns if t["date"][3:] == month]

    expenses = [t for t in txns if t["type"] == "expense"]


    if not expenses:

        print("\n  No expense data to plot.")

        return


    cat_totals = defaultdict(float)

    for t in expenses:

        cat_totals[t["category"]] += t["amount"]


    labels = list(cat_totals.keys())

    values = list(cat_totals.values())

    colors = plt.cm.Set3.colors[:len(labels)]


    fig, ax = plt.subplots(figsize=(9, 7))

    wedges, texts, autotexts = ax.pie(

        values, labels=labels, autopct="%1.1f%%",

        colors=colors, startangle=140,

        wedgeprops={"edgecolor": "white", "linewidth": 1.5}

    )

    for at in autotexts:

        at.set_fontsize(9)


    title = f"Expense Breakdown"

    if month:

        title += f" — {month}"

    ax.set_title(title, fontsize=13, fontweight="bold")


    total = sum(values)

    ax.text(0, -1.3, f"Total: {fmt(total)}",

            ha="center", fontsize=11, color="#333")


    plt.tight_layout()

    plt.savefig("expense_pie.png", dpi=120, bbox_inches="tight")

    print("\n  Pie chart saved: expense_pie.png")

    plt.show()



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

# DELETE TRANSACTION

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


def delete_transaction(data):

    try:

        tid = int(input("\n  Enter transaction ID to delete: ").strip())

    except ValueError:

        print("  Invalid ID.")

        return


    for i, t in enumerate(data["transactions"]):

        if t["id"] == tid:

            confirm = input(f"  Delete: {t['type']} {fmt(t['amount'])} "

                            f"— {t['category']} on {t['date']}? (y/n): ").strip().lower()

            if confirm == "y":

                data["transactions"].pop(i)

                save_data(data)

                print("  Deleted.")

            else:

                print("  Cancelled.")

            return


    print(f"  Transaction ID {tid} not found.")



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

# QUICK STATS

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


def quick_stats(data):

    txns = data["transactions"]

    if not txns:

        print("\n  No data yet.")

        return


    this_month = datetime.now().strftime("%m-%Y")

    this_txns  = [t for t in txns if t["date"][3:] == this_month]


    total_in   = sum(t["amount"] for t in txns if t["type"] == "income")

    total_out  = sum(t["amount"] for t in txns if t["type"] == "expense")

    balance    = total_in - total_out


    month_in   = sum(t["amount"] for t in this_txns if t["type"] == "income")

    month_out  = sum(t["amount"] for t in this_txns if t["type"] == "expense")

    month_bal  = month_in - month_out


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

    print("  QUICK STATS")

    print("="*50)

    print(f"  This Month ({this_month}):")

    print(f"    Income   : {fmt(month_in)}")

    print(f"    Expense  : {fmt(month_out)}")

    b = "+" if month_bal >= 0 else ""

    print(f"    Balance  : {b}{fmt(month_bal)}")

    print(f"\n  All Time:")

    print(f"    Income   : {fmt(total_in)}")

    print(f"    Expense  : {fmt(total_out)}")

    b = "+" if balance >= 0 else ""

    print(f"    Balance  : {b}{fmt(balance)}")

    print(f"    Entries  : {len(txns)}")

    print("="*50)



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

# MAIN MENU

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


def print_menu():

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

    print("  PERSONAL BUDGET TRACKER")

    print("-"*48)

    print("  1.  Add income")

    print("  2.  Add expense")

    print("  3.  View transactions")

    print("  4.  Monthly summary")

    print("  5.  Category breakdown")

    print("  6.  Budget limits & status")

    print("  7.  Set budget limit for a category")

    print("  8.  Plot monthly income vs expense chart")

    print("  9.  Plot expense pie chart")

    print("  10. Quick stats")

    print("  11. Delete a transaction")

    print("  0.  Exit")

    print("-"*48)



def main():

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

    print("     PERSONAL BUDGET TRACKER")

    print("="*55)


    data = load_data()

    count = len(data["transactions"])

    if count:

        print(f"\n  Loaded {count} transaction(s) from {DATA_FILE}")

    else:

        print(f"\n  No data yet. Start by adding income or expenses.")


    while True:

        print_menu()

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


        if choice == "1":

            add_transaction(data, "income")


        elif choice == "2":

            add_transaction(data, "expense")


        elif choice == "3":

            print("\n  Filter options:")

            print("  1. All transactions")

            print("  2. By month")

            print("  3. Income only")

            print("  4. Expenses only")

            sub = input("  Choice: ").strip()

            if sub == "1":

                view_transactions(data)

            elif sub == "2":

                month = input("  Month (MM-YYYY e.g. 04-2026): ").strip()

                view_transactions(data, month=month)

            elif sub == "3":

                view_transactions(data, t_type="income")

            elif sub == "4":

                view_transactions(data, t_type="expense")


        elif choice == "4":

            monthly_summary(data)


        elif choice == "5":

            month = input("\n  Month (MM-YYYY) or Enter for all time: ").strip()

            category_breakdown(data, month=month or None)


        elif choice == "6":

            check_budgets(data)


        elif choice == "7":

            set_budget(data)


        elif choice == "8":

            plot_monthly_chart(data)


        elif choice == "9":

            month = input("\n  Month (MM-YYYY) or Enter for all time: ").strip()

            plot_expense_pie(data, month=month or None)


        elif choice == "10":

            quick_stats(data)


        elif choice == "11":

            view_transactions(data)

            delete_transaction(data)


        elif choice == "0":

            print("\n  Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



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

# RUN

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


if __name__ == "__main__":

    main()

Currency Converter with Live Rates

 import json

import os

import time

from datetime import datetime

from pathlib import Path

from collections import defaultdict


try:

    import requests

    REQUESTS_OK = True

except ImportError:

    REQUESTS_OK = False


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

# CONFIGURATION

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


# Free API — no key needed (up to 1500 requests/month)

BASE_URL      = "https://api.exchangerate-api.com/v4/latest/"

CACHE_FILE    = "exchange_rates_cache.json"

HISTORY_FILE  = "conversion_history.json"

CACHE_EXPIRY  = 3600   # seconds (1 hour)


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

# ALL SUPPORTED CURRENCIES WITH NAMES

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


CURRENCY_NAMES = {

    "USD": "US Dollar",

    "EUR": "Euro",

    "GBP": "British Pound",

    "INR": "Indian Rupee",

    "JPY": "Japanese Yen",

    "AUD": "Australian Dollar",

    "CAD": "Canadian Dollar",

    "CHF": "Swiss Franc",

    "CNY": "Chinese Yuan",

    "SGD": "Singapore Dollar",

    "AED": "UAE Dirham",

    "SAR": "Saudi Riyal",

    "MYR": "Malaysian Ringgit",

    "HKD": "Hong Kong Dollar",

    "NZD": "New Zealand Dollar",

    "SEK": "Swedish Krona",

    "NOK": "Norwegian Krone",

    "DKK": "Danish Krone",

    "ZAR": "South African Rand",

    "MXN": "Mexican Peso",

    "BRL": "Brazilian Real",

    "RUB": "Russian Ruble",

    "KRW": "South Korean Won",

    "TRY": "Turkish Lira",

    "IDR": "Indonesian Rupiah",

    "THB": "Thai Baht",

    "PKR": "Pakistani Rupee",

    "BDT": "Bangladeshi Taka",

    "LKR": "Sri Lankan Rupee",

    "NPR": "Nepalese Rupee",

    "PHP": "Philippine Peso",

    "VND": "Vietnamese Dong",

    "EGP": "Egyptian Pound",

    "NGN": "Nigerian Naira",

    "KWD": "Kuwaiti Dinar",

    "BHD": "Bahraini Dinar",

    "OMR": "Omani Rial",

    "QAR": "Qatari Riyal",

    "ILS": "Israeli Shekel",

    "PLN": "Polish Zloty",

    "CZK": "Czech Koruna",

    "HUF": "Hungarian Forint",

    "RON": "Romanian Leu",

    "HRK": "Croatian Kuna",

    "BGN": "Bulgarian Lev",

    "ISK": "Icelandic Krona",

    "ARS": "Argentine Peso",

    "CLP": "Chilean Peso",

    "COP": "Colombian Peso",

    "PEN": "Peruvian Sol",

    "TWD": "Taiwan Dollar",

}


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

# CACHE MANAGEMENT

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


def load_cache():

    if Path(CACHE_FILE).exists():

        try:

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

                return json.load(f)

        except:

            pass

    return {}



def save_cache(data):

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

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



def is_cache_valid(cache, base):

    if base not in cache:

        return False

    ts = cache[base].get("timestamp", 0)

    return (time.time() - ts) < CACHE_EXPIRY



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

# FETCH RATES FROM API

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


def fetch_rates(base="USD"):

    """

    Fetch live exchange rates for a base currency.

    Returns dict of rates or None on failure.

    """

    if not REQUESTS_OK:

        print("  requests library not installed.")

        return None


    cache = load_cache()


    # Return cached rates if still fresh

    if is_cache_valid(cache, base):

        age = int(time.time() - cache[base]["timestamp"])

        print(f"  Using cached rates (age: {age}s)")

        return cache[base]["rates"]


    # Fetch from API

    print(f"  Fetching live rates for {base}...")

    try:

        resp = requests.get(BASE_URL + base, timeout=10)

        resp.raise_for_status()

        data  = resp.json()

        rates = data.get("rates", {})


        # Save to cache

        cache[base] = {

            "rates":     rates,

            "timestamp": time.time(),

            "date":      data.get("date", "")

        }

        save_cache(cache)


        print(f"  Live rates fetched successfully! ({len(rates)} currencies)")

        return rates


    except requests.exceptions.ConnectionError:

        print("  No internet connection. Checking for cached rates...")

        if base in cache:

            print("  Using expired cache as fallback.")

            return cache[base]["rates"]

        return None


    except Exception as e:

        print(f"  API error: {e}")

        if base in cache:

            return cache[base]["rates"]

        return None



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

# CONVERT CURRENCY

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


def convert(amount, from_cur, to_cur, rates):

    """

    Convert amount from from_cur to to_cur using rates

    (rates are relative to their base currency).

    """

    from_cur = from_cur.upper()

    to_cur   = to_cur.upper()


    if from_cur not in rates:

        print(f"  Currency not found: {from_cur}")

        return None

    if to_cur not in rates:

        print(f"  Currency not found: {to_cur}")

        return None


    # Convert: amount / from_rate * to_rate

    result = (amount / rates[from_cur]) * rates[to_cur]

    return round(result, 4)



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

# MULTI-CURRENCY CONVERSION (1 to Many)

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


def convert_to_many(amount, from_cur, targets, rates):

    """Convert one amount to multiple currencies at once."""

    results = {}

    for to_cur in targets:

        result = convert(amount, from_cur, to_cur, rates)

        if result is not None:

            results[to_cur] = result

    return results



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

# HISTORY MANAGEMENT

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


def load_history():

    if Path(HISTORY_FILE).exists():

        try:

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

                return json.load(f)

        except:

            pass

    return []



def save_to_history(entry):

    history = load_history()

    history.append(entry)

    history = history[-100:]   # keep last 100

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

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



def display_history():

    history = load_history()

    if not history:

        print("\n  No conversion history yet.")

        return


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

    print(f"  CONVERSION HISTORY  (last {len(history)} entries)")

    print("="*58)

    print(f"  {'TIME':<20} {'FROM':<18} {'TO':<18} RATE")

    print("  " + "-"*54)


    for h in reversed(history[-20:]):

        from_str = f"{h['amount']} {h['from']}"

        to_str   = f"{h['result']} {h['to']}"

        print(f"  {h['time']:<20} {from_str:<18} {to_str:<18} "

              f"1 {h['from']} = {h['rate']} {h['to']}")

    print("="*58)



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

# DISPLAY RATE TABLE

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


def display_rate_table(base, rates, filter_list=None):

    currencies = filter_list if filter_list else list(CURRENCY_NAMES.keys())

    available  = [(c, rates[c]) for c in currencies if c in rates and c != base]


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

    print(f"  EXCHANGE RATES  (Base: {base} — {CURRENCY_NAMES.get(base, '')})")

    print("="*55)

    print(f"  {'CODE':<6} {'CURRENCY NAME':<25} {'RATE':>12}")

    print("  " + "-"*44)


    for code, rate in sorted(available, key=lambda x: x[0]):

        name = CURRENCY_NAMES.get(code, code)

        print(f"  {code:<6} {name:<25} {rate:>12.4f}")


    print("="*55)

    print(f"  Rates relative to 1 {base}")



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

# CURRENCY SEARCH

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


def search_currency(query):

    query = query.lower()

    matches = [

        (code, name) for code, name in CURRENCY_NAMES.items()

        if query in code.lower() or query in name.lower()

    ]

    if not matches:

        print(f"\n  No currencies found for: '{query}'")

    else:

        print(f"\n  Found {len(matches)} match(es):")

        for code, name in matches:

            print(f"  {code:<6} {name}")



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

# POPULAR PAIRS QUICK VIEW

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


def popular_pairs(rates):

    pairs = [

        ("USD", "EUR"), ("USD", "GBP"), ("USD", "INR"),

        ("USD", "JPY"), ("USD", "AED"), ("EUR", "GBP"),

        ("EUR", "INR"), ("GBP", "INR"), ("USD", "CNY"),

        ("USD", "AUD"), ("USD", "CAD"), ("USD", "SAR"),

    ]


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

    print("  POPULAR CURRENCY PAIRS")

    print("="*45)

    print(f"  {'PAIR':<10} {'RATE':>12}  {'REVERSE':>12}")

    print("  " + "-"*40)


    for fr, to in pairs:

        if fr in rates and to in rates:

            rate     = round((1 / rates[fr]) * rates[to], 4)

            rev_rate = round((1 / rates[to]) * rates[fr], 4)

            print(f"  {fr}/{to:<7} {rate:>12.4f}  {rev_rate:>12.4f}")


    print("="*45)

    print("  All rates vs USD base")



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

# MAIN MENU

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


def print_menu():

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

    print("  CURRENCY CONVERTER  (Live Rates)")

    print("-"*48)

    print("  1. Convert currency")

    print("  2. Convert to multiple currencies")

    print("  3. View exchange rate table")

    print("  4. Popular currency pairs")

    print("  5. Search currency by name/code")

    print("  6. View conversion history")

    print("  7. Refresh rates (clear cache)")

    print("  0. Exit")

    print("-"*48)



def main():

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

    print("     CURRENCY CONVERTER  —  Live Rates")

    print("="*55)


    if not REQUESTS_OK:

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

        print("  Install it:  pip install requests")

        return


    # Load initial USD rates

    print("\n  Loading exchange rates...")

    rates = fetch_rates("USD")


    if not rates:

        print("\n  Could not load rates. Check your internet connection.")

        return


    # Show cache info

    cache = load_cache()

    if "USD" in cache:

        ts = datetime.fromtimestamp(cache["USD"]["timestamp"])

        print(f"  Rates date: {cache['USD'].get('date','')}  "

              f"(cached at {ts.strftime('%H:%M:%S')})")


    while True:

        print_menu()

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


        # --------------------------------------------------

        if choice == "1":

            print("\n  Single Currency Conversion")

            print(f"  (Type currency code e.g. USD, INR, EUR)")


            from_cur = input("  From currency: ").strip().upper()

            to_cur   = input("  To currency  : ").strip().upper()


            if from_cur not in rates:

                print(f"  Unknown currency: {from_cur}. Use option 5 to search.")

                continue

            if to_cur not in rates:

                print(f"  Unknown currency: {to_cur}. Use option 5 to search.")

                continue


            try:

                amount = float(input("  Amount       : ").strip())

            except ValueError:

                print("  Invalid amount.")

                continue


            result = convert(amount, from_cur, to_cur, rates)

            if result is not None:

                rate_val = round((1 / rates[from_cur]) * rates[to_cur], 6)

                from_name = CURRENCY_NAMES.get(from_cur, from_cur)

                to_name   = CURRENCY_NAMES.get(to_cur, to_cur)


                print(f"\n  {'='*45}")

                print(f"  {amount:,.2f} {from_cur} ({from_name})")

                print(f"  =  {result:,.4f} {to_cur} ({to_name})")

                print(f"  Rate: 1 {from_cur} = {rate_val} {to_cur}")

                print(f"  {'='*45}")


                save_to_history({

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

                    "amount": amount,

                    "from":   from_cur,

                    "to":     to_cur,

                    "result": result,

                    "rate":   rate_val

                })


        # --------------------------------------------------

        elif choice == "2":

            print("\n  Convert to Multiple Currencies")

            from_cur = input("  From currency: ").strip().upper()


            if from_cur not in rates:

                print(f"  Unknown currency: {from_cur}")

                continue


            try:

                amount = float(input("  Amount: ").strip())

            except ValueError:

                print("  Invalid amount.")

                continue


            print("  Target currencies (space-separated, e.g. EUR GBP INR JPY):")

            targets_input = input("  > ").strip().upper().split()


            if not targets_input:

                # Default popular targets

                targets_input = ["USD", "EUR", "GBP", "INR", "JPY",

                                 "AUD", "CAD", "AED", "SGD", "CHF"]


            results = convert_to_many(amount, from_cur, targets_input, rates)


            print(f"\n  {amount:,.2f} {from_cur} = ")

            print("  " + "-"*40)

            for cur, val in results.items():

                name = CURRENCY_NAMES.get(cur, cur)

                print(f"  {val:>14,.4f}  {cur}  ({name})")


        # --------------------------------------------------

        elif choice == "3":

            base = input("\n  Base currency (default USD): ").strip().upper() or "USD"

            if base != "USD":

                rates = fetch_rates(base)

                if not rates:

                    print("  Could not fetch rates for that base.")

                    continue

            display_rate_table(base, rates)


        # --------------------------------------------------

        elif choice == "4":

            popular_pairs(rates)


        # --------------------------------------------------

        elif choice == "5":

            query = input("\n  Search (name or code): ").strip()

            search_currency(query)


        # --------------------------------------------------

        elif choice == "6":

            display_history()


        # --------------------------------------------------

        elif choice == "7":

            if Path(CACHE_FILE).exists():

                os.remove(CACHE_FILE)

                print("\n  Cache cleared.")

            print("  Fetching fresh rates...")

            rates = fetch_rates("USD")

            if rates:

                print("  Rates updated successfully!")


        # --------------------------------------------------

        elif choice == "0":

            print("\n  Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



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

# RUN

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


if __name__ == "__main__":

    main()

System Health Monitor

import os

import json

import time

import threading

from datetime import datetime, timedelta

from pathlib import Path

from collections import deque


try:

    import psutil

    PSUTIL_OK = True

except ImportError:

    PSUTIL_OK = False


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

# CONFIGURATION

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


LOG_FILE        = "system_health_log.json"

REFRESH_INTERVAL = 2      # seconds between live updates

HISTORY_LEN      = 60     # data points to keep in history

ALERT_CPU        = 85.0   # % CPU alert threshold

ALERT_RAM        = 85.0   # % RAM alert threshold

ALERT_DISK       = 90.0   # % Disk alert threshold

ALERT_TEMP       = 80.0   # °C CPU temp alert


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

# HELPERS

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


def format_size(size_bytes):

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

        if size_bytes < 1024:

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

        size_bytes /= 1024

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



def format_uptime(seconds):

    td = timedelta(seconds=int(seconds))

    days    = td.days

    hours   = td.seconds // 3600

    minutes = (td.seconds % 3600) // 60

    secs    = td.seconds % 60

    if days:

        return f"{days}d {hours}h {minutes}m"

    elif hours:

        return f"{hours}h {minutes}m {secs}s"

    else:

        return f"{minutes}m {secs}s"



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

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

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



def color_bar(value, width=30):

    """Return bar with level indicator based on value."""

    bar = draw_bar(value, width=width)

    if value >= 90:

        level = "CRITICAL"

    elif value >= 75:

        level = "HIGH    "

    elif value >= 50:

        level = "MEDIUM  "

    else:

        level = "OK      "

    return bar, level



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

# DATA COLLECTORS

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


def get_cpu_info():

    data = {

        "percent":      psutil.cpu_percent(interval=0.5),

        "per_core":     psutil.cpu_percent(interval=0.5, percpu=True),

        "count_logical": psutil.cpu_count(logical=True),

        "count_physical": psutil.cpu_count(logical=False),

        "freq":         None,

        "temp":         None,

    }

    try:

        freq = psutil.cpu_freq()

        if freq:

            data["freq"] = {

                "current": round(freq.current, 1),

                "min":     round(freq.min, 1),

                "max":     round(freq.max, 1)

            }

    except:

        pass


    try:

        temps = psutil.sensors_temperatures()

        if temps:

            for key in ["coretemp", "cpu_thermal", "k10temp", "acpitz"]:

                if key in temps and temps[key]:

                    data["temp"] = round(temps[key][0].current, 1)

                    break

    except:

        pass


    return data



def get_ram_info():

    vm  = psutil.virtual_memory()

    swp = psutil.swap_memory()

    return {

        "total":        vm.total,

        "available":    vm.available,

        "used":         vm.used,

        "percent":      vm.percent,

        "swap_total":   swp.total,

        "swap_used":    swp.used,

        "swap_percent": swp.percent,

    }



def get_disk_info():

    disks = []

    for part in psutil.disk_partitions(all=False):

        try:

            usage = psutil.disk_usage(part.mountpoint)

            disks.append({

                "device":     part.device,

                "mountpoint": part.mountpoint,

                "fstype":     part.fstype,

                "total":      usage.total,

                "used":       usage.used,

                "free":       usage.free,

                "percent":    usage.percent,

            })

        except (PermissionError, OSError):

            continue

    return disks



def get_network_info():

    net     = psutil.net_io_counters()

    addrs   = psutil.net_if_addrs()

    stats   = psutil.net_if_stats()


    active_ifaces = []

    for iface, stat in stats.items():

        if stat.isup and iface in addrs:

            for addr in addrs[iface]:

                if addr.family.name == "AF_INET":

                    active_ifaces.append({

                        "name":  iface,

                        "ip":    addr.address,

                        "speed": stat.speed

                    })

                    break


    return {

        "bytes_sent":   net.bytes_sent,

        "bytes_recv":   net.bytes_recv,

        "packets_sent": net.packets_sent,

        "packets_recv": net.packets_recv,

        "interfaces":   active_ifaces,

    }



def get_battery_info():

    try:

        bat = psutil.sensors_battery()

        if bat:

            return {

                "percent":    round(bat.percent, 1),

                "plugged":    bat.power_plugged,

                "secs_left":  bat.secsleft if bat.secsleft != psutil.POWER_TIME_UNLIMITED else -1,

            }

    except:

        pass

    return None



def get_top_processes(top_n=10):

    procs = []

    for p in psutil.process_iter(["pid", "name", "cpu_percent",

                                   "memory_percent", "status"]):

        try:

            procs.append(p.info)

        except (psutil.NoSuchProcess, psutil.AccessDenied):

            continue

    # Sort by CPU then memory

    return sorted(procs,

                  key=lambda x: (x.get("cpu_percent") or 0),

                  reverse=True)[:top_n]



def get_system_info():

    boot_time = psutil.boot_time()

    uptime    = time.time() - boot_time

    return {

        "platform":   os.name,

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

        "uptime_sec": uptime,

        "uptime_str": format_uptime(uptime),

        "hostname":   os.uname().nodename if hasattr(os, "uname") else os.environ.get("COMPUTERNAME", "N/A"),

        "user":       os.environ.get("USER") or os.environ.get("USERNAME", "N/A"),

    }



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

# DISPLAY SECTIONS

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


def clear_screen():

    os.system("cls" if os.name == "nt" else "clear")



def display_header():

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

    print("=" * 65)

    print(f"   SYSTEM HEALTH MONITOR          {now}")

    print("=" * 65)



def display_cpu(cpu):

    bar, level = color_bar(cpu["percent"])

    print(f"\n  CPU Usage   : {cpu['percent']:>5.1f}%  {bar}  {level}")

    if cpu.get("freq"):

        f = cpu["freq"]

        print(f"  CPU Freq    : {f['current']} MHz  "

              f"(min: {f['min']}  max: {f['max']})")

    print(f"  Cores       : {cpu['count_physical']} physical / "

          f"{cpu['count_logical']} logical")

    if cpu.get("temp"):

        temp_bar, _ = color_bar(cpu["temp"], width=20)

        print(f"  Temperature : {cpu['temp']}°C  {temp_bar}")


    # Per-core bars

    if cpu.get("per_core") and len(cpu["per_core"]) <= 16:

        print(f"  Per-Core    :", end="")

        for i, pct in enumerate(cpu["per_core"]):

            mini_bar = draw_bar(pct, width=8)

            print(f"  C{i}: {pct:4.1f}% {mini_bar}", end="")

            if (i + 1) % 2 == 0:

                print()

                print("              ", end="")

        print()



def display_ram(ram):

    bar, level = color_bar(ram["percent"])

    print(f"\n  RAM Usage   : {ram['percent']:>5.1f}%  {bar}  {level}")

    print(f"  Used / Total: {format_size(ram['used'])} / {format_size(ram['total'])}")

    print(f"  Available   : {format_size(ram['available'])}")


    if ram["swap_total"] > 0:

        sbar, slevel = color_bar(ram["swap_percent"])

        print(f"  Swap Usage  : {ram['swap_percent']:>5.1f}%  {sbar}  {slevel}")

        print(f"  Swap Used   : {format_size(ram['swap_used'])} / "

              f"{format_size(ram['swap_total'])}")



def display_disk(disks):

    print(f"\n  DISK USAGE")

    print("  " + "-"*60)

    for d in disks:

        bar, level = color_bar(d["percent"], width=20)

        print(f"  {d['mountpoint']:<12} {d['percent']:>5.1f}%  {bar}  {level}")

        print(f"  {'':12} Used: {format_size(d['used'])} / "

              f"{format_size(d['total'])}  "

              f"Free: {format_size(d['free'])}")

        print(f"  {'':12} FS: {d['fstype']}  Device: {d['device']}")



def display_network(net):

    print(f"\n  NETWORK")

    print("  " + "-"*60)

    print(f"  Sent         : {format_size(net['bytes_sent'])}")

    print(f"  Received     : {format_size(net['bytes_recv'])}")

    print(f"  Packets Sent : {net['packets_sent']:,}")

    print(f"  Packets Recv : {net['packets_recv']:,}")

    if net["interfaces"]:

        print(f"  Interfaces   :")

        for iface in net["interfaces"]:

            speed = f"{iface['speed']} Mbps" if iface["speed"] else "N/A"

            print(f"    {iface['name']:<15} IP: {iface['ip']:<18} Speed: {speed}")



def display_battery(bat):

    if not bat:

        return

    print(f"\n  BATTERY")

    print("  " + "-"*60)

    bar, level = color_bar(bat["percent"])

    plug = "Plugged In" if bat["plugged"] else "On Battery"

    print(f"  Charge      : {bat['percent']:>5.1f}%  {bar}  {level}")

    print(f"  Status      : {plug}")

    if bat["secs_left"] > 0 and not bat["plugged"]:

        print(f"  Time Left   : {format_uptime(bat['secs_left'])}")



def display_processes(procs):

    print(f"\n  TOP PROCESSES  (by CPU%)")

    print("  " + "-"*60)

    print(f"  {'PID':<8} {'NAME':<22} {'CPU%':>6}  {'MEM%':>6}  STATUS")

    print("  " + "-"*55)

    for p in procs[:10]:

        name   = (p.get("name") or "?")[:20]

        cpu    = p.get("cpu_percent") or 0.0

        mem    = p.get("memory_percent") or 0.0

        status = p.get("status") or "?"

        print(f"  {p['pid']:<8} {name:<22} {cpu:>5.1f}%  {mem:>5.1f}%  {status}")



def display_system(sys_info):

    print(f"\n  SYSTEM INFO")

    print("  " + "-"*60)

    print(f"  Hostname    : {sys_info['hostname']}")

    print(f"  User        : {sys_info['user']}")

    print(f"  Boot Time   : {sys_info['boot_time']}")

    print(f"  Uptime      : {sys_info['uptime_str']}")



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

# ALERTS

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


def check_alerts(cpu, ram, disks, bat, temp):

    alerts = []

    if cpu["percent"] >= ALERT_CPU:

        alerts.append(f"HIGH CPU: {cpu['percent']:.1f}% (threshold: {ALERT_CPU}%)")

    if ram["percent"] >= ALERT_RAM:

        alerts.append(f"HIGH RAM: {ram['percent']:.1f}% (threshold: {ALERT_RAM}%)")

    for d in disks:

        if d["percent"] >= ALERT_DISK:

            alerts.append(f"DISK FULL: {d['mountpoint']} at "

                          f"{d['percent']:.1f}% (threshold: {ALERT_DISK}%)")

    if cpu.get("temp") and cpu["temp"] >= ALERT_TEMP:

        alerts.append(f"HIGH TEMP: {cpu['temp']}°C (threshold: {ALERT_TEMP}°C)")

    if bat and not bat["plugged"] and bat["percent"] <= 15:

        alerts.append(f"LOW BATTERY: {bat['percent']}% — please plug in!")

    return alerts



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

# LIVE DASHBOARD

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


def live_dashboard(duration_secs=0):

    """

    Show a live-refreshing dashboard.

    duration_secs=0 runs until user presses Ctrl+C.

    """

    print("\n  Starting live dashboard... Press Ctrl+C to stop.\n")

    time.sleep(1)


    start      = time.time()

    prev_net   = psutil.net_io_counters()

    prev_time  = time.time()


    try:

        while True:

            clear_screen()

            display_header()


            cpu     = get_cpu_info()

            ram     = get_ram_info()

            disks   = get_disk_info()

            net     = get_network_info()

            bat     = get_battery_info()

            procs   = get_top_processes()

            sysinfo = get_system_info()


            # Network speed (bytes per second)

            now_net   = psutil.net_io_counters()

            now_time  = time.time()

            elapsed   = now_time - prev_time or 1

            dl_speed  = (now_net.bytes_recv - prev_net.bytes_recv) / elapsed

            ul_speed  = (now_net.bytes_sent - prev_net.bytes_sent) / elapsed

            prev_net  = now_net

            prev_time = now_time


            display_system(sysinfo)

            display_cpu(cpu)

            display_ram(ram)

            display_disk(disks)


            # Network with live speed

            print(f"\n  NETWORK  (Live speed)")

            print("  " + "-"*60)

            print(f"  Download Speed : {format_size(dl_speed)}/s")

            print(f"  Upload Speed   : {format_size(ul_speed)}/s")

            print(f"  Total Sent     : {format_size(net['bytes_sent'])}")

            print(f"  Total Received : {format_size(net['bytes_recv'])}")

            if net["interfaces"]:

                for iface in net["interfaces"]:

                    print(f"  {iface['name']:<15} {iface['ip']}")


            display_battery(bat)

            display_processes(procs)


            # Alerts

            alerts = check_alerts(cpu, ram, disks, bat, cpu.get("temp"))

            if alerts:

                print(f"\n  *** ALERTS ***")

                for a in alerts:

                    print(f"  ! {a}")


            print(f"\n  Refreshing every {REFRESH_INTERVAL}s  |  Ctrl+C to stop")


            if duration_secs > 0 and (time.time() - start) >= duration_secs:

                break


            time.sleep(REFRESH_INTERVAL)


    except KeyboardInterrupt:

        print("\n\n  Dashboard stopped.")



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

# SNAPSHOT REPORT

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


def snapshot_report():

    print("\n  Taking system snapshot...")

    cpu     = get_cpu_info()

    ram     = get_ram_info()

    disks   = get_disk_info()

    net     = get_network_info()

    bat     = get_battery_info()

    sysinfo = get_system_info()


    clear_screen()

    display_header()

    display_system(sysinfo)

    display_cpu(cpu)

    display_ram(ram)

    display_disk(disks)

    display_network(net)

    display_battery(bat)


    alerts = check_alerts(cpu, ram, disks, bat, cpu.get("temp"))

    if alerts:

        print(f"\n  *** ALERTS ***")

        for a in alerts:

            print(f"  ! {a}")

    else:

        print("\n  All systems OK. No alerts.")



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

# SAVE SNAPSHOT TO JSON

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


def save_snapshot():

    cpu     = get_cpu_info()

    ram     = get_ram_info()

    disks   = get_disk_info()

    net     = get_network_info()

    bat     = get_battery_info()

    sysinfo = get_system_info()


    snapshot = {

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

        "system":    sysinfo,

        "cpu": {

            "percent": cpu["percent"],

            "freq":    cpu.get("freq"),

            "temp":    cpu.get("temp"),

            "cores":   cpu["count_logical"]

        },

        "ram": {

            "percent":   ram["percent"],

            "used_gb":   round(ram["used"] / 1e9, 2),

            "total_gb":  round(ram["total"] / 1e9, 2),

        },

        "disks": [

            {

                "mount":   d["mountpoint"],

                "percent": d["percent"],

                "free_gb": round(d["free"] / 1e9, 2),

            }

            for d in disks

        ],

        "battery": bat,

        "network": {

            "bytes_sent": net["bytes_sent"],

            "bytes_recv": net["bytes_recv"],

        }

    }


    # Append to log file

    log = []

    if Path(LOG_FILE).exists():

        try:

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

                log = json.load(f)

        except:

            log = []


    log.append(snapshot)

    log = log[-200:]   # keep last 200 snapshots


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

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


    print(f"\n  Snapshot saved to {LOG_FILE}  ({len(log)} total)")



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

# MAIN MENU

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


def print_menu():

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

    print("  SYSTEM HEALTH MONITOR")

    print("-"*48)

    print("  1. Live dashboard (auto-refresh)")

    print("  2. Snapshot report (one-time view)")

    print("  3. Top processes")

    print("  4. Disk usage only")

    print("  5. Network info")

    print("  6. Save snapshot to JSON log")

    print("  7. Alert thresholds info")

    print("  0. Exit")

    print("-"*48)



def main():

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

    print("     SYSTEM HEALTH MONITOR")

    print("="*55)


    if not PSUTIL_OK:

        print("\n  psutil is not installed!")

        print("  Install it with:  pip install psutil")

        return


    while True:

        print_menu()

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


        if choice == "1":

            live_dashboard()


        elif choice == "2":

            snapshot_report()

            input("\n  Press Enter to return to menu...")


        elif choice == "3":

            procs = get_top_processes(15)

            clear_screen()

            display_header()

            display_processes(procs)

            input("\n  Press Enter to return to menu...")


        elif choice == "4":

            disks = get_disk_info()

            clear_screen()

            display_header()

            display_disk(disks)

            input("\n  Press Enter to return to menu...")


        elif choice == "5":

            net = get_network_info()

            clear_screen()

            display_header()

            display_network(net)

            input("\n  Press Enter to return to menu...")


        elif choice == "6":

            save_snapshot()


        elif choice == "7":

            print(f"\n  Current alert thresholds:")

            print(f"  CPU usage   : >= {ALERT_CPU}%")

            print(f"  RAM usage   : >= {ALERT_RAM}%")

            print(f"  Disk usage  : >= {ALERT_DISK}%")

            print(f"  CPU temp    : >= {ALERT_TEMP}°C")

            print(f"  Battery low : <= 15%")

            print(f"\n  Edit ALERT_* constants at the top of the script to change.")


        elif choice == "0":

            print("\n  Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



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

# RUN

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


if __name__ == "__main__":

    main()