class CsvFileImporter(Importer): logger = AppLogger("csv_file_importer").get_logger() def __init__(self, tasks): super().__init__(tasks) self.__tasks = tasks def get_tasks_by_id(self, remote_task): return self.__tasks.get_task_by_id(remote_task.unique_id) @staticmethod def convert(obj_list: list) -> List[Task]: task_list = list() for obj_dict in obj_list: task = Task(obj_dict["text"]) for key, value in obj_dict.items(): if key == "deleted": value = ast.literal_eval(value) task.deleted = value elif key == "due_date": task.due_date.date_string = value elif key == "done": value = ast.literal_eval(value) task.due_date.completed = value else: setattr(task, key, value) task_list.append(task) return task_list
class TaskKeyError(IndexError): logger = AppLogger("task_key_error").get_logger() msg = "task.key cannot be found" def __init__(self): super().__init__(self.msg) self.logger.error(self.msg)
class Snapshots: logger = AppLogger("snapshots").get_logger() def __init__(self, tasks: Tasks): self.__tasks = tasks self.__task_list = [] @staticmethod def __summarize(task_list: list) -> Snapshot: snapshot = Snapshot() for task in task_list: if task.deleted: snapshot.deleted += 1 elif task.is_completed(): snapshot.completed += 1 elif not task.is_completed(): snapshot.incomplete += 1 snapshot.due_date = task.due_date.date_string snapshot.count = len(task_list) return snapshot def get_snapshot(self) -> tuple: if len(self.__task_list) > 0: # snapshot object has index and due_date that are not needed # for the summary because there is always one object. summary = self.__summarize(self.__task_list) snapshot_list = [] due_date_list = list( set([task.due_date.date_string for task in self.__task_list])) for index, due_date in enumerate(due_date_list, start=1): task_list = self.__tasks.get_tasks_by_date(due_date) snapshot = self.__summarize(task_list) snapshot.index = index snapshot_list.append(snapshot) return summary, sorted(snapshot_list, key=lambda sn: sn.due_date) else: return Snapshot(), list() def count_all_tasks(self): self.__task_list = self.__tasks.get_object_list() def count_tasks_by_due_date_range(self, min_date: str, max_date: str): self.__task_list = self.__tasks.get_tasks_within_date_range( min_date, max_date) def count_tasks_by_due_date(self, due_date: str): self.__task_list = self.__tasks.get_tasks_by_date(due_date) def count_tasks_by_project(self, project_name: str): self.__task_list = self.__tasks.get_tasks_by_project(project_name)
class File: logger = AppLogger("file").get_logger() def __init__(self): self.__output_dir = CommonVariables().resources_dir @staticmethod def get_timestamp(): return time.strftime(CommonVariables().file_name_timestamp) def open(self, path): if os.path.exists(path): self.logger.info(f"Reading file {path}") return self.read_file(path) else: raise FileNotFoundError(f"Path {path} does not exist") def save(self, obj_list) -> str: if obj_list is not None: assert type(obj_list) is list file_name = self.get_filename() path = self.__make_path(self.__output_dir, file_name) if os.path.exists(self.__output_dir): self.write_file(path, obj_list) self.logger.info(f"Saved results to {path}") return path else: raise FileNotFoundError(f"Path {path} does not exist") @staticmethod def __make_path(file_path, file_name) -> str: if str(file_path).endswith("/"): path = f"{file_path}{file_name}" else: path = f"{file_path}/{file_name}" return path @abstractmethod def write_file(self, path, task_list): pass @abstractmethod def read_file(self, path): pass @abstractmethod def get_filename(self): pass
class DatabaseManager: def __init__(self, variables=None): self.logger = AppLogger("database_manager").get_logger() if variables is None: self.variables = CommonVariables() else: self.variables = variables def get_database(self): if self.variables.enable_redis: redis_db = RedisDatabase(self.variables.redis_host, self.variables.redis_port) if redis_db.exists(): self.logger.debug("Connecting to redis") return redis_db else: self.logger.info( "Failed to connect to redis. Check redis host and port") return JsonFileDatabase() def get_tasks_model(self): return Tasks(self.get_database())
class Converter: logger = AppLogger("converter").get_logger() @staticmethod def rfc3339_to_date_string(rfc3339_string) -> str: dt = datetime.strptime(rfc3339_string, CommonVariables().rfc3339_date_time_format) return Day(dt).to_date_string() @staticmethod def date_string_to_rfc3339(date_string) -> str: variables = CommonVariables() if len(date_string) > 0: dt = datetime.strptime(date_string, variables.date_format) return dt.strftime(variables.rfc3339_date_time_format) return date_string
class Importer: logger = AppLogger("importer").get_logger() def __init__(self, tasks): self.__tasks = tasks @abstractmethod def get_tasks_by_id(self, remote_task): pass def import_tasks(self, remote_task_list) -> SyncResultsList: """ Manage task import from Google Tasks Service :param remote_task_list: :return ImportResultsList: """ assert type(remote_task_list) is list sync_results = SyncResultsList() for remote_task in remote_task_list: assert type(remote_task) is Task local_task = self.get_tasks_by_id(remote_task) action = ImportAction(local_task, remote_task) if action.can_delete(): self.__tasks.delete(local_task.unique_id) sync_results.append(SyncAction.DELETED) elif action.can_update(): self.__tasks.replace(local_task, remote_task) sync_results.append(SyncAction.UPDATED) elif action.can_insert(): self.__tasks.append(remote_task) sync_results.append(SyncAction.ADDED) else: sync_results.append(SyncAction.SKIPPED) self.logger.debug(f"Skipping local task {remote_task.text}") return sync_results
class Rules: """ Aggregates rule objects and retrieves the result from multiple rules. It also provides an effective way to debug the result of import and export rules. """ logger = AppLogger("rules").get_logger() def __init__(self, action, state): self.action = action self.state = state self.rule_list = list() self.summary_list = list() def add(self, rule): assert type(rule) is Rule self.rule_list.append(rule) def get_result(self): for rule in self.rule_list: if rule.rule_result is False: return False return True @staticmethod def decompose(task): if task is not None: return dict(task) return 'None' def print_summary(self, local_task, remote_task, rule_result): for rule in self.rule_list: self.summary_list.append(dict(rule)) self.logger.debug( f"action: {self.action}, state: {self.state}, rule_result: {rule_result}, " f"summary: {self.summary_list}, local_task: " f"{self.decompose(local_task)}, remote_task: {self.decompose(remote_task)}" )
def __init__(self, variables=None): self.logger = AppLogger("database_manager").get_logger() if variables is None: self.variables = CommonVariables() else: self.variables = variables
class Tasks(Model): """ Main entry point for querying and managing local tasks. """ logger = AppLogger("tasks").get_logger() def __init__(self, database): super().__init__(database, Task()) self.__calendar = Calendar() self.__vars = CommonVariables() self.__date_generator = DateGenerator() def add(self, text, label, project, date_expression) -> List[Task]: assert type(text) and type(label) and type(project)\ and type(date_expression) is str task_list = list() if self.__date_generator.validate_input(date_expression): for due_date in self.__date_generator.get_due_dates( date_expression): task = Task(text) task.label = label task.project = project task.date_expression = date_expression task.due_date = due_date self.append(task) task_list.append(task) else: raise AttributeError( f"Provided date expression {date_expression} is invalid") return task_list def append(self, obj: Task): assert isinstance(obj, Task) return self.append_object(obj) def contains_due_date_range(self, task, min_date_string, max_date_string): assert isinstance(task, Task) assert type(min_date_string) and type(max_date_string) is str min_day = Day( datetime.strptime(min_date_string, self.__vars.date_format)) max_day = Day( datetime.strptime(max_date_string, self.__vars.date_format)) if len(task.due_date.date_string) > 0: day = Day( datetime.strptime(task.due_date.date_string, self.__vars.date_format)) if min_day.to_date_time() < day.to_date_time( ) < max_day.to_date_time(): return task def get_task(self, func) -> Task: for task in self.get_object_list(): if func(task) is not None: self.logger.debug( f"Retrieved task by index: {task.index}, text: {task.text}" ) return task def get_task_list(self): return sorted(self.get_object_list(), key=lambda task: task.due_date.date_string) def get_tasks_containing_text(self, value) -> List[Task]: """ Selects all tasks that with a text value that contain the provided value :param value: :return: list of Task """ assert type(value) is str return [ task for task in self.get_task_list() if str(value).lower() in str(task.text).lower() ] def get_tasks_matching_text(self, value) -> List[Task]: """ Selects all tasks with a text value that matches the provided value :param value: :return: """ assert type(value) is str return [ task for task in self.get_task_list() if str(value.lower() == str(task.text).lower()) ] def get_task_by_index(self, index: int) -> Task: assert type(index) is int return self.get_task(lambda task: task if task.index == index else None) def get_task_by_external_id(self, external_id: int) -> Task: assert type(external_id) is str return self.get_task(lambda task: task if task.external_id == external_id else None) def get_task_by_id(self, task_id: str) -> Task: assert type(task_id) is str return self.get_task(lambda task: task if task.unique_id == task_id else None) def get_task_by_name(self, task_name: str) -> Task: assert type(task_name) is str return self.get_task(lambda task: task if task.text == task_name else None) def get_tasks_by_date(self, date_expression: str) -> List[Task]: assert type(date_expression) is str task_list = list() for due_date in self.__date_generator.get_due_dates(date_expression): for task in self.get_task_list(): if task.due_date.date_string == due_date.date_string: task_list.append(task) return task_list def get_tasks_within_date_range(self, min_date_string: str, max_date_string: str) -> List[Task]: assert type(min_date_string) is str assert type(max_date_string) is str return [ task for task in self.get_task_list() if self.contains_due_date_range( task, min_date_string, max_date_string) ] def get_tasks_by_status(self, is_completed: bool) -> List[Task]: assert type(is_completed) is bool if is_completed: return [ task for task in self.get_task_list() if task.is_completed() ] else: return [ task for task in self.get_task_list() if not task.is_completed() ] def get_tasks_by_project(self, project: str) -> List[Task]: assert type(project) is str return self.get_list_by_type("project", project) def get_tasks_by_label(self, label: str) -> List[Task]: assert type(label) is str return self.get_list_by_type("label", label) def get_filtered_list(self) -> List[Task]: return [task for task in self.get_task_list() if not task.deleted] def delete(self, task_id: str) -> Task: assert type(task_id) is str task = self.get_task_by_id(task_id) if task is not None: task.deleted = True self.replace_object(task.index, task) return task else: raise TaskKeyError() def undelete(self, task_id: str) -> Task: assert type(task_id) is str task = self.get_task_by_id(task_id) if task is not None: task.deleted = False self.replace_object(task.index, task) return task else: raise TaskKeyError() def complete(self, task_id: str) -> Task: assert type(task_id) is str task = self.get_task_by_id(task_id) if task is not None: task.complete() self.replace_object(task.index, task) return task else: raise TaskKeyError() def reset(self, task_id: str) -> Task: """ Resets the due date to today on the selected task :param task_id: :return: """ assert type(task_id) is str task = self.get_task_by_id(task_id) if task is not None: due_date = DueDate() due_date.completed = False due_date.date_string = Today().to_date_string() task.due_date = due_date self.replace_object(task.index, task) return task else: raise TaskKeyError() def replace(self, local_task: Task, remote_task: Task) -> Task: assert isinstance(remote_task, Task) assert isinstance(local_task, Task) remote_task.index = local_task.index remote_task.unique_id = local_task.unique_id remote_task.due_date = local_task.due_date self.replace_object(local_task.index, remote_task) self.logger.debug( f"Replaced local_task: {dict(local_task)} with remote_task: {dict(remote_task)}" ) return remote_task def edit(self, index: int, text: str, label: str, project: str, date_expression: str) -> Task: task = self.get_task_by_index(index) if task is not None: task.text = text task.project = project task.label = label due_date = self.__date_generator.get_due_date(date_expression) if due_date is not None: task.date_expression = date_expression task.due_date.date_string = due_date.date_string self.replace_object(task.index, task) return task else: raise TaskKeyError() def reschedule(self, today) -> None: assert type(today) is Today or Day task_list = self.get_task_list() for task in task_list: if self.__calendar.is_past( task.due_date, today ) and task.due_date.completed is False and task.deleted is False: task.due_date.date_string = today.to_date_string() self.update_objects(task_list) def get_list_by_type(self, parameter_name: str, value: str, task_list=None) -> list: """ Returns list of tasks when a task parameter (ie. project, text, label) matches a single value. :param parameter_name: :param value: :param task_list: :return: """ assert type(parameter_name) is str assert type(value) is str if task_list is None: task_list = self.get_task_list() else: assert type(task_list) is list return list( filter(lambda t: getattr(t, parameter_name) == value, task_list)) def __sort(self, parameter_name: str) -> list: assert type(parameter_name) is str return [ t for t in sorted(self.get_task_list(), key=lambda t: getattr(t, parameter_name)) ] def get_label_list(self) -> List[str]: return self.unique("label", self.get_task_list()) def get_project_list(self) -> List[str]: return self.unique("project", self.get_task_list()) def get_due_date_list(self) -> List[str]: return list( set([task.due_date.date_string for task in self.get_task_list()])) @staticmethod def unique(parameter_name: str, task_list: list) -> List[str]: assert type(parameter_name) is str assert type(task_list) is list unique_set = set([getattr(task, parameter_name) for task in task_list]) return sorted(list(unique_set)) def clear(self): self.clear_objects()
class Client: """ Base client facade that that provides access to all the application features. It integrates the import/export, tasks, snapshot, common variables, and date generator classes. It also makes it possible to support additional clients. Currently only the console client is supported, but a rest api could also extend this class. """ logger = AppLogger("client").get_logger() def __init__(self, db_manager): assert isinstance(db_manager, DatabaseManager) self.tasks = db_manager.get_tasks_model() self.__date_generator = DateGenerator() self.__variables = CommonVariables() @abstractmethod def display_tasks(self, task_list: list): pass @abstractmethod def display_snapshots(self, snapshots: Snapshots, **kwargs): pass @abstractmethod def display_invalid_index_error(self, index: int): pass def edit_task(self, index: int, text: str, label: str, project: str, date_expression: str) -> List[Task]: """ Edits an existing task by replacing string values. None are allowed and handled by the Task object. :param index: integer starting at 0 :param text: text string describing the task :param label: label name of the task :param project: project name of the task :param date_expression: Must be one of [today, tomorrow, m-s, every *, month / day, etc]. :return: Task """ assert type(index) is int assert type(text) is str assert type(project) is str assert type(label) is str assert type(date_expression) is str try: if self.__date_generator.validate_input(date_expression) is False: self.logger.info( f"Provided due date {date_expression} is invalid") else: task = self.tasks.edit(index, text, label, project, date_expression) return self.display_tasks([task]) except TaskKeyError: self.display_invalid_index_error(index) def get_task(self, task_index: int) -> List[Task]: task = self.tasks.get_task_by_index(int(task_index)) return self.display_tasks([task]) def filter_tasks_by_today(self) -> List[Task]: date_string = Today().to_date_string() task_list = self.tasks.get_tasks_by_date(date_string) return self.display_tasks(task_list) def filter_tasks_by_due_date(self, date_string: str) -> List[Task]: task_list = self.tasks.get_tasks_by_date(date_string) return self.display_tasks(task_list) def filter_tasks_by_due_date_range(self, min_date: str, max_date: str) -> List[Task]: task_list = self.tasks.get_tasks_within_date_range(min_date, max_date) return self.display_tasks(task_list) def filter_tasks_by_status(self, status: str) -> List[Task]: assert status in ["incomplete", "complete"] if status == "incomplete": task_list = self.tasks.get_tasks_by_status(is_completed=False) else: task_list = self.tasks.get_tasks_by_status(is_completed=True) return self.display_tasks(task_list) def filter_tasks_by_project(self, project: str) -> List[Task]: assert type(project) is str task_list = self.tasks.get_tasks_by_project(project) return self.display_tasks(task_list) def filter_tasks_by_label(self, label: str) -> List[Task]: assert type(label) is str task_list = self.tasks.get_tasks_by_label(label) return self.display_tasks(task_list) def filter_tasks_by_text(self, text: str) -> List[Task]: assert type(text) is str task_list = self.tasks.get_tasks_containing_text(text) return self.display_tasks(task_list) def group_tasks_by_project(self) -> List[Task]: task_list = list() for project in self.get_unique_project_list(): for task in self.tasks.get_tasks_by_project(project): task_list.append(task) return self.display_tasks(task_list) def group_tasks_by_due_date(self) -> List[Task]: task_list = list() for due_date_string in self.__get_unique_due_date_list(): for task in self.tasks.get_tasks_by_date(due_date_string): task_list.append(task) return self.display_tasks(task_list) def group_tasks_by_label(self) -> List[Task]: task_list = list() for label in self.get_unique_label_list(): for task in self.tasks.get_tasks_by_label(label): task_list.append(task) return self.display_tasks(task_list) def get_unique_label_list(self) -> List[str]: """Returns a list of labels from the tasks.""" return self.tasks.get_label_list() def get_unique_project_list(self) -> List[str]: """Returns list of project names from the tasks. """ return self.tasks.get_project_list() def __get_unique_due_date_list(self) -> List[str]: """Returns list of due_date strings from the tasks.""" return self.tasks.get_due_date_list() def count_all_tasks(self, **kwargs) -> List[Snapshot]: snapshots = Snapshots(self.tasks) snapshots.count_all_tasks() return self.display_snapshots(snapshots, **kwargs) def count_tasks_by_due_date_range(self, min_date: str, max_date: str, **kwargs) -> List[Snapshot]: assert type(min_date) and type(max_date) is str snapshots = Snapshots(self.tasks) snapshots.count_tasks_by_due_date_range(min_date, max_date) return self.display_snapshots(snapshots, **kwargs) def count_tasks_by_due_date(self, due_date: str, **kwargs) -> List[Snapshot]: assert type(due_date) is str snapshots = Snapshots(self.tasks) snapshots.count_tasks_by_due_date(due_date) return self.display_snapshots(snapshots, **kwargs) def count_tasks_by_project(self, project_name: str, **kwargs) -> List[Snapshot]: assert type(project_name) is str snapshots = Snapshots(self.tasks) snapshots.count_tasks_by_project(project_name) return self.display_snapshots(snapshots, **kwargs) def reschedule_tasks(self, today=Today()): self.tasks.reschedule(today) def remove_all_tasks(self): self.tasks.clear() def set_default_variables(self, **kwargs): """ Sets the defaults variables to the variables.ini file. :param variable_dict: Contains key value pairs matching the properties in Variables class :return: None """ assert type(kwargs) is dict for key, value in kwargs.items(): if hasattr(self.__variables, key): setattr(self.__variables, key, value) @staticmethod def get_duration(start_datetime): """ Gets a formatted time string using the provided datetime object :param start_datetime: :return: time string """ end_datetime = datetime.now() total_seconds = (end_datetime - start_datetime).total_seconds() hours, remainder = divmod(total_seconds, 3600) minutes, seconds = divmod(remainder, 60) return '{:02}m:{:02}s'.format(int(minutes), int(seconds)) @staticmethod def get_variables_list(): return dict(CommonVariables()).items()
class GenericDatabase(ABC): """Generic base class to support Yaml, Json file databases and redis.""" logger = AppLogger("database").get_logger() def __init__(self): self.resources_dir = CommonVariables().resources_dir @abstractmethod def initialize(self, obj): """ Executes initialization activities such as constructing the file name if it is a file database or selects the logical redis database :param obj: Object that subclasses DatabaseObject :return: None """ pass @abstractmethod def replace(self, obj): """ Replaces specific object using obj.index. :param index: integer :param obj: Object that subclasses DatabaseObject :return: obj """ pass @abstractmethod def append(self, obj): """ Adds the object to end of the list of objects and assigns a new index id based on the number of existing objects. Objects are preserved using a soft delete method. Only the deleted parameter is changed from False to True. The index id is unique and should not be re-used. :param obj: :return: obj """ pass @abstractmethod def set(self, obj_list): """ Acts on objects in a list. It replaces an object when the index matches OR adds to the end of the list when the index does not match an existing one. :param obj_list: :return: """ pass @abstractmethod def get(self) -> List[dict]: """ Converts a stored database object (ie. json object) to a python dictionary. Use the to_object_list method to convert the dictionary to an object that implements the DatabaseObject class. :return: """ pass @abstractmethod def exists(self) -> bool: pass @abstractmethod def clear(self): pass @staticmethod def contains_dict(dict_list) -> bool: if dict_list is not None: assert type(dict_list) is list if len(dict_list) >= 1: assert type(dict_list[0]) is dict return True return False @staticmethod def contains_database_object(obj_list) -> bool: if obj_list is not None: assert type(obj_list) is list if len(obj_list) >= 1: if isinstance(obj_list[0], DatabaseObject): return True else: raise ValueError( "Provided obj must subclass DatabaseObject") return False @staticmethod def to_object_list(dict_list, obj) -> List: if dict_list is not None: assert isinstance(obj, DatabaseObject) assert GenericDatabase.contains_dict(dict_list) obj_list = list() for obj_dict in dict_list: obj = copy(obj) obj = obj.deserialize(obj_dict) obj_list.append(obj) return obj_list else: return [] @staticmethod def to_dict_list(obj_list) -> List: if obj_list is not None: assert GenericDatabase.contains_database_object(obj_list) try: return [dict(obj) for obj in obj_list] except TypeError as ex: GenericDatabase.logger.error( f"{ex}. Provided obj must contain __iter__ method") raise ex return [] @staticmethod def to_json(obj): assert isinstance(obj, DatabaseObject) try: return json.dumps(dict(obj)) except TypeError as ex: GenericDatabase.logger.error( f"{ex}. Provided obj must contain __iter__ method") raise ex @staticmethod def get_last_updated() -> str: return datetime.strftime(datetime.now(), "%Y-%m-%d %H:%M:%S") def get_file_path(self, obj, file_ext) -> str: assert isinstance(obj, DatabaseObject) path = f"{self.resources_dir}{str(obj.object_name).lower()}_db.{file_ext}" if not self.file_exists(path): os.makedirs(self.resources_dir, 0o777, exist_ok=True) return path @staticmethod def file_exists(path) -> bool: return os.path.exists(path) def remove_file(self, path): try: if self.file_exists(path): os.remove(path) except FileNotFoundError as ex: self.logger.error(f"Cannot remove database file {ex}") raise @staticmethod def get_list_index(obj, dict_list): assert type(dict_list) is list assert isinstance(obj, DatabaseObject) index_list = [ index for index, obj_dict in enumerate(dict_list, start=0) if obj_dict["index"] == obj.index ] if len(index_list) == 1: return index_list[0] @staticmethod def set_unique_id(obj) -> DatabaseObject: """ A unique id should only be assigned once when the object is appended or replaced. :param obj: Object that subclasses DatabaseObject :return: """ assert isinstance(obj, DatabaseObject) if obj.unique_id is None: obj.unique_id = uuid.uuid4().hex return obj @staticmethod def get_last_index(dict_list) -> int: if dict_list is not None: count = len(dict_list) if count > 0: return count + 1 else: return 1
class RedisExperimentDatabase(GenericDatabase): """ Manages saving and retrieving objects to a redis database. Retrieve method should be called before save to maintain a consistent state. """ logger = AppLogger("redis_experiment_database").get_logger() def __init__(self, host, port): super().__init__() self.host = host self.port = port self.db = None def build_db_list(self): # Max number of keys in a redis logical databases is 16. Scan all # logical databases to identify what object belongs in each. db_list = [] for index in range(0, 16): db = redis.Redis(host=self.host, port=self.port, db=index) keys = db.keys("1:*") if len(keys) == 1: key = str(keys[0].decode("utf-8")).lstrip("1:") db_list.append((db, key)) else: db_list.append((db, None)) return db_list def initialize(self, obj, test_mode=False): """ Retrieves the redis db id from the object so it can be saved or updated. :param obj: Object that subclasses DatabaseObject :param test_mode: Unsupported :return: None """ self.db = redis.Redis(host=self.host, port=self.port, db=obj.get_redis_db_id()) @staticmethod def __key(obj): return f"{obj.index}:{obj.object_name}" def replace(self, obj): assert isinstance(obj, DatabaseObject) assert obj.index > 0 self.set([obj]) def append(self, obj): assert isinstance(obj, DatabaseObject) self.set([obj]) def set(self, obj_list): """ Updates or inserts new objects that inherit from DatabaseObject. When key matches existing key an update occurs, but if there is no match then a new key is created. :param obj_list: List containing class objects :return: None """ assert self.contains_database_object(obj_list) if self.exists(): for obj in obj_list: if not self.db.exists(self.__key(obj)): obj.index = self.get_last_index(self.db.keys()) obj = self.set_unique_id(obj) obj.last_updated = self.get_last_updated() self.db.hset(self.__key(obj), "last_updated", obj.last_updated) self.db.hset(self.__key(obj), "data", self.to_json(obj)) def get(self) -> List[dict]: if self.db is not None: return [ json.loads(self.db.hget(key, "data")) for key in sorted(self.db.keys()) ] else: return [] def exists(self): db = redis.Redis(host=self.host, port=self.port, db=0) return db.ping() def clear(self): for db_item in self.build_db_list(): for key in db_item[0].keys(): db_item[0].delete(key)
class JsonFileDatabase(GenericDatabase): """ Manages saving and retrieving tasks as dict object to a json file located in the resource directory. """ logger = AppLogger("json_file_database").get_logger() def __init__(self): super().__init__() self.path = None def initialize(self, obj): """ Constructs the db file path :param obj: Class object that implements DatabaseObject :return: None """ self.path = self.get_file_path(obj, "json") def replace(self, obj): assert isinstance(obj, DatabaseObject) assert obj.index > 0 dict_list = self.get() if dict_list is not None: index = self.get_list_index(obj, dict_list) if index is not None: obj = self.set_unique_id(obj) obj.last_updated = self.get_last_updated() dict_list[index] = dict(obj) self.save(dict_list) def append(self, obj): assert isinstance(obj, DatabaseObject) dict_list = self.get() if dict_list is not None: obj.index = self.get_last_index(dict_list) obj = self.set_unique_id(obj) obj.last_updated = self.get_last_updated() dict_list.append(dict(obj)) self.save(dict_list) else: obj.index = 1 obj = self.set_unique_id(obj) obj.last_updated = self.get_last_updated() self.save([dict(obj)]) def save(self, dict_list): with open(self.path, 'w') as outfile: self.logger.debug("Saved json database") ujson.dump(dict_list, outfile) def set(self, obj_list): self.save(self.to_dict_list(obj_list)) def get(self) -> List[dict]: if self.exists(): self.logger.debug("Retrieved json database") with open(self.path, 'r') as infile: return ujson.load(infile) return [] def exists(self) -> bool: return self.file_exists(self.path) def clear(self): if self.exists(): self.remove_file(self.path)
class CliClient(Client): """ Provides cli specific features. """ logger = AppLogger("cli_client").get_logger() def __init__(self, db_manager, file_manager): super().__init__(db_manager) assert isinstance(file_manager, FileManager) self.__file_manager = file_manager self.task_table = TaskConsoleTable() self.snapshot_list_table = SnapshotListConsoleTable() self.snapshot_summary_table = SnapshotSummaryConsoleTable() self.variables_table = VariableConsoleTable() def display_tasks(self, task_list): return self.__print_tasks_table(task_list) def display_snapshots(self, snapshots: Snapshots, **kwargs): summary, snapshot_list = snapshots.get_snapshot() self.__print_snapshot_summary_table(summary) return self.__print_snapshot_list_table(snapshot_list, kwargs.get("page")) def list_labels(self): """ Lists all labels contained in the tasks :return: """ print("Labels: {}".format(self.get_unique_label_list())) def list_projects(self): """ Lists all projects contained in the tasks :return: """ print("Projects: {}".format(self.get_unique_project_list())) def list_default_variables(self): for key, value in self.get_variables_list(): self.variables_table.add_row([key, value]) self.variables_table.print() def __print_tasks_table(self, task_list): self.task_table.clear() for task in task_list: self.task_table.add_row(task) return self.task_table.print() def __print_snapshot_summary_table(self, summary: Snapshot): CliClient.logger.info("Snapshot summary:") self.snapshot_summary_table.clear() self.snapshot_summary_table.add_row(summary) return self.snapshot_summary_table.print() def __print_snapshot_list_table(self, snapshot_list: list, page_number: int): assert type(snapshot_list) is list assert type(page_number) is int if snapshot_list: self.snapshot_list_table.clear() pager = Pager(snapshot_list).assemble() page = pager.get_page(page_number) snapshots = pager.get_items(page) self.logger.info("Snapshot list:") for snapshot in snapshots: self.snapshot_list_table.add_row(snapshot) snapshot_list = self.snapshot_list_table.print() self.logger.info( f"Displayed {len(snapshots)} items - Page {page_number} of {pager.get_page_count()}" ) return snapshot_list def export_tasks(self, task_list): self.__file_manager.save_tasks(task_list) def export_snapshots(self, snapshot_list: list): self.__file_manager.save_snapshots(snapshot_list) def import_tasks(self, csv_file_importer: CsvFileImporter, path: str): """ Imports tasks from the Csv file. :param csv_file_importer: CsvFileImporter class :param path: path to csv file :return: None """ assert isinstance(csv_file_importer, CsvFileImporter) assert type(path) is str start_datetime = datetime.now() self.logger.info(f"Starting import") obj_list = self.__file_manager.open_tasks(path) self.logger.info(f"Retrieved {len(obj_list)} tasks from file") task_list = csv_file_importer.convert(obj_list) sync_results = csv_file_importer.import_tasks(task_list) self.logger.info(f"Import summary: {sync_results.get_summary()}") self.logger.info( f"Import complete: Duration: {self.get_duration(start_datetime)}") def add_task(self, text: str, label: str, project: str, date_expression: str) -> List[Task]: """ Adds a task :param text: text string describing the task :param label: label for the task :param project: project name for the task :param date_expression: Must be one of [today, tomorrow, m-s, every *, month / day, etc]. For complete list see the expression_lists in handler objects in date_generator.py :return: list of Task """ assert type(text) is str assert type(label) is str assert type(project) is str assert type(date_expression) is str try: task_list = self.tasks.add(text, label, project, date_expression) if len(task_list) == 1: self.logger.info(f"Added task #{task_list[0].index}") else: self.logger.info(f"Added {len(task_list)} tasks") return task_list except AttributeError as ex: self.logger.info(ex) def display_invalid_index_error(self, index: int): assert type(index) is int self.logger.info(f"Provided index {index} is invalid") def delete_tasks(self, index_tuple: tuple) -> list: assert type(index_tuple) is tuple results = list() for index in index_tuple: task = self.tasks.get_task_by_index(index) if task is not None: results.append(self.tasks.delete(task.unique_id)) else: self.display_invalid_index_error(index) return results def complete_tasks(self, index_tuple: tuple) -> list: """ Changes the completed status in the DueDate object :param index_tuple: int tuple :return: list """ assert type(index_tuple) is tuple results = list() for index in index_tuple: task = self.tasks.get_task_by_index(index) if task is not None: if task.is_completed() is False: results.append(self.tasks.complete(task.unique_id)) else: self.display_invalid_index_error(index) return results def undelete_tasks(self, index_tuple: tuple) -> list: assert type(index_tuple) is tuple results = list() for index in index_tuple: task = self.tasks.get_task_by_index(index) if task is not None: results.append(self.tasks.undelete(task.unique_id)) else: self.display_invalid_index_error(index) return results def reset_tasks(self, index_tuple: tuple) -> list: """ Copies tasks from the past into the present. :param index_tuple: int tuple :return: list """ assert type(index_tuple) is tuple results = list() for index in index_tuple: task = self.tasks.get_task_by_index(index) if task is not None: results.append(self.tasks.reset(task.unique_id)) else: self.display_invalid_index_error(index) return results def list_all_tasks(self, display_all: bool = False) -> List[Task]: if display_all: task_list = self.tasks.get_object_list() else: task_list = [ task for task in self.tasks.get_object_list() if not task.deleted ] return self.display_tasks(task_list)