From bd8e9aea758a737ddaa73016df1b79d56e792f6b Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 2 Mar 2023 17:05:25 +0900 Subject: [PATCH] Implementing reading from ics folders --- README.md | 2 +- calcure/__main__.py | 9 +- calcure/calendars.py | 3 +- calcure/colors.py | 6 +- calcure/loaders.py | 219 +++++++++++++++++++++++-------------------- 5 files changed, 128 insertions(+), 111 deletions(-) diff --git a/README.md b/README.md index daff836..328c734 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Modern TUI calendar and task manager with customizable interface. Manages your e ## Features - Vim keys -- View tasks and events from .ics file synced with clouds +- View tasks and events from .ics files synced with clouds - Operation with fewest key presses possible - Todo list with subtasks, deadlines, and timers - Birthdays of your abook contacts diff --git a/calcure/__main__.py b/calcure/__main__.py index 09f4fca..61bdbc3 100644 --- a/calcure/__main__.py +++ b/calcure/__main__.py @@ -2,7 +2,6 @@ """This is the main module that contains views and the main logic""" -# Libraries: import curses import time import getopt @@ -10,7 +9,6 @@ import importlib import logging -# Modules: from calcure.calendars import Calendar from calcure.configuration import cf from calcure.weather import Weather @@ -41,7 +39,7 @@ from calcure.translations.en import * -__version__ = "2.7.6" +__version__ = "2.8.0" def read_items_from_user_arguments(screen, user_tasks, user_events, task_saver_csv, event_saver_csv): @@ -247,7 +245,7 @@ def icon(self): @property def color(self): - """Select the color depending on the status and type""" + """Assign color depending on the status or calendar number if it's from .ics file""" if self.event.calendar_number is None: if self.event.status == Status.IMPORTANT: return Color.IMPORTANT @@ -916,6 +914,7 @@ def main(stdscr) -> None: weather.load_from_wttr() screen = Screen(stdscr, cf) + # Initialise loaders: event_loader_csv = EventLoaderCSV(cf) task_loader_csv = TaskLoaderCSV(cf) event_loader_ics = EventLoaderICS(cf) @@ -923,6 +922,7 @@ def main(stdscr) -> None: birthday_loader = BirthdayLoader(cf) holiday_loader = HolidayLoader(cf) + # Load the data: user_events = event_loader_csv.load() user_tasks = task_loader_csv.load() user_ics_events = event_loader_ics.load() @@ -930,6 +930,7 @@ def main(stdscr) -> None: holidays = holiday_loader.load() birthdays = birthday_loader.load() + # Initialise savers and importers: event_saver_csv = EventSaverCSV(user_events, cf) task_saver_csv = TaskSaverCSV(user_tasks, cf) importer = Importer(user_tasks, user_events, cf) diff --git a/calcure/calendars.py b/calcure/calendars.py index 12a93ab..9f74f3c 100644 --- a/calcure/calendars.py +++ b/calcure/calendars.py @@ -2,9 +2,8 @@ import enum import datetime -from itertools import repeat - import jdatetime +from itertools import repeat def convert_to_persian_date(year, month, day): diff --git a/calcure/colors.py b/calcure/colors.py index 6fb3b16..856a5c7 100644 --- a/calcure/colors.py +++ b/calcure/colors.py @@ -83,13 +83,13 @@ def initialize_colors(cf): if not cf.MINIMAL_DAYS_INDICATOR: curses.init_pair(Color.DAYS.value, curses.COLOR_BLACK, cf.COLOR_DAYS) + # Assign color pair for each ics resourse: if cf.ICS_EVENT_FILES is None: return - # Assign color pair for each ics file: for index in range(len(cf.ICS_EVENT_FILES)): if index < len(cf.COLOR_ICS_CALENDARS): - color = cf.COLOR_ICS_CALENDARS[index] + color = cf.COLOR_ICS_CALENDARS[index] # Take colors from config else: - color = cf.COLOR_EVENTS + color = cf.COLOR_ICS_CALENDARS[-1] # Remaining resourses assume the last assigned color curses.init_pair(Color.ICS_CALENDARS0.value + index, color, cf.COLOR_BACKGROUND) diff --git a/calcure/loaders.py b/calcure/loaders.py index 6a42453..d16190e 100644 --- a/calcure/loaders.py +++ b/calcure/loaders.py @@ -233,26 +233,48 @@ def read_lines(self, file): previous_line = line return text - def read_file(self, filename): - """Parse the file or url from user config""" + def read_file(self, path): + """Parse an ics file if it exists""" + if not os.path.exists(path): + logging.error("Failed to load %s. Probably path is incorrect.", path) + return "" + with open(path, 'r', encoding="utf-8") as file: + return self.read_lines(file) + + def read_url(self, path): + """Parse an ics URL if it exists and networks works""" + try: + with urllib.request.urlopen(path) as response: + file = io.TextIOWrapper(response, 'utf-8') + return self.read_lines(file) + except urllib.error.HTTPError: + logging.error("Failed to load %s. Probably url is wrong.", path) + return "" + except urllib.error.URLError: + logging.error("Failed to load %s. Probably no internet connection.", path) + return "" + + def read_resource(self, path): + """Determine type of the resourse, parse it, and return list of strings for each file""" + ics_files = [] # If it's a URL, try to load it: - if filename.startswith('http'): - try: - with urllib.request.urlopen(filename) as response: - file = io.TextIOWrapper(response, 'utf-8') - return self.read_lines(file) - - except urllib.error.HTTPError: - logging.error("Failed to load %s. Probably url is wrong.", filename) - return "" - except urllib.error.URLError: - logging.error("Failed to load %s. Probably no internet connection.", filename) - return "" + if path.startswith('http'): + ics_files.append(self.read_url(path)) + return ics_files # If it's a local file, read it: - with open(filename, 'r', encoding="utf-8") as file: - return self.read_lines(file) + if path.endswith('.ics'): + ics_files.append(self.read_file(path)) + return ics_files + + # Otherwise, assume it's a folder, and read every file inside: + for root, directories, files in os.walk(path): + for filename in files: + # Get the full path to the file + file_path = os.path.join(root, filename) + ics_files.append(self.read_file(file_path)) + return ics_files class TaskLoaderICS(LoaderICS): @@ -266,59 +288,56 @@ def __init__(self, cf): def load(self): """Load tasks from each of the ics files""" - # Quit if the file is not specified: + # Quit if the files are not specified in config: if self.ics_task_files is None: return self.user_ics_tasks for calendar_number, filename in enumerate(self.ics_task_files): - # Quit if file does not exists: - if not os.path.exists(filename) and not filename.startswith('http'): - logging.error("Failed to load %s as it does not seem to exist.", filename) - return self.user_ics_tasks - - ics_text = self.read_file(filename) - - # Try parcing ics file: - try: - cal = ics.Calendar(ics_text) - except NotImplementedError: # More than one calendar in the file - logging.error("Failed to load %s.", filename) - return self.user_ics_tasks - - - for task in cal.todos: - if task.status != "CANCELLED": - task_id = self.user_ics_tasks.generate_id() - - # Assign status from priority: - status = Status.NORMAL - if task.priority is not None: - if task.priority > 5: - status = Status.UNIMPORTANT - if task.priority < 5: - status = Status.IMPORTANT - - # Correct according to status: - if task.status == "COMPLETED": - status = Status.DONE - - name = task.name - - # Try reading task due date: - try: - year = task.due.year - month = task.due.month - day = task.due.day - except AttributeError: - year, month, day = 0, 0, 0 - - timer = Timer([]) - is_private = False - - # Add task: - new_task = Task(task_id, name, status, timer, is_private, year, month, day, calendar_number) - self.user_ics_tasks.add_item(new_task) + # For each resourse from config, load a list that has one or more ics files: + ics_files = self.read_resource(filename) + for ics_file in ics_files: + + # Try parcing content of the ics file: + try: + cal = ics.Calendar(ics_file) + except NotImplementedError: # More than one calendar in the file + logging.error("Failed to load %s.", filename) + return self.user_ics_tasks + + for task in cal.todos: + if task.status != "CANCELLED": + task_id = self.user_ics_tasks.generate_id() + + # Assign status from priority: + status = Status.NORMAL + if task.priority is not None: + if task.priority > 5: + status = Status.UNIMPORTANT + if task.priority < 5: + status = Status.IMPORTANT + + # Correct according to status: + if task.status == "COMPLETED": + status = Status.DONE + + name = task.name + + # Try reading task due date: + try: + year = task.due.year + month = task.due.month + day = task.due.day + except AttributeError: + year, month, day = 0, 0, 0 + + timer = Timer([]) + is_private = False + + # Add task: + new_task = Task(task_id, name, status, timer, is_private, + year, month, day, calendar_number) + self.user_ics_tasks.add_item(new_task) return self.user_ics_tasks @@ -334,54 +353,52 @@ def __init__(self, cf): def load(self): """Load events from each of the ics files""" - # Quit if the file is not specified: + # Quit if the files are not specified in config: if self.ics_event_files is None: return self.user_ics_events for calendar_number, filename in enumerate(self.ics_event_files): - # Quit if file does not exists: - if not os.path.exists(filename) and not filename.startswith('http'): - logging.error("Failed to load %s as it does not seem to exist.", filename) - return self.user_ics_events + # For each resourse from config, load a list that has one or more ics files: + ics_files = self.read_resource(filename) + for ics_file in ics_files: - ics_text = self.read_file(filename) + # Try parcing content of the ics file: + try: + cal = ics.Calendar(ics_file) + except NotImplementedError: # More than one calendar in the file + logging.error("Failed to load %s.", filename) + return self.user_ics_events - # Try parcing ics file: - try: - cal = ics.Calendar(ics_text) - except NotImplementedError: # More than one calendar in the file - logging.error("Failed to load %s.", filename) - return self.user_ics_events + for index, event in enumerate(cal.events): - for index, event in enumerate(cal.events): - - # Default parameters: - event_id = index - repetition = '1' - frequency = Frequency.ONCE - status = Status.NORMAL - is_private = False + # Default parameters: + event_id = index + repetition = '1' + frequency = Frequency.ONCE + status = Status.NORMAL + is_private = False - # Parameters of the event from ics if they exist: - name = event.name if event.name is not None else "" - all_day = event.all_day if event.all_day is not None else True - year = event.begin.year if event.begin else 0 - month = event.begin.month if event.begin else 1 - day = event.begin.day if event.begin else 1 + # Parameters of the event from ics if they exist: + name = event.name if event.name is not None else "" + all_day = event.all_day if event.all_day is not None else True + year = event.begin.year if event.begin else 0 + month = event.begin.month if event.begin else 1 + day = event.begin.day if event.begin else 1 - # Add start time to name of non-all-day events: - if not all_day: - hour = event.begin.hour if event.begin else 0 - minute = event.begin.minute if event.begin else 0 - name = f"{hour:0=2}:{minute:0=2} {name}" + # Add start time to name of non-all-day events: + if not all_day: + hour = event.begin.hour if event.begin else 0 + minute = event.begin.minute if event.begin else 0 + name = f"{hour:0=2}:{minute:0=2} {name}" - # Convert to persian date if needed: - if self.use_persian_calendar: - year, month, day = convert_to_persian_date(year, month, day) + # Convert to persian date if needed: + if self.use_persian_calendar: + year, month, day = convert_to_persian_date(year, month, day) - # Add event: - new_event = UserEvent(event_id, year, month, day, name, repetition, frequency, status, is_private, calendar_number) - self.user_ics_events.add_item(new_event) + # Add event: + new_event = UserEvent(event_id, year, month, day, name, repetition, + frequency, status, is_private, calendar_number) + self.user_ics_events.add_item(new_event) return self.user_ics_events