364 lines
13 KiB
Python
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.")
|