- Added CLI functionality for adding, editing, viewing, and deleting fountain pens. - Introduced TUI using Textual for a more interactive experience. - Created Pen and PenTracker classes to manage pen data and CSV storage. - Implemented input validation for date fields. - Added export functionality to JSON format. - Updated project version to 0.2.0. - Added unit tests for PenTracker functionality.
85 lines
3.1 KiB
Python
85 lines
3.1 KiB
Python
import csv
|
|
import os
|
|
import logging
|
|
from dataclasses import dataclass, asdict
|
|
from typing import List
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
@dataclass
|
|
class Pen:
|
|
Make: str = "N/A"
|
|
Model: str = "N/A"
|
|
Date_Purchased: str = "N/A"
|
|
Vendor: str = "N/A"
|
|
Nib: str = "N/A"
|
|
Nib_Material: str = "N/A"
|
|
Body: str = "N/A"
|
|
Cap: str = "N/A"
|
|
Post: str = "N/A"
|
|
Current_Ink: str = "N/A"
|
|
Inked_date: str = "N/A"
|
|
Notes: str = "N/A"
|
|
|
|
class PenTracker:
|
|
def __init__(self, storage_file: str = None):
|
|
if storage_file is None:
|
|
storage_file = os.getenv('PEN_TRACKER_CSV', 'Pens.csv')
|
|
self.storage_file = storage_file
|
|
self.headers = [
|
|
'Make', 'Model', 'Date-Purchased', 'Vendor', 'Nib',
|
|
'Nib-Material', 'Body', 'Cap', 'Post',
|
|
'Current-Ink', 'Inked-date', 'Notes'
|
|
]
|
|
self.key_map = {
|
|
'Date-Purchased': 'Date_Purchased',
|
|
'Inked-date': 'Inked_date',
|
|
'Nib-Material': 'Nib_Material',
|
|
'Current-Ink': 'Current_Ink'
|
|
}
|
|
self.reverse_key_map = {v: k for k, v in self.key_map.items()}
|
|
self.pens: List[Pen] = self.load_data()
|
|
|
|
def _sort_pens(self):
|
|
"""Sorts the pens list by Make, then by Model alphabetically."""
|
|
self.pens.sort(key=lambda x: (x.Make.lower(), x.Model.lower()))
|
|
|
|
def load_data(self) -> List[Pen]:
|
|
"""Loads data from the CSV file."""
|
|
if not os.path.exists(self.storage_file):
|
|
self._create_empty_csv()
|
|
return []
|
|
|
|
pens = []
|
|
try:
|
|
# Standard, clean way to open the file
|
|
with open(self.storage_file, mode='r', encoding='utf-8-sig') as f:
|
|
reader = csv.DictReader(f)
|
|
for row in reader:
|
|
# Strip whitespace from keys and values, handle empty values
|
|
clean_row = {k.strip(): (v.strip() if v else "N/A") for k, v in row.items()}
|
|
# Map keys to dataclass field names
|
|
mapped_row = {self.key_map.get(k, k): v for k, v in clean_row.items()}
|
|
pens.append(Pen(**mapped_row))
|
|
pens.sort(key=lambda x: (x.Make.lower(), x.Model.lower()))
|
|
except Exception as e:
|
|
logger.error(f"Error loading CSV: {e}")
|
|
return pens
|
|
|
|
def _create_empty_csv(self):
|
|
"""Creates the CSV file with headers if it is missing."""
|
|
with open(self.storage_file, mode='w', newline='', encoding='utf-8') as f:
|
|
writer = csv.DictWriter(f, fieldnames=self.headers)
|
|
writer.writeheader()
|
|
|
|
def save_data(self):
|
|
"""Sorts the data before writing to ensure persistent alphabetical order."""
|
|
self._sort_pens()
|
|
with open(self.storage_file, mode='w', newline='', encoding='utf-8') as f:
|
|
writer = csv.DictWriter(f, fieldnames=self.headers)
|
|
writer.writeheader()
|
|
for pen in self.pens:
|
|
row = asdict(pen)
|
|
# Map back to CSV keys
|
|
csv_row = {k.replace('_', '-'): v for k, v in row.items()}
|
|
writer.writerow(csv_row)
|