25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

291 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. try:
  114. self.items_by_id.get(str(todoNote.item_id)).addNote(todoNote)
  115. except:
  116. pass
  117. ##@brief magic method definition to so this object can be used as a list.
  118. # @param key index
  119. def __getitem__(self,key):
  120. return self.items[key]
  121. ##@brief magic method definition to so this object can be used as a list.
  122. def __len__(self):
  123. return len(self.items)
  124. ##@brief magic method definition to so this object can be used as a list.
  125. def __iter__(self):
  126. return iter(self.items)
  127. ##@brief magic method definition to so this object can be used as a list.
  128. def __reversed__(self):
  129. return reversed(self.items)
  130. ##@brief get all items that are due for specified day.
  131. # @_date date-object to specify the search day. Defaults to today.
  132. # @return list of items that have a duedate as specified in _date.
  133. def itemsForDay(self,_date=date.today()):
  134. _items = []
  135. for item in self.items:
  136. if item.dueOnDate(_date):
  137. _items.append(item)
  138. return _items
  139. ##@brief get all items that are due for specified day and not closed yet.
  140. # @_date date-object to specify the search day. Defaults to today.
  141. # @return list of items that have a duedate as specified in _date and are still open.
  142. def pendingItemsForDay(self,_date=date.today()):
  143. _items = []
  144. for item in self.items:
  145. if item.dueOnDate(_date) and not item.checked:
  146. _items.append(item)
  147. return _items
  148. ##@brief get all items that closed but do not have notes attached to them
  149. # @return list of closed items that do not have notes attached.
  150. def itemsClosedWithoutNotes(self):
  151. _items = []
  152. for item in self.items:
  153. if item.closedWithoutNotes():
  154. _items.append(item)
  155. return _items
  156. ##@brief get all items that are still open but passed there deadline
  157. # @return list of open items that passed their deadline
  158. def itemsOverDue(self):
  159. _items = []
  160. for item in self.items:
  161. if item.isOverDue():
  162. _items.append(item)
  163. return _items
  164. ##@class TodoItem
  165. # @brief Information of an Item/Task
  166. class TodoItem:
  167. ##@brief Constructor of TodoItem-object
  168. # @param item dict that is given by the TodoistAPI
  169. # @param dict that is given via the TodoistAPI
  170. # @param api TodoistAPI object
  171. def __init__(self,item,api):
  172. self.api = api
  173. self.id = item["id"]
  174. self.user_id = item["user_id"]
  175. self.project_id = item["project_id"]
  176. self.content = item["content"]
  177. self.priority = item["priority"]
  178. self.parent_id = item["parent_id"]
  179. self.child_order = item["child_order"]
  180. self.section_id = item["section_id"]
  181. self.day_order = item["day_order"]
  182. self.collapsed = item["collapsed"]
  183. self.labels = item["labels"]
  184. self.assigned_by_uid = item["assigned_by_uid"]
  185. self.responsible_uid = item["responsible_uid"]
  186. self.checked = item["checked"]==1
  187. self.in_history = item["in_history"]
  188. self.is_deleted = item["is_deleted"]
  189. self.sync_id = item["sync_id"]
  190. self.date_added = fromisoformat(item["date_added"])
  191. self.date_completed = fromisoformat(item["date_completed"])
  192. #add also empty list for nodes
  193. self.notes = []
  194. self._due = item["due"]
  195. #check if due is set.
  196. if self._due != None:
  197. self.due_date = fromisoformat(self._due["date"])
  198. self.due_timezone = self._due["timezone"]
  199. self.due_string = self._due["string"]
  200. self.due_lang = self._due["lang"]
  201. self.due_is_recurring = self._due["is_recurring"]
  202. else:
  203. self.due_date = None
  204. self.due_timezone = None
  205. self.due_string = None
  206. self.due_lang = None
  207. self.due_is_recurring = None
  208. ##@brief Check wether a duedate is set
  209. # @return True if duedate is set
  210. def hasDue(self):
  211. return self._due != None
  212. ##@brief Check wether the item has passed its deadline
  213. # @return True is overDue
  214. def isOverDue(self):
  215. if self.hasDue() and not self.checked:
  216. return self.due_date.date() < date.today()
  217. else:
  218. return False
  219. ##@brief Check wether the item has has any notes attached to it
  220. # @return True if notes are attached
  221. def hasNotes(self):
  222. return (len(self.notes)>0)
  223. ##@brief add a note to this item.
  224. # @param note object that should be appended to this item
  225. def addNote(self,note):
  226. self.notes.append(note)
  227. ##@brief Check if this task is closed but does not have any notes
  228. # @return True is closed without notes attached
  229. def closedWithoutNotes(self):
  230. return self.checked and not self.hasNotes()
  231. ##@brief Check if this item is almost due(today) but has not notes yet
  232. # @return True if it has no notes but has be finished today
  233. def almostDueWithoutNotes(self):
  234. return self.dueOnDate() and not self.hasNotes()
  235. ##@brief Check if the item is due on specific day
  236. # @param _date date(time) object for which day to check. Defaults to today.
  237. # @return False if not due on specified date
  238. def dueOnDate(self,_date = date.today()):
  239. if self.hasDue():
  240. if isinstance(_date,datetime):
  241. _date = date.date()
  242. return self.due_date.date() == _date
  243. else:
  244. return False
  245. ##@class TodoNote
  246. # @brief Simple implementation for note-objects
  247. class TodoNote:
  248. ##@brief constructor of class
  249. # @param note dict with note-information from api
  250. # @param api TodoistAPI-object
  251. def __init__(self,note,api):
  252. self.id = note["id"]
  253. self.posted_uid = note["posted_uid"]
  254. self.project_id = note["project_id"]
  255. self.item_id = note["item_id"]
  256. self.content = note["content"]
  257. self.file_attachment = note["file_attachment"]
  258. self.uids_to_notify = note["uids_to_notify"]
  259. self.is_deleted = note["is_deleted"]
  260. self.posted = note["posted"]
  261. self.reactions = note["reactions"]