refactor(add project module folder; added more logs):
This commit is contained in:
parent
0c187d6684
commit
38a6fcbed6
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
*.pyc
|
||||||
|
log
|
||||||
|
include/
|
||||||
|
bin/
|
||||||
|
lib/
|
||||||
|
*.egg-info
|
@ -0,0 +1 @@
|
|||||||
|
.. readme
|
@ -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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
269
apps/grocy.py
269
apps/grocy.py
@ -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')
|
|
72
cli.py
72
cli.py
@ -1,46 +1,43 @@
|
|||||||
import click
|
import click
|
||||||
from pydoc import importfile, ErrorDuringImport
|
from pkg_resources import iter_entry_points
|
||||||
import yaml
|
import yaml
|
||||||
from sys import exit
|
from sys import exit
|
||||||
from shutil import copy
|
from shutil import copy
|
||||||
from os import path, chmod, makedirs
|
from os import path, chmod, makedirs
|
||||||
import stat
|
from taskw import TaskWarriorShellout
|
||||||
|
import logging
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
APP_NAME = 'twservices'
|
APP_NAME = 'twservices'
|
||||||
SAMPLE_CONFIG_FILE = 'sample.config.yml'
|
SAMPLE_CONFIG_FILE = 'sample.config.yml'
|
||||||
CONFIG_FILE = 'config.yml'
|
CONFIG_FILE = 'config.yml'
|
||||||
CONFIG_DIR = click.get_app_dir(APP_NAME)
|
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():
|
def __create_config_file():
|
||||||
user_cfg_file = path.join(CONFIG_DIR, CONFIG_FILE)
|
user_cfg_file = path.join(CONFIG_DIR, CONFIG_FILE)
|
||||||
sample_cfg_file = path.join(PROJ_DIR, SAMPLE_CONFIG)
|
sample_cfg_file = path.join(PROJ_DIR, SAMPLE_CONFIG_FILE)
|
||||||
if not os.path.exists(CONFIG_DIR):
|
if not path.exists(CONFIG_DIR):
|
||||||
print('Config {} director does not exist, create...'.format(CONFIG_DIR))
|
print('Config {} director does not exist, create...'.format(CONFIG_DIR))
|
||||||
makedirs(CONFIG_DIR)
|
makedirs(CONFIG_DIR)
|
||||||
|
|
||||||
copy(sample_cfg_file, user_cfg_file)
|
copy(sample_cfg_file, user_cfg_file)
|
||||||
print('Copying {} to {}'.format(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
|
return user_cfg_file
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
|
||||||
@click.group()
|
@click.group()
|
||||||
def main():
|
def main():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_config_file()
|
|
||||||
no_config_msg = 'A config file was not found ' +
|
def get_config_file():
|
||||||
'and will be created under {0}. Is that Ok?'.format(click.get_app_dir(APP_NAME))
|
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')
|
cfg_file = path.join(click.get_app_dir(APP_NAME), 'config.yml')
|
||||||
if not path.exists(cfg_file):
|
if not path.exists(cfg_file):
|
||||||
create_config_app_dir = click.confirm(no_config_msg)
|
create_config_app_dir = click.confirm(no_config_msg)
|
||||||
@ -51,35 +48,42 @@ def get_config_file()
|
|||||||
return cfg_file
|
return cfg_file
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@main.command()
|
@main.command()
|
||||||
def sync():
|
def sync():
|
||||||
cfg_file = get_config_file()
|
cfg_file = get_config_file()
|
||||||
fd = open(cfdg_file);
|
fd = open(cfg_file)
|
||||||
parse_cfg_file = yaml.safe_load(fd)
|
parse_cfg_file = yaml.safe_load(fd)
|
||||||
taskwarrior_cfg = parse_cfg_file['taskwarrior']
|
taskwarrior_cfg = parse_cfg_file['taskwarrior']
|
||||||
services = parse_cfg_file['services']
|
services = parse_cfg_file['services']
|
||||||
|
|
||||||
log_cfg = parse_cfg_file['logger']
|
log_cfg = parse_cfg_file['logger']
|
||||||
log_level = 'info' if not 'level'in log_cfg else log_cfg['level']
|
log_level = 'info' if 'level' not in log_cfg else log_cfg['level']
|
||||||
log_filename = 'log' if not 'file' in log_cfg else log_cfg['file']
|
log_filename = 'log' if 'file'not in log_cfg else log_cfg['file']
|
||||||
|
|
||||||
|
|
||||||
logging.basicConfig(level=log_level, filname=log_filename
|
|
||||||
|
|
||||||
|
|
||||||
|
logging.basicConfig(level=log_level, filename=log_filename)
|
||||||
|
|
||||||
for service in services:
|
for service in services:
|
||||||
try:
|
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)
|
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()
|
ServiceInstance.sync()
|
||||||
except ErrorDuringImport as e:
|
except BaseException as e:
|
||||||
print(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)
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
SERVICES = ['grocy']
|
|
||||||
|
|
||||||
def getListOfServicesNames():
|
|
||||||
return SERVICES
|
|
@ -1,11 +1,13 @@
|
|||||||
logger:
|
logger:
|
||||||
level: debug
|
level: DEBUG
|
||||||
|
file_location: '~/.config/twservices/log'
|
||||||
taskwarrior:
|
taskwarrior:
|
||||||
rc: ~/.taskrc
|
rc: ~/.taskrc
|
||||||
services:
|
services:
|
||||||
- name: 'grocy'
|
- name: grocy
|
||||||
api: 'https://aerex.me/grocy/api'
|
api: 'https://aerex.me/grocy/api'
|
||||||
token: 'McaeCf5FrT9Sqr96tPcZg9l4uUCexR1fGVGIfDR6qNQxsWECpv'
|
token: 'McaeCf5FrT9Sqr96tPcZg9l4uUCexR1fGVGIfDR6qNQxsWECpv'
|
||||||
|
resolution: tw
|
||||||
tags:
|
tags:
|
||||||
- grocy
|
- grocy
|
||||||
|
|
||||||
|
29
setup.py
29
setup.py
@ -1,32 +1,37 @@
|
|||||||
import os
|
import os
|
||||||
from setuptools import setup
|
from setuptools import setup, find_packages
|
||||||
f = open('README.rst')
|
f = open('README.rst')
|
||||||
|
|
||||||
long_description = f.read().strip()
|
long_description = f.read().strip()
|
||||||
long_description = long_description.split('readme', 1)[1]
|
long_description = long_description.split('readme', 1)[1]
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
|
|
||||||
def read(fname):
|
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(
|
setup(
|
||||||
name = "taskwarrior-services",
|
name='twservices',
|
||||||
version = "0.0.1",
|
version='0.0.1',
|
||||||
author = "Aerex",
|
author='Aerex',
|
||||||
author_email = "aerex@aerex.me",
|
author_email='aerex@aerex.me',
|
||||||
description = ("A plugin to create, delete, and modify tasks across various services "),
|
description=('A plugin to create, delete, and modify tasks across various services'),
|
||||||
keywords = "taskwarrior, grocy",
|
keywords='taskwarrior, grocy',
|
||||||
url = "http://packages.python.org/an_example_pypi_project",
|
url='http://packages.python.org/an_example_pypi_project',
|
||||||
packages=['requests', 'pyyaml', 'taskw', 'pytz', 'click'],
|
packages=find_packages(),
|
||||||
install_requires=['Click'],
|
install_requires=['Click', 'pyyaml', 'requests', 'taskw'],
|
||||||
long_description=read('README'),
|
long_description=read('README.rst'),
|
||||||
classifiers=[
|
classifiers=[
|
||||||
"Development Status :: 3 - Alpha",
|
"Development Status :: 3 - Alpha",
|
||||||
"Topic :: Utilities",
|
"Topic :: Utilities",
|
||||||
"License :: OSI Approved :: BSD License",
|
"License :: OSI Approved :: BSD License",
|
||||||
],
|
],
|
||||||
entry_points="
|
entry_points='''
|
||||||
[console_scripts]
|
[console_scripts]
|
||||||
twservices=cli:main
|
twservices=cli:main
|
||||||
|
[twservices.apps]
|
||||||
|
grocy=twservices.apps.grocy:Grocy
|
||||||
|
''',
|
||||||
|
|
||||||
)
|
)
|
||||||
|
96
twservices/apps/__init__.py
Normal file
96
twservices/apps/__init__.py
Normal file
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
308
twservices/apps/grocy.py
Normal file
308
twservices/apps/grocy.py
Normal file
@ -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)
|
Loading…
Reference in New Issue
Block a user