commit a8842aef3902916f8a3e9024e56a1f5ebc55bd45 parent 9b1614a88e22ab5ac7844103e20b38b0079b2710 Author: Carlosokumu <carlosokumu254@gmail.com> Date: Mon, 25 Aug 2025 15:34:49 +0300 change project name and associated submodules from calendarapp to ungana. Diffstat:
14 files changed, 339 insertions(+), 339 deletions(-)
diff --git a/calendarapp/cmd/args_parser.py b/calendarapp/cmd/args_parser.py @@ -1,329 +0,0 @@ -import argparse -from pathlib import Path -from datetime import datetime, timedelta -import re -import logging -from calendarapp.attachment.attachment_manager import AttachmentManager -from calendarapp.ical.ical_manager import ICalManager -from calendarapp.logging.logging_manager import LoggingManager - -class ArgsParser: - - EMAIL_RE = re.compile(r"^[^@]+@[^@]+\.[^@]+$") - - def __init__(self): - self.parser = argparse.ArgumentParser( - description="Create a customized iCalendar (.ics) event" - ) - self.ical_manager = ICalManager() - self.attachment_manager = AttachmentManager() - self._add_field_arguments() - - def _read_file_or_exit(self, file_path: str) -> str: - try: - return Path(file_path).read_text(encoding="utf-8").strip() - except Exception as e: - raise SystemExit(f"Error: Could not read file '{file_path}': {e}") - - def _validate_datetime(self, dt_str: str) -> str: - try: - dt = datetime.fromisoformat(dt_str) - return dt.isoformat() - except ValueError: - pass - try: - dt = datetime.strptime(dt_str, "%d-%m-%Y %H:%M") - return dt.isoformat() - except ValueError: - pass - - raise argparse.ArgumentTypeError( - f"Invalid datetime format: '{dt_str}'. " - "Expected ISO format (YYYY-MM-DDTHH:MM:SS+ZZ:ZZ) or DD-MM-YYYY HH:MM" - ) - - def _validate_duration(self, duration_str: str) -> str: - """Validate duration format (e.g., '2h' or '30m').""" - if not re.match(r'^(\d+h)?(\d+m)?$', duration_str): - raise argparse.ArgumentTypeError( - f"Invalid duration format: '{duration_str}'. Expected format like '2h' or '30m'" - ) - return duration_str - - 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, - 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, - 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, - help="Event start time (ISO format or DD-MM-YYYY HH:MM)") - parser.add_argument("-l", "--location", required=required_flags, - help="Event location") - parser.add_argument("-o", "--organizer", required=required_flags, - help="Event organizer") - parser.add_argument("--tzid", required=required_flags, - 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") - - - 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) - - 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("-i", "--ical", required=True, - help="Path to ical file to edit") - 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) - - - def _add_logging_arguments(self, parser): - parser.add_argument("-v", "--verbose", action="store_true", - help="Enable verbose debug output") - parser.add_argument("-q", "--quiet", action="store_true", - help="Suppress all non-error output") - - def parse_args(self): - """Parses CLI args and resolves file-based inputs.""" - args = self.parser.parse_args() - self.logging = LoggingManager(verbose=args.verbose, quiet=args.quiet) - self.logger = self.logging.get_logger("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) - - 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) - - 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" - - 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 - - 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.ical: - self.parser.error("missing required ical file path to edit") - - ical_file_path = args.ical - try: - cal = self.ical_manager.load_ical_file(ical_file_path) - except (FileNotFoundError, ValueError): - self.parser.error("corrupted or missing ical file") - - if args.all: - return self._edit_multiple_events(cal, ical_file_path) - - updates, attachments = self._get_user_updates_from_args(args) - if any([updates, attachments]): - # Non-interactive mode - self.ical_manager.update_event(cal, updates, attachments, args.ical) - logging.info("Calendar updated successfully") - else: - # Interactive mode - self._edit_most_recent_event(cal, args, args.ical) - - def _edit_most_recent_event(self, cal, args, ical_file_path): - event = self.ical_manager.get_first_event(cal) - - # only interactive prompts - updates = self._get_user_event_updates(event) - - if updates: - self.ical_manager.update_event(cal, updates, [], ical_file_path) - logging.info("Calendar updated successfully") - else: - logging.info("No changes made to calendar file") - - - def _edit_multiple_events(self, cal, ical_file_path): - """Edit a specific event from multiple events (requires --all).""" - events = self.ical_manager.get_all_events(cal) - if not events: - 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") - if isinstance(start, list): - start = start[0] - start_str = start.dt if hasattr(start, "dt") else "No date" - print(f"{idx}. {summary} — {start_str}") - - try: - choice = int(input("\nEnter the number of the event you want to edit: ")) - if not (1 <= choice <= len(events)): - raise ValueError - except ValueError: - self.parser.error("invalid selection") - return - except KeyboardInterrupt: - self.parser.error("Operation cancelled by user") - return - - event = events[choice - 1] - updates = self._get_user_event_updates(event) - - if updates: - self.ical_manager.update_event(cal, updates, None, ical_file_path) - logging.info("Calendar updated successfully") - else: - logging.info("No changes made to calendar file") - - - - - def _get_user_event_updates(self, event) -> dict: - updates = {} - editable_fields = [ - f for f in event.keys() - if f not in ("UID", "DTSTAMP", "CATEGORIES") - ] - - for field in editable_fields: - current_value = event.get(field, "") - - if field == "DTSTART": - try: - dt_val = event.decoded("DTSTART") - if isinstance(dt_val, list): - dt_val = next((d for d in dt_val if d.tzinfo and d.tzinfo.key != "UTC"), dt_val[0]) - - if isinstance(dt_val, datetime): - tz = dt_val.tzinfo.tzname(dt_val) if dt_val.tzinfo else "UTC" - current_value = dt_val.strftime("%Y-%m-%d %H:%M:%S") + f" ({tz})" - else: - current_value = str(dt_val) - except Exception as e: - print(f"DTSTART formatting error: {e}") - - elif field == "DURATION": - try: - dur_val = event.decoded("DURATION") - if isinstance(dur_val, timedelta): - hours, remainder = divmod(dur_val.total_seconds(), 3600) - minutes, _ = divmod(remainder, 60) - current_value = f"{int(hours)}h {int(minutes)}m" - except Exception: - pass - - elif field == "ATTACH": - attaches = event.get("ATTACH", []) - current_value = [str(a)[:12] + "..." for a in attaches] - - new_value = input(f"{field} [{current_value}]: ").strip() - if new_value: - updates[field] = new_value - - return updates - - def _get_user_updates_from_args(self, args): - updates = {} - attachments = [] - - if args.start: updates["DTSTART"] = args.start - if args.duration: updates["DURATION"] = args.duration - if args.summary: updates["SUMMARY"] = args.summary - if args.location: updates["LOCATION"] = args.location - if args.description: updates["DESCRIPTION"] = args.description - if args.organizer: updates["ORGANIZER"] = args.organizer - if args.contact: - value, params = self._process_contact_arg(args.contact) - if not params and self.EMAIL_RE.match(value): - params = {"ALTREP": f"mailto:{value}"} - updates["CONTACT"] = (value, params) - elif params: - updates["CONTACT"] = (value, params) - else: - updates["CONTACT"] = value - - for ctx_name in ("poster", "long"): - arg_val = getattr(args, ctx_name, None) - if arg_val: - try: - prop, value, params = self.attachment_manager.create_attachment(arg_val, ctx=ctx_name) - attachments.append((prop, value, params)) - except Exception as e: - self.parser.error(f"Unexpected error: {e}") - - return updates, attachments - - def _process_contact_arg(self, raw_contact: str): - if "|" in raw_contact: - value, param_str = raw_contact.split("|", 1) - params = {} - for token in param_str.split(";"): - if "=" in token: - k, v = token.split("=", 1) - params[k.strip().upper()] = v.strip() - return value.strip(), params - return raw_contact.strip(), {} - - - - - def run(self): - args = self.parse_args() - - if args.command == "create": - self.handle_create(args) - elif args.command == "edit": - self.handle_edit(args) - else: - self.parser.error(f"Unknown command: {args.command}") diff --git a/calendarapp/runnable/calendar_cli.py b/calendarapp/runnable/calendar_cli.py @@ -1,9 +0,0 @@ -from calendarapp.cmd.args_parser import ArgsParser - -def main(): - args_handler = ArgsParser() - args_handler.run() - - -if __name__ == "__main__": - main() -\ No newline at end of file diff --git a/calendarapp/__init__.py b/ungana/__init__.py diff --git a/calendarapp/attachment/__init__.py b/ungana/attachment/__init__.py diff --git a/calendarapp/attachment/attachment_manager.py b/ungana/attachment/attachment_manager.py diff --git a/calendarapp/cmd/__init__.py b/ungana/cmd/__init__.py diff --git a/ungana/cmd/args_parser.py b/ungana/cmd/args_parser.py @@ -0,0 +1,329 @@ +import argparse +from pathlib import Path +from datetime import datetime, timedelta +import re +import logging +from ungana.attachment.attachment_manager import AttachmentManager +from ungana.ical.ical_manager import ICalManager +from ungana.logging.logging_manager import LoggingManager + +class ArgsParser: + + EMAIL_RE = re.compile(r"^[^@]+@[^@]+\.[^@]+$") + + def __init__(self): + self.parser = argparse.ArgumentParser( + description="Create a customized iCalendar (.ics) event" + ) + self.ical_manager = ICalManager() + self.attachment_manager = AttachmentManager() + self._add_field_arguments() + + def _read_file_or_exit(self, file_path: str) -> str: + try: + return Path(file_path).read_text(encoding="utf-8").strip() + except Exception as e: + raise SystemExit(f"Error: Could not read file '{file_path}': {e}") + + def _validate_datetime(self, dt_str: str) -> str: + try: + dt = datetime.fromisoformat(dt_str) + return dt.isoformat() + except ValueError: + pass + try: + dt = datetime.strptime(dt_str, "%d-%m-%Y %H:%M") + return dt.isoformat() + except ValueError: + pass + + raise argparse.ArgumentTypeError( + f"Invalid datetime format: '{dt_str}'. " + "Expected ISO format (YYYY-MM-DDTHH:MM:SS+ZZ:ZZ) or DD-MM-YYYY HH:MM" + ) + + def _validate_duration(self, duration_str: str) -> str: + """Validate duration format (e.g., '2h' or '30m').""" + if not re.match(r'^(\d+h)?(\d+m)?$', duration_str): + raise argparse.ArgumentTypeError( + f"Invalid duration format: '{duration_str}'. Expected format like '2h' or '30m'" + ) + return duration_str + + 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, + 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, + 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, + help="Event start time (ISO format or DD-MM-YYYY HH:MM)") + parser.add_argument("-l", "--location", required=required_flags, + help="Event location") + parser.add_argument("-o", "--organizer", required=required_flags, + help="Event organizer") + parser.add_argument("--tzid", required=required_flags, + 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") + + + 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) + + 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("-i", "--ical", required=True, + help="Path to ical file to edit") + 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) + + + def _add_logging_arguments(self, parser): + parser.add_argument("-v", "--verbose", action="store_true", + help="Enable verbose debug output") + parser.add_argument("-q", "--quiet", action="store_true", + help="Suppress all non-error output") + + def parse_args(self): + """Parses CLI args and resolves file-based inputs.""" + args = self.parser.parse_args() + self.logging = LoggingManager(verbose=args.verbose, quiet=args.quiet) + self.logger = self.logging.get_logger("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) + + 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) + + 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" + + 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 + + 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.ical: + self.parser.error("missing required ical file path to edit") + + ical_file_path = args.ical + try: + cal = self.ical_manager.load_ical_file(ical_file_path) + except (FileNotFoundError, ValueError): + self.parser.error("corrupted or missing ical file") + + if args.all: + return self._edit_multiple_events(cal, ical_file_path) + + updates, attachments = self._get_user_updates_from_args(args) + if any([updates, attachments]): + # Non-interactive mode + self.ical_manager.update_event(cal, updates, attachments, args.ical) + logging.info("Calendar updated successfully") + else: + # Interactive mode + self._edit_most_recent_event(cal, args, args.ical) + + def _edit_most_recent_event(self, cal, args, ical_file_path): + event = self.ical_manager.get_first_event(cal) + + # only interactive prompts + updates = self._get_user_event_updates(event) + + if updates: + self.ical_manager.update_event(cal, updates, [], ical_file_path) + logging.info("Calendar updated successfully") + else: + logging.info("No changes made to calendar file") + + + def _edit_multiple_events(self, cal, ical_file_path): + """Edit a specific event from multiple events (requires --all).""" + events = self.ical_manager.get_all_events(cal) + if not events: + 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") + if isinstance(start, list): + start = start[0] + start_str = start.dt if hasattr(start, "dt") else "No date" + print(f"{idx}. {summary} — {start_str}") + + try: + choice = int(input("\nEnter the number of the event you want to edit: ")) + if not (1 <= choice <= len(events)): + raise ValueError + except ValueError: + self.parser.error("invalid selection") + return + except KeyboardInterrupt: + self.parser.error("Operation cancelled by user") + return + + event = events[choice - 1] + updates = self._get_user_event_updates(event) + + if updates: + self.ical_manager.update_event(cal, updates, None, ical_file_path) + logging.info("Calendar updated successfully") + else: + logging.info("No changes made to calendar file") + + + + + def _get_user_event_updates(self, event) -> dict: + updates = {} + editable_fields = [ + f for f in event.keys() + if f not in ("UID", "DTSTAMP", "CATEGORIES") + ] + + for field in editable_fields: + current_value = event.get(field, "") + + if field == "DTSTART": + try: + dt_val = event.decoded("DTSTART") + if isinstance(dt_val, list): + dt_val = next((d for d in dt_val if d.tzinfo and d.tzinfo.key != "UTC"), dt_val[0]) + + if isinstance(dt_val, datetime): + tz = dt_val.tzinfo.tzname(dt_val) if dt_val.tzinfo else "UTC" + current_value = dt_val.strftime("%Y-%m-%d %H:%M:%S") + f" ({tz})" + else: + current_value = str(dt_val) + except Exception as e: + print(f"DTSTART formatting error: {e}") + + elif field == "DURATION": + try: + dur_val = event.decoded("DURATION") + if isinstance(dur_val, timedelta): + hours, remainder = divmod(dur_val.total_seconds(), 3600) + minutes, _ = divmod(remainder, 60) + current_value = f"{int(hours)}h {int(minutes)}m" + except Exception: + pass + + elif field == "ATTACH": + attaches = event.get("ATTACH", []) + current_value = [str(a)[:12] + "..." for a in attaches] + + new_value = input(f"{field} [{current_value}]: ").strip() + if new_value: + updates[field] = new_value + + return updates + + def _get_user_updates_from_args(self, args): + updates = {} + attachments = [] + + if args.start: updates["DTSTART"] = args.start + if args.duration: updates["DURATION"] = args.duration + if args.summary: updates["SUMMARY"] = args.summary + if args.location: updates["LOCATION"] = args.location + if args.description: updates["DESCRIPTION"] = args.description + if args.organizer: updates["ORGANIZER"] = args.organizer + if args.contact: + value, params = self._process_contact_arg(args.contact) + if not params and self.EMAIL_RE.match(value): + params = {"ALTREP": f"mailto:{value}"} + updates["CONTACT"] = (value, params) + elif params: + updates["CONTACT"] = (value, params) + else: + updates["CONTACT"] = value + + for ctx_name in ("poster", "long"): + arg_val = getattr(args, ctx_name, None) + if arg_val: + try: + prop, value, params = self.attachment_manager.create_attachment(arg_val, ctx=ctx_name) + attachments.append((prop, value, params)) + except Exception as e: + self.parser.error(f"Unexpected error: {e}") + + return updates, attachments + + def _process_contact_arg(self, raw_contact: str): + if "|" in raw_contact: + value, param_str = raw_contact.split("|", 1) + params = {} + for token in param_str.split(";"): + if "=" in token: + k, v = token.split("=", 1) + params[k.strip().upper()] = v.strip() + return value.strip(), params + return raw_contact.strip(), {} + + + + + def run(self): + args = self.parse_args() + + if args.command == "create": + self.handle_create(args) + elif args.command == "edit": + self.handle_edit(args) + else: + self.parser.error(f"Unknown command: {args.command}") diff --git a/calendarapp/ical/__init__.py b/ungana/ical/__init__.py diff --git a/calendarapp/ical/ical_manager.py b/ungana/ical/ical_manager.py diff --git a/calendarapp/logging/__init__.py b/ungana/logging/__init__.py diff --git a/calendarapp/logging/logging_manager.py b/ungana/logging/logging_manager.py diff --git a/calendarapp/runnable/__init__.py b/ungana/runnable/__init__.py diff --git a/ungana/runnable/calendar_cli.py b/ungana/runnable/calendar_cli.py @@ -0,0 +1,9 @@ +from ungana.cmd.args_parser import ArgsParser + +def main(): + args_handler = ArgsParser() + args_handler.run() + + +if __name__ == "__main__": + main() +\ No newline at end of file diff --git a/calendarapp/uri/__init__.py b/ungana/uri/__init__.py