pen-tracker/build/lib/pen_tracker/cli.py
2026-05-02 22:25:35 -05:00

364 lines
13 KiB
Python

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.")