#!/usr/bin/env python3 """Log work expenses to MongoDB (wip.work_expenses). Usage: python log_expense.py [s2_url_2] [--route stop1|stop2] python log_expense.py update [--account X] [--note X] [--route stop1|stop2] type: "meal" or "mileage" account: e.g. "LTISD" date: YYYY-MM-DD note: free-text context (quote if it has spaces) s2_url: one or more S2 URLs of uploaded files --route: pipe-separated list of stops (mileage only). Home address is automatically prepended and appended. update: Modify an existing entry by doc ID. Accepts --account, --note, and/or --route. Rules enforced: - meal: exactly 1 file per invocation - mileage: exactly 2 files per invocation (start + end odometer) """ import sys from datetime import datetime from bson import ObjectId from pymongo import MongoClient HOME = "11600 Minda Cir Austin, TX 78758" ALIASES = { "airport": "3201 Presidential Blvd, Austin, TX 78719", "aus": "3201 Presidential Blvd, Austin, TX 78719", } MONGO_URI = "mongodb://root:3wwfoUjyk2E2zWELXFlLuHqfw1ALlOp4pb2H5Vq3TImbMIHL2h1u8Jej2mjzCPl@docdb.connorrhodes.com:35563?tls=true&tlsAllowInvalidCertificates=true" def handle_update(args): if not args: print("Usage: log_expense.py update [--account X] [--note X] [--route stop1|stop2]") sys.exit(1) doc_id = args.pop(0) updates = {} i = 0 while i < len(args): if args[i] == "--account" and i + 1 < len(args): updates["account"] = args[i + 1]; i += 2 elif args[i] == "--note" and i + 1 < len(args): updates["note"] = args[i + 1]; i += 2 elif args[i] == "--route" and i + 1 < len(args): raw = [s.strip() for s in args[i + 1].split("|") if s.strip()] raw = [ALIASES.get(s.lower(), s) for s in raw] updates["route"] = [HOME] + raw + [HOME] i += 2 else: print(f"Error: unknown argument '{args[i]}'"); sys.exit(1) if not updates: print("Error: no fields to update"); sys.exit(1) client = MongoClient(MONGO_URI) result = client["wip"]["work_expenses"].update_one({"_id": ObjectId(doc_id)}, {"$set": updates}) if result.matched_count: print(f"Updated {doc_id}: {list(updates.keys())}") else: print(f"Error: document {doc_id} not found") def main(): args = sys.argv[1:] # Handle update subcommand first if args and args[0] == "update": handle_update(args[1:]) return # Parse --route for insert route_stops = None if "--route" in args: idx = args.index("--route") args.pop(idx) if idx >= len(args): print("Error: --route requires a value") sys.exit(1) route_val = args.pop(idx) route_stops = [s.strip() for s in route_val.split("|") if s.strip()] route_stops = [ALIASES.get(s.lower(), s) for s in route_stops] route_stops = [HOME] + route_stops + [HOME] if len(args) < 5: print('Usage: log_expense.py [s2_url_2] [--route stop1|stop2]') sys.exit(1) exp_type = args[0] account = args[1] date = args[2] note = args[3] files = args[4:] valid_types = ("meal", "mileage", "other", "professional-development", "software") if exp_type not in valid_types: print(f"Error: type must be one of {valid_types}, got '{exp_type}'") sys.exit(1) if exp_type == "meal" and len(files) != 1: print(f"Error: meal entries must have exactly 1 file, got {len(files)}") sys.exit(1) if exp_type == "mileage" and len(files) != 2: print(f"Error: mileage entries must have exactly 2 files (start + end odometer), got {len(files)}") sys.exit(1) # Non-mileage/non-meal types require exactly 1 file if exp_type not in ("meal", "mileage") and len(files) != 1: print(f"Error: {exp_type} entries must have exactly 1 file, got {len(files)}") sys.exit(1) if route_stops is not None and exp_type != "mileage": print("Error: --route is only valid for mileage entries") sys.exit(1) client = MongoClient(MONGO_URI) db = client["wip"] col = db["work_expenses"] doc = { "files": files, "date": datetime.strptime(date, "%Y-%m-%d"), "type": exp_type, "account": account, "note": note, "status": "todo" } if route_stops is not None: doc["route"] = route_stops result = col.insert_one(doc) print(f"Inserted {exp_type} for {account} on {date}: {result.inserted_id}") if __name__ == "__main__": main()