Initial commit (clean, ignores in place)
This commit is contained in:
189
mileage_logger/cli.py
Normal file
189
mileage_logger/cli.py
Normal file
@@ -0,0 +1,189 @@
|
||||
"""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()
|
||||
|
Reference in New Issue
Block a user