feat: set up modularized version of project with testing

This commit is contained in:
Jef Roosens 2026-05-22 10:09:59 +02:00
commit 7bea08ddac
Signed by: Jef Roosens
GPG key ID: 119385BCAA005C21
19 changed files with 1138 additions and 0 deletions

85
src/timesheets/cli.py Normal file
View file

@ -0,0 +1,85 @@
import argparse
import os
import sys
from datetime import date
from .output import print_summary, write_csv
from .parser import aggregate_rows, detect_has_duration_column, parse_table
from .projects import load_project_map
from .utils import format_date
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Parse a markdown timesheet table and output a CSV file."
)
parser.add_argument(
"input",
help="Path to the markdown file containing the timesheet table, or '-' to read from stdin.",
)
parser.add_argument(
"-o", "--output",
help="Path to the output CSV file. Defaults to stdout.",
default=None,
)
parser.add_argument(
"--date",
help="Date to use in the output (DD/MM/YY). Defaults to today.",
default=None,
)
parser.add_argument(
"--map",
help=(
"Path to a JSON file mapping project keys to Project+Task pairs. "
"Defaults to project_map.json in the current working directory if it exists."
),
default=None,
)
parser.add_argument(
"--summary",
action="store_true",
help="Print a human-readable summary instead of writing CSV.",
)
return parser
def main() -> None:
args = build_parser().parse_args()
date_str = args.date or format_date(date.today())
if args.input == "-":
content = sys.stdin.read()
else:
try:
with open(args.input, "r", encoding="utf-8") as f:
content = f.read()
except FileNotFoundError:
print(f"Error: file not found: {args.input}", file=sys.stderr)
sys.exit(1)
lines = content.splitlines()
rows = parse_table(lines, has_duration_col=detect_has_duration_column(lines))
if not rows:
print("Warning: no timesheet rows found in input.", file=sys.stderr)
aggregated = aggregate_rows(rows)
# Resolve project map: explicit --map flag, else project_map.json in cwd
map_path = args.map
if map_path is None:
default_map = os.path.join(os.getcwd(), "project_map.json")
if os.path.exists(default_map):
map_path = default_map
project_map = load_project_map(map_path)
if args.summary:
print_summary(aggregated, project_map)
elif args.output:
with open(args.output, "w", newline="", encoding="utf-8") as f:
write_csv(aggregated, f, date_str, project_map)
print(f"Written to {args.output}", file=sys.stderr)
else:
write_csv(aggregated, sys.stdout, date_str, project_map)