"""Command line interface for the mileage logging tool.""" from __future__ import annotations import argparse import os from datetime import date, datetime, timedelta from typing import Optional, Tuple import pytz from .ingest.semantic_reader import load_place_visits from .logic.detect_itinerary import SiteConfig, detect_itinerary from .distance.resolve import DistanceResolver from .export.excel_writer import build_monthly_rows, write_monthly_workbook TZ = pytz.timezone("Europe/London") def _today_local() -> date: return datetime.now(TZ).date() def _prev_month_bounds(today: Optional[date] = None) -> Tuple[date, date]: """Return (start_date, end_date) for the previous calendar month in Europe/London.""" if today is None: today = _today_local() first_this_month = today.replace(day=1) last_prev_month = first_this_month - timedelta(days=1) start_prev_month = last_prev_month.replace(day=1) return start_prev_month, last_prev_month def _month_bounds(ym: str) -> Tuple[date, date]: """Return (start_date, end_date) for the given YYYY-MM.""" year, month = map(int, ym.split("-")) start = date(year, month, 1) if month == 12: end = date(year + 1, 1, 1) - timedelta(days=1) else: end = date(year, month + 1, 1) - timedelta(days=1) return start, end def _parse_date(s: str) -> date: y, m, d = map(int, s.split("-")) return date(y, m, d) def import_file( json_path: str, site_config_path: str, route_csv_path: str, output_dir: str, assume_home_start: bool, weekdays_only: bool, month: Optional[str], last_month: bool, since: Optional[str], until: Optional[str], days: Optional[int], ) -> None: """Import a single JSON file and write Excel workbooks (one per month).""" visits = load_place_visits(json_path) if not visits: print(f"No place visits found in {json_path}") return # 1) Determine date range filter start_date: Optional[date] = None end_date: Optional[date] = None if month: start_date, end_date = _month_bounds(month) elif last_month: start_date, end_date = _prev_month_bounds() elif since or until: if since: start_date = _parse_date(since) if until: end_date = _parse_date(until) elif days: end_date = _today_local() start_date = end_date - timedelta(days=days - 1) # 2) Apply date filtering to visits (by visit.start_time local date) if start_date or end_date: def in_range(v): d = v.start_time.date() if start_date and d < start_date: return False if end_date and d > end_date: return False return True visits = [v for v in visits if in_range(v)] if not visits: label = f"{start_date or ''}..{end_date or ''}" print(f"No place visits in requested range {label}") return site_config = SiteConfig.from_yaml(site_config_path) hops = detect_itinerary(visits, site_config, assume_home_start=assume_home_start) if not hops: print("No recognised hops detected after filtering.") return # 3) Weekday filter (Sat=5, Sun=6) if weekdays_only: hops = [h for h in hops if h.date.weekday() < 5] if not hops: print("All hops fell on weekends; nothing to write.") return resolver = DistanceResolver(route_csv_path) rows_by_month = build_monthly_rows(hops, site_config, resolver) # 4) Write one workbook per month present os.makedirs(output_dir, exist_ok=True) for month_key, rows in sorted(rows_by_month.items()): # If a specific month/range was requested, rows_by_month will already reflect it. output_path = os.path.join(output_dir, f"mileage_{month_key}.xlsx") write_monthly_workbook({month_key: rows}, output_path) print(f"Wrote {output_path} ({len(rows)} rows)") def main(argv: list[str] | None = None) -> None: parser = argparse.ArgumentParser(description="Mileage logging tool") subparsers = parser.add_subparsers(dest="command", required=True) import_parser = subparsers.add_parser("import", help="Import a single JSON export") import_parser.add_argument("json_path", help="Path to the JSON file to import") import_parser.add_argument( "--sites", dest="site_config_path", default=os.path.join(os.path.dirname(__file__), "../config/sites.yml"), help="Path to the sites.yml configuration", ) import_parser.add_argument( "--routes", dest="route_csv_path", default=os.path.join(os.path.dirname(__file__), "../tests/data/routes_golden.csv"), help="Path to the routes CSV catalogue", ) import_parser.add_argument( "--output", dest="output_dir", default=os.getcwd(), help="Directory to write the Excel workbook(s)", ) # Behavior toggles import_parser.add_argument( "--no-assume-home-start", action="store_true", help="Do not inject a Home→first-site hop when a day doesn't start at Home.", ) import_parser.add_argument( "--weekdays-only", action="store_true", help="Exclude Saturday/Sunday hops.", ) # Date filters (choose one style) import_parser.add_argument("--last-month", action="store_true", help="Process the previous calendar month.") import_parser.add_argument("--month", metavar="YYYY-MM", help="Process a specific calendar month, e.g. 2025-08.") import_parser.add_argument("--since", metavar="YYYY-MM-DD", help="Lower bound (inclusive) for visits to process.") import_parser.add_argument("--until", metavar="YYYY-MM-DD", help="Upper bound (inclusive) for visits to process.") import_parser.add_argument("--days", type=int, help="Process the last N days (relative to today).") args = parser.parse_args(argv) if args.command == "import": import_file( args.json_path, args.site_config_path, args.route_csv_path, args.output_dir, assume_home_start=(not args.no_assume_home_start), weekdays_only=args.weekdays_only, month=args.month, last_month=args.last_month, since=args.since, until=args.until, days=args.days, ) if __name__ == "__main__": main()