158 lines
6.1 KiB
Python
158 lines
6.1 KiB
Python
import csv
|
|
import os
|
|
import logging
|
|
from dataclasses import dataclass, asdict
|
|
from typing import List
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
@dataclass
|
|
class Ink:
|
|
Vendor: str = "N/A"
|
|
Name: str = "N/A"
|
|
Color: str = "N/A"
|
|
Purchased: str = "N/A"
|
|
Size: str = "N/A"
|
|
Notes: str = "N/A"
|
|
|
|
@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 InkTracker:
|
|
def __init__(self, storage_file: str = None):
|
|
if storage_file is None:
|
|
storage_file = os.path.abspath('inks.csv')
|
|
self.storage_file = os.path.abspath(storage_file)
|
|
self.headers = ['Vendor', 'Name', 'Color', 'Purchased', 'Size', 'Notes']
|
|
self.inks: List[Ink] = self.load_data()
|
|
|
|
def _sort_inks(self):
|
|
"""Sorts the inks list by Vendor, then by Name alphabetically."""
|
|
self.inks.sort(key=lambda x: (x.Vendor.lower(), x.Name.lower()))
|
|
|
|
def load_data(self) -> List[Ink]:
|
|
"""Loads data from the CSV file."""
|
|
if not os.path.exists(self.storage_file):
|
|
self._create_empty_csv()
|
|
return []
|
|
|
|
inks = []
|
|
try:
|
|
with open(self.storage_file, mode='r', encoding='utf-8-sig') as f:
|
|
reader = csv.DictReader(f)
|
|
if reader.fieldnames:
|
|
reader.fieldnames = [h.strip() for h in reader.fieldnames]
|
|
for row in reader:
|
|
if None in row:
|
|
extras = row.pop(None)
|
|
if extras:
|
|
extra_text = ",".join(extras).strip()
|
|
if extra_text:
|
|
row['Notes'] = (row.get('Notes') or '')
|
|
if row['Notes']:
|
|
row['Notes'] += ", "
|
|
row['Notes'] += extra_text
|
|
clean_row = {
|
|
k.strip(): (v.strip() if v else "N/A")
|
|
for k, v in row.items() if k is not None
|
|
}
|
|
inks.append(Ink(**clean_row))
|
|
inks.sort(key=lambda x: (x.Vendor.lower(), x.Name.lower()))
|
|
except Exception as e:
|
|
logger.error(f"Error loading inks CSV: {e}")
|
|
return inks
|
|
|
|
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_inks()
|
|
with open(self.storage_file, mode='w', newline='', encoding='utf-8') as f:
|
|
writer = csv.DictWriter(f, fieldnames=self.headers)
|
|
writer.writeheader()
|
|
for ink in self.inks:
|
|
writer.writerow(asdict(ink))
|
|
|
|
class PenTracker:
|
|
def __init__(self, storage_file: str = None):
|
|
if storage_file is None:
|
|
storage_file = os.getenv('PEN_TRACKER_CSV')
|
|
if storage_file is None:
|
|
data_home = os.getenv('XDG_DATA_HOME', os.path.expanduser('~/.local/share'))
|
|
app_data_dir = os.path.join(data_home, 'pen-tracker')
|
|
os.makedirs(app_data_dir, exist_ok=True)
|
|
storage_file = os.path.join(app_data_dir, '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)
|