#!/usr/bin/env python """ CHURCH CALENDAR CSV GENERATOR """ import calendar from dataclasses import dataclass from datetime import date, datetime import sys import click from dataclass_csv import DataclassWriter from events import EVENT_LIST def initalize_events(event_list): """ convert EVENT_LIST into monthly_events """ monthly_events = [[] for _ in range(7)] for event in event_list: monthly_events[event[0]].append(event[1:]) return monthly_events def debug_print(data, condition=True, end="\n"): """Conditionally print data.""" if condition: print(data, end=end) def initalize_year(year: int) -> dict[int, list[list[int]]]: months = dict(enumerate ([calendar.monthcalendar(year, month) for month in range(1, 13)], start=1)) return months @dataclass class Event(): """Event for CSV export""" event_name: str venue_name: str organizer_name: str start_date: str start_time: str end_date: str end_time: str all_day_event: str categories: str event_cost: str event_phone: str event_website: str show_map_link: str show_map: str event_description: str def suffix(day: int) -> str: """convert day to suffix""" result: str = 'th' if day in [1, 21, 31]: result = 'st' elif day in [2, 22]: result = 'nd' elif day in [3, 23]: result = 'rd' return result def create_event(name: str, location: str, categories: str, description: str, start, finish) -> Event: """create event object""" event = Event(name, location, "", start.strftime("%Y-%m-%d"), start.strftime("%I:%M %p"), finish.strftime("%Y-%m-%d"), finish.strftime("%I:%M %p"), "FALSE", categories, "", "", "", "", "", description) return event def write_calendar(events: list[Event], filename) -> None: """write calendar to csv file""" with open(filename, "w", encoding="utf-8", newline="") as handle: writer = DataclassWriter(handle, events, Event) writer.map("event_name").to("EVENT NAME") writer.map("venue_name").to("VENUE NAME") writer.map("organizer_name").to("ORGANIZER NAME") writer.map("start_date").to("START DATE") writer.map("start_time").to("START TIME") writer.map("end_date").to("END DATE") writer.map("end_time").to("END TIME") writer.map("all_day_event").to("ALL DAY EVENT") writer.map("categories").to("CATEGORIES") writer.map("event_cost").to("EVENT COST") writer.map("event_phone").to("EVENT PHONE") writer.map("event_website").to("EVENT_WEBSITE") writer.map("show_map_link").to("SHOW MAP LINK?") writer.map("show_map").to("SHOW MAP?") writer.map("event_description").to("EVENT DESCRIPTION") writer.write() def process_day(events, year, month, day, day_events): """process all events for one day""" for event_item in day_events: start = datetime(year, month, day, event_item[0], event_item[1]) finish = datetime(year, month, day, event_item[2], event_item[3]) debug_print(f" {start:%H:%M}-{finish:%H:%M} {event_item[6]:25} {event_item[4]:11} {event_item[5]:16} {event_item[7]:70.70}", DEBUG) event = create_event(event_item[6], event_item[4], event_item[5], event_item[7].replace("\n","\\n"), start, finish) events.append(event) return events def process_month(monthly_events, year, month, week, events, weekday_counters): """process one week of events using weekday occurrence counters""" for day_of_week, day in enumerate(week): if day > 0: # Increment the occurrence count for this specific weekday (0=Mon, 6=Sun) weekday_counters[day_of_week] += 1 current_occurrence = weekday_counters[day_of_week] debug_print(f" {day_of_week}-{day:02d} (Occurrence: {current_occurrence})", DEBUG) # Filter events matching this day of week and this specific occurrence (e.g., 2nd Monday) day_events = [x[1:] for x in monthly_events[day_of_week] if current_occurrence in x[0]] events = process_day(events, year, month, day, day_events) return events def process_months(months_dict, monthly_events, year, month_range): """process selected months and return list of events""" events = [] for month in range(1, 13): if month not in month_range: continue debug_print(f"MONTH: {month}", DEBUG) # Initialize counters for the month to track 1st, 2nd, etc. occurrence of each weekday weekday_counters = [0] * 7 for week_of_month, week in enumerate(months_dict[month]): debug_print(f"{week_of_month} {week}", DEBUG) events = process_month(monthly_events, year, month, week, events, weekday_counters) return events def validate_months(ctx, param, value): """Validates and processes the --months option.""" if value is None: return list(range(1, 13)) parts = value.split('-') if len(parts) == 1: try: month = int(parts[0]) if 1 <= month <= 12: return [month] raise click.BadParameter("Month must be between 1 and 12.") except ValueError: raise click.BadParameter("Invalid month format.") elif len(parts) == 2: try: start, end = int(parts[0]), int(parts[1]) if 1 <= start <= 12 and 1 <= end <= 12 and start <= end: return list(range(start, end + 1)) raise click.BadParameter("Invalid month range.") except ValueError: raise click.BadParameter("Invalid range format.") raise click.BadParameter("Use a single number or a range (e.g., 1-3).") @click.command() @click.option("--debug", "-d", is_flag=True, default=False, help="Print debug output") @click.option('--year', "-y", type=int, default=date.today().year, help='Specify the year.') @click.option('--months', '-m', 'month_range', callback=validate_months, default=None, help='A single month (1-12) or a range (e.g., 1-3).') @click.argument('filename', default="calendar.csv", required=False) def main(debug, year, month_range, filename): """Create a .csv file of church events for the year.""" global DEBUG DEBUG = debug months_dict = initalize_year(year) monthly_events = initalize_events(EVENT_LIST) events = process_months(months_dict, monthly_events, year, month_range) write_calendar(events, filename) sys.exit(0) if __name__ == "__main__": main()