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.

288 lines
11KB

  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. ##
  26. # @class TodoProject
  27. # @brief functions to load a single or all projects and has methods to check the state of the project
  28. class TodoProject:
  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. ##@brief synchronize data from Todoist with the api object.
  48. # @return response of server
  49. def sync(self):
  50. return self.api.sync()
  51. ##@brief verify if there are atleast three tasks scheduled for today.
  52. # @param tasklimit Number of tasks required. if not met notification will be given.
  53. def checktaskstoday(self,tasklimit):
  54. numOfItems = len(self.items.itemsForDay())
  55. if numOfItems < tasklimit:
  56. self.sendnotification("TASKS","Not enough Tasks Scheduled for today!\n" + "Only " + str(numOfItems) + " of " + str(tasklimit) + " tasks set!" )
  57. ##@brief verify if all tasks that are closed also have notes attached.
  58. # A notification is send by this function if there are tasks that do not meet this condition.
  59. # @sa TodoProject::openTasksWithoutNotes
  60. def checkTasksWithoutNotes(self):
  61. if len(self.items.itemsClosedWithoutNotes())>0:
  62. self.sendnotification("TASKS","There are tasks without a log.")
  63. ##@brief verify if all tasks that are closed also have notes attached.
  64. def checkTasksOverDue(self):
  65. _overdueItems = len(self.items.itemsOverDue())
  66. if _overdueItems==1:
  67. self.sendnotification("TASKS","There is one task over due!")
  68. elif _overdueItems>1:
  69. self.sendnotification("TASKS","There are " + str(_overdueItems) + " tasks over due!")
  70. ##@brief reopen all tasks that are closed but do not have notes attached.
  71. # @sa TodoProject::checkTasksWithoutNotes
  72. def openTasksWithoutNotes(self):
  73. for item in self.items.itemsClosedWithoutNotes():
  74. item_handle = item.api.items.get_by_id(item.id)
  75. item_handle.uncomplete()
  76. self.api.commit()
  77. ##@brief Show a notification on Linux desktop
  78. # Uses Gio-2.0 package to make a notification
  79. # @param title string with title of the notification
  80. # @param body string with the body text of the notification
  81. def sendnotification(self,title,body):
  82. Application = Gio.Application.new("todoist.tasks", Gio.ApplicationFlags.FLAGS_NONE)
  83. Application.register()
  84. Notification = Gio.Notification.new(title)
  85. Notification.set_body(body)
  86. Priority = Gio.NotificationPriority(2)
  87. Notification.set_priority(Priority)
  88. Icon = Gio.ThemedIcon.new("flag")
  89. Notification.set_icon(Icon)
  90. Application.send_notification(None, Notification)
  91. ##
  92. # @class TodoItemList
  93. # @brief Contains a list of items
  94. # Can be called like a list of items but has some aditional methods.
  95. class TodoItemList:
  96. ##
  97. # @brief Constructor
  98. # @param project can be the "name" or "id" of a project. If None the constructor will load all items from all projects
  99. # @param api object form TodoistAPI
  100. def __init__(self,project,api):
  101. self.api = api
  102. self.api.sync()
  103. self.items = []
  104. self.items_by_id = {}
  105. for item in self.api['items']:
  106. todoItem = TodoItem(item,self.api)
  107. if project==None or todoItem.project_id == project:
  108. self.items_by_id[str(todoItem.id)] = todoItem
  109. self.items.append(todoItem)
  110. # Also load all notes and append them to their corresponding item.
  111. for note in self.api['notes']:
  112. todoNote = TodoNote(note,self.api)
  113. self.items_by_id.get(str(todoNote.item_id)).addNote(todoNote)
  114. ##@brief magic method definition to so this object can be used as a list.
  115. # @param key index
  116. def __getitem__(self,key):
  117. return self.items[key]
  118. ##@brief magic method definition to so this object can be used as a list.
  119. def __len__(self):
  120. return len(self.items)
  121. ##@brief magic method definition to so this object can be used as a list.
  122. def __iter__(self):
  123. return iter(self.items)
  124. ##@brief magic method definition to so this object can be used as a list.
  125. def __reversed__(self):
  126. return reversed(self.items)
  127. ##@brief get all items that are due for specified day.
  128. # @_date date-object to specify the search day. Defaults to today.
  129. # @return list of items that have a duedate as specified in _date.
  130. def itemsForDay(self,_date=date.today()):
  131. _items = []
  132. for item in self.items:
  133. if item.dueOnDate(_date):
  134. _items.append(item)
  135. return _items
  136. ##@brief get all items that are due for specified day and not closed yet.
  137. # @_date date-object to specify the search day. Defaults to today.
  138. # @return list of items that have a duedate as specified in _date and are still open.
  139. def pendingItemsForDay(self,_date=date.today()):
  140. _items = []
  141. for item in self.items:
  142. if item.dueOnDate(_date) and not item.checked:
  143. _items.append(item)
  144. return _items
  145. ##@brief get all items that closed but do not have notes attached to them
  146. # @return list of closed items that do not have notes attached.
  147. def itemsClosedWithoutNotes(self):
  148. _items = []
  149. for item in self.items:
  150. if item.closedWithoutNotes():
  151. _items.append(item)
  152. return _items
  153. ##@brief get all items that are still open but passed there deadline
  154. # @return list of open items that passed their deadline
  155. def itemsOverDue(self):
  156. _items = []
  157. for item in self.items:
  158. if item.isOverDue():
  159. _items.append(item)
  160. return _items
  161. ##@class TodoItem
  162. # @brief Information of an Item/Task
  163. class TodoItem:
  164. ##@brief Constructor of TodoItem-object
  165. # @param item dict that is given by the TodoistAPI
  166. # @param dict that is given via the TodoistAPI
  167. # @param api TodoistAPI object
  168. def __init__(self,item,api):
  169. self.api = api
  170. self.id = item["id"]
  171. self.user_id = item["user_id"]
  172. self.project_id = item["project_id"]
  173. self.content = item["content"]
  174. self.priority = item["priority"]
  175. self.parent_id = item["parent_id"]
  176. self.child_order = item["child_order"]
  177. self.section_id = item["section_id"]
  178. self.day_order = item["day_order"]
  179. self.collapsed = item["collapsed"]
  180. self.labels = item["labels"]
  181. self.assigned_by_uid = item["assigned_by_uid"]
  182. self.responsible_uid = item["responsible_uid"]
  183. self.checked = item["checked"]==1
  184. self.in_history = item["in_history"]
  185. self.is_deleted = item["is_deleted"]
  186. self.sync_id = item["sync_id"]
  187. self.date_added = fromisoformat(item["date_added"])
  188. self.date_completed = fromisoformat(item["date_completed"])
  189. #add also empty list for nodes
  190. self.notes = []
  191. self._due = item["due"]
  192. #check if due is set.
  193. if self._due != None:
  194. self.due_date = fromisoformat(self._due["date"])
  195. self.due_timezone = self._due["timezone"]
  196. self.due_string = self._due["string"]
  197. self.due_lang = self._due["lang"]
  198. self.due_is_recurring = self._due["is_recurring"]
  199. else:
  200. self.due_date = None
  201. self.due_timezone = None
  202. self.due_string = None
  203. self.due_lang = None
  204. self.due_is_recurring = None
  205. ##@brief Check wether a duedate is set
  206. # @return True if duedate is set
  207. def hasDue(self):
  208. return self._due != None
  209. ##@brief Check wether the item has passed its deadline
  210. # @return True is overDue
  211. def isOverDue(self):
  212. if self.hasDue() and not self.checked:
  213. return self.due_date.date() < date.today()
  214. else:
  215. return False
  216. ##@brief Check wether the item has has any notes attached to it
  217. # @return True if notes are attached
  218. def hasNotes(self):
  219. return (len(self.notes)>0)
  220. ##@brief add a note to this item.
  221. # @param note object that should be appended to this item
  222. def addNote(self,note):
  223. self.notes.append(note)
  224. ##@brief Check if this task is closed but does not have any notes
  225. # @return True is closed without notes attached
  226. def closedWithoutNotes(self):
  227. return self.checked and not self.hasNotes()
  228. ##@brief Check if this item is almost due(today) but has not notes yet
  229. # @return True if it has no notes but has be finished today
  230. def almostDueWithoutNotes(self):
  231. return self.dueOnDate() and not self.hasNotes()
  232. ##@brief Check if the item is due on specific day
  233. # @param _date date(time) object for which day to check. Defaults to today.
  234. # @return False if not due on specified date
  235. def dueOnDate(self,_date = date.today()):
  236. if self.hasDue():
  237. if isinstance(_date,datetime):
  238. _date = date.date()
  239. return self.due_date.date() == _date
  240. else:
  241. return False
  242. ##@class TodoNote
  243. # @brief Simple implementation for note-objects
  244. class TodoNote:
  245. ##@brief constructor of class
  246. # @param note dict with note-information from api
  247. # @param api TodoistAPI-object
  248. def __init__(self,note,api):
  249. self.id = note["id"]
  250. self.posted_uid = note["posted_uid"]
  251. self.project_id = note["project_id"]
  252. self.item_id = note["item_id"]
  253. self.content = note["content"]
  254. self.file_attachment = note["file_attachment"]
  255. self.uids_to_notify = note["uids_to_notify"]
  256. self.is_deleted = note["is_deleted"]
  257. self.posted = note["posted"]
  258. self.reactions = note["reactions"]