commit fa128da25e735a52d157e3c2f88ffeae83921689
parent b859dd44b85fe97a78c578938b586d4aa721c1db
Author: Carlosokumu <carlosokumu254@gmail.com>
Date: Wed, 27 Aug 2025 20:13:07 +0300
add interactive calendar creation
Diffstat:
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: