# src/calendar_smith/cli.py
import argparse
import csv
import sys
from pathlib import Path
from .core import (
get_fiscal_year,
get_iso_weeks_for_year,
get_iso_week_span,
get_nth_week_of_month,
get_dates_windows,
)
from .utils import ensure_date, format_ordinal
from .time import from_iso, to_timezone, tz, to_iso
[docs]
def process_csv_args(args):
"""Add a fiscal year column to a CSV."""
try:
if not args.input_csv.exists():
print(f"Error: File {args.input_csv} not found.")
sys.exit(1)
with open(args.input_csv, mode="r", encoding="utf-8") as f_in, \
open(args.output_csv, mode="w", encoding="utf-8", newline="") as f_out:
reader = csv.DictReader(f_in)
if args.date_column not in reader.fieldnames:
print(f"Error: Column '{args.date_column}' not found in CSV.")
sys.exit(1)
fieldnames = reader.fieldnames + ["fiscal_year"]
writer = csv.DictWriter(f_out, fieldnames=fieldnames)
writer.writeheader()
for row in reader:
d = ensure_date(row[args.date_column])
row["fiscal_year"] = get_fiscal_year(d, args.system)
writer.writerow(row)
print(f"Successfully saved to {args.output_csv} (System: {args.system.upper()})")
except Exception as e:
print(f"An unexpected error occurred: {e}")
sys.exit(1)
[docs]
def solve_weeks_args(args):
"""List ISO week ranges for a year."""
print(f"\nISO Weeks in {args.year} (Monday–Sunday):")
weeks = get_iso_weeks_for_year(args.year)
for w in weeks:
print(f"Week {w.number:2}: {w.start} {w.end}")
[docs]
def solve_week_span_args(args):
"""Show the span for a specific ISO week."""
try:
span = get_iso_week_span(args.iso_year, args.iso_week)
print(f"\nISO Week {span.number} in {args.iso_year}:")
print(f" Start: {span.start}")
print(f" End: {span.end}")
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
[docs]
def determine_nth_week_args(_args):
"""Show the week number within a month."""
print("--- Nth Week of Month Calculator ---")
date_input = input("Date? [yyyy-mm-dd] (leave blank for today) >> ").strip()
try:
d = ensure_date(date_input if date_input else None)
nth = get_nth_week_of_month(d)
print("\nResult:")
print(f" Date: {d.isoformat()} ({d.strftime('%A')})")
print(f" Month Week: The {format_ordinal(nth)} week")
print(f" Year Week: The {format_ordinal(d.isocalendar().week)} week")
except ValueError as e:
print(f"Error: {e}")
[docs]
def generate_windows_args(args):
"""Generate date windows with optional sampling rate."""
try:
start = ensure_date(args.start_date)
dates = get_dates_windows(start, args.window_size, args.repeats, args.sampling_rate)
print(f"\nGenerated {args.repeats} windows starting from {start} with size {args.window_size}:")
if args.sampling_rate:
print(f" (Sampling rate: {args.sampling_rate} days)")
for i, window in enumerate(dates, 1):
print(f" Window {i:2}: {window.start} to {window.end}")
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
[docs]
def convert_timezone_args(args):
"""Convert an ISO 8601 datetime to another timezone."""
try:
dt = from_iso(args.dt_iso)
target = tz(args.target_tz)
converted = to_timezone(dt, target)
print(to_iso(converted))
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
[docs]
def get_fiscal_year_args(args):
"""Return the fiscal year for a date."""
try:
d = ensure_date(args.date)
fy = get_fiscal_year(d, args.system)
print(fy)
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
[docs]
def build_parser():
parser = argparse.ArgumentParser(
prog="calendar-smith",
description="Calendar-Smith command line tools.",
)
subparsers = parser.add_subparsers(dest="command", required=True)
p_csv = subparsers.add_parser("csv", help="Add fiscal year column to a CSV.")
p_csv.add_argument("input_csv", type=Path)
p_csv.add_argument("output_csv", type=Path)
p_csv.add_argument("--date-column", default="date")
p_csv.add_argument("--system", choices=["us", "jp"], default="us")
p_csv.set_defaults(func=process_csv_args)
p_solve = subparsers.add_parser("solve", help="List ISO week date ranges for a year.")
p_solve.add_argument("year", type=int)
p_solve.set_defaults(func=solve_weeks_args)
p_nth = subparsers.add_parser("nth", help="Interactive week-of-month calculator.")
p_nth.set_defaults(func=determine_nth_week_args)
p_span = subparsers.add_parser("week-span", help="Show the span for a specific ISO week.")
p_span.add_argument("iso_year", type=int)
p_span.add_argument("iso_week", type=int)
p_span.set_defaults(func=solve_week_span_args)
p_windows = subparsers.add_parser("windows", help="Generate date windows.")
p_windows.add_argument("start_date")
p_windows.add_argument("window_size", type=int)
p_windows.add_argument("repeats", type=int)
p_windows.add_argument("--sampling-rate", "-s", type=int)
p_windows.set_defaults(func=generate_windows_args)
p_tz = subparsers.add_parser("tz", help="Convert an ISO 8601 datetime to another timezone.")
p_tz.add_argument("dt_iso")
p_tz.add_argument("target_tz")
p_tz.set_defaults(func=convert_timezone_args)
p_fy = subparsers.add_parser("fiscal-year", help="Return the fiscal year for a date.")
p_fy.add_argument("date", help="Date in YYYY-MM-DD format")
p_fy.add_argument("--system", choices=["us", "jp"], default="us", help="Fiscal system (us or jp)")
p_fy.set_defaults(func=get_fiscal_year_args)
return parser
[docs]
def main(argv=None):
"""Top-level CLI entry point."""
parser = build_parser()
args = parser.parse_args(argv)
return args.func(args)
[docs]
def process_csv():
return main(["csv", *sys.argv[1:]])
[docs]
def solve_weeks():
return main(["solve", *sys.argv[1:]])
[docs]
def solve_week_span():
return main(["week-span", *sys.argv[1:]])
[docs]
def determine_nth_week():
return main(["nth"])
[docs]
def generate_windows():
return main(["windows", *sys.argv[1:]])
[docs]
def convert_timezone():
return main(["tz", *sys.argv[1:]])
[docs]
def fiscal_year():
return main(["fiscal-year", *sys.argv[1:]])