From 0c187d6684d533d00222e343bee0617b024332fb Mon Sep 17 00:00:00 2001 From: Aerex Date: Sat, 8 Dec 2018 21:19:17 -0600 Subject: [PATCH] feat: added impl code to merge from and to grocy --- apps/__init__.py | 3 +- apps/grocy.py | 202 ++++++++++++++++++++++++++++++++++++++++------ cli.py | 14 +++- sample.config.yml | 4 + 4 files changed, 195 insertions(+), 28 deletions(-) diff --git a/apps/__init__.py b/apps/__init__.py index f70615e..874c15e 100644 --- a/apps/__init__.py +++ b/apps/__init__.py @@ -1,7 +1,6 @@ import requests class TaskService(object): - def __init__(self, config): - self.config = config + def __init__(self, **entries): def findAll(): raise NotImplementedError() diff --git a/apps/grocy.py b/apps/grocy.py index dd19ac6..c991d80 100644 --- a/apps/grocy.py +++ b/apps/grocy.py @@ -1,45 +1,69 @@ from twservices.apps import TaskService +from sys import exit +import twservices.Taskwarrior from datetime import datetime from twservices.apps import RestService -GROCY_API_KEY_HEADER = 'GROCY_API_KEY' +import logging +log = logging.getLogger(__name__) -# Endpoints -GROCY_FIND_ALL_TASKS_ENDPOINT = '/get-objects/tasks' -GROCY_DELTE_TASK_ENDPOINT = '/delete-object/tasks/{0}' -GROCY_ADD_TASK_ENDPOINT = '/add-object/tasks' class Grocy(TaskService): + GROCY_API_KEY_HEADER = 'GROCY_API_KEY' - def __init__(self, config): - if config['api'] == None: - raise Exception('api url for Grocy is not defined') - # TODO: check if api is a url - self.api = config['api'] + # Endpoints + GROCY_FIND_ALL_TASKS_ENDPOINT = '/get-objects/tasks' + GROCY_DELTE_TASK_ENDPOINT = '/delete-object/tasks/{0}' + GROCY_ADD_TASK_ENDPOINT = '/add-object/tasks' - if config['token'] == None: - raise Exception('token for Grocy is not defined') - self.token = config['token'] - self.rest_service = self.__initRestService() + # UDA + GROCY_ID = 'grocy_id' + UDAS = { + GROCY_ID: { + 'type': 'numeric', + 'label': 'Grocy Task ID' + } + } - def __convertToGrocyTask(self, task): + def __init__(self, **entries): + super(entries) + self.tw = None + + def get_udas(): + return UDAS + + def set_taskwarrior(tw): + self.tw = tw + + def get_grocy_tw_tasks(): + if(not self.tw): + raise TaskwarriorError('taskwarrior instance not defined') + + return tw.filter_tasks({ + 'and': [('%s.any' % key, None) for key in list(UDA.keys())], + 'or': [ ('status', 'pending'), ('status', 'waiting') ] + }) + + def to_grocy(self, task): grocy_task = {} - grocy_task['id'] = int(task['id']) + + if GROCY_ID in task + grocy_task['id'] = task[GROCY_ID] - if task['description']: - grocy_task['name'] = str(task['description']) + if 'description' in task: + grocy_task['name'] = task['description'] - if task['annotations'] + if 'annotations' in task: grocy_task['description'] = '' for note in task['annotations']: grocy_task['description'] += '{}\n'.format(note) - if task['entry'] and type(task['entry']) is datettime: + if 'entry' in task and type(task['entry']) is datettime: grocy_task['row_created_timestamp'] = task['entry'].format('%Y-%m-%d') - if task['due'] and type(task['due']) is datetime: + if 'due' in task and type(task['due']) is datetime: grocy_task['due_date'] = task['due'].format('%Y-%m-%d') - if task['done']: + if 'done' in task: grocy_task['done'] = task['done'] return grocy_task @@ -86,7 +110,7 @@ class Grocy(TaskService): try: for next_task in tasks: - self.__convertToGrocyTask(next_task) + self.to_grocy(next_task) response = self.rest_service.post(GROCY_ADD_TASK_ENDPOINT, task) responses.append(response) except Exception as e: @@ -105,7 +129,7 @@ class Grocy(TaskService): tasks = task try: for next_task in tasks: - self.__convertToGrocyTask(next_task) + self.to_grocy(next_task) response = self.rest_service.patch(GROCY_MODIFY_TASK_ENDPOINT, task['id'], task) responses.append(response) except Exception as e: @@ -113,5 +137,133 @@ class Grocy(TaskService): raise e return responses - # TODO: do this part next + def sort_by_grocy_id(task_a, task_b): + if 'GROCY_ID' in task_a and 'GROCY_ID' in task_b: + return task_a['GROCY_ID'] - task_b['GROCY_ID'] + elif 'id' in task_a and 'id' in task_b: + return task_a['id'] - task_b['id'] + else: + return 0 + + + def merge_from_tw(self, taskwarrior_tasks, grocy_tasks): + + # clone taskwarrior_tasks for processing + remaining_grocy_tasks_to_process = grocy_tasks[:] + + # Convert to taskwarrior tasks into grocy tasks + converted_tw_to_grocy_tasks = [] + modified_grocy_tasks = [] + for task in taskwarrior_tasks: + converted_tw_to_grocy_tasks.append(self.to_grocy(task)) + + # Sort grocy tasks and converted taskwarrior tasks by GROCY_ID + converted_tw_to_grocy_tasks.sort(self.sort_by_grocy_id) + grocy_tasks.sort(self.sort_by_grocy_id) + + # Iterate through grocy tasks to find any conflicts + for index in range(len(converted_tw_to_grocy_tasks)): + grocy_task = grocy_tasks[index] + converted_tw_to_grocy_task = converted_tw_to_grocy_tasks[index] + finished_merge_process = index > len(remaining_grocy_tasks_to_process) - 1 + # Add taskwarrior task to grocy if no more grocy tasks to process + if finished_merge_process: + tasks_to_add_to_grocy.append(converted_tw_to_grocy_task) + log.debug('Added grocy task %s to taskwarrior task', grocy_task['id']) + continue + + + # Merge from grocy into taskwarrior if match found + # Delete taskwarrior task + if self.should_merge(converted_tw_to_grocy_task, grocy_task): + try: + modified_grocy_tasks.append(converted_tw_to_grocy_task) + log.debug('Merged taskwarrior task %s to grocy task %s', converted_tw_to_grocy_task['uuid'], grocy_task['id']) + del remaining_grocy_tasks_to_process[index] + except TaskwarriorError as e: + log.exception('Could not update task: %s', % e.stderr) + ## Add taskwarrior task into taskwarrior + else: + tasks_to_add_to_grocy(converted_tw_to_grocy_task) + log.debug('Added grocy task %s to taskwarrior task', grocy_task['id']) + + # Add any remaining taskwarrior tasks not found in grocy into grocy + for index in range(len(converted_tw_to_grocy_tasks)): + tw_task = self.to_taskwarrior(converted_tw_to_grocy_tasks[index]) + tw.task_add(tw_task) + + # Send requests to Grocy service + self.modify(modified_grocy_tasks) + self.add(tasks_to_add_to_grocy) + + + def merge_into_tw(self, taskwarrior_tasks, grocy_tasks): + # Convert to taskwarrior tasks into grocy tasks + converted_tw_to_grocy_tasks = [] + for task in taskwarrior_tasks: + converted_tw_to_grocy_tasks.append(self.to_grocy(task)) + + # Sort grocy tasks and converted taskwarrior tasks by GROCY_ID + converted_tw_to_grocy_tasks.sort(self.sort_by_grocy_id) + grocy_tasks.sort(self.sort_by_grocy_id) + + # Iterate through grocy tasks to find any conflicts + for index in range(len(grocy_tasks)): + grocy_task = grocy_tasks[index] + converted_tw_to_grocy_task = converted_tw_to_grocy_tasks[index] + current_index_exceeded_tw_list = index > len(converted_tw_to_grocy_tasks) - 1 + # Add if no more taskwarrior tasks to process + if current_index_exceeded_tw_list: + try: + tw.task_add(convert_tw_task) + log.debug('Added grocy task %s to taskwarrior task', grocy_task['id']) + except TaskwarriorError as e: + log.exception('Could not add task: %s', % e.stderr) + + continue + + + # Merge from grocy into taskwarrior if match found + # Delete taskwarrior task + if self.should_merge(converted_tw_to_grocy_task, grocy_task): + try: + converted_grocy_task_to_tw = self.to_taskwarrior(grocy_task) + tw.update(converted_grocy_task_to_tw) + log.debug('Merged grocy task %s to taskwarrior task %s', grocy_task['id'], converted_grocy_task_to_tw['uuid']) + del converted_tw_to_grocy_tasks[index] + except TaskwarriorError as e: + log.exception('Could not update task: %s', % e.stderr) + ## Add grocy task into taskwarrior + else: + try: + tw.task_add(convert_tw_task) + log.debug('Added grocy task %s to taskwarrior task', grocy_task['id']) + except TaskwarriorError as e: + log.exception('Could not add task: %s', % e.stderr) + + # Add any remaining taskwarrior tasks not found in grocy into grocy + for index in range(len(convert_tw_task)): + tw_task = self.to_taskwarrior(converted_tw_to_grocy_tasks[index]) + tw.task_add(tw_task) + + def sync(): + # Get all tasks from taskwarrior + # Get all tasks from grocy + # Push to taskwarrior + taskwarrior_tasks = self.get_grocy_tw_tasks() + + try: + grocy_tasks = self.findAll() + except BaseException as e: + logger.error(e) + logger.error('Could not get all grocy tasks') + + try: + if self.resolution == 'tw': + self.merge_from_tw(taskwarrior_tasks, grocy_tasks) + else: + self.merge_into_tw(taskwarrior_tasks, grocy_task) + except BaseException as e: + print(e) + print('Could not sync') diff --git a/cli.py b/cli.py index a621732..8512da0 100644 --- a/cli.py +++ b/cli.py @@ -1,7 +1,7 @@ import click from pydoc import importfile, ErrorDuringImport import yaml -from site import exit +from sys import exit from shutil import copy from os import path, chmod, makedirs import stat @@ -57,11 +57,23 @@ def sync(): cfg_file = get_config_file() fd = open(cfdg_file); parse_cfg_file = yaml.safe_load(fd) + taskwarrior_cfg = parse_cfg_file['taskwarrior'] services = parse_cfg_file['services'] + + log_cfg = parse_cfg_file['logger'] + log_level = 'info' if not 'level'in log_cfg else log_cfg['level'] + log_filename = 'log' if not 'file' in log_cfg else log_cfg['file'] + + + logging.basicConfig(level=log_level, filname=log_filename + + + for service in services: try: ServiceClass = importfile('{}/apps/{}.py'.format(PROJ_DIR, service['name']) ServiceInstance = ServiceClass(**service) + ServiceClass.setTaskwarrior(tw) ServiceInstance.sync() except ErrorDuringImport as e: print(e) diff --git a/sample.config.yml b/sample.config.yml index dd99b7f..973a75f 100644 --- a/sample.config.yml +++ b/sample.config.yml @@ -1,7 +1,11 @@ logger: level: debug +taskwarrior: + rc: ~/.taskrc services: - name: 'grocy' api: 'https://aerex.me/grocy/api' token: 'McaeCf5FrT9Sqr96tPcZg9l4uUCexR1fGVGIfDR6qNQxsWECpv' + tags: + - grocy