diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a9c7c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.pyc +log +include/ +bin/ +lib/ +*.egg-info diff --git a/README.rst b/README.rst index e69de29..152ecb0 100644 --- a/README.rst +++ b/README.rst @@ -0,0 +1 @@ +.. readme diff --git a/apps/__init__.py b/apps/__init__.py deleted file mode 100644 index 874c15e..0000000 --- a/apps/__init__.py +++ /dev/null @@ -1,86 +0,0 @@ -import requests -class TaskService(object): - def __init__(self, **entries): - - def findAll(): - raise NotImplementedError() - - def modify(): - raise NotImplementedError() - -class RestService(object): - def __init__(self, api_url, json): - self.api_url = api_url - self.headers = {} - self.json - - def get(self, path): - api_url = self.api_url - - if api_url.endswith('/'): - url = '{0}{1}'.format(api_url, path) - else - url = '{0}/{1}'.format(api_url, path) - - - r = requests.get(url, headers=self.headers) - - if self.json: - - - return r.json() - - return r.content - - def delete(self, id): - api_url = self.api_url - - if api_url.endswith('/'): - url = '{0}{1}'.format(api_url, path) - else - url = '{0}/{1}'.format(api_url, path) - - - r = requests.get(url, headers=self.headers) - - if self.json: - - - return r.json() - - return r.content - - def post(self, data): - api_url = self.api_url - - if api_url.endswith('/'): - url = '{0}{1}'.format(api_url, path) - else - url = '{0}/{1}'.format(api_url, path) - - - r = requests.post(url, data, headers=self.headers) - - if self.json: - - - return r.json() - - return r.content - - - def addHeader(self, type, value): - self.header[type] = value - - - - - - - - - - - - - diff --git a/apps/grocy.py b/apps/grocy.py deleted file mode 100644 index c991d80..0000000 --- a/apps/grocy.py +++ /dev/null @@ -1,269 +0,0 @@ -from twservices.apps import TaskService -from sys import exit -import twservices.Taskwarrior -from datetime import datetime -from twservices.apps import RestService - -import logging -log = logging.getLogger(__name__) - -class Grocy(TaskService): - GROCY_API_KEY_HEADER = 'GROCY_API_KEY' - - # Endpoints - GROCY_FIND_ALL_TASKS_ENDPOINT = '/get-objects/tasks' - GROCY_DELTE_TASK_ENDPOINT = '/delete-object/tasks/{0}' - GROCY_ADD_TASK_ENDPOINT = '/add-object/tasks' - - # UDA - GROCY_ID = 'grocy_id' - UDAS = { - GROCY_ID: { - 'type': 'numeric', - 'label': 'Grocy Task ID' - } - } - - 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 = {} - - if GROCY_ID in task - grocy_task['id'] = task[GROCY_ID] - - if 'description' in task: - grocy_task['name'] = task['description'] - - if 'annotations' in task: - grocy_task['description'] = '' - for note in task['annotations']: - grocy_task['description'] += '{}\n'.format(note) - - if 'entry' in task and type(task['entry']) is datettime: - grocy_task['row_created_timestamp'] = task['entry'].format('%Y-%m-%d') - - if 'due' in task and type(task['due']) is datetime: - grocy_task['due_date'] = task['due'].format('%Y-%m-%d') - - if 'done' in task: - grocy_task['done'] = task['done'] - - return grocy_task - - - def __initRestService(self): - if self.api.startswith == '/': - api_url = self.api_url[1:] - if self.api.endswith == '/': - api_url = - pruned_api_url = self.api - rest_service = RestService(self.api, json=True) - rest_service.addHeader('Authorization', self.token) - return rest_service - - def findAll(self): - # Get all tasks as json - try: - response = self.rest_service.get(GROCY_FIND_ALL_TASKS_ENDPOINT) - except Exception as e: - print(str(e)) - raise e - return response - - def delete(self, id): - if not id: - raise Exception('id is not defined') - # Delete a task - try: - response = self.rest_service.get(GROCY_DELTE_TASK_ENDPOINT, id) - except Exception as e: - print(str(e)) - raise e - return response - - def add(self, task): - responses = [] - if not task: - raise Exception('task is not defined') - if not type(task) is list: - tasks = [task] - else: - tasks = task - - try: - for next_task in tasks: - self.to_grocy(next_task) - response = self.rest_service.post(GROCY_ADD_TASK_ENDPOINT, task) - responses.append(response) - except Exception as e: - print(str(e)) - raise e - return responses - - - def modify(self, task); - response = [] - if not task: - raise Exception('task is not defined') - if not type(task) is list: - tasks = [task] - else: - tasks = task - try: - for next_task in tasks: - self.to_grocy(next_task) - response = self.rest_service.patch(GROCY_MODIFY_TASK_ENDPOINT, task['id'], task) - responses.append(response) - except Exception as e: - print(str(e)) - raise e - return responses - - 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 8512da0..45a3d4a 100644 --- a/cli.py +++ b/cli.py @@ -1,46 +1,43 @@ import click -from pydoc import importfile, ErrorDuringImport +from pkg_resources import iter_entry_points import yaml from sys import exit -from shutil import copy +from shutil import copy from os import path, chmod, makedirs -import stat +from taskw import TaskWarriorShellout +import logging + +log = logging.getLogger(__name__) APP_NAME = 'twservices' SAMPLE_CONFIG_FILE = 'sample.config.yml' CONFIG_FILE = 'config.yml' CONFIG_DIR = click.get_app_dir(APP_NAME) -PROJ_DIR = os.path.dirname(os.path.realpath(__file__)) +PROJ_DIR = path.join(path.dirname(path.realpath(__file__)), APP_NAME) + def __create_config_file(): user_cfg_file = path.join(CONFIG_DIR, CONFIG_FILE) - sample_cfg_file = path.join(PROJ_DIR, SAMPLE_CONFIG) - if not os.path.exists(CONFIG_DIR): + sample_cfg_file = path.join(PROJ_DIR, SAMPLE_CONFIG_FILE) + if not path.exists(CONFIG_DIR): print('Config {} director does not exist, create...'.format(CONFIG_DIR)) makedirs(CONFIG_DIR) copy(sample_cfg_file, user_cfg_file) print('Copying {} to {}'.format(sample_cfg_file, user_cfg_file)) - chmod(user_cfg_file, 0o664) - + chmod(user_cfg_file, 0o664) return user_cfg_file - - - - - - - -@click.command() @click.group() def main(): pass -def get_config_file() - no_config_msg = 'A config file was not found ' + - 'and will be created under {0}. Is that Ok?'.format(click.get_app_dir(APP_NAME)) + +def get_config_file(): + no_config_msg = 'A config file was not found' + no_config_msg+= ' and will be created under {}. Is that Ok?' + no_config_msg = no_config_msg.format(click.get_app_dir(APP_NAME)) cfg_file = path.join(click.get_app_dir(APP_NAME), 'config.yml') if not path.exists(cfg_file): create_config_app_dir = click.confirm(no_config_msg) @@ -50,36 +47,43 @@ def get_config_file() exit(0) return cfg_file - @main.command() def sync(): cfg_file = get_config_file() - fd = open(cfdg_file); + fd = open(cfg_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 - + log_level = 'info' if 'level' not in log_cfg else log_cfg['level'] + log_filename = 'log' if 'file'not in log_cfg else log_cfg['file'] + logging.basicConfig(level=log_level, filename=log_filename) for service in services: try: - ServiceClass = importfile('{}/apps/{}.py'.format(PROJ_DIR, service['name']) + entry_service_point = iter_entry_points(group='twservices.apps', name=service['name']) + entry_service_point = next(entry_service_point) + except StopIteration as e: + log.error(e) + log.error('Could not import %s service', services['name']) + + try: + ServiceClass = entry_service_point.load() ServiceInstance = ServiceClass(**service) - ServiceClass.setTaskwarrior(tw) + udas = ServiceInstance.get_udas() + tw = TaskWarriorShellout( + config_filename=taskwarrior_cfg['rc'], + config_overrides=udas, + marshal=True, + ) + ServiceInstance.set_taskwarrior(tw) ServiceInstance.sync() - except ErrorDuringImport as e: - print(e) + except BaseException as e: + # TODO: Need to handle base exceptions better + # See if you can print stacktrace + log.exception('Could not sync %s', e) + print(e.stderr) exit(1) - - - - - diff --git a/config.py b/config.py deleted file mode 100644 index 45922bf..0000000 --- a/config.py +++ /dev/null @@ -1,4 +0,0 @@ -SERVICES = ['grocy'] - -def getListOfServicesNames(): - return SERVICES diff --git a/sample.config.yml b/sample.config.yml index 973a75f..a9b24ea 100644 --- a/sample.config.yml +++ b/sample.config.yml @@ -1,11 +1,13 @@ logger: - level: debug + level: DEBUG + file_location: '~/.config/twservices/log' taskwarrior: rc: ~/.taskrc services: - - name: 'grocy' + - name: grocy api: 'https://aerex.me/grocy/api' token: 'McaeCf5FrT9Sqr96tPcZg9l4uUCexR1fGVGIfDR6qNQxsWECpv' + resolution: tw tags: - grocy diff --git a/setup.py b/setup.py index 63ac174..d829639 100644 --- a/setup.py +++ b/setup.py @@ -1,32 +1,37 @@ import os -from setuptools import setup +from setuptools import setup, find_packages f = open('README.rst') long_description = f.read().strip() long_description = long_description.split('readme', 1)[1] f.close() + def read(fname): - return open(os.path.join(os.path.dirname(__file__), fname)).read() + return open(os.path.join(os.path.dirname(__file__), fname)).read() + setup( - name = "taskwarrior-services", - version = "0.0.1", - author = "Aerex", - author_email = "aerex@aerex.me", - description = ("A plugin to create, delete, and modify tasks across various services "), - keywords = "taskwarrior, grocy", - url = "http://packages.python.org/an_example_pypi_project", - packages=['requests', 'pyyaml', 'taskw', 'pytz', 'click'], - install_requires=['Click'], - long_description=read('README'), + name='twservices', + version='0.0.1', + author='Aerex', + author_email='aerex@aerex.me', + description=('A plugin to create, delete, and modify tasks across various services'), + keywords='taskwarrior, grocy', + url='http://packages.python.org/an_example_pypi_project', + packages=find_packages(), + install_requires=['Click', 'pyyaml', 'requests', 'taskw'], + long_description=read('README.rst'), classifiers=[ "Development Status :: 3 - Alpha", "Topic :: Utilities", "License :: OSI Approved :: BSD License", ], - entry_points=" + entry_points=''' [console_scripts] - twservices = cli:main + twservices=cli:main + [twservices.apps] + grocy=twservices.apps.grocy:Grocy + ''', ) diff --git a/twservices/apps/__init__.py b/twservices/apps/__init__.py new file mode 100644 index 0000000..73306ff --- /dev/null +++ b/twservices/apps/__init__.py @@ -0,0 +1,96 @@ +import requests + +import logging +logger = logging.getLogger(__name__) + + +class TaskService(object): + def __init__(self, **entries): + self.__dict__.update(entries) + + def findAll(): + raise NotImplementedError() + + def modify(): + raise NotImplementedError() + + +class RestService(object): + def __init__(self, api_url, json=False): + self.api_url = api_url + self.headers = {} + self.json = json + + def get(self, path): + + if self.api_url.endswith('/'): + url = '{0}{1}'.format(self.api_url, path[1:]) + else: + url = '{0}{1}'.format(self.api_url, path) + + r = requests.get(url, headers=self.headers) + + if r.raise_for_status(): + logger.error(r.raise_for_status()) + raise r.raise_for_status() + + if self.json: + return r.json() + + return r.content + + def delete(self, path, id): + api_url = self.api_url + + if api_url.endswith('/'): + url = '{0}{1}'.format(api_url, path[1:]) + else: + url = '{0}/{1}'.format(api_url, path) + + r = requests.get(url, headers=self.headers) + + if r.raise_for_status(): + logger.error(r.raise_for_status()) + raise r.raise_for_status() + + if self.json: + return r.json() + + return r.content + + def post(self, path, data): + api_url = self.api_url + print(api_url) + + if self.api_url.endswith('/'): + api_url = api_url[1:] + + if path.startswith('/'): + url ='{0}{1}'.format(api_url, path) + else: + url = '{0}/{1}'.format(api_url, path) + + r = requests.post(url, data, headers=self.headers) + + if r.raise_for_status(): + logger.error(r.raise_for_status()) + raise r.raise_for_status() + + if self.json: + return r.json() + + return r.content + + def addHeader(self, type, value): + self.headers[type] = value + + + + + + + + + + + diff --git a/twservices/apps/grocy.py b/twservices/apps/grocy.py new file mode 100644 index 0000000..a27509c --- /dev/null +++ b/twservices/apps/grocy.py @@ -0,0 +1,308 @@ +from taskw.exceptions import TaskwarriorError +from twservices.apps import TaskService +from datetime import datetime +from twservices.apps import RestService + +import logging +logger = logging.getLogger(__name__) + + +class Grocy(TaskService): + API_KEY_HEADER = 'GROCY-API-KEY' + + # Endpoints + FIND_ALL_TASKS_ENDPOINT = '/get-objects/tasks' + DELETE_TASK_ENDPOINT = '/delete-object/tasks/{0}' + ADD_TASK_ENDPOINT = '/add-object/tasks' + MODIFY_TASK_ENDPOINT = '/edit-object/tasks' + # UDA + GROCY_ID = 'grocy_id' + UDAS = { + GROCY_ID: { + 'type': 'string', + 'label': 'Grocy Task ID' + } + } + + def __init__(self, **entries): + super().__init__(**entries) + self.tw = None + self.rest_service = self.__initRestService() + + def get_udas(self): + return { + 'uda': Grocy.UDAS + } + + def set_taskwarrior(self, tw): + self.tw = tw + + def get_grocy_tw_tasks(self): + if not self.tw: + raise TaskwarriorError('taskwarrior instance not defined') + + filters = { + 'and': [('%s.any' % key, None) for key in list(Grocy.UDAS.keys())], + 'or': [('status', 'pending'), ('status', 'waiting')] + } + + tasks = self.tw.filter_tasks(filters) + + return tasks + + def to_grocy(self, task): + print('before the dawn') + print(task) + grocy_task = {} + + if Grocy.GROCY_ID in task: + grocy_task['id'] = task[Grocy.GROCY_ID] + + if 'description' in task: + grocy_task['name'] = task['description'] + + if 'annotations' in task: + grocy_task['description'] = '' + for note in task['annotations']: + grocy_task['description'] += '{}\n'.format(note) + + if 'entry' in task and type(task['entry']) is datetime: + grocy_task['row_created_timestamp'] = task['entry'].strftime('%Y-%m-%d %H:%M:%S') + + if 'due' in task and type(task['due']) is datetime: + grocy_task['due_date'] = task['due'].strftime('%Y-%m-%d') + + if 'done' in task: + grocy_task['done'] = task['done'] + + return grocy_task + + def to_taskwarrior(self, grocy_task): + taskwarrior_task = {} + + if 'id' in grocy_task: + taskwarrior_task[Grocy.GROCY_ID] = grocy_task['id'] + + if 'name' in grocy_task: + taskwarrior_task['description'] = grocy_task['name'] + + if 'description' in grocy_task: + taskwarrior_task['annotations'] = grocy_task['description'].split('\n') + + if 'row_created_timestamp' in grocy_task: + taskwarrior_task['entry'] = grocy_task['row_created_timestamp'] + + if 'due_date' in grocy_task: + taskwarrior_task['due'] = grocy_task['due_date'] + + if 'done' in grocy_task and grocy_task['done'] == '1': + taskwarrior_task['status'] = 'completed' + + return taskwarrior_task + + + def __initRestService(self): + if self.api.startswith == '/': + self.api = self.api[1:] + if self.api.endswith == '/': + self.api = self.api[0:-1] + rest_service = RestService(self.api, json=True) + rest_service.addHeader(Grocy.API_KEY_HEADER, self.token) + return rest_service + + def findAll(self): + if not self.rest_service: + raise Exception('rest service for grocy is not defined') + # Get all tasks as json + try: + response = self.rest_service.get(Grocy.FIND_ALL_TASKS_ENDPOINT) + except BaseException as e: + logger.error(e) + raise e + return response + + def delete(self, id): + if not self.rest_service: + raise Exception('rest service for grocy is not defined') + if not id: + raise Exception('id is not defined') + # Delete a task + try: + response = self.rest_service.get(Grocy.DELETE_TASK_ENDPOINT, id) + except Exception as e: + logger.error(e) + raise e + return response + + def add(self, task): + if not self.rest_service: + raise Exception('rest service for grocy is not defined') + + responses = [] + if not task: + raise Exception('task is not defined') + if not type(task) is list: + tasks = [task] + else: + tasks = task + + try: + for next_task in tasks: + self.to_grocy(next_task) + response = self.rest_service.post(Grocy.ADD_TASK_ENDPOINT, task) + responses.append(response) + except Exception as e: + logger.error(e) + raise e + return responses + + def modify(self, task): + if not self.rest_service: + raise Exception('rest service for grocy is not defined') + response = [] + if not task: + raise Exception('task is not defined') + if not type(task) is list: + tasks = [task] + else: + tasks = task + try: + for next_task in tasks: + modify_endpoint = '{0}/{1}'.format(Grocy.MODIFY_TASK_ENDPOINT, next_task['id']) + response = self.rest_service.post(modify_endpoint, next_task) + except Exception as e: + logger.exception('Could not send post to modify grocy task %s', e.stderr) + + def _should_merge(self, converted_tw_to_grocy_task, grocy_task): + return True if converted_tw_to_grocy_task['id'] == grocy_task['id'] else False + + 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 = [] + tasks_to_add_to_grocy = [] + converted_tw_to_grocy_tasks = [self.to_grocy(task) for task in taskwarrior_tasks] + + # Sort grocy tasks by id and converted taskwarrior tasks by GROCY_ID + grocy_tasks.sort(key=lambda x: x['id']) + converted_tw_to_grocy_tasks.sort(key=lambda x: x['id']) + + # If no taskwarrior tasks add grocy tasks to taskwarrior + if len(converted_tw_to_grocy_tasks) == 0: + convert_grocy_tasks_to_tw = [self.to_taskwarrior(grocy_task) for grocy_task in grocy_tasks] + for task in convert_grocy_tasks_to_tw: + try: + added_task = self.tw.task_add(**task) + logger.debug('Added grocy task %s to taskwarrior', task) + except TaskwarriorError as e: + logger.exception('Unable to add task from grocy: %s', e.stderr) + + # Iterate through grocy tasks to find any conflicts + for index in range(len(converted_tw_to_grocy_tasks)): + + 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) + logger.debug('Added grocy task %s to taskwarrior task', grocy_task['id']) + continue + grocy_task = grocy_tasks[index] + converted_tw_to_grocy_task = converted_tw_to_grocy_tasks[index] + + # Merge from grocy into taskwarrior if match found + # Delete taskwarrior task + if self._should_merge(converted_tw_to_grocy_task, grocy_task): + modified_grocy_tasks.append(converted_tw_to_grocy_task) + logger.debug('Merged taskwarrior task %s to grocy task %s', converted_tw_to_grocy_task['id'], grocy_task['id']) + del remaining_grocy_tasks_to_process[index] + # Add taskwarrior task into taskwarrior + else: + tasks_to_add_to_grocy.append(converted_tw_to_grocy_task) + logger.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]) + try: + self.tw.task_add(**tw_task) + except TaskwarriorError as e: + logger.exception('Could not add task %s', e.stderr) + + # Send requests to Grocy service + if len(modified_grocy_tasks) > 0: + self.modify(modified_grocy_tasks) + if len(tasks_to_add_to_grocy) > 0: + self.add(tasks_to_add_to_grocy) + + def merge_into_tw(self, taskwarrior_tasks, grocy_tasks): + print('here') + # Convert to taskwarrior tasks into grocy tasks + converted_tw_to_grocy_tasks = [] + converted_tw_to_grocy_tasks = [self.to_grocy(task) for task in taskwarrior_tasks] + + # Sort grocy tasks by id and converted taskwarrior tasks by GROCY_ID + grocy_tasks.sort(key=lambda x: x['id']) + converted_tw_to_grocy_tasks.sort(key=lambda x: x[Grocy.GROCY_ID]) + + if len(grocy_tasks) == 0: + self.add(converted_tw_to_grocy_tasks) + + # 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: + self.tw.task_add(converted_tw_to_grocy_task) + logger.debug('Added grocy task %s to taskwarrior task', grocy_task['id']) + except TaskwarriorError as e: + logger.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) + self.tw.update(converted_grocy_task_to_tw) + logger.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: + logger.exception('Could not update task: %s', e.stderr) + # Add grocy task into taskwarrior + else: + try: + self.tw.task_add(converted_grocy_task_to_tw) + logger.debug('Added grocy task %s to taskwarrior task', grocy_task['id']) + except TaskwarriorError as e: + logger.exception('Could not add task: %s', e.stderr) + + # 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]) + self.tw.task_add(tw_task) + + def sync(self): + # 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_tasks) + except TaskwarriorError as e: + logger.exception('Could not sync tasks %s', e.stderr)