ungana

Unnamed repository; edit this file 'description' to name the repository.
Info | Log | Files | Refs | README

commit fa128da25e735a52d157e3c2f88ffeae83921689
parent b859dd44b85fe97a78c578938b586d4aa721c1db
Author: Carlosokumu <carlosokumu254@gmail.com>
Date:   Wed, 27 Aug 2025 20:13:07 +0300

add interactive calendar creation

Diffstat:
Mungana/cmd/args_parser.py | 180++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Mungana/ical/ical_manager.py | 34+++++++++++++++++++++++++---------
2 files changed, 139 insertions(+), 75 deletions(-)

diff --git a/ungana/cmd/args_parser.py b/ungana/cmd/args_parser.py @@ -54,44 +54,46 @@ class ArgsParser: def _add_field_arguments(self): subparsers = self.parser.add_subparsers(dest='command', required=True) - def add_event_field_args(parser, required_flags=False): - parser.add_argument("-s", "--summary", required=required_flags, + def add_event_field_args(parser): + parser.add_argument("-i", "--interactive", + action="store_true", + help="Run interactive calendar creation") + parser.add_argument("-s", "--summary", help="Event summary (text or via --summary-file)") parser.add_argument("--sf", "--summary-file", dest="summary_file", help="File containing event summary") - parser.add_argument("-d", "--description", required=required_flags, + parser.add_argument("-d", "--description", help="Event description (text or via --description-file)") parser.add_argument("--df", "--description-file", dest="description_file", help="File containing event description") - parser.add_argument("--start", type=self._validate_datetime, required=required_flags, + parser.add_argument("--start", type=self._validate_datetime, help="Event start time (ISO format or DD-MM-YYYY HH:MM)") - parser.add_argument("-l", "--location", required=required_flags, + parser.add_argument("-l", "--location", help="Event location") - parser.add_argument("-o", "--organizer", required=required_flags, - help="Event organizer") - parser.add_argument("--tzid",help="Time zone ID (e.g., 'Europe/Berlin')") + parser.add_argument("-o", "--organizer", + help="Event organizer") + parser.add_argument("--tzid", help="Time zone ID (e.g., 'Europe/Berlin')") parser.add_argument("--duration", type=self._validate_duration, help="Event duration (e.g., '2h' or '30m')") parser.add_argument("--end", type=self._validate_datetime, help="Event end time (ISO format or DD-MM-YYYY HH:MM)") parser.add_argument("-f", "--file", help="Output .ics filename (default: event_<date>.ics)") - parser.add_argument("-p", "--poster", - help="Event headline image") - parser.add_argument("-ld", "--long", - help="Exhaustive description of the event") - parser.add_argument("-c","--contact",help="Contact details") + parser.add_argument("-p", "--poster", help="Event headline image") + parser.add_argument("-ld", "--long", help="Exhaustive description of the event") + parser.add_argument("-c", "--contact", help="Contact details") create_event_parser = subparsers.add_parser('create', help='Create a new calendar event') self._add_logging_arguments(create_event_parser) - add_event_field_args(create_event_parser, required_flags=True) + create_event_parser.add_argument("ics_filename",help="Output .ics filename (default: event_<date>.ics") + add_event_field_args(create_event_parser) edit_event_parser = subparsers.add_parser('edit', help='Edit a calendar ical file') self._add_logging_arguments(edit_event_parser) edit_event_parser.add_argument("ics_file", help="Path to calendar .ics file") edit_event_parser.add_argument("-a", "--all", action="store_true", help="If your ical file has more than one event, interactively choose one to edit") - add_event_field_args(edit_event_parser, required_flags=False) + add_event_field_args(edit_event_parser) def _add_logging_arguments(self, parser): @@ -109,59 +111,106 @@ class ArgsParser: return args - def handle_create(self, args): - if args.summary_file: - args.summary = self._read_file_or_exit(args.summary_file) - if args.description_file: - args.description = self._read_file_or_exit(args.description_file) + def prompt_compulsory_event_fields(self): + """Prompt user for compulsory event fields interactively.""" + summary = input("Enter event summary (title): ").strip() + description = input("Enter description: ").strip() + start = input("Enter start datetime (YYYY-MM-DD HH:MM): ").strip() + + end = input("Enter end datetime (YYYY-MM-DD HH:MM) [leave blank if using duration]: ").strip() + start_dt = datetime.strptime(start, "%Y-%m-%d %H:%M") - if not args.end and not args.duration: - self.parser.error("Either --end or --duration must be specified") + if not end: + duration = input("Enter duration (e.g. 1h, 30m): ").strip() + else: + end_dt = datetime.strptime(end, "%Y-%m-%d %H:%M") + delta = end_dt - start_dt + hours, remainder = divmod(delta.seconds, 3600) + minutes, _ = divmod(remainder, 60) + duration = f"PT{delta.days}D{hours}H{minutes}M" if delta.days else f"PT{hours}H{minutes}M" + + + location = input("Enter location: ").strip() + organizer = input("Enter organizer (name/email): ").strip() + + tzid = input("Enter timezone ID (e.g. Europe/Berlin) [default: UTC]: ").strip() + if not tzid: + tzid = "UTC" + + return { + "summary": summary, + "description": description, + "start": start, + "end": end if end else None, + "duration": duration if duration else None, + "location": location, + "organizer": organizer, + "tzid": tzid, + } - args.start_dt = datetime.fromisoformat(args.start) - - if args.end: - end_dt = datetime.fromisoformat(args.end) - duration = end_dt - args.start_dt - args.duration = f"{duration.seconds//3600}h{(duration.seconds%3600)//60}m" - - if args.tzid: - tzinfo = tz.gettz(args.tzid) - if tzinfo is None: - self.parser.error(f"Invalid timezone ID: {args.tzid}") + + + def handle_create(self, args): + if args.interactive: + if args.ics_filename: + cal = self.ical_manager.load_ical_file(args.ics_filename) + if cal: + event_data = self.prompt_compulsory_event_fields() + event = self.ical_manager.create_event(event_data) + self.ical_manager.save_ical_file(event, args.ics_filename) + return else: - # Fallback to UTC - tzinfo = tz.UTC - args.tzid = "UTC" - - event_data = { - 'start': args.start_dt, - 'duration': args.duration, - 'summary': args.summary, - 'location': args.location, - 'description': args.description, - 'organizer': args.organizer, - 'tzid': args.tzid - } - if args.file: - filename = args.file - cal = self.ical_manager.load_ical_file(args.file) - exists = self.ical_manager.check_existing_event(cal,event_data) - if exists: - details = ( - f"Summary='{event_data.get('summary')}', " - f"Start={event_data.get('start')}, " - f"Tzid={event_data.get('tzid')}, " - f"Location='{event_data.get('location')}'" - ) - self.parser.error( f"Duplicate event detected: An event with these details already exists: {details}. "f"Try using the 'edit' command instead.") - return + if args.summary_file: + args.summary = self._read_file_or_exit(args.summary_file) + if args.description_file: + args.description = self._read_file_or_exit(args.description_file) + + if not args.end and not args.duration: + self.parser.error("Either --end or --duration must be specified") + + args.start_dt = datetime.fromisoformat(args.start) - else: - filename = f"event_{datetime.now().strftime('%Y%m%d_%H%M%S')}.ics" - - event = self.ical_manager.create_event(event_data) - self.ical_manager.save_ical_file(event, filename) + if args.end: + end_dt = datetime.fromisoformat(args.end) + duration = end_dt - args.start_dt + args.duration = f"{duration.seconds//3600}h{(duration.seconds%3600)//60}m" + + if args.tzid: + tzinfo = tz.gettz(args.tzid) + if tzinfo is None: + self.parser.error(f"Invalid timezone ID: {args.tzid}") + else: + # Fallback to UTC + tzinfo = tz.UTC + args.tzid = "UTC" + + event_data = { + 'start': args.start_dt, + 'duration': args.duration, + 'summary': args.summary, + 'location': args.location, + 'description': args.description, + 'organizer': args.organizer, + 'tzid': args.tzid + } + if args.ics_filename: + filename = args.ics_filename + cal = self.ical_manager.load_ical_file(filename) + exists = self.ical_manager.check_existing_event(cal,event_data) + if exists: + details = ( + f"Summary='{event_data.get('summary')}', " + f"Start={event_data.get('start')}, " + f"Tzid={event_data.get('tzid')}, " + f"Location='{event_data.get('location')}'" + ) + self.parser.error( f"Duplicate event detected: An event with these details already exists: {details}. "f"Try using the 'edit' command instead.") + return + else: + filename = f"event_{datetime.now().strftime('%Y%m%d_%H%M%S')}.ics" + + event = self.ical_manager.create_event(event_data) + self.ical_manager.save_ical_file(event, filename) def handle_edit(self, args): if not args.ics_file: @@ -183,7 +232,7 @@ class ArgsParser: logging.info("Calendar updated successfully") else: # Interactive mode - self._edit_most_recent_event(cal, args, args.ical) + self._edit_most_recent_event(cal, args, ical_file_path) def _edit_most_recent_event(self, cal, args, ical_file_path): event = self.ical_manager.get_first_event(cal) @@ -205,7 +254,6 @@ class ArgsParser: self.parser.error("no events found for the selected ical file") return - # list events for idx, e in enumerate(events, start=1): summary = e.get("SUMMARY", "No title") start = e.get("DTSTART") diff --git a/ungana/ical/ical_manager.py b/ungana/ical/ical_manager.py @@ -1,16 +1,20 @@ import os import uuid from zoneinfo import ZoneInfo -from icalendar import Calendar, Event +from icalendar import Calendar, Event, vDatetime,prop from datetime import datetime, timedelta, timezone from typing import Dict, Any +from ungana.logging.logging_manager import LoggingManager + class ICalManager: """Handles parsing, representing, and writing iCalendar files""" def __init__(self): self.calendar = Calendar() + self.logging_manager = LoggingManager() + self.logger = self.logging_manager.get_logger(__name__) self.calendar.add('prodid', '-//Ungana//mxm.dk//') self.calendar.add('version', '2.0') @@ -20,13 +24,15 @@ class ICalManager: start_dt = event_data['start'] tzid = event_data.get('tzid') + if isinstance(start_dt, str): + start_dt = self.parse_datetime(start_dt) + event.add('dtstart', start_dt.astimezone(timezone.utc)) if tzid: - event.add('dtstart', start_dt) - event['dtstart'][-1].params['TZID'] = tzid - else: - event.add('dtstart', start_dt) + dt_prop = vDatetime(start_dt) + dt_prop.params["TZID"] = tzid + event.add("dtstart", dt_prop) event.add('summary', event_data['summary']) event.add('location', event_data['location']) @@ -98,16 +104,26 @@ class ICalManager: return cal def _parse_duration(self, duration_str: str): + duration_str = duration_str.replace(",", "").strip() hours, minutes = 0, 0 if "h" in duration_str: parts = duration_str.split("h") - hours = int(parts[0]) - if "m" in parts[1]: - minutes = int(parts[1].replace("m", "")) + hours = int(parts[0]) if parts[0] else 0 + if len(parts) > 1 and "m" in parts[1]: + minutes = int(parts[1].replace("m", "").strip() or 0) elif "m" in duration_str: - minutes = int(duration_str.replace("m", "")) + minutes = int(duration_str.replace("m", "").strip() or 0) return timedelta(hours=hours, minutes=minutes) + + def parse_datetime(self,dt_str: str) -> datetime: + formats = ["%Y-%m-%d %H:%M", "%d-%m-%Y %H:%M"] + for fmt in formats: + try: + return datetime.strptime(dt_str, fmt) + except ValueError: + continue + raise ValueError(f"Invalid datetime format: {dt_str}. Expected one of {formats}") def _generate_uid(self) -> str: