diff --git a/scripts/runHabitica-todo.sh b/scripts/oneWaySync.sh similarity index 100% rename from scripts/runHabitica-todo.sh rename to scripts/oneWaySync.sh diff --git a/source/habitica.py b/source/habitica.py index 0c639c0..12544de 100644 --- a/source/habitica.py +++ b/source/habitica.py @@ -1,24 +1,34 @@ '''Habitica related functions''' +# TODO: convert to class that makes all +# calls to Habitica import requests from hab_task import HabTask def get_all_habtasks(auth): #Todoist tasks are, I think, classes. Let's make Habitica tasks classes, too. url = 'https://habitica.com/api/v3/tasks/user/' + # TODO: handle error cases for response response = requests.get(url,headers=auth) - hab_raw = response.json() - hab_tasklist = hab_raw['data'] #FINALLY getting something I can work with... this will be a list of dicts I want to turn into a list of objects with class hab_tasks. Hrm. Weeeelll, if I make a class elsewhere.... - + if response.ok == True: + hab_raw = response.json() + """FINALLY getting something I can work with... this will be a list of + dicts I want to turn into a list of objects with class hab_tasks. + Hrm. Weeeelll, if I make a class elsewhere....""" + hab_tasklist = hab_raw['data'] + else: + hab_tasklist = [] + print(response.reason) + #keeping records of all our tasks - hab_tasks = [] - + hab_tasks = [] + #No habits right now, I'm afraid, in hab_tasks--Todoist gets upset. So we're going to make a list of dailies and todos instead... - for task in hab_tasklist: + for task in hab_tasklist: item = HabTask(task) if item.category == 'reward': pass - elif item.category == 'habit': + elif item.category == 'habit': pass else: hab_tasks.append(item) - return(hab_tasks, response) + return hab_tasks diff --git a/source/main.py b/source/main.py index 15bc5a3..21b913f 100644 --- a/source/main.py +++ b/source/main.py @@ -38,28 +38,28 @@ """ List of utilities and what they do: scroll down for specific things -add_hab_id: +add_hab_id: used to add a new alias (usually the tod ID number) to a habitica task -check_matchDict: +check_matchDict: -check_newMatches: +check_newMatches: clean_matchDict: complete_hab: -delete_hab: +delete_hab: Takes a HabTask object and sends an API call to delete it from the habitica account. get_all_habtasks: get_hab_fromID: Takes an integer, like a tod ID, and calls hab tasks by that alias from API. get_started: - Takes auth document and logs the user into todoist and habitica for active work. + Takes auth document and logs the user into todoist and habitica for active work. get_uniqs: make_daily_from_tod: - Takes a repeating tod task and turns it into a habitica daily. + Takes a repeating tod task and turns it into a habitica daily. make_hab_from_tod: Takes a single tod task and turns it into a habitica todo. make_tod_from_hab: @@ -85,7 +85,7 @@ update_hab_matchDict: update_tod_matchDict: - + write_hab_task: takes HabTask object, writes to habitica API (used to make a new task in habitica) @@ -96,7 +96,7 @@ Small utilities written by me start here. """ -def add_hab_id(tid,hab): +def add_hab_id(tid,hab): import requests import json auth = get_started('auth.cfg') @@ -115,7 +115,7 @@ def check_matchDict(matchDict): print("both undone") elif t.completed == True: print("hab done, tod undone") - else: + else: print("something is wroooong check hab %s" % t) elif matchDict[t].complete == 1: if t.completed == False: @@ -133,7 +133,7 @@ def check_newMatches(matchDict,tod_uniq,hab_uniq): matchesHab = [] matchesTod = [] for tod in tod_uniq: - tid = tod.id + tid = tod.id for hab in hab_uniq: if tod.id == hab.alias: matchDict[tid] = {} @@ -144,7 +144,7 @@ def check_newMatches(matchDict,tod_uniq,hab_uniq): matchesHab.append(hab) hab_uniqest = list(set(hab_uniq) - set(matchesHab)) tod_uniqest = list(set(tod_uniq) - set(matchesTod)) - + for tod_task in tod_uniqest: tid = tod_task.id if tid not in matchDict.keys(): @@ -185,7 +185,7 @@ def complete_hab(hab): hab_dict['completed'] = True data = json.dumps(hab_dict) r = requests.post(headers=auth, url=url, data=data) - return r + return r def delete_hab(hab): import requests @@ -202,16 +202,16 @@ def get_all_habtasks(auth): response = requests.get(url,headers=auth) hab_raw = response.json() hab_tasklist = hab_raw['data'] #FINALLY getting something I can work with... this will be a list of dicts I want to turn into a list of objects with class hab_tasks. Hrm. Weeeelll, if I make a class elsewhere.... - + #keeping records of all our tasks - hab_tasks = [] - + hab_tasks = [] + #No habits right now, I'm afraid, in hab_tasks--Todoist gets upset. So we're going to make a list of dailies and todos instead... - for task in hab_tasklist: + for task in hab_tasklist: item = HabTask(task) if item.category == 'reward': pass - elif item.category == 'habit': + elif item.category == 'habit': pass else: hab_tasks.append(item) @@ -224,8 +224,12 @@ def get_hab_fromID(tid): url = 'https://habitica.com/api/v3/tasks/' url += str(tid) r = requests.get(headers=auth, url=url) - task = r.json() - hab = HabTask(task['data']) + if r.ok == True: + task = r.json() + hab = HabTask(task['data']) + else: + #TODO: log error + hab = HabTask() return hab def get_started(configfile): @@ -264,12 +268,13 @@ def get_started(configfile): return rv def get_uniqs(matchDict,tod_tasks,hab_tasks): +# TODO: Rename this function tod_uniq = [] hab_uniq = [] for tod in tod_tasks: tid = tod.id - if tod.complete: + if tod.is_completed: if tid not in matchDict.keys(): tod_uniq.append(tod) @@ -277,7 +282,7 @@ def get_uniqs(matchDict,tod_tasks,hab_tasks): tid = hab.alias if tid not in matchDict.keys(): hab_uniq.append(hab) - + return tod_uniq, hab_uniq def getNewTodoTasks(matchDict,tod_tasks,hab_tasks): @@ -301,7 +306,7 @@ def make_daily_from_tod(tod): new_hab['text'] = tod.name new_hab['alias'] = tod.id reg = re.compile(r"ev.{0,}(? hab.due.date(): + if lastHab > hab.due.date(): newHab = sync_hab2todo(hab, tod) r = update_hab(newHab) if lastTod != lastHab: @@ -594,6 +600,7 @@ def syncHistories(matchDict): ''' def update_hab(hab): + # TODO: Only update when there are actual changes import requests import json from datetime import datetime @@ -611,9 +618,9 @@ def update_hab(hab): if r.ok == 'No': print(r.text) return r - + def update_hab_matchDict(hab_tasks, matchDict): - from main import delete_hab + from main import delete_hab from main import sync_hab2todo from main import update_hab from dates import parse_date_utc @@ -621,7 +628,7 @@ def update_hab_matchDict(hab_tasks, matchDict): tid_list = [] expired_tids = [] aliasError = [] - for hab in hab_tasks: + for hab in hab_tasks: if 'alias' in hab.task_dict.keys(): try: tid = int(hab.alias) @@ -635,11 +642,11 @@ def update_hab_matchDict(hab_tasks, matchDict): date1 = hab.due.date() except: date1 = '' - try: + try: date2 = matchDict[tid]['hab'].due.date() except: date2 = '' - + if date1 != date2 and matchDict[tid]['recurs'] == 'No': #if the hab I see and the matchDict don't agree... sync to the todoist task print(date1) @@ -687,12 +694,12 @@ def update_tod_matchDict(tod_tasks, matchDict): for tid in list(matchDict): if tid not in tid_list: matchDict.pop(tid) - + return matchDict -def write_hab_task(task): +def write_hab_task(task): """ - writes a task, if inserted, to Habitica API as a todo. + writes a task, if inserted, to Habitica API as a todo. To be added: functionality allowing you to specify things like difficulty """ import requests diff --git a/source/oneWaySync.py b/source/oneWaySync.py index efad63b..542157c 100644 --- a/source/oneWaySync.py +++ b/source/oneWaySync.py @@ -2,7 +2,7 @@ """ One way sync. All the features of todoist-habitrpg; nothing newer or shinier. -Well. Okay, not *technically* one-way--it will sync two way for simple tasks/habitica to-dos, +Well. Okay, not *technically* one-way--it will sync two way for simple tasks/habitica to-dos, just not for recurring todo tasks or dailies. I'm workin' on that. """ @@ -12,18 +12,22 @@ import pickle #import todoist from todoist_api_python.api import TodoistAPI +from todoist_api_python.endpoints import get_sync_url +from todoist_api_python.http_requests import get +from todoist_api_python.models import CompletedItems +from todoist_api_python.models import Task import main import random import json -#from hab_task import HabTask from todo_task import TodTask from datetime import datetime from datetime import timedelta # from dateutil import parser import logging -import configparser +import configparser import config import habitica +import time def get_tasks(token): tasks = [] @@ -34,167 +38,196 @@ def get_tasks(token): print(error) return tasks, api -# todayFilter = todoApi.filters.add('todayFilter', 'today') - -#Telling the site where the config stuff for Habitica can go and get a list of habitica tasks... -auth = config.get_started('auth.cfg') - -#Getting all complete and incomplete habitica dailies and todos -hab_tasks, r1 = habitica.get_all_habtasks(auth) - -# get token for todoist -todoToken = config.getTodoistToken('auth.cfg') - -#Okay, now I need a list of todoist tasks. -todoist_tasks, todoApi = get_tasks(todoToken) # todoist_tasks used to be tod_tasks - -tod_tasks = [] -for i in range(0, len(todoist_tasks)): - tod_tasks.append(TodTask(todoist_tasks[i])) - -# date stuff -today = datetime.now() -today_str = today.strftime("%Y-%m-%d") -one_day = timedelta(days=1) -yesterday = datetime.now() - one_day -yesterday_str = yesterday.strftime("%Y-%m-%d") - -""" -Okay, I want to write a little script that checks whether or not a task is there or not and, if not, ports it. -""" -matchDict = main.openMatchDict() - -#Also, update lists of tasks with matchDict file... -matchDict = main.update_tod_matchDict(tod_tasks, matchDict) -matchDict = main.update_hab_matchDict(hab_tasks, matchDict) - -#We'll want to just... pull all the unmatched completed tasks out of our lists of tasks. Yeah? -tod_uniq, hab_uniq = main.get_uniqs(matchDict, tod_tasks, hab_tasks) - -#Okay, so what if there are two matched tasks in the two uniq lists that really should be paired? -matchDict = main.check_newMatches(matchDict,tod_uniq,hab_uniq) - -#Here anything new in todoist gets added to habitica -tod_uniq = [] -hab_uniq = [] -tod_uniq, hab_uniq = main.getNewTodoTasks(matchDict, tod_tasks, hab_tasks) - -for tod in tod_uniq: - tid = tod.id - if tod.recurring == "Yes": - new_hab = main.make_daily_from_tod(tod) - else: - new_hab = main.make_hab_from_tod(tod) - newDict = new_hab.task_dict - r = main.write_hab_task(newDict) - if r.ok == False: - errMsg = r.json()['errors'][0]['message'] - alias = r.json()['errors'][0]['value'] - print("ERROR: Code: "+str(r.status_code)+", Error message: \"" - +errMsg+"\", Task alias: "+alias) - else: - print("Added hab to %s!" % tod.name) - fin_hab = main.get_hab_fromID(tid) - matchDict[tid] = {} - matchDict[tid]['tod'] = tod - matchDict[tid]['hab'] = fin_hab - matchDict[tid]['recurs'] = tod.recurring - if matchDict[tid]['recurs'] == 'Yes': - if tod.dueToday == 'Yes': - matchDict[tid]['duelast'] = 'Yes' +def dict_to_Task(obj, url): + obj['comment_count'] = obj['note_count'] + obj['is_completed'] = (obj['completed_at'] != '') + obj['created_at'] = "unknown" + obj['creator_id'] = obj['user_id'] + obj['description'] = obj['content'] + obj["priority"] = '' + obj['url'] = url + return Task.from_dict(obj) + +def get_all_completed_items(api): + url = get_sync_url('completed/get_all') + completed_items = get(api._session, url, api._token) + tasks = completed_items['items'] + return [dict_to_Task(obj, url) for obj in tasks] + +def sync_todoist_to_habitica(): + # todayFilter = todoApi.filters.add('todayFilter', 'today') + + #Telling the site where the config stuff for Habitica can go and get a list of habitica tasks... + auth = config.get_started('auth.cfg') + + #Getting all complete and incomplete habitica dailies and todos + hab_tasks = habitica.get_all_habtasks(auth) + + # get token for todoist + todoToken = config.getTodoistToken('auth.cfg') + + #Okay, now I need a list of todoist tasks. + todoist_tasks, todoApi = get_tasks(todoToken) # todoist_tasks used to be tod_tasks + + tod_tasks = [] + for i in range(0, len(todoist_tasks)): + tod_tasks.append(TodTask(todoist_tasks[i])) + + # date stuff + today = datetime.now() + today_str = today.strftime("%Y-%m-%d") + one_day = timedelta(days=1) + yesterday = datetime.now() - one_day + yesterday_str = yesterday.strftime("%Y-%m-%d") + + """ + Okay, I want to write a little script that checks whether or not a task is there or not and, if not, ports it. + """ + matchDict = main.openMatchDict() + + #Also, update lists of tasks with matchDict file... + matchDict = main.update_tod_matchDict(tod_tasks, matchDict) + matchDict = main.update_hab_matchDict(hab_tasks, matchDict) + + #We'll want to just... pull all the unmatched completed tasks out of our lists of tasks. Yeah? + tod_done = [TodTask(task) for task in get_all_completed_items(todoApi)] + tod_uniq, hab_uniq = main.get_uniqs(matchDict, tod_done, hab_tasks) + + #Okay, so what if there are two matched tasks in the two uniq lists that really should be paired? + matchDict = main.check_newMatches(matchDict,tod_uniq,hab_uniq) + + #Here anything new in todoist gets added to habitica + tod_uniq = [] + hab_uniq = [] + tod_uniq, hab_uniq = main.getNewTodoTasks(matchDict, tod_tasks, hab_tasks) + + tod_uniqSize = len(tod_uniq) + for tod in tod_uniq: + tid = tod.id + if tod.recurring == "Yes": + new_hab = main.make_daily_from_tod(tod) + else: + new_hab = main.make_hab_from_tod(tod) + newDict = new_hab.task_dict + + # sleep to stay within rate limits + time.sleep(2) + r = main.write_hab_task(newDict) + if r.ok == False: + #TODO: check ['errors'], due to it sometimes not having it + try: + json = r.json() + except: + print("Unknown json error!") else: - matchDict[tid]['duelast'] = 'No' + errMsg = json['errors'][0]['message'] + alias = json['errors'][0]['value'] + print("Error Code "+str(r.status_code)+": \"" + +errMsg+"\", Task alias: "+alias) else: - matchDict[tid]['duelast'] = 'NA' - -#Check that anything which has recently been completed gets updated in habitica -for tid in matchDict: - tod = matchDict[tid]['tod'] - hab = matchDict[tid]['hab'] - if tod.recurring == 'Yes': - if hab.dueToday == True: - if hab.completed == False: + print("Added hab to %s!" % tod.name) + fin_hab = main.get_hab_fromID(tid) + matchDict[tid] = {} + matchDict[tid]['tod'] = tod + matchDict[tid]['hab'] = fin_hab + matchDict[tid]['recurs'] = tod.recurring + if matchDict[tid]['recurs'] == 'Yes': + if tod.dueToday == 'Yes': + matchDict[tid]['duelast'] = 'Yes' + else: + matchDict[tid]['duelast'] = 'No' + else: + matchDict[tid]['duelast'] = 'NA' + + #Check that anything which has recently been completed gets updated in habitica + for tid in matchDict: + tod = matchDict[tid]['tod'] + hab = matchDict[tid]['hab'] + if tod.recurring == 'Yes': + if hab.dueToday == True: + if hab.completed == False: + if tod.dueToday == 'Yes': + matched_hab = main.sync_hab2todo(hab, tod) + r = main.update_hab(matched_hab) + elif tod.dueToday == 'No': + r = main.complete_hab(hab) + print('Completed daily hab %s' % hab.name) + else: + print("error in daily Hab") + elif hab.completed == True: + if tod.dueToday == 'Yes': + fix_tod = todoApi.items.get_by_id(tid) + # fix_tod.close() + print('fix the tod! TID %s, NAMED %s' %(tid, tod.name)) + elif tod.dueToday == 'No': + continue + else: + print("error, check todoist daily") + elif hab.dueToday == False: + try: + matchDict[tid]['duelast'] + except: + matchDict[tid]['duelast'] = 'No' if tod.dueToday == 'Yes': + matchDict[tid]['duelast'] = 'Yes' #this is me keeping a record of recurring tods being completed or not for some of the complicated bits + if hab.completed == False: + if matchDict[tid]['duelast'] == 'Yes': + if tod.dueToday == 'No': + r = main.complete_hab(hab) + if r.ok == True: + print('Completed Habitica task: %s' % hab.name) + else: + print('Check Habitica ID %s' %tid) + print(r.reason) + matchDict[tid]['duelast'] = 'No' + else: + print("error, check hab daily") + print(hab.id) + elif tod.recurring == 'No': + if tod.complete == 0: + try: + hab.completed + except: + print(tid) + if hab.completed == False: matched_hab = main.sync_hab2todo(hab, tod) r = main.update_hab(matched_hab) - elif tod.dueToday == 'No': + elif hab.completed == True: + fix_tod = todoApi.items.get_by_id(tid) + fix_tod.close() + print('completed tod %s' % tod.name) + else: + print("ERROR: check HAB %s" % tid) + #matchDict.pop(tid) + elif tod.complete == 1: + if hab.completed == False: r = main.complete_hab(hab) - print('Completed daily hab %s' % hab.name) print(r) - else: - print("error in daily Hab") - elif hab.completed == True: - if tod.dueToday == 'Yes': - fix_tod = todoApi.items.get_by_id(tid) -# fix_tod.close() - print('fix the tod! TID %s, NAMED %s' %(tid, tod.name)) - elif tod.dueToday == 'No': + if r.ok == True: + print('Completed hab %s' % hab.name) + else: + print('check hab ID %s' %tid) + print(r.reason) + elif hab.completed == True: continue else: - print("error, check todoist daily") - elif hab.dueToday == False: - try: - matchDict[tid]['duelast'] - except: - matchDict[tid]['duelast'] = 'No' - if tod.dueToday == 'Yes': - matchDict[tid]['duelast'] = 'Yes' #this is me keeping a record of recurring tods being completed or not for some of the complicated bits - if hab.completed == False: - if matchDict[tid]['duelast'] == 'Yes': - if tod.dueToday == 'No': - r = main.complete_hab(hab) - if r.ok == True: - print('Completed hab %s' % hab.name) - else: - print('check hab ID %s' %tid) - print(r.reason) - matchDict[tid]['duelast'] = 'No' - else: - print("error, check hab daily") - print(hab.id) - elif tod.recurring == 'No': - if tod.complete == 0: - try: - hab.completed - except: - print(tid) - if hab.completed == False: - matched_hab = main.sync_hab2todo(hab, tod) - r = main.update_hab(matched_hab) - elif hab.completed == True: - fix_tod = todoApi.items.get_by_id(tid) - fix_tod.close() - print('completed tod %s' % tod.name) - else: - print("ERROR: check HAB %s" % tid) - #matchDict.pop(tid) - elif tod.complete == 1: - if hab.completed == False: - r = main.complete_hab(hab) - print(r) - if r.ok == True: - print('Completed hab %s' % hab.name) - else: - print('check hab ID %s' %tid) - print(r.reason) - elif hab.completed == True: - continue - else: - print("ERROR: check HAB %s" % tid) - else: - print("ERROR: check TOD %s" % tid) - r = [] -# try: -# dueNow = str(parser.parse(matchDict[tid]['tod'].due_date).date()) -# except: -# dueNow = '' -# if dueNow != matchDict[tid]['hab'].date and matchDict[tid]['hab'].category == 'todo': -# matchDict[tid]['hab'].task_dict['date'] = dueNow -# r = main.update_hab(matchDict[tid]['hab']) - -pkl_file = open('oneWay_matchDict.pkl','wb') -pkl_out = pickle.Pickler(pkl_file, -1) -pkl_out.dump(matchDict) -pkl_file.close() -#todoApi.commit() - + print("ERROR: check HAB %s" % tid) + else: + print("ERROR: check TOD %s" % tid) + r = [] + # try: + # dueNow = str(parser.parse(matchDict[tid]['tod'].due_date).date()) + # except: + # dueNow = '' + # if dueNow != matchDict[tid]['hab'].date and matchDict[tid]['hab'].category == 'todo': + # matchDict[tid]['hab'].task_dict['date'] = dueNow + # r = main.update_hab(matchDict[tid]['hab']) + + pkl_file = open('oneWay_matchDict.pkl','wb') + pkl_out = pickle.Pickler(pkl_file, -1) + pkl_out.dump(matchDict) + pkl_file.close() + #todoApi.commit() + +if __name__ == "__main__": + sync_todoist_to_habitica() \ No newline at end of file diff --git a/source/todo_task.py b/source/todo_task.py index 0c75e57..e8e8d11 100644 --- a/source/todo_task.py +++ b/source/todo_task.py @@ -37,7 +37,7 @@ def __init__(self, task=None): raise TypeError(type(task_dict)) self.__task_dict = task_dict - + @property #Get the task dictionary as is def task_dict(self): @@ -74,12 +74,12 @@ def history(self): tod_user = main.tod_login('auth.cfg') activity = tod_user.activity.get(object_type='item', object_id = self.__task_dict['id'], event_type='completed') return activity - + @property #task name def name(self): return self.__task_dict['content'] - + @property #date task was added to todoist def date_added(self): @@ -100,38 +100,38 @@ def hardness(self): return "B" elif diffID == 2: return "C" - else: + else: return "C" @property #is task complete? 0 for no, 1 for yes - def complete(self): + def is_completed(self): return self.__task_dict['is_completed'] - - @complete.setter + + @is_completed.setter def complete(self, status): self.__task_dict['checked'] = status - + @property #due date def due_date(self): - return self.__task_dict['due_date_utc'] - + return self.__task_dict['due'] + @due_date.setter def due_date(self, date): - self.__task_dict['due_date_utc'] = date - + self.__task_dict['due'] = date + @property #due date def due(self): from dateutil import parser import datetime - if self.__task_dict['due_date_utc'] != None: - date = parser.parse(self.__task_dict['due_date_utc']) + if self.__task_dict['due'] != None: + date = parser.parse(self.__task_dict['due']['date']) return date else: return '' - + @property #is it due TODAY? def dueToday(self): @@ -141,11 +141,11 @@ def dueToday(self): import pytz today = datetime.utcnow().replace(tzinfo=pytz.UTC) try: - wobble = parser.parse(self.__task_dict['due_date_utc']) - timedelta(hours=6) #that datetime thing is pulling todoist's due dates to my time zone + wobble = parser.parse(self.__task_dict['due']) - timedelta(hours=6) #that datetime thing is pulling todoist's due dates to my time zone dueDate = wobble.date() except: dueDate = "" - + if today.date() >= dueDate: return 'Yes' elif dueDate == "": @@ -153,12 +153,12 @@ def dueToday(self): else: return 'No' - + @property #date in string form def date_string(self): return self.__task_dict['date_string'] - + @property #should it be due today? def dueLater(self): @@ -167,16 +167,14 @@ def dueLater(self): import pytz today = datetime.datetime.utcnow().replace(tzinfo=pytz.UTC) try: - wobble = parser.parse(self.__task_dict['due_date_utc']) + wobble = parser.parse(self.__task_dict['due']) dueDate = wobble.date() except: dueDate = "" - + if today.date() == dueDate: return 'Yes' elif dueDate == "": return "No due date" else: return 'No' - - \ No newline at end of file