|
import csv |
|
import datetime |
|
import uuid |
|
import sys |
|
import os |
|
|
|
def create_ics_event(start_dt, end_dt, description, is_all_day=False, repeats=None): |
|
"""Creates a single ICS event string.""" |
|
dt_format_full = "%Y%m%dT%H%M%S" |
|
dt_format_date = "%Y%m%d" |
|
|
|
now = datetime.datetime.now().strftime(dt_format_full) |
|
uid = str(uuid.uuid4()) |
|
|
|
if is_all_day: |
|
end_dt_exclusive = end_dt + datetime.timedelta(days=1) |
|
dt_start_str = f"DTSTART;VALUE=DATE:{start_dt.strftime(dt_format_date)}" |
|
dt_end_str = f"DTEND;VALUE=DATE:{end_dt_exclusive.strftime(dt_format_date)}" |
|
else: |
|
dt_start_str = f"DTSTART:{start_dt.strftime(dt_format_full)}" |
|
dt_end_str = f"DTEND:{end_dt.strftime(dt_format_full)}" |
|
|
|
event = [ |
|
"BEGIN:VEVENT", |
|
f"DTSTAMP:{now}", |
|
f"UID:{uid}", |
|
dt_start_str, |
|
dt_end_str, |
|
f"SUMMARY:{description}", |
|
f"DESCRIPTION:{description}" |
|
] |
|
|
|
if repeats: |
|
freq = repeats.upper().strip() |
|
if freq == "ANNUAL": |
|
freq = "YEARLY" |
|
|
|
if freq in ["MONTHLY", "YEARLY"]: |
|
event.append(f"RRULE:FREQ={freq}") |
|
|
|
event.append("END:VEVENT") |
|
return "\n".join(event) |
|
|
|
def parse_date(date_str): |
|
"""Parses date string and returns datetime object and boolean indicating if it's all-day.""" |
|
formats = [ |
|
("%Y-%m-%d %H:%M", False), |
|
("%Y-%m-%d", True) |
|
] |
|
for fmt, is_all_day in formats: |
|
try: |
|
return datetime.datetime.strptime(date_str, fmt), is_all_day |
|
except ValueError: |
|
continue |
|
raise ValueError(f"Unknown date format: {date_str}") |
|
|
|
def convert_csv_to_ics(csv_file): |
|
"""Converts CSV events to ICS file.""" |
|
# Determine output filename |
|
base, ext = os.path.splitext(csv_file) |
|
ics_file = f"{base}.ics" |
|
|
|
try: |
|
with open(csv_file, 'r', encoding='utf-8') as f: |
|
reader = csv.DictReader(f) |
|
reader.fieldnames = [name.lower().strip() for name in reader.fieldnames] |
|
|
|
events = [] |
|
for row in reader: |
|
try: |
|
start_str = row['start date'] |
|
end_str = row['end date'] |
|
desc = row['description'] |
|
repeats = (row.get('repeats') or '').strip() # Handle None if row is shorter than header |
|
|
|
start_dt, start_is_all_day = parse_date(start_str) |
|
end_dt, end_is_all_day = parse_date(end_str) |
|
|
|
is_all_day = start_is_all_day or end_is_all_day |
|
|
|
if is_all_day: |
|
start_dt = start_dt.replace(hour=0, minute=0, second=0, microsecond=0) |
|
end_dt = end_dt.replace(hour=0, minute=0, second=0, microsecond=0) |
|
|
|
events.append(create_ics_event(start_dt, end_dt, desc, is_all_day, repeats)) |
|
except ValueError as e: |
|
print(f"Skipping row due to date parse error: {row}. Error: {e}") |
|
except KeyError as e: |
|
print(f"Skipping row due to missing column: {row}. Missing: {e}") |
|
|
|
with open(ics_file, 'w', encoding='utf-8') as f: |
|
f.write("BEGIN:VCALENDAR\n") |
|
f.write("VERSION:2.0\n") |
|
f.write("PRODID:-//My Calendar//CSV to ICS//EN\n") |
|
f.write("CALSCALE:GREGORIAN\n") |
|
for event in events: |
|
f.write(event + "\n") |
|
f.write("END:VCALENDAR\n") |
|
|
|
print(f"Successfully created {ics_file} with {len(events)} events.") |
|
|
|
except FileNotFoundError: |
|
print(f"Error: File {csv_file} not found.") |
|
except Exception as e: |
|
print(f"An unexpected error occurred: {e}") |
|
|
|
if __name__ == "__main__": |
|
if len(sys.argv) < 2: |
|
print("Usage: python csv_to_ics.py <input_csv>") |
|
sys.exit(1) |
|
|
|
input_csv = sys.argv[1] |
|
|
|
convert_csv_to_ics(input_csv) |