import os import csv import argparse import json from datetime import datetime from .engine import PenTracker, Pen, InkTracker, Ink def validate_date(date_str: str) -> bool: """Validate date in YYYY-MM-DD format.""" if date_str == "N/A": return True try: datetime.strptime(date_str, '%Y-%m-%d') return True except ValueError: return False class CLITracker(PenTracker): def __init__(self, storage_file=None): super().__init__(storage_file) self.ink_tracker = InkTracker() def add_pen(self): print("\n--- Add New Fountain Pen ---") new_pen_data = {} fields = [ ("Make", "Make"), ("Model", "Model"), ("Date Purchased (YYYY-MM-DD)", "Date-Purchased"), ("Vendor", "Vendor"), ("Nib Size", "Nib"), ("Nib Material", "Nib-Material"), ("Body Material", "Body"), ("Cap Material", "Cap"), ("Postable", "Post"), ("Current Ink", "Current-Ink"), ("Inked Date (YYYY-MM-DD)", "Inked-date"), ("Notes", "Notes") ] for label, key in fields: while True: if key == 'Current-Ink': value = self.select_ink() break value = input(f"Enter {label}: ").strip() if not value: value = "N/A" break if key in ['Date-Purchased', 'Inked-date']: if validate_date(value): break else: print("Invalid date format. Use YYYY-MM-DD.") else: break new_pen_data[key] = value # Map to Pen fields mapped_data = {self.key_map.get(k, k): v for k, v in new_pen_data.items()} new_pen = Pen(**mapped_data) self.pens.append(new_pen) self.save_data() print("\n[✔] Pen added successfully to Pens.csv!") def edit_pen(self): """Allows the user to select a pen and modify specific fields.""" if not self.pens: print("\n[!] No pens available to edit.") return # Show summary so user knows which ID to pick print("\n--- Select Pen to Edit ---") self.show_summary_list() try: idx = int(input("\nEnter the ID of the pen to edit: ")) pen = self.pens[idx] except (ValueError, IndexError): print("[!] Invalid ID.") return print("\n--- Editing Pen Details ---") print("(Press ENTER without typing to keep the current value)\n") # We iterate through headers so we don't miss any column for header in self.headers: field = self.key_map.get(header, header) current_val = getattr(pen, field) while True: if header == 'Current-Ink': print(f"Current Ink: {current_val}") change_ink = input("Change ink? (y/n): ").strip().lower() if change_ink == 'y': new_val = self.select_ink() break else: new_val = "" break new_val = input(f"{header} [{current_val}]: ").strip() if not new_val: break if header in ['Date-Purchased', 'Inked-date']: if validate_date(new_val): break else: print("Invalid date format. Use YYYY-MM-DD.") else: break if new_val: # If the user actually typed something new setattr(pen, field, new_val) self.save_data() print("\n[✔] Pen updated successfully!") def show_summary_list(self): """Helper to print a list without the interactive menu logic.""" print(f"{'ID':<4} | {'MAKE':<12} | {'MODEL':<12} | {'INK':<15}") print("-" * 55) for idx, pen in enumerate(self.pens): make = pen.Make[:12] model = pen.Model[:12] ink = pen.Current_Ink[:15] print(f"{idx:<4} | {make:<12} | {model:<12} | {ink:<15}") def view_all_pens(self): if not self.pens: print("\n[!] Your collection is currently empty.") return print("\n" + "="*85) print(f"{'ID':<4} | {'MAKE':<12} | {'MODEL':<12} | {'INK':<15} | {'INKED DATE':<12}") print("-" * 85) for idx, pen in enumerate(self.pens): make = pen.Make[:12] model = pen.Model[:12] ink = pen.Current_Ink[:15] inkdate = pen.Inked_date[:12] print(f"{idx:<4} | {make:<12} | {model:<12} | {ink:<15} | {inkdate:<12}") print("="*85) choice = input("\nEnter ID to see full details (or 'b' to go back): ") if choice.lower() != 'b': try: self.view_pen_details(int(choice)) except (ValueError, IndexError): print("[!] Invalid ID.") def view_pen_details(self, index): pen = self.pens[index] print("\n" + "═"*45) print(f"{' FOUNTAIN PEN DETAILS ':=^45}") for header in self.headers: field = self.key_map.get(header, header) value = getattr(pen, field) print(f"{header:<20}: {value}") print("═"*45) input("\nPress Enter to return to menu...") def delete_pen(self): if not self.pens: print("\n[!] Nothing to delete.") return self.show_summary_list() try: idx = int(input("\nEnter the ID of the pen to delete: ")) removed = self.pens.pop(idx) self.save_data() print(f"\n[!] Removed: {removed.Make} {removed.Model}") except (ValueError, IndexError): print("[!] Invalid ID.") def add_ink(self): print("\n--- Add New Ink ---") new_ink_data = {} fields = [ ("Vendor", "Vendor"), ("Name", "Name"), ("Color", "Color"), ("Purchased (YYYY-MM-DD)", "Purchased"), ("Size", "Size"), ("Notes", "Notes") ] for label, key in fields: while True: value = input(f"Enter {label}: ").strip() if not value: value = "N/A" break if key == 'Purchased': if validate_date(value): break else: print("Invalid date format. Use YYYY-MM-DD.") else: break new_ink_data[key] = value new_ink = Ink(**new_ink_data) self.ink_tracker.inks.append(new_ink) self.ink_tracker.save_data() print("\n[✔] Ink added successfully to inks.csv!") def view_all_inks(self): if not self.ink_tracker.inks: print("\n[!] Your ink collection is currently empty.") return print("\n" + "="*80) print(f"{'ID':<4} | {'VENDOR':<12} | {'NAME':<20} | {'COLOR':<15} | {'SIZE':<10}") print("-" * 80) for idx, ink in enumerate(self.ink_tracker.inks): vendor = ink.Vendor[:12] name = ink.Name[:20] color = ink.Color[:15] size = ink.Size[:10] print(f"{idx:<4} | {vendor:<12} | {name:<20} | {color:<15} | {size:<10}") print("="*80) def select_ink(self): """Helper method to select an ink from the list.""" if not self.ink_tracker.inks: print("[!] No inks available. Add some inks first.") return "N/A" self.view_all_inks() try: idx = int(input("\nEnter the ID of the ink to select (or -1 for N/A): ")) if idx == -1: return "N/A" ink = self.ink_tracker.inks[idx] return ink.Name except (ValueError, IndexError): print("[!] Invalid ID.") return "N/A" def clear_screen(): os.system('cls' if os.name == 'nt' else 'clear') def main(): # This is the entry point defined in pyproject.toml parser = argparse.ArgumentParser(description="Fountain Pen Tracker") parser.add_argument('--csv', default=None, help='Path to CSV file') subparsers = parser.add_subparsers(dest='command', help='Commands') # Add command add_parser = subparsers.add_parser('add', help='Add a new pen') add_parser.add_argument('--make', required=True) add_parser.add_argument('--model', required=True) add_parser.add_argument('--date-purchased') add_parser.add_argument('--vendor') add_parser.add_argument('--nib') add_parser.add_argument('--nib-material') add_parser.add_argument('--body') add_parser.add_argument('--cap') add_parser.add_argument('--post') add_parser.add_argument('--current-ink') add_parser.add_argument('--inked-date') add_parser.add_argument('--notes') # List command list_parser = subparsers.add_parser('list', help='List all pens') # Export command export_parser = subparsers.add_parser('export', help='Export to JSON') export_parser.add_argument('--output', default='pens.json', help='Output file') # Ink commands add_ink_parser = subparsers.add_parser('add-ink', help='Add a new ink') add_ink_parser.add_argument('--vendor', required=True) add_ink_parser.add_argument('--name', required=True) add_ink_parser.add_argument('--color') add_ink_parser.add_argument('--purchased') add_ink_parser.add_argument('--size') add_ink_parser.add_argument('--notes') list_inks_parser = subparsers.add_parser('list-inks', help='List all inks') args = parser.parse_args() tracker = CLITracker(args.csv) if args.command == 'add': # Validate dates if args.date_purchased and not validate_date(args.date_purchased): print("Invalid date-purchased format.") return if args.inked_date and not validate_date(args.inked_date): print("Invalid inked-date format.") return pen_data = { 'Make': args.make, 'Model': args.model, 'Date-Purchased': args.date_purchased or 'N/A', 'Vendor': args.vendor or 'N/A', 'Nib': args.nib or 'N/A', 'Nib-Material': args.nib_material or 'N/A', 'Body': args.body or 'N/A', 'Cap': args.cap or 'N/A', 'Post': args.post or 'N/A', 'Current-Ink': args.current_ink or 'N/A', 'Inked-date': args.inked_date or 'N/A', 'Notes': args.notes or 'N/A' } mapped_data = {tracker.key_map.get(k, k): v for k, v in pen_data.items()} new_pen = Pen(**mapped_data) tracker.pens.append(new_pen) tracker.save_data() print("Pen added successfully.") elif args.command == 'list': tracker.view_all_pens() elif args.command == 'export': with open(args.output, 'w') as f: json.dump([asdict(p) for p in tracker.pens], f, indent=2) print(f"Exported to {args.output}") elif args.command == 'add-ink': if args.purchased and not validate_date(args.purchased): print("Invalid purchased date format.") return ink_data = { 'Vendor': args.vendor, 'Name': args.name, 'Color': args.color or 'N/A', 'Purchased': args.purchased or 'N/A', 'Size': args.size or 'N/A', 'Notes': args.notes or 'N/A' } new_ink = Ink(**ink_data) tracker.ink_tracker.inks.append(new_ink) tracker.ink_tracker.save_data() print("Ink added successfully.") elif args.command == 'list-inks': tracker.view_all_inks() else: # Interactive mode while True: print("\n🖋️ FOUNTAIN PEN TRACKER (CSV Edition)") print("1. View Collection Summary") print("2. Add New Pen") print("3. Edit a Pen") print("4. Delete a Pen") print("5. View Ink Collection") print("6. Add New Ink") print("7. Exit") choice = input("\nSelect an option: ") if choice == '1': clear_screen() tracker.view_all_pens() elif choice == '2': clear_screen() tracker.add_pen() elif choice == '3': clear_screen() tracker.edit_pen() elif choice == '4': clear_screen() tracker.delete_pen() elif choice == '5': clear_screen() tracker.view_all_inks() elif choice == '6': clear_screen() tracker.add_ink() elif choice == '7': print("Goodbye! Happy writing! ✒️") break else: print("[!] Invalid selection. Please try again.")