You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

268 lines
10KB

  1. from todoist.api import TodoistAPI
  2. import sys
  3. from datetime import date, time, datetime, timedelta
  4. import gi
  5. gi.require_version('Gio', '2.0')
  6. from gi.repository import Gio
  7. import re
  8. ##@brief Simple function to parse iso formated string from api to a datetimeformat.
  9. # @param datastr String with date and time in iso format
  10. # @return datetime object with date as specified in string.
  11. def fromisoformat(datestr):
  12. if datestr == None:
  13. return None
  14. # Matches on YYYY-MM-DD, does not check if date is in range. Ex: 2019-11-08
  15. if (re.match(r'^\d{4}-\d{2}-\d{2}$',datestr, 0) != None):
  16. return datetime.strptime(datestr,"%Y-%m-%d")
  17. # Matches on YYYY-MM-DDTHH:MM:SSZ, does not check if date is in range. Ex: 2019-11-08T15:45:02Z
  18. if (re.match(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$',datestr, 0) != None):
  19. return datetime.strptime(datestr,"%Y-%m-%dT%H:%M:%SZ")
  20. # Matches on YYYY-MM-DDTHH:MM:SS, does not check if date is in range. Ex: 2019-11-08T15:45:02
  21. if (re.match(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$',datestr, 0) != None):
  22. return datetime.strptime(datestr,"%Y-%m-%dT%H:%M:%S")
  23. else:
  24. return None
  25. ##@class TodoProject
  26. # @brief functions to load a single or all projects and has methods to check the state of the project
  27. class TodoProject:
  28. ''' Common class for api calls'''
  29. ##@brief constructor of class
  30. # tries to load api-token from apikey-file.
  31. # @param project String with project name or integer with project_id
  32. def __init__(self,project=None):
  33. fh_api = open("apikey","r")
  34. self.apikey = fh_api.read()
  35. self.api = TodoistAPI(self.apikey)
  36. self.sync()
  37. if project != None:
  38. if isinstance(project,int):
  39. self.project= project
  40. else:
  41. for project in self.api["projects"]:
  42. if(project["name"] == project):
  43. self.project = project["id"]
  44. else:
  45. self.project == None
  46. self.items = TodoItemList(None,self.api)
  47. def sync(self):
  48. return self.api.sync()
  49. def checktaskstoday(self,tasklimit):
  50. numOfItems = len(self.items.itemsForDay())
  51. if numOfItems < tasklimit:
  52. self.sendnotification("TASKS","Not enough Tasks Scheduled for today!\n" + "Only " + str(numOfItems) + " of " + str(tasklimit) + " tasks set!" )
  53. def checkTasksWithoutNotes(self,date=date.today()):
  54. if len(self.items.itemsClosedWithoutNotes())>0:
  55. self.sendnotification("TASKS","There are tasks without a log.")
  56. def checkTasksOverDue(self):
  57. _overdueItems = len(self.items.itemsOverDue())
  58. if _overdueItems==1:
  59. self.sendnotification("TASKS","There is one task over due!")
  60. elif _overdueItems>1:
  61. self.sendnotification("TASKS","There are " + str(_overdueItems) + " tasks over due!")
  62. def sendnotification(self,title,body):
  63. Application = Gio.Application.new("todoist.tasks", Gio.ApplicationFlags.FLAGS_NONE)
  64. Application.register()
  65. Notification = Gio.Notification.new(title)
  66. Notification.set_body(body)
  67. Priority = Gio.NotificationPriority(2)
  68. Notification.set_priority(Priority)
  69. Icon = Gio.ThemedIcon.new("flag")
  70. Notification.set_icon(Icon)
  71. Application.send_notification(None, Notification)
  72. ##
  73. # @class TodoItemList
  74. # @brief Contains a list of items
  75. # Can be called like a list of items but has some aditional methods.
  76. class TodoItemList:
  77. ##
  78. # @brief Constructor
  79. # @param project can be the "name" or "id" of a project. If None the constructor will load all items from all projects
  80. # @param api object form TodoistAPI
  81. def __init__(self,project,api):
  82. self.api = api
  83. self.api.sync()
  84. self.items = []
  85. self.items_by_id = {}
  86. for item in self.api['items']:
  87. todoItem = TodoItem(item,self.api)
  88. if project==None or todoItem.project_id == project:
  89. self.items_by_id[str(todoItem.id)] = todoItem
  90. self.items.append(todoItem)
  91. # Also load all notes and append them to their corresponding item.
  92. for note in self.api['notes']:
  93. todoNote = TodoNote(note,self.api)
  94. self.items_by_id.get(str(todoNote.item_id)).addNote(todoNote)
  95. ##@brief magic method definition to so this object can be used as a list.
  96. # @param key index
  97. def __getitem__(self,key):
  98. return self.items[key]
  99. ##@brief magic method definition to so this object can be used as a list.
  100. def __len__(self):
  101. return len(self.items)
  102. ##@brief magic method definition to so this object can be used as a list.
  103. def __iter__(self):
  104. return iter(self.items)
  105. ##@brief magic method definition to so this object can be used as a list.
  106. def __reversed__(self):
  107. return reversed(self.items)
  108. ##@brief get all items that are due for specified day.
  109. # @_date date-object to specify the search day. Defaults to today.
  110. # @return list of items that have a duedate as specified in _date.
  111. def itemsForDay(self,_date=date.today()):
  112. _items = []
  113. for item in self.items:
  114. if item.dueOnDate(_date):
  115. _items.append(item)
  116. return _items
  117. ##@brief get all items that are due for specified day and not closed yet.
  118. # @_date date-object to specify the search day. Defaults to today.
  119. # @return list of items that have a duedate as specified in _date and are still open.
  120. def pendingItemsForDay(self,_date=date.today()):
  121. _items = []
  122. for item in self.items:
  123. if item.dueOnDate(_date) and not item.checked:
  124. _items.append(item)
  125. return _items
  126. ##@brief get all items that closed but do not have notes attached to them
  127. # @return list of closed items that do not have notes attached.
  128. def itemsClosedWithoutNotes(self):
  129. _items = []
  130. for item in self.items:
  131. if item.closedWithoutNotes():
  132. _items.append(item)
  133. return _items
  134. ##@brief get all items that are still open but passed there deadline
  135. # @return list of open items that passed their deadline
  136. def itemsOverDue(self):
  137. _items = []
  138. for item in self.items:
  139. if item.isOverDue():
  140. _items.append(item)
  141. return _items
  142. ##@class TodoItem
  143. # @brief Information of an Item/Task
  144. class TodoItem:
  145. ##@brief Constructor of TodoItem-object
  146. # @param item dict that is given by the TodoistAPI
  147. # @param dict that is given via the TodoistAPI
  148. # @param api TodoistAPI object
  149. def __init__(self,item,api):
  150. self.api = api
  151. self.id = item["id"]
  152. self.user_id = item["user_id"]
  153. self.project_id = item["project_id"]
  154. self.content = item["content"]
  155. self.priority = item["priority"]
  156. self.parent_id = item["parent_id"]
  157. self.child_order = item["child_order"]
  158. self.section_id = item["section_id"]
  159. self.day_order = item["day_order"]
  160. self.collapsed = item["collapsed"]
  161. self.labels = item["labels"]
  162. self.assigned_by_uid = item["assigned_by_uid"]
  163. self.responsible_uid = item["responsible_uid"]
  164. self.checked = item["checked"]==1
  165. self.in_history = item["in_history"]
  166. self.is_deleted = item["is_deleted"]
  167. self.sync_id = item["sync_id"]
  168. self.date_added = fromisoformat(item["date_added"])
  169. self.date_completed = fromisoformat(item["date_completed"])
  170. #add also empty list for nodes
  171. self.notes = []
  172. self._due = item["due"]
  173. #check if due is set.
  174. if self._due != None:
  175. self.due_date = fromisoformat(self._due["date"])
  176. self.due_timezone = self._due["timezone"]
  177. self.due_string = self._due["string"]
  178. self.due_lang = self._due["lang"]
  179. self.due_is_recurring = self._due["is_recurring"]
  180. else:
  181. self.due_date = None
  182. self.due_timezone = None
  183. self.due_string = None
  184. self.due_lang = None
  185. self.due_is_recurring = None
  186. ##@brief Check wether a duedate is set
  187. # @return True if duedate is set
  188. def hasDue(self):
  189. return self._due != None
  190. ##@brief Check wether the item has passed its deadline
  191. # @return True is overDue
  192. def isOverDue(self):
  193. if self.hasDue() and not self.checked:
  194. return self.due_date.date() < date.today()
  195. else:
  196. return False
  197. ##@brief Check wether the item has has any notes attached to it
  198. # @return True if notes are attached
  199. def hasNotes(self):
  200. return (len(self.notes)>0)
  201. ##@brief add a note to this item.
  202. # @param note object that should be appended to this item
  203. def addNote(self,note):
  204. self.notes.append(note)
  205. ##@brief Check if this task is closed but does not have any notes
  206. # @return True is closed without notes attached
  207. def closedWithoutNotes(self):
  208. return self.checked and not self.hasNotes()
  209. ##@brief Check if this item is almost due(today) but has not notes yet
  210. # @return True if it has no notes but has be finished today
  211. def almostDueWithoutNotes(self):
  212. return self.dueOnDate() and not self.hasNotes()
  213. ##@brief Check if the item is due on specific day
  214. # @param _date date(time) object for which day to check. Defaults to today.
  215. # @return False if not due on specified date
  216. def dueOnDate(self,_date = date.today()):
  217. if self.hasDue():
  218. if isinstance(_date,datetime):
  219. _date = date.date()
  220. return self.due_date.date() == _date
  221. else:
  222. return False
  223. ##@class TodoNote
  224. # @brief Simple implementation for note-objects
  225. class TodoNote:
  226. ##@brief constructor of class
  227. # @param note dict with note-information from api
  228. # @param api TodoistAPI-object
  229. def __init__(self,note,api):
  230. self.id = note["id"]
  231. self.posted_uid = note["posted_uid"]
  232. self.project_id = note["project_id"]
  233. self.item_id = note["item_id"]
  234. self.content = note["content"]
  235. self.file_attachment = note["file_attachment"]
  236. self.uids_to_notify = note["uids_to_notify"]
  237. self.is_deleted = note["is_deleted"]
  238. self.posted = note["posted"]
  239. self.reactions = note["reactions"]