feat: Implement CLI and TUI for Fountain Pen Tracker
- 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.
This commit is contained in:
parent
1a12e6d3c5
commit
51a1697c83
18 changed files with 866 additions and 166 deletions
85
build/lib/pen_tracker/engine.py
Normal file
85
build/lib/pen_tracker/engine.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue