# /// script # requires-python = ">=3.12" # dependencies = ["typer", "rich", "xlwings"] # /// import json import re from collections.abc import Generator from contextlib import contextmanager from pathlib import Path from typing import Annotated import xlwings as xw import typer from rich.console import Console console = Console() def get_reports(base_dir: Path): report_regex = re.compile(r'(?P\w+)_2025_[\w ]+') for report in base_dir.glob("*Mileage Report.xlsx"): if not report.is_file() or report.stem.startswith('~$'): continue if m := report_regex.match(report.stem): yield m.group("name"), report def find_in_range(rng: xw.Range, query: str) -> xw.Range | None: for cell in rng: match cell.value: case str(cell_value) if cell_value.lower().startswith(query.lower()): return cell return None def find_total_row(table_start: xw.Range) -> int: desc_col = find_in_range(table_start.expand('right'), "Desc") col = desc_col.get_address(column_absolute=False)[0] return find_in_range(table_start.sheet.range(f'{col}:{col}'), 'total').row def get_total_cost(sheet: xw.Sheet) -> float: table_start = find_in_range(sheet.range("A1:A10"), "Date") cost_col = find_in_range(table_start.expand('right'), "Cost") total_row = find_total_row(table_start) cost_cell = sheet.range(f'{cost_col.get_address(column_absolute=False)[0]}{total_row}') return cost_cell.value def get_all_totals(book: xw.Book) -> Generator[str, float]: for sheet in book.sheets: if sheet.name.endswith('Rate'): continue total = float(get_total_cost(sheet)) console.print(f'Got ${total} for sheet {sheet.name}') yield sheet.name, float(total) def get_grand_total(book: xw.Book): totals = dict(get_all_totals(book)) grand_total = sum(totals.values()) return grand_total @contextmanager def switch_values(book: xw.Book): og_vals = book.sheets['GSA Rate'].range("B2").expand("down").value try: book.sheets['GSA Rate'].range("B2").expand("down").value = 0.70 yield finally: book.sheets['GSA Rate'].range("B2").expand("down").options(transpose=True).value = og_vals def get_diff(book: xw.Book) -> dict[str, float]: book.sheets['GSA Rate'].range("B2:B10").value = 0.67 console.print('Calculating initial total...') initial_totals = dict(get_all_totals(book)) with switch_values(book): console.print('Calculating new rate total...') new_totals = dict(get_all_totals(book)) return { 'old_rate': initial_totals, 'new_rate': new_totals, 'difference': round(sum(new_totals.values()) - sum(initial_totals.values()), 2) } def get_all_diffs(base: Path): reports = dict(get_reports(base)) console.print(f"Found {len(reports)} report(s).") for name, report_path in reports.items(): with console.status(f'Processing [bold blue]{report_path.name}[/bold blue]'): console.print('Opening workbook...') book = xw.Book(report_path) try: res = get_diff(book) console.print(f"[cyan]Report:[/] {report_path.name}") console.print(f" Results: ${res}") yield name, res except Exception as e: console.print(f"[red]Error processing {report_path.name}: {e}[/red]") finally: book.close() def main( directory: Annotated[ Path, typer.Argument( exists=True, file_okay=False, dir_okay=True, readable=True, writable=True, resolve_path=True ) ] = None) -> None: console.print(f"Processing files in directory: {directory}") results = dict(get_all_diffs(directory)) with Path('results.jsonl').open('w', encoding='utf-8') as f: json.dump(results, f, indent=4) if __name__ == "__main__": typer.run(main)